Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e7f3020
v0.9.4-rc1 changelog notes (#437)
JasonVranek Mar 18, 2026
04ae8b7
address Dirk issues in docker_init.rs:
JasonVranek Mar 25, 2026
6ec3782
cleaner error message if TLS CryptoProvider fails
JasonVranek Mar 25, 2026
58b4b22
users get error message on missing [signer] section instead of toml d…
JasonVranek Mar 25, 2026
001b312
support partial jwt reloads
JasonVranek Mar 25, 2026
76905de
require the nonce in signer-api.yml - previously was marked optional …
JasonVranek Mar 25, 2026
8268572
fix suffix when displaying X-Forwaded-For
JasonVranek Mar 25, 2026
7c8cce6
only take the `jwt_auth_failures` writelock if strictly necessary, de…
JasonVranek Mar 25, 2026
4a9aff7
remove mark_jwt_failure() calls from failures unrelated to jwts
JasonVranek Mar 25, 2026
d834242
add round-trip sign->verify unit tests
JasonVranek Mar 26, 2026
2a1d315
add get_header auction winner log (#443)
ninaiiad Mar 26, 2026
a9a5c11
Merge remote-tracking branch 'upstream/main' into sigp-audit-fixes
JasonVranek Mar 26, 2026
dd87334
remove unutilized BuilderApi trait to simplify abstraction (keep pbs/…
JasonVranek Mar 24, 2026
eb90215
Removed BuilderApiState trait to complete simplifying abstraction and…
JasonVranek Mar 25, 2026
7e4648f
address Dirk issues in docker_init.rs:
JasonVranek Mar 25, 2026
84a0899
cleaner error message if TLS CryptoProvider fails
JasonVranek Mar 25, 2026
b3b3251
users get error message on missing [signer] section instead of toml d…
JasonVranek Mar 25, 2026
7cb8742
support partial jwt reloads
JasonVranek Mar 25, 2026
f0820e6
require the nonce in signer-api.yml - previously was marked optional …
JasonVranek Mar 25, 2026
e23c3ee
fix suffix when displaying X-Forwaded-For
JasonVranek Mar 25, 2026
170977b
only take the `jwt_auth_failures` writelock if strictly necessary, de…
JasonVranek Mar 25, 2026
b48e3fb
remove mark_jwt_failure() calls from failures unrelated to jwts
JasonVranek Mar 25, 2026
64f962f
add round-trip sign->verify unit tests
JasonVranek Mar 26, 2026
d71aff0
remove optional signer client from PbsModuleConfig as signer should o…
JasonVranek Mar 26, 2026
2d842f5
refactor get_header and submit_block into modules for readability
JasonVranek Mar 26, 2026
d1976c5
Merge remote-tracking branch 'upstream/sigp-audit-fixes' into remove-…
JasonVranek Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### v0.9.4-rc1
- Unifies the `pbs`, `signer`, and `cli` binaries into one: `commit-boost`. This change changes the CLI, notably the `init` command is now invoked as `commit-boost init --config <config_name>`.
- Includes new quality of life testing improvements in the Justfile: unit test coverage tooling, local Kurtosis testnet, and microbenchmark diffing.
- Robustifies the release process to ensure no compromised maintainer can unilaterally cut a release. Additionally all binaries are now signed during CI and can easily be verified before use.
18 changes: 0 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["benches/*", "bin", "crates/*", "examples/da_commit", "examples/status_api", "tests"]
members = ["benches/*", "bin", "crates/*", "examples/da_commit", "tests"]
resolver = "2"

[workspace.package]
Expand Down
15 changes: 10 additions & 5 deletions api/signer-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ paths:
application/json:
schema:
type: object
required: [pubkey, object_root]
required: [pubkey, object_root, nonce]
properties:
pubkey:
description: The 48-byte BLS public key, with optional `0x` prefix, of the proposer key that you want to request a signature from.
Expand Down Expand Up @@ -234,7 +234,7 @@ paths:
application/json:
schema:
type: object
required: [proxy, object_root]
required: [proxy, object_root, nonce]
properties:
proxy:
description: The 48-byte BLS public key (for `proxy_bls` mode) or the 20-byte Ethereum address (for `proxy_ecdsa` mode), with optional `0x` prefix, of the proxy key that you want to request a signature from.
Expand Down Expand Up @@ -382,7 +382,7 @@ paths:
application/json:
schema:
type: object
required: [proxy, object_root]
required: [proxy, object_root, nonce]
properties:
proxy:
description: The 20-byte Ethereum address, with optional `0x` prefix, of the proxy key that you want to request a signature from.
Expand Down Expand Up @@ -695,7 +695,12 @@ components:
$ref: "#/components/schemas/EcdsaSignature"
Nonce:
type: integer
description: If your module tracks nonces per signature (e.g., to prevent replay attacks), this is the unique nonce to use for the signature. It should be an unsigned 64-bit integer in big-endian format. It must be between 0 and 2^64-2, inclusive. If your module doesn't use nonces, we suggest setting this to 2^64-1 instead of 0 because 0 is a legal nonce and will cause complications with your module if you ever want to use a nonce in the future.
description: |
Replay-protection nonce, always mixed into the signing root via `PropCommitSigningInfo`. It
must be an unsigned 64-bit integer between 0 and 2^64-2 (18446744073709551614), inclusive.

Modules that track nonces for replay protection should use a monotonically increasing value
per key. Modules that do not use replay protection should always send `0`.
minimum: 0
maximum: 18446744073709551614 // 2^64-2
maximum: 18446744073709551614
example: 1
37 changes: 18 additions & 19 deletions benches/microbench/src/get_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,18 @@
//! - `HeaderMap` allocation (created once in setup, cloned cheaply per
//! iteration)

use std::{path::PathBuf, sync::Arc, time::Duration};
use std::{collections::HashSet, path::PathBuf, sync::Arc};

use alloy::primitives::B256;
use axum::http::HeaderMap;
use cb_common::{pbs::GetHeaderParams, signer::random_secret, types::Chain};
use cb_common::{pbs::GetHeaderParams, signer::random_secret, types::Chain, utils::EncodingType};
use cb_pbs::{PbsState, get_header};
use cb_tests::{
mock_relay::{MockRelayState, start_mock_relay_service},
utils::{generate_mock_relay, get_pbs_static_config, to_pbs_config},
mock_relay::{MockRelayState, start_mock_relay_service_with_listener},
utils::{generate_mock_relay, get_free_listener, get_pbs_config, to_pbs_config},
};
use criterion::{Criterion, black_box, criterion_group, criterion_main};

// Ports 19201–19205 are reserved for the microbenchmark mock relays.
const BASE_PORT: u16 = 19200;
const CHAIN: Chain = Chain::Hoodi;
const MAX_RELAYS: usize = 5;
const RELAY_COUNTS: [usize; 3] = [1, 3, MAX_RELAYS];
Expand Down Expand Up @@ -77,34 +75,34 @@ fn bench_get_header(c: &mut Criterion) {

// Start all mock relays once and build one PbsState per relay-count variant.
// All relays share the same MockRelayState (and therefore the same signing
// key).
// key). Each relay gets its own OS-assigned port via get_free_listener() so
// there is no TOCTOU race and no hardcoded port reservations.
let (states, params) = rt.block_on(async {
let signer = random_secret();
let pubkey = signer.public_key();
let mock_state = Arc::new(MockRelayState::new(CHAIN, signer));

let relay_clients: Vec<_> = (0..MAX_RELAYS)
.map(|i| {
let port = BASE_PORT + 1 + i as u16;
tokio::spawn(start_mock_relay_service(mock_state.clone(), port));
generate_mock_relay(port, pubkey.clone()).expect("relay client")
})
.collect();
let mut relay_clients = Vec::with_capacity(MAX_RELAYS);
for _ in 0..MAX_RELAYS {
let listener = get_free_listener().await;
let port = listener.local_addr().unwrap().port();
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), listener));
relay_clients.push(generate_mock_relay(port, pubkey.clone()).expect("relay client"));
}

// Give all servers time to bind before benchmarking starts.
tokio::time::sleep(Duration::from_millis(200)).await;
// Give all servers time to start accepting before benchmarking begins.
tokio::time::sleep(std::time::Duration::from_millis(200)).await;

let params = GetHeaderParams { slot: 0, parent_hash: B256::ZERO, pubkey };

// Port 0 here is the port the PBS service itself would bind to for incoming
// validator requests. We call get_header() as a function directly, so no
// PBS server is started and this port is never used. The actual relay
// endpoints are carried inside the RelayClient objects (ports 19201–19205).
// endpoints are carried inside the RelayClient objects.
let states: Vec<PbsState> = RELAY_COUNTS
.iter()
.map(|&n| {
let config =
to_pbs_config(CHAIN, get_pbs_static_config(0), relay_clients[..n].to_vec());
let config = to_pbs_config(CHAIN, get_pbs_config(0), relay_clients[..n].to_vec());
PbsState::new(config, PathBuf::new())
})
.collect();
Expand Down Expand Up @@ -138,6 +136,7 @@ fn bench_get_header(c: &mut Criterion) {
black_box(params.clone()),
black_box(headers.clone()),
black_box(state.clone()),
black_box(HashSet::from([EncodingType::Json, EncodingType::Ssz])),
))
.expect("get_header failed")
})
Expand Down
4 changes: 2 additions & 2 deletions bin/commit-boost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use cb_common::{
},
utils::{initialize_tracing_log, print_logo, wait_for_signal},
};
use cb_pbs::{DefaultBuilderApi, PbsService, PbsState};
use cb_pbs::{PbsService, PbsState};
use cb_signer::service::SigningService;
use clap::{Parser, Subcommand};
use eyre::Result;
Expand Down Expand Up @@ -68,7 +68,7 @@ async fn run_pbs_service() -> Result<()> {

PbsService::init_metrics(pbs_config.chain)?;
let state = PbsState::new(pbs_config, config_path);
let server = PbsService::run::<_, DefaultBuilderApi>(state);
let server = PbsService::run(state);

tokio::select! {
maybe_err = server => {
Expand Down
5 changes: 1 addition & 4 deletions bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ pub mod prelude {
utils::{initialize_tracing_log, utcnow_ms, utcnow_ns, utcnow_sec, utcnow_us},
};
pub use cb_metrics::provider::MetricsProvider;
pub use cb_pbs::{
BuilderApi, BuilderApiState, DefaultBuilderApi, PbsService, PbsState, PbsStateGuard,
get_header, get_status, register_validator, submit_block,
};
pub use cb_pbs::{PbsService, PbsState, PbsStateGuard};
// The TreeHash derive macro requires tree_hash as import
pub mod tree_hash {
pub use tree_hash::*;
Expand Down
5 changes: 1 addition & 4 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ chain = "Holesky"
# Docker image to use for the PBS module.
# OPTIONAL, DEFAULT: ghcr.io/commit-boost/pbs:latest
docker_image = "ghcr.io/commit-boost/pbs:latest"
# Whether to enable the PBS module to request signatures from the Signer module (not used in the default PBS image)
# OPTIONAL, DEFAULT: false
with_signer = false
# Host to receive BuilderAPI calls from beacon node
# OPTIONAL, DEFAULT: 127.0.0.1
host = "127.0.0.1"
Expand Down Expand Up @@ -174,7 +171,7 @@ timeout_get_header_ms = 900
id = "mux-relay-1"
url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e@def.xyz"

# Configuration for the Signer Module, only required if any `commit` module is present, or if `pbs.with_signer = true`
# Configuration for the Signer Module, only required if any `commit` module is present
# Currently three types of Signer modules are supported (only one can be used at a time):
# - Remote: a remote Web3Signer instance
# - Dirk: a remote Dirk instance
Expand Down
93 changes: 85 additions & 8 deletions crates/cli/src/docker_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ fn create_signer_service_dirk(
let mut envs = IndexMap::from([
get_env_val(CONFIG_ENV, CONFIG_DEFAULT),
get_env_same(JWTS_ENV),
get_env_same(ADMIN_JWT_ENV),
get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT),
get_env_val(DIRK_CERT_ENV, DIRK_CERT_DEFAULT),
get_env_val(DIRK_KEY_ENV, DIRK_KEY_DEFAULT),
get_env_val(DIRK_DIR_SECRETS_ENV, DIRK_DIR_SECRETS_DEFAULT),
Expand Down Expand Up @@ -548,6 +550,7 @@ fn create_signer_service_dirk(

// write jwts to env
service_config.envs.insert(JWTS_ENV.into(), format_comma_separated(&service_config.jwts));
service_config.envs.insert(ADMIN_JWT_ENV.into(), random_jwt_secret());

// CA cert volume and env
if let Some(ca_cert_path) = ca_cert_path {
Expand Down Expand Up @@ -589,8 +592,8 @@ fn create_signer_service_dirk(
environment: Environment::KvPair(envs),
healthcheck: Some(Healthcheck {
test: Some(HealthcheckTest::Single(format!(
"curl -f http://localhost:{}/status",
signer_config.port,
"curl -k -f {}/status",
cb_config.signer_server_url(SIGNER_PORT_DEFAULT),
))),
interval: Some("30s".into()),
timeout: Some("5s".into()),
Expand Down Expand Up @@ -932,6 +935,13 @@ mod tests {
service.volumes.iter().any(|v| matches!(v, Volumes::Simple(s) if s.contains(substr)))
}

fn get_healthcheck_cmd(service: &Service) -> Option<String> {
service.healthcheck.as_ref().and_then(|hc| match &hc.test {
Some(HealthcheckTest::Single(cmd)) => Some(cmd.clone()),
_ => None,
})
}

fn has_port(service: &Service, substr: &str) -> bool {
match &service.ports {
Ports::Short(ports) => ports.iter().any(|p| p.contains(substr)),
Expand Down Expand Up @@ -1309,12 +1319,33 @@ mod tests {
assert!(env_str(&service, DIRK_CERT_ENV).is_some());
assert!(env_str(&service, DIRK_KEY_ENV).is_some());
assert!(env_str(&service, DIRK_DIR_SECRETS_ENV).is_some());
assert!(has_env_key(&service, ADMIN_JWT_ENV));
assert!(has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV));
assert!(has_volume(&service, "client.crt"));
assert!(has_volume(&service, "client.key"));
assert!(has_volume(&service, "dirk_secrets"));
Ok(())
}

#[test]
fn test_create_signer_service_dirk_generates_admin_jwt() -> eyre::Result<()> {
let mut sc = minimal_service_config();
let signer_config = dirk_signer_config();
create_signer_service_dirk(
&mut sc,
&signer_config,
Path::new("/certs/client.crt"),
Path::new("/certs/client.key"),
Path::new("/dirk_secrets"),
&None,
&None,
)?;

let admin_jwt = sc.envs.get(ADMIN_JWT_ENV).expect("ADMIN_JWT_ENV must be set");
assert!(!admin_jwt.is_empty(), "admin JWT secret must not be empty");
Ok(())
}

#[test]
fn test_create_signer_service_dirk_with_ca_cert() -> eyre::Result<()> {
let mut sc = minimal_service_config();
Expand Down Expand Up @@ -1489,11 +1520,10 @@ mod tests {
config
}

/// Returns a `ServiceCreationInfo` whose CB config has `pbs.with_signer =
/// true` and a local signer with `TlsMode::Certificate(certs_path)`.
/// Returns a `ServiceCreationInfo` whose CB config has a local signer with
/// `TlsMode::Certificate(certs_path)`.
fn service_config_with_tls(certs_path: PathBuf) -> ServiceCreationInfo {
let mut sc = minimal_service_config();
sc.config_info.cb_config.pbs.with_signer = true;
sc.config_info.cb_config.signer = Some(local_signer_config_with_tls(certs_path));
sc
}
Expand Down Expand Up @@ -1589,12 +1619,15 @@ mod tests {
// -------------------------------------------------------------------------

#[test]
fn test_create_pbs_service_with_tls_adds_cert_env_and_volume() -> eyre::Result<()> {
fn test_create_pbs_service_with_tls_but_no_commit_module_no_cert() -> eyre::Result<()> {
// PBS no longer connects to the signer directly; only commit modules do.
// Even when the signer is configured with TLS, the cert env/volume must
// NOT be injected into the PBS container unless a Commit module is present.
let mut sc = service_config_with_tls(PathBuf::from("/my/certs"));
let service = create_pbs_service(&mut sc)?;

assert!(has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV));
assert!(has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME));
assert!(!has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV));
assert!(!has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME));
Ok(())
}

Expand Down Expand Up @@ -1690,6 +1723,50 @@ mod tests {
Ok(())
}

#[test]
fn test_create_signer_service_dirk_healthcheck_uses_https_with_tls() -> eyre::Result<()> {
let dir = tempfile::tempdir()?;
let certs_path = dir.path().to_path_buf();
std::fs::write(certs_path.join(SIGNER_TLS_CERTIFICATE_NAME), b"cert")?;
std::fs::write(certs_path.join(SIGNER_TLS_KEY_NAME), b"key")?;

let mut sc = service_config_with_tls(certs_path);
let signer_config = dirk_signer_config();
let service = create_signer_service_dirk(
&mut sc,
&signer_config,
Path::new("/certs/client.crt"),
Path::new("/certs/client.key"),
Path::new("/dirk_secrets"),
&None,
&None,
)?;

let cmd = get_healthcheck_cmd(&service).expect("healthcheck must be set");
assert!(cmd.contains("https://"), "healthcheck must use https with TLS: {cmd}");
assert!(cmd.contains("-k"), "healthcheck must use -k flag for self-signed certs: {cmd}");
Ok(())
}

#[test]
fn test_create_signer_service_dirk_healthcheck_uses_http_without_tls() -> eyre::Result<()> {
let mut sc = minimal_service_config();
let signer_config = dirk_signer_config();
let service = create_signer_service_dirk(
&mut sc,
&signer_config,
Path::new("/certs/client.crt"),
Path::new("/certs/client.key"),
Path::new("/dirk_secrets"),
&None,
&None,
)?;

let cmd = get_healthcheck_cmd(&service).expect("healthcheck must be set");
assert!(cmd.contains("http://"), "healthcheck must use http without TLS: {cmd}");
Ok(())
}

// -------------------------------------------------------------------------
// create_module_service – TLS cert env/volume
// -------------------------------------------------------------------------
Expand Down
Loading
Loading