Skip to main content

Configuration

Commit-Boost needs a configuration file detailing all the services that you want to run. Create a cb-config.toml and modify it depending on which modules you plan to run.

Minimal PBS setup on Hoodi​

chain = "Hoodi"

[pbs]
port = 18550

[[relays]]
# Replace this with your relay's own URL, in the form scheme://<relay-pubkey>@<host>
url = "https://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@relay.example.com"

[metrics]
enabled = true
warning

url must be a full URL containing the relay's BLS pubkey; an empty URL or one without a pubkey (invalid BLS pubkey) prevents the sidecar from starting.

You can find a list of MEV-Boost Hoodi relays here. After the sidecar is started, it will expose a port (18550 in this example), that you need to point your CL to. This may be different depending on which CL you're running; see the consensus client configuration guides. (from v0.12.0-rc1) After the Gloas fork, point each validator key at Commit-Boost as ePBS setup describes.

note

In this setup, the Signer service will not be started.

Custom chains​

Besides the known chain names (Mainnet, Holesky, Sepolia, Hoodi), the chain field also accepts a custom chain in two forms:

  • Spec file: a genesis time plus a path to a chain spec file, either in JSON (as returned by the beacon endpoint /eth/v1/config/spec, with or without its data wrapper) or YAML format:
chain = { genesis_time_secs = 1695902400, path = "/path/to/spec.json" }

genesis_time_secs is the genesis_time the beacon endpoint /eth/v1/beacon/genesis returns. commit-boost init mounts the spec file into the PBS container; when you run the image yourself, mount it as Docker describes.

  • Inline object: all parameters specified directly:
chain = { genesis_time_secs = 1695902400, slot_time_secs = 12, genesis_fork_version = "0x01017000", fulu_fork_slot = 5283840, chain_id = 17000 }

All inline fields are required; omitting one makes the chain value fail to parse.

With the spec-file form, the CB_CHAIN_SPEC environment variable can override the spec file path at runtime (see Binary).

PBS safety and tuning options​

Beyond the basics shown above, the [pbs] section supports additional knobs. The annotated config example is the full field reference with defaults; the options below carry extra operational context:

  • timeout_get_header_ms: timeout, in milliseconds, for the get_header call to relays. Must be greater than 0, and must be less than late_in_slot_time_ms. The CL also has a timeout (e.g. 1 second), so this should be lower than that to leave some margin for overhead. Default: 950.
  • late_in_slot_time_ms: how late, in milliseconds into the slot, is considered "late". This shortens get_header timeouts to make sure a header is returned within this deadline; if the CL request arrives later in the slot, fetching headers is skipped to force local building and minimize the risk of a missed slot. Must be greater than 0. Default: 2000.
  • extra_validation_enabled: whether to enable extra validation of get_header responses. If enabled, rpc_url must also be set to an Execution Layer RPC on the same chain as the sidecar (this is checked at startup). Default: false.
  • relay_check: whether GET /eth/v1/builder/status asks the relays, in [[relays]] or a mux, giving each up to 30 s. When it asks, it answers 200 if at least one answers with a 2xx and 502 otherwise. With false it answers 200 without asking. Commit-Boost calls it at startup and exits if it fails. Default: true.
  • mux_registry_refresh_interval_seconds: refresh interval, in seconds, for registry-based muxes with dynamic refreshing enabled. Default: 384.
warning

validator_registration_batch_size used to be a per-relay option. It is no longer accepted per relay: setting it inside a [[relays]] entry makes the sidecar fail at startup; set it in the [pbs] section instead.

Per-relay options​

Each [[relays]] entry supports, besides id and url:

  • headers: custom headers sent with every request to this relay, which is how a relay API key is supplied. See Relay API keys.
  • get_params: optional GET parameters to add to each request URL for this relay.
  • get_header: how bids are fetched from this relay, either "http" (one request per get_header) or "stream" (a websocket stream of bid updates, for relays that support it). Default: "http". See Bid streaming.
  • enable_timing_games: whether to enable timing games for this relay, as tuned by target_first_request_ms and frequency_get_header_ms. If neither of those is set, this flag has no effect. Advanced users only: misconfiguration can result in e.g. fetching a lower header value or missing a slot (caveats and worked examples in the annotated config example). Default: false.
  • target_first_request_ms: target time in the slot, in milliseconds, at which to send the first get_header request.
  • frequency_get_header_ms: frequency, in milliseconds, at which to send get_header requests.

The same fields are available on [[mux.relays]] entries (see Mux key loaders).

Relay API keys​

Some relays require an API key for bid streaming, sent in the X-Api-Key header as a v4 UUID. Generate one however you like, for example with uuidgen or an online generator such as uuidgenerator.net. Keep the same key for a given relay: it also goes out on register_validator, which is where the relay ties it to your validators.

headers accepts the value three ways. Written in the config:

headers = { X-Api-Key = "00000000-0000-4000-8000-000000000000" }

Read from a file:

headers = { X-Api-Key = { file = "/run/secrets/relay-key" } }

Read from an environment variable:

headers = { X-Api-Key = { env = "RELAY_API_KEY" } }

The file and env forms are read when the relay loads, at startup and on each config reload. Trailing whitespace is stripped, so a file ending in a newline is fine. Startup fails if the file or variable is missing or empty.

Under Docker, commit-boost init mounts each file path into the container at the same path and passes each env name through, so a file path must be absolute and must already exist when init runs.

Bid streaming​

get_header = "stream" replaces the per-slot HTTP request with a websocket. It is set per relay, on [[relays]] and [[mux.relays]] entries alike. examples/configs/pbs_bid_stream.toml streams from two default relays and two mux relays.

(from v0.12.0-rc1) After the Gloas fork the same setting also streams ePBS bids.

Connection​

For each get_header call PBS dials:

ws(s)://<relay host>/eth/v1/builder/header_stream/{slot}/{parent_hash}/{pubkey}

The scheme and host come from the relay's url: https becomes wss, http becomes ws, any other scheme fails at startup. The pubkey is dropped from the userinfo and get_params are appended as usual. The stream connects directly, ignoring HTTP_PROXY, HTTPS_PROXY and ALL_PROXY. The stream checks a wss certificate against the Mozilla root certificates built into Commit-Boost only, while HTTP requests use the system's certificate store (under Docker, the container's), so a relay whose certificate comes from a private CA fails on the stream and works over HTTP when the system trusts that CA.

The handshake carries what the HTTP request would: slot, parent hash and pubkey in the path, plus Date-Milliseconds, X-Timeout-Ms, X-CommitBoost-Version and every configured header. X-Timeout-Ms is the time left in the bid window when PBS starts to connect.

The stream needs the same API key as the HTTP path, sent on the handshake with the rest of headers. See Relay API keys.

Bid window​

Once connected, the relay sends one binary frame per bid update. PBS keeps the most recent update, and validates and returns it when the window ends or the relay closes the stream. Validation is the same as on the HTTP path, so skip_sigverify, min_bid_eth and extra_validation_enabled apply unchanged. Frames that do not parse as a bid are skipped and counted.

The window is the deadline the HTTP path already computes: timeout_get_header_ms, capped by the time left until late_in_slot_time_ms, and capped again by the CL's X-Timeout-Ms if it sends one. An HTTP get_header normally returns early, while a stream is held to the end, so on a streaming relay timeout_get_header_ms sets how long PBS holds the CL's request. A CL request arriving at or after late_in_slot_time_ms still skips relays entirely and forces local building.

The timing-game options have no effect on a stream. They schedule repeated HTTP requests, and the stream already delivers every update the relay produces. They do apply to the HTTP fallback.

Fallback​

If the handshake fails, PBS falls back to a plain HTTP get_header at the relay's normal URL with whatever time is left in the window, and counts cb_pbs_relay_stream_fallback_total. That covers a refused connection, a DNS or TLS failure, and a relay that rejects the upgrade with an HTTP response.

There is no fallback if the handshake is still unanswered when the window ends (recorded as 555), or if it fails too late to leave time for an HTTP request. There is also none once the stream is open. If it breaks before any bid arrives, that relay contributes no header for the slot (recorded as 556). If a bid already arrived, PBS validates and returns it as usual.

On a high-latency connection, where the handshake risks timing out, you can add the relay a second time with get_header = "http" and a different id. PBS asks both entries every slot and keeps the better bid, so the HTTP entry still returns a header when the stream does not. (from v0.12.0-rc1) After the Gloas fork only the first of the two entries serves ePBS bids.

[[relays]]
id = "relay-1-stream"
url = "https://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@relay-1.xyz"
get_header = "stream"
headers = { X-Api-Key = "..." }

[[relays]]
id = "relay-1-http"
url = "https://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@relay-1.xyz"
get_header = "http"
headers = { X-Api-Key = "..." }

Metrics​

Stream and fallback outcomes are recorded separately. cb_pbs_relay_status_code_total and cb_pbs_relay_latency carry endpoint="get_header_stream" for the stream and endpoint="get_header" for the fallback, and four cb_pbs_relay_stream_* series cover handshake latency, updates per window, unparseable frames and fallbacks. See Metrics > Bid stream.

SSZ support​

On get_header and v1 submit_blinded_block requests, PBS negotiates the response encoding with the beacon node through the Accept header. Both SSZ and JSON are supported: the response follows the client's preference (q-values first, then listing order), defaults to JSON when no preference is expressed, and a request that accepts neither is rejected with 406. v2 submit_blinded_block responses are empty 202s, so there is nothing to negotiate and Accept is not enforced on them. Towards relays PBS always requests SSZ first and falls back to JSON for relays that do not support it. Bids delivered over a websocket stream are always SSZ.

Logs​

Logging is configured via the optional [logs.stdout] and [logs.file] sections:

[logs.stdout]
enabled = true # Whether to enable stdout logging. Default: true
level = "info" # Log level: trace, debug, info, warn, error. Default: "info"
use_json = false # Log in JSON format. Default: false
color = true # Whether to use colors in the output. Default: true

[logs.file]
enabled = true # Whether to enable file logging. Default: false
level = "info" # Log level: trace, debug, info, warn, error. Default: "info"
use_json = true # Log in JSON format. Default: true
dir_path = "/var/logs/commit-boost" # Directory to store logs. Default: "/var/logs/commit-boost"
max_files = 30 # Maximum number of log files to keep. Default: unlimited

The CB_LOGS_DIR environment variable overrides dir_path (see Binary).

Metrics​

Prometheus metrics are configured via the optional [metrics] section; without it, commit-boost init doesn't set up metrics:

[metrics]
enabled = true # Whether to collect metrics. Default: true
host = "127.0.0.1" # Host to expose the metrics servers on. Default: 127.0.0.1
start_port = 10000 # First Prometheus scrape port; each service uses start_port, start_port + 1, ... Default: 10000

A service serves metrics only on the port in the CB_METRICS_PORT environment variable, which commit-boost init sets from start_port (see Binary).

Signer service​

Commit-Boost supports both local and remote signers. The Signer service is responsible for signing the transactions that commit modules generate. It is not used by the default PBS image; custom PBS builds can opt in via pbs.with_signer. Only one signer at a time is allowed. The config file must still contain at least one [[relays]] entry even on a host that runs only the signer; the shared config schema requires it.

Local signer​

To start a local Signer service, you need to include its parameters in the config file:

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "lighthouse"
keys_path = "/path/to/keys"
secrets_path = "/path/to/secrets"

Supported keystore formats: Lighthouse, Prysm, Teku, Lodestar, and Nimbus. Expected file structures for each format:

Lighthouse

File structure:​

├── keys
│   ├── <PUBLIC_KEY_1>
│   │   └── voting-keystore.json
│   └── <PUBLIC_KEY_2>
│   └── voting-keystore.json
└── secrets
   ├── <PUBLIC_KEY_1>
   └── <PUBLIC_KEY_2>

Config:​

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "lighthouse"
keys_path = "keys"
secrets_path = "secrets"
Prysm

File structure:​

├── wallet
│   └── direct
│      └── accounts
│         └──all-accounts.keystore.json
└── secrets
   └── password.txt

Config:​

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "prysm"
keys_path = "wallet/direct/accounts/all-accounts.keystore.json"
secrets_path = "secrets/password.txt"
Teku

File structure:​

├── keys
│   ├── <PUBLIC_KEY_1>.json
│   └── <PUBLIC_KEY_2>.json
└── secrets
   ├── <PUBLIC_KEY_1>.txt
   └── <PUBLIC_KEY_2>.txt

Config:​

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "teku"
keys_path = "keys"
secrets_path = "secrets"
Lodestar

File structure:​

├── keys
│   ├── <PUBLIC_KEY_1>.json
│   └── <PUBLIC_KEY_2>.json
└── secrets
   └── password.txt

Config:​

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "lodestar"
keys_path = "keys"
secrets_path = "secrets/password.txt"
note

All keys have the same password stored in secrets/password.txt

Nimbus

File structure:​

├── keys
│   ├── <PUBLIC_KEY_1>
│   │   └── keystore.json
│   └── <PUBLIC_KEY_2>
│   └── keystore.json
└── secrets
   ├── <PUBLIC_KEY_1>
   └── <PUBLIC_KEY_2>

Config:​

[pbs]
# ...
with_signer = true

[signer]
port = 20000

[signer.local.loader]
format = "nimbus"
keys_path = "keys"
secrets_path = "secrets"

Proxy keys store​

Proxy keys can be used to sign transactions with a different key than the one used to sign the block. Proxy keys are generated by the Signer service and authorized by the validator key. Each module can have its own proxy keys, which can be BLS or ECDSA.

To persist proxy keys across restarts, you must enable the proxy store in the config file. There are 2 options for this:

File

The keys are stored in plain text in a file. This method is unsafe and should only be used for testing.

File structure​

<proxy_dir>
└── <MODULE_ID>
└── bls
├── <PROXY_PUBKEY1>
└── <PROXY_PUBKEY2>

Configuration​

[signer.local.store]
proxy_dir = "path/to/proxy_dir"

Where each <PROXY_PUBKEY> file contains the following:

{
"secret": "0x...",
"delegation": {
"message": {
"delegator": "0x...",
"proxy": "0x..."
},
"signature": "0x..."
}
}
ERC2335

The keys are stored in an ERC-2335-style keystore, along with a password. This way, you can safely share the keys directory: without the password they are useless.

File structure​

├── <keys_path>
│ └── <CONSENSUS_PUBLIC_KEY>
│ └── <MODULE_ID>
│ ├── bls/
│ │ ├── <PROXY_PUBLIC_KEY1>.json
│ │ ├── <PROXY_PUBLIC_KEY1>.sig
│ │ ├── <PROXY_PUBLIC_KEY2>.json
│ │ └── <PROXY_PUBLIC_KEY2>.sig
│ └── ecdsa/
│ ├── <PROXY_PUBLIC_KEY3>.json
│ └── <PROXY_PUBLIC_KEY3>.sig
└── <secrets_path>
└── <CONSENSUS_PUBLIC_KEY>
└── <MODULE_ID>
├── bls/
│ ├── <PROXY_PUBLIC_KEY1>
│ └── <PROXY_PUBLIC_KEY2>
└── ecdsa
└── <PROXY_PUBLIC_KEY3>

Configuration​

[signer.local.store]
keys_path = "path/to/keys"
secrets_path = "path/to/secrets"

Where the <PROXY_PUBLIC_KEY>.json files contain ERC-2335 keystore, the <PROXY_PUBLIC_KEY>.sig files contain the signature of the delegation, and <PROXY_PUBLIC_KEY> files contain the password to decrypt the keystores.

Remote signer​

You might choose to use an external service to sign the transactions. For now, one remote signer is supported: Dirk.

Dirk​

Dirk is a distributed key management system that can be used to sign transactions. In this case the Signer service is needed as an intermediary between the modules and Dirk. The following parameters are needed:

[signer.dirk]
cert_path = "/path/to/client.crt"
key_path = "/path/to/client.key"
secrets_path = "/path/to/secrets"
# Optional parameters
ca_cert_path = "/path/to/ca.crt"
max_response_size_bytes = 4194304

# Add one entry like this for each host
[[signer.dirk.hosts]]
server_name = "localhost-1"
url = "https://localhost-1:8081"
wallets = ["SomeWallet", "DistributedWallet"]

[[signer.dirk.hosts]]
server_name = "localhost-2"
url = "https://localhost-2:8082"
wallets = ["AnotherWallet", "DistributedWallet"]
  • cert_path and key_path are the paths to the client certificate and key used to authenticate with Dirk.
  • wallets is a list of wallets from which the Signer service will load all accounts as consensus keys. Generated proxy keys will have format <WALLET_NAME>/<ACCOUNT>/<MODULE_ID>/<UUID>, so accounts found with that pattern will be ignored.
  • secrets_path is the path to the folder containing the passwords of the generated proxy accounts, which will be stored in <secrets_path>/<WALLET_NAME>/<ACCOUNT>/<MODULE_ID>/<UUID>.pass.

Additionally, you can set a proxy store so that the delegation signatures for generated proxy keys are stored locally. As these signatures are not sensitive, the only supported store type is File:

[signer.dirk.store]
proxy_dir = "/path/to/proxy_dir"

Delegation signatures will be stored in files with the format <proxy_dir>/delegations/<MODULE_ID>/bls/<PROXY_KEY>.sig.

A full example of a config file with Dirk can be found here.

TLS​

By default the Signer API uses plain HTTP with no TLS. That is fine for testing or a single-machine Docker network; enable TLS for any traffic that crosses machines.

To enable TLS, you must first create a certificate / key pair. We strongly advise using a well-known Certificate Authority to create and sign the certificate and do not recommend using a self-signed certificate / key pair for production environments.

When configuring TLS support, the Signer service expects a single folder containing:

  • cert.pem: The SSL certificate file signed by a certificate authority, in PEM format
  • key.pem: The private key corresponding to cert.pem that will be used for signing TLS traffic, in PEM format

Specifying it is done within Commit-Boost's configuration file using the [signer.tls_mode] table as follows:

[pbs]
# ...
with_signer = true

[signer]
port = 20000
# ...

[signer.tls_mode]
type = "certificate"
path = "path/to/your/cert/folder"

With type = "certificate", an existing cert.pem and key.pem must be present at path; there is no default path and the files are not auto-generated.

Rate limit​

The Signer service implements a rate limit system of 3 failed authentications every 5 minutes. These values can be modified in the config file:

[signer]
# ...
jwt_auth_fail_limit = 3 # The amount of failed requests allowed
jwt_auth_fail_timeout_seconds = 300 # The time window in seconds

The rate limit is applied to the IP address of the client making the request. By default, the IP is extracted directly from the TCP connection. If you're running the Signer service behind a reverse proxy (e.g. Nginx), you can configure it to extract the IP from a custom HTTP header instead. There are two options:

  • unique: Provides an HTTP header that contains the IP. This header is expected to appear only once in the request. This is common when using X-Real-IP, True-Client-IP, etc. If a request has multiple values for this header, it will be considered invalid and rejected.
  • rightmost: Provides an HTTP header that contains a comma-separated list of IPs. The nth rightmost IP in the list is used. If the header appears multiple times, all occurrences are concatenated in order and the rightmost counting spans the combined list. This is common when using X-Forwarded-For.

Examples:

[signer.reverse_proxy]
type = "unique"
header = "X-Real-IP"
[signer.reverse_proxy]
type = "rightmost"
header = "X-Forwarded-For"
trusted_count = 1
note

trusted_count is the number of trusted proxies in front of the Signer service, but the last proxy won't add its address, so the number of skipped IPs is trusted_count - 1. See MDN docs for more info.

warning

Only enable [signer.reverse_proxy] when the signer is reachable exclusively through the trusted proxy: this configuration decides whose IP gets rate-limited, so a directly reachable signer or a wrong trusted_count lets a client spoof the header to bypass the limit or lock other clients out.

Custom module​

We currently provide a test module that needs to be built locally. To build the module run:

just docker-build-test-modules
note

We use just as command runner. If you don't have it installed, either install it from https://github.com/casey/just or run the commands manually from the justfile at the root of the repo.

This will create a Docker image called test_da_commit that periodically requests signatures from the validator.

The cb-config.toml file needs to be updated as follows:

[pbs]
port = 18550

[[relays]]
# Replace this with your relay's own URL, in the form scheme://<relay-pubkey>@<host>
url = "https://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@relay.example.com"

[signer]
port = 20000

[signer.local.loader]
format = "lighthouse"
keys_path = "/path/to/keys"
secrets_path = "/path/to/secrets"

[metrics]
enabled = true

[[modules]]
id = "DA_COMMIT"
type = "commit"
docker_image = "test_da_commit"
signing_id = "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b"
sleep_secs = 5

The [[modules]] section at a minimum needs to specify the module id, type and docker_image. For modules with type commit, which will be used to access the Signer service and request signatures for preconfs, you will also need to specify the module's unique signing_id (see the proposer commitment documentation). You can also pass environment variables to the module with env (a map of variable name to value) and env_file (path to an environment file for the module). Additional parameters needed for the business logic of the module will also be here.

To learn more about developing modules, see Commit modules.

Vouch​

Vouch is a multi-node validator client built by Attestant. Vouch is particular in that it also integrates an MEV-Boost client to interact with relays. The Commit-Boost PBS service is compatible with the Vouch blockrelay since it implements the same Builder-API as relays. For example, depending on your setup and preference, you may want to fetch headers from a given relay using Commit-Boost vs using the built-in Vouch blockrelay.

Configuration​

Get familiar on how to set up Vouch here.

You can set up Commit-Boost with Vouch in two ways. For simplicity, assume that in Vouch blockrelay.listen-address: 127.0.0.1:19550 and in Commit-Boost pbs.port = 18550.

Beacon Node to Vouch​

In this setup, the BN Builder-API endpoint will be pointing to the Vouch blockrelay (e.g. for Lighthouse you will need the flag --builder=http://127.0.0.1:19550).

Modify the blockrelay.config file to add Commit-Boost to its relays:

{
"relays": {
"http://127.0.0.1:18550": {}
}
}

Beacon Node to Commit-Boost​

In this setup, the BN Builder-API endpoint will be pointing to the PBS service (e.g. for Lighthouse you will need the flag --builder=http://127.0.0.1:18550).

This will bypass the blockrelay entirely so make sure all relays are properly configured in the [[relays]] section.

note

This approach could also work if you have a multi-beacon-node setup, where some BNs fetch directly via Commit-Boost while others go through the blockrelay.

Notes​

  • It's up to you to decide which relays will be connected via Commit-Boost ([[relays]] section in the toml config) and which via Vouch (additional entries in the relays field). Remember that any rate-limit will be shared across the two sidecars, if running on the same machine.
  • You may occasionally see a timeout error during registrations, especially if you're running a large number of validators in the same instance. This can resolve itself as registrations will be cleared later in the epoch when relays are less busy processing other registrations. Alternatively you can also adjust the builderclient.timeout option in .vouch.yml.

Hot reload​

Commit-Boost can hot-reload cb-config.toml without restarting modules: send POST /reload to each module you want to reload.

On the signer, the /reload and /revoke_jwt endpoints require admin authentication. CB_SIGNER_ADMIN_JWT holds the admin secret (an HMAC key), not a ready-to-use token: mint a short-lived HS256 admin JWT from it (claims spec: Signer API > Admin token). Sending the raw secret as the Bearer token fails with 401. Commit-Boost ships no CLI helper for minting this token today; a few lines of Python do it (deps: pip install pyjwt "eth-hash[pycryptodome]"):

import os, time, jwt
from eth_hash.auto import keccak

secret = os.environ["CB_SIGNER_ADMIN_JWT"]
route = "/reload"
body = b"{}" # the request body you will send

claims = {"admin": True, "route": route, "exp": int(time.time()) + 30}
if body:
claims["payload_hash"] = "0x" + keccak(body).hex()
print(jwt.encode(claims, secret, algorithm="HS256"))

Then send the request with the minted token. The signer's /reload endpoint requires a JSON body with Content-Type: application/json even when no overrides are sent; send an empty JSON object ({}) at minimum, and mint the token over the exact body you send. In the case the module is running in a Docker container without the port exposed (like the signer), you can run the request inside the container (assuming the snippet above is saved as mint_admin_jwt.py). In the Docker setup the admin secret lives in .cb.env, so export it into your shell first:

export $(grep CB_SIGNER_ADMIN_JWT .cb.env)
TOKEN=$(python3 mint_admin_jwt.py)
docker compose -f cb.docker-compose.yml exec cb_signer curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' http://localhost:20000/reload

Passing the minted token on the command line is acceptable only because it expires in 30 seconds and is bound to the route and body; the admin secret itself must never appear on a command line.

Automatic reload (PBS only)​

In addition to the manual /reload endpoint, the PBS service watches the config file for changes and automatically reloads the configuration whenever the file is modified, with no restart or API call needed. If a reload fails (e.g. because of a misconfigured option), the previous configuration is kept: the watcher logs a warning and the /reload endpoint returns a 500 error.

Custom PBS binaries

Custom PBS binaries only get the file watcher if they pass the real config path to PbsState::new; see Extending PBS. POST /reload works either way.

Signer service reload​

When the signer receives a reload request it re-reads the configuration file and environment variables, rebuilding its internal state to match:

  • New modules added to the config are registered with the signer.
  • Removed modules are dropped from the signer's access list.
  • JWT secrets and the admin secret are reset to their current values from the environment variables.
  • Any runtime changes from previous /revoke_jwt or /reload calls are reverted.

The request body accepts 2 optional override parameters, applied on top of the config:

  • jwt_secrets: a comma-separated list of <MODULE_ID>=<JWT_SECRET> pairs to override specific module secrets. Only modules present in the config can be overridden.
  • admin_secret: a string to override the admin JWT secret.

With an empty JSON object body ({}), the signer state is simply synced to match the config.

Common patterns​

Add a new module without restarting: This works only if the module's JWT secret was already included in CB_JWTS (a comma-separated list of <MODULE_ID>=<JWT_SECRET> pairs) when the signer started: the reload re-reads CB_JWTS from the signer's own process environment, which is fixed at startup, and fails with a 500 (internal error; the signer log shows JWT secret for module X is missing) otherwise. For a module whose secret is not yet in CB_JWTS, there are two Docker paths: re-run commit-boost init and docker compose up -d with the new env file so every container is recreated with consistent fresh secrets, or hand-add the module's service to the compose file and append its secret to CB_JWTS in .cb.env, then run the up -d command again so the signer is recreated with it. docker compose restart doesn't re-read .cb.env. Re-running init rotates every JWT secret, including the admin secret.

  1. Add the [[modules]] entry to cb-config.toml.
  2. Send POST /reload with an empty JSON object body ({}). The signer picks up the new module from config.
  3. Start the new module container with the matching JWT secret.

Rotate a JWT secret remotely: Send POST /reload with the new secret in the body. The module must already exist in the config. Scripted rotation works without SSH access to edit config files. Unless TLS is enabled (see TLS), the request sends the new secret in cleartext over the network. A secret passed on a curl command line also lands in shell history and the process list; read it from a file or stdin instead.

Revoke a compromised module immediately: Send POST /revoke_jwt with the module ID. This removes the module from the signer's access list without touching the config. The next /reload will restore the module if it is still in the config, so remove it from config as well if the revocation should be permanent.

Footguns​

  • Body overrides are not persisted: if the signer crashes or restarts after a body-based secret rotation, it falls back to the config/environment values. The module container will still have the rotated secret and authentication will fail. To avoid this, update the environment variable to match after rotating via the body.
  • Override validation is strict: if the body references a module ID that does not exist in the config, the entire reload request is rejected and no changes are applied. This prevents typos from silently failing.

Notes​

  • The hot reload feature is available for the PBS service (both default and custom) and the Signer service.
  • Changes to listening hosts and ports are not applied, since they require a server restart.
  • If running in Docker containers, changes in volumes will not be applied, as it requires the container to be recreated. Be careful if changing a path to a local file as it may not be accessible from the container.
  • Custom PBS services may override the default behavior of the hot reload feature to parse extra configuration fields. Check the examples for more details.