From 5935659e615b4590378ad8343e7f43f37b972b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Sun, 27 Jul 2025 18:13:51 -0300 Subject: [PATCH 01/30] CBST2-04: Update JWT secrets on reload and revoke module endpoint (#295) --- Cargo.lock | 1 + crates/cli/src/docker_init.rs | 22 +++--- crates/common/src/commit/constants.rs | 1 + crates/common/src/commit/request.rs | 38 ++++++++-- crates/common/src/config/constants.rs | 1 + crates/common/src/config/signer.rs | 5 +- crates/common/src/config/utils.rs | 9 +-- crates/common/src/types.rs | 6 ++ crates/common/src/utils.rs | 20 +++++- crates/signer/src/error.rs | 4 ++ crates/signer/src/service.rs | 72 ++++++++++++++++--- docs/docs/get_started/configuration.md | 9 +++ docs/docs/get_started/running/binary.md | 1 + tests/Cargo.toml | 1 + tests/src/utils.rs | 2 + tests/tests/signer_jwt_auth.rs | 92 ++++++++++++++++++++++++- 16 files changed, 250 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63de92dd..7eed3c0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1596,6 +1596,7 @@ dependencies = [ "cb-pbs", "cb-signer", "eyre", + "jsonwebtoken", "reqwest", "serde_json", "tempfile", diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 7f418e97..84473a6d 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,16 +6,16 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, BUILDER_PORT_ENV, - BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, - DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, - DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, - LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, - PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, - PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, - SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, - SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, - SIGNER_URL_ENV, + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, ADMIN_JWT_ENV, + BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, + DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, + DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, + LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, + PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, + PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, + PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, + SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, + SIGNER_MODULE_NAME, SIGNER_URL_ENV, }, pbs::{BUILDER_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader, DEFAULT_SIGNER_PORT}, @@ -333,6 +333,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut signer_envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), + get_env_same(ADMIN_JWT_ENV), ]); // Bind the signer API to 0.0.0.0 @@ -366,6 +367,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // write jwts to env envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts)); + envs.insert(ADMIN_JWT_ENV.into(), random_jwt_secret()); // volumes let mut volumes = vec![config_volume.clone()]; diff --git a/crates/common/src/commit/constants.rs b/crates/common/src/commit/constants.rs index 7c9f948c..ea9cd9bb 100644 --- a/crates/common/src/commit/constants.rs +++ b/crates/common/src/commit/constants.rs @@ -3,3 +3,4 @@ pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature"; pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key"; pub const STATUS_PATH: &str = "/status"; pub const RELOAD_PATH: &str = "/reload"; +pub const REVOKE_MODULE_PATH: &str = "/revoke_jwt"; diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index b8843234..9a67dcc2 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fmt::{self, Debug, Display}, str::FromStr, }; @@ -9,13 +10,17 @@ use alloy::{ rpc::types::beacon::BlsSignature, }; use derive_more::derive::From; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - constants::COMMIT_BOOST_DOMAIN, error::BlstErrorWrapper, signature::verify_signed_message, - signer::BlsPublicKey, types::Chain, + config::decode_string_to_map, + constants::COMMIT_BOOST_DOMAIN, + error::BlstErrorWrapper, + signature::verify_signed_message, + signer::BlsPublicKey, + types::{Chain, ModuleId}, }; pub trait ProxyId: AsRef<[u8]> + Debug + Clone + Copy + TreeHash + Display {} @@ -198,6 +203,31 @@ pub struct GetPubkeysResponse { pub keys: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReloadRequest { + #[serde(default, deserialize_with = "deserialize_jwt_secrets")] + pub jwt_secrets: Option>, + pub admin_secret: Option, +} + +pub fn deserialize_jwt_secrets<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let raw: String = Deserialize::deserialize(deserializer)?; + + decode_string_to_map(&raw) + .map(Some) + .map_err(|_| serde::de::Error::custom("Invalid format".to_string())) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RevokeModuleRequest { + pub module_id: ModuleId, +} + /// Map of consensus pubkeys to proxies #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ConsensusProxyMap { @@ -288,7 +318,7 @@ mod tests { let _: SignedProxyDelegationBls = serde_json::from_str(data).unwrap(); - let data = r#"{ + let data = r#"{ "message": { "delegator": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050", "proxy": "0x4ca9939a8311a7cab3dde201b70157285fa81a9d" diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 8b07f732..743cdbe9 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -42,6 +42,7 @@ pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV: &str = /// Comma separated list module_id=jwt_secret pub const JWTS_ENV: &str = "CB_JWTS"; +pub const ADMIN_JWT_ENV: &str = "CB_SIGNER_ADMIN_JWT"; /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 7e5fbd58..d0adcdf4 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -133,6 +133,7 @@ pub struct StartSignerConfig { pub store: Option, pub endpoint: SocketAddr, pub jwts: HashMap, + pub admin_secret: String, pub jwt_auth_fail_limit: u32, pub jwt_auth_fail_timeout_seconds: u32, pub dirk: Option, @@ -142,7 +143,7 @@ impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let jwts = load_jwt_secrets()?; + let (admin_secret, jwts) = load_jwt_secrets()?; let signer_config = config.signer.ok_or_eyre("Signer config is missing")?; @@ -177,6 +178,7 @@ impl StartSignerConfig { loader: Some(loader), endpoint, jwts, + admin_secret, jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds, store, @@ -207,6 +209,7 @@ impl StartSignerConfig { chain: config.chain, endpoint, jwts, + admin_secret, jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds, loader: None, diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index 13784316..7ab346f1 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -4,7 +4,7 @@ use alloy::rpc::types::beacon::BlsPublicKey; use eyre::{bail, Context, Result}; use serde::de::DeserializeOwned; -use super::JWTS_ENV; +use super::{ADMIN_JWT_ENV, JWTS_ENV}; use crate::{config::MUXER_HTTP_MAX_LENGTH, types::ModuleId, utils::read_chunked_body_with_max}; pub fn load_env_var(env: &str) -> Result { @@ -26,9 +26,10 @@ pub fn load_file_from_env(env: &str) -> Result { } /// Loads a map of module id -> jwt secret from a json env -pub fn load_jwt_secrets() -> Result> { +pub fn load_jwt_secrets() -> Result<(String, HashMap)> { + let admin_jwt = std::env::var(ADMIN_JWT_ENV).wrap_err(format!("{ADMIN_JWT_ENV} is not set"))?; let jwt_secrets = std::env::var(JWTS_ENV).wrap_err(format!("{JWTS_ENV} is not set"))?; - decode_string_to_map(&jwt_secrets) + decode_string_to_map(&jwt_secrets).map(|secrets| (admin_jwt, secrets)) } /// Reads an HTTP response safely, erroring out if it failed or if the body is @@ -71,7 +72,7 @@ pub fn remove_duplicate_keys(keys: Vec) -> Vec { unique_keys } -fn decode_string_to_map(raw: &str) -> Result> { +pub fn decode_string_to_map(raw: &str) -> Result> { // trim the string and split for comma raw.trim() .split(',') diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 5293a789..3d07e89c 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -23,6 +23,12 @@ pub struct JwtClaims { pub module: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtAdmin { + pub exp: u64, + pub admin: bool, +} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum Chain { Mainnet, diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index ccaf8888..7f2fbbca 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -30,7 +30,7 @@ use crate::{ config::LogsSettings, constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, - types::{Chain, Jwt, JwtClaims, ModuleId}, + types::{Chain, Jwt, JwtAdmin, JwtClaims, ModuleId}, }; const MILLIS_PER_SECOND: u64 = 1_000; @@ -405,6 +405,24 @@ pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { .map_err(From::from) } +/// Validate an admin JWT with the given secret +pub fn validate_admin_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { + let mut validation = jsonwebtoken::Validation::default(); + validation.leeway = 10; + + let token = jsonwebtoken::decode::( + jwt.as_str(), + &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), + &validation, + )?; + + if token.claims.admin { + Ok(()) + } else { + eyre::bail!("Token is not admin") + } +} + /// Generates a random string pub fn random_jwt_secret() -> String { rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect() diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index a2a113f3..b0fc88de 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -25,6 +25,9 @@ pub enum SignerModuleError { #[error("Dirk signer does not support this operation")] DirkNotSupported, + #[error("module id not found")] + ModuleIdNotFound, + #[error("internal error: {0}")] Internal(String), @@ -48,6 +51,7 @@ impl IntoResponse for SignerModuleError { (StatusCode::INTERNAL_SERVER_ERROR, "internal error".to_string()) } SignerModuleError::SignerError(err) => (StatusCode::BAD_REQUEST, err.to_string()), + SignerModuleError::ModuleIdNotFound => (StatusCode::NOT_FOUND, self.to_string()), SignerModuleError::RateLimited(duration) => { (StatusCode::TOO_MANY_REQUESTS, format!("rate limited for {duration:?}")) } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 1a41a008..59da3c3d 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -18,17 +18,17 @@ use cb_common::{ commit::{ constants::{ GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, RELOAD_PATH, REQUEST_SIGNATURE_PATH, - STATUS_PATH, + REVOKE_MODULE_PATH, STATUS_PATH, }, request::{ - EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, SignConsensusRequest, - SignProxyRequest, SignRequest, + EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, ReloadRequest, + RevokeModuleRequest, SignConsensusRequest, SignProxyRequest, SignRequest, }, }, config::StartSignerConfig, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{Chain, Jwt, ModuleId}, - utils::{decode_jwt, validate_jwt}, + utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; use eyre::Context; @@ -63,7 +63,9 @@ struct SigningState { /// Map of modules ids to JWT secrets. This also acts as registry of all /// modules running - jwts: Arc>, + jwts: Arc>>, + /// Secret for the admin JWT + admin_secret: Arc>, /// Map of JWT failures per peer jwt_auth_failures: Arc>>, @@ -84,7 +86,8 @@ impl SigningService { let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - jwts: config.jwts.into(), + jwts: Arc::new(ParkingRwLock::new(config.jwts)), + admin_secret: Arc::new(ParkingRwLock::new(config.admin_secret)), jwt_auth_failures: Arc::new(ParkingRwLock::new(HashMap::new())), jwt_auth_fail_limit: config.jwt_auth_fail_limit, jwt_auth_fail_timeout: Duration::from_secs(config.jwt_auth_fail_timeout_seconds as u64), @@ -113,20 +116,30 @@ impl SigningService { SigningService::init_metrics(config.chain)?; - let app = axum::Router::new() + let signer_app = axum::Router::new() .route(REQUEST_SIGNATURE_PATH, post(handle_request_signature)) .route(GET_PUBKEYS_PATH, get(handle_get_pubkeys)) .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) + .with_state(state.clone()) + .route_layer(middleware::from_fn(log_request)); + + let admin_app = axum::Router::new() .route(RELOAD_PATH, post(handle_reload)) + .route(REVOKE_MODULE_PATH, post(handle_revoke_module)) + .route_layer(middleware::from_fn_with_state(state.clone(), admin_auth)) .with_state(state.clone()) .route_layer(middleware::from_fn(log_request)) - .route(STATUS_PATH, get(handle_status)) - .into_make_service_with_connect_info::(); + .route(STATUS_PATH, get(handle_status)); let listener = TcpListener::bind(config.endpoint).await?; - axum::serve(listener, app).await.wrap_err("signer server exited") + axum::serve( + listener, + signer_app.merge(admin_app).into_make_service_with_connect_info::(), + ) + .await + .wrap_err("signer server exited") } fn init_metrics(network: Chain) -> eyre::Result<()> { @@ -214,7 +227,8 @@ fn check_jwt_auth( SignerModuleError::Unauthorized })?; - let jwt_secret = state.jwts.get(&module_id).ok_or_else(|| { + let guard = state.jwts.read(); + let jwt_secret = guard.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; @@ -226,6 +240,22 @@ fn check_jwt_auth( Ok(module_id) } +async fn admin_auth( + State(state): State, + TypedHeader(auth): TypedHeader>, + req: Request, + next: Next, +) -> Result { + let jwt: Jwt = auth.token().to_string().into(); + + validate_admin_jwt(jwt, &state.admin_secret.read()).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; + + Ok(next.run(req).await) +} + /// Requests logging middleware layer async fn log_request(req: Request, next: Next) -> Result { let url = &req.uri().clone(); @@ -360,6 +390,7 @@ async fn handle_generate_proxy( async fn handle_reload( State(mut state): State, + Json(request): Json, ) -> Result { let req_id = Uuid::new_v4(); @@ -373,6 +404,14 @@ async fn handle_reload( } }; + if let Some(jwt_secrets) = request.jwt_secrets { + *state.jwts.write() = jwt_secrets; + } + + if let Some(admin_secret) = request.admin_secret { + *state.admin_secret.write() = admin_secret; + } + let new_manager = match start_manager(config).await { Ok(manager) => manager, Err(err) => { @@ -386,6 +425,17 @@ async fn handle_reload( Ok(StatusCode::OK) } +async fn handle_revoke_module( + State(state): State, + Json(request): Json, +) -> Result { + let mut guard = state.jwts.write(); + guard + .remove(&request.module_id) + .ok_or(SignerModuleError::ModuleIdNotFound) + .map(|_| StatusCode::OK) +} + async fn start_manager(config: StartSignerConfig) -> eyre::Result { let proxy_store = if let Some(store) = config.store.clone() { Some(store.init_from_env()?) diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 5dd46329..b65e73ad 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -398,6 +398,15 @@ Commit-Boost supports hot-reloading the configuration file. This means that you docker compose -f cb.docker-compose.yml exec cb_signer curl -X POST http://localhost:20000/reload ``` +### Signer module reload + +The signer module takes 2 optional parameters in the JSON body: + +- `jwt_secrets`: a string with a comma-separated list of `=` for all modules. +- `admin_secret`: a string with the secret for the signer admin JWT. + +Parameters that are not provided will not be updated; they will be regenerated using their original on-disk data as though the signer service was being restarted. Note that any changes you made with calls to `/revoke_jwt` or `/reload` will be reverted, so make sure you provide any modifications again as part of this call. + ### Notes - The hot reload feature is available for PBS modules (both default and custom) and signer module. diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 385e7a0c..97991ee5 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -26,6 +26,7 @@ Modules need some environment variables to work correctly. - `CB_MUX_PATH_{ID}`: optional, override where to load mux validator keys for mux with `id=\{ID\}`. ### Signer Module +- `CB_SIGNER_ADMIN_JWT`: secret to use for admin JWT. - `CB_SIGNER_ENDPOINT`: optional, override to specify the `IP:port` endpoint to bind the signer server to. - For loading keys we currently support: - `CB_SIGNER_LOADER_FILE`: path to a `.json` with plaintext keys (for testing purposes only). diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f1b5c9d9..573cfa20 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -11,6 +11,7 @@ cb-common.workspace = true cb-pbs.workspace = true cb-signer.workspace = true eyre.workspace = true +jsonwebtoken.workspace = true reqwest.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/tests/src/utils.rs b/tests/src/utils.rs index b677d800..04aa371a 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -118,6 +118,7 @@ pub fn get_start_signer_config( signer_config: SignerConfig, chain: Chain, jwts: HashMap, + admin_secret: String, ) -> StartSignerConfig { match signer_config.inner { SignerType::Local { loader, .. } => StartSignerConfig { @@ -126,6 +127,7 @@ pub fn get_start_signer_config( store: None, endpoint: SocketAddr::new(signer_config.host.into(), signer_config.port), jwts, + admin_secret, jwt_auth_fail_limit: signer_config.jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds: signer_config.jwt_auth_fail_timeout_seconds, dirk: None, diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index 90a0365f..820afbcc 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -2,10 +2,14 @@ use std::{collections::HashMap, time::Duration}; use alloy::{hex, primitives::FixedBytes}; use cb_common::{ - commit::{constants::GET_PUBKEYS_PATH, request::GetPubkeysResponse}, + commit::{ + constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, + request::GetPubkeysResponse, + }, config::StartSignerConfig, + constants::SIGNER_JWT_EXPIRATION, signer::{SignerLoader, ValidatorKeysFormat}, - types::{Chain, ModuleId}, + types::{Chain, Jwt, JwtAdmin, ModuleId}, utils::create_jwt, }; use cb_signer::service::SigningService; @@ -16,6 +20,7 @@ use tracing::info; const JWT_MODULE: &str = "test-module"; const JWT_SECRET: &str = "test-jwt-secret"; +const ADMIN_SECRET: &str = "test-admin-secret"; #[tokio::test] async fn test_signer_jwt_auth_success() -> Result<()> { @@ -86,6 +91,74 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_signer_revoked_jwt_fail() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(JWT_MODULE.to_string()); + let start_config = start_server(20400).await?; + + // Run as many pubkeys requests as the fail limit + let jwt = create_jwt(&module_id, JWT_SECRET)?; + let admin_jwt = create_admin_jwt()?; + let client = reqwest::Client::new(); + + // At first, test module should be allowed to request pubkeys + let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); + let response = client.get(&url).bearer_auth(&jwt).send().await?; + assert!(response.status() == StatusCode::OK); + + let revoke_url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); + let response = client + .post(&revoke_url) + .header("content-type", "application/json") + .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) + .bearer_auth(&admin_jwt) + .send() + .await?; + assert!(response.status() == StatusCode::OK); + + // After revoke, test module shouldn't be allowed anymore + let response = client.get(&url).bearer_auth(&jwt).send().await?; + assert!(response.status() == StatusCode::UNAUTHORIZED); + + Ok(()) +} + +#[tokio::test] +async fn test_signer_only_admin_can_revoke() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(JWT_MODULE.to_string()); + let start_config = start_server(20500).await?; + + // Run as many pubkeys requests as the fail limit + let jwt = create_jwt(&module_id, JWT_SECRET)?; + let admin_jwt = create_admin_jwt()?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); + + // Module JWT shouldn't be able to revoke modules + let response = client + .post(&url) + .header("content-type", "application/json") + .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) + .bearer_auth(&jwt) + .send() + .await?; + assert!(response.status() == StatusCode::UNAUTHORIZED); + + // Admin should be able to revoke modules + let response = client + .post(&url) + .header("content-type", "application/json") + .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) + .bearer_auth(&admin_jwt) + .send() + .await?; + assert!(response.status() == StatusCode::OK); + + Ok(()) +} + // Starts the signer moduler server on a separate task and returns its // configuration async fn start_server(port: u16) -> Result { @@ -107,7 +180,7 @@ async fn start_server(port: u16) -> Result { config.port = port; config.jwt_auth_fail_limit = 3; // Set a low fail limit for testing config.jwt_auth_fail_timeout_seconds = 3; // Set a short timeout for testing - let start_config = get_start_signer_config(config, chain, jwts); + let start_config = get_start_signer_config(config, chain, jwts, ADMIN_SECRET.to_string()); // Run the Signer let server_handle = tokio::spawn(SigningService::run(start_config.clone())); @@ -144,3 +217,16 @@ async fn verify_pubkeys(response: Response) -> Result<()> { } Ok(()) } + +fn create_admin_jwt() -> Result { + jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &JwtAdmin { + admin: true, + exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + }, + &jsonwebtoken::EncodingKey::from_secret(ADMIN_SECRET.as_ref()), + ) + .map_err(Into::into) + .map(Jwt::from) +} From 498eed9bb120c13eacf78adc803ffe6da090b92e Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 19 Aug 2025 01:00:11 -0400 Subject: [PATCH 02/30] CBST2-02: Make proposer commitment signatures unique to modules (#329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: eltitanb Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao --- api/signer-api.yml | 83 +++- bin/src/lib.rs | 3 + config.example.toml | 6 +- crates/common/src/commit/request.rs | 5 +- crates/common/src/config/module.rs | 3 + crates/common/src/config/signer.rs | 396 +++++++++++++++++++- crates/common/src/config/utils.rs | 8 +- crates/common/src/pbs/types/get_header.rs | 5 +- crates/common/src/signature.rs | 152 +++++--- crates/common/src/signer/schemes/bls.rs | 26 +- crates/common/src/signer/schemes/ecdsa.rs | 65 +++- crates/common/src/signer/store.rs | 6 +- crates/common/src/types.rs | 45 ++- crates/pbs/src/mev_boost/get_header.rs | 5 +- crates/signer/src/error.rs | 6 + crates/signer/src/manager/dirk.rs | 54 ++- crates/signer/src/manager/local.rs | 101 ++++- crates/signer/src/service.rs | 82 ++-- docs/docs/developing/prop-commit-signing.md | 60 +++ docs/docs/get_started/configuration.md | 5 +- docs/docs/res/img/prop_commit_tree.png | Bin 0 -> 57442 bytes examples/da_commit/src/main.rs | 44 ++- tests/data/configs/signer.happy.toml | 52 +++ tests/src/lib.rs | 1 + tests/src/mock_relay.rs | 4 +- tests/src/signer_service.rs | 87 +++++ tests/src/utils.rs | 47 ++- tests/tests/pbs_get_header.rs | 10 +- tests/tests/pbs_get_status.rs | 6 +- tests/tests/pbs_mux.rs | 4 +- tests/tests/pbs_post_blinded_blocks.rs | 6 +- tests/tests/pbs_post_validators.rs | 10 +- tests/tests/signer_jwt_auth.rs | 134 ++----- tests/tests/signer_request_sig.rs | 113 ++++++ 34 files changed, 1337 insertions(+), 297 deletions(-) create mode 100644 docs/docs/developing/prop-commit-signing.md create mode 100644 docs/docs/res/img/prop_commit_tree.png create mode 100644 tests/data/configs/signer.happy.toml create mode 100644 tests/src/signer_service.rs create mode 100644 tests/tests/signer_request_sig.rs diff --git a/api/signer-api.yml b/api/signer-api.yml index c876a3a2..69239e38 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -60,7 +60,7 @@ paths: /signer/v1/request_signature: post: - summary: Send a signature request + summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the requested BLS or ECDSA key. tags: - Signer security: @@ -81,15 +81,15 @@ paths: type: string enum: [consensus, proxy_bls, proxy_ecdsa] pubkey: - description: Public key of the validator for consensus signatures + description: The 48-byte BLS public key, with optional `0x` prefix, of the proposer key that you want to request a signature from. $ref: "#/components/schemas/BlsPubkey" proxy: - description: BLS proxy pubkey or ECDSA address for proxy signatures + 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. oneOf: - $ref: "#/components/schemas/BlsPubkey" - $ref: "#/components/schemas/EcdsaAddress" object_root: - description: The root of the object to be signed + description: The 32-byte data you want to sign, with optional `0x` prefix. type: string format: hex pattern: "^0x[a-fA-F0-9]{64}$" @@ -112,7 +112,7 @@ paths: object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" responses: "200": - description: Success + description: A successful signature response. The returned signature is the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID as specified in the Commit-Boost configuration. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). content: application/json: schema: @@ -126,8 +126,45 @@ paths: value: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" ProxyEcdsa: value: "0x985b495f49d1b96db3bba3f6c5dd1810950317c10d4c2042bd316f338cdbe74359072e209b85e56ac492092d7860063dd096ca31b4e164ef27e3f8d508e656801c" + "400": + description: | + This can occur in several scenarios: + + - You requested an operation while using the Dirk signer mode instead of locally-managed signer mode, but Dirk doesn't support that operation. + - Something went wrong while preparing your request; the error text will provide more information. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 400 + message: + type: string + example: "Bad request: Invalid pubkey format" + "401": + description: The requesting module did not provide a JWT string in the request's authorization header, or the JWT string was not configured in the signer service's configuration file as belonging to the module. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 401 + message: + type: string + example: "Unauthorized" + "404": - description: Unknown value (pubkey, etc.) + description: You either requested a route that doesn't exist, or you requested a signature from a key that does not exist. content: application/json: schema: @@ -142,8 +179,24 @@ paths: message: type: string example: "Unknown pubkey" + "429": + description: Your module attempted and failed JWT authentication too many times recently, and is currently timed out. It cannot make any more requests until the timeout ends. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 429 + message: + type: string + example: "Too many requests" "500": - description: Internal error + description: Your request was valid, but something went wrong internally that prevented it from being fulfilled. content: application/json: schema: @@ -158,6 +211,22 @@ paths: message: type: string example: "Internal error" + "502": + description: The signer service is running in Dirk signer mode, but Dirk could not be reached. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 502 + message: + type: string + example: "Bad gateway: Dirk signer service is unreachable" /signer/v1/generate_proxy_key: post: diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 126847b6..122a35fc 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -10,6 +10,9 @@ pub mod prelude { load_pbs_custom_config, LogsSettings, StartCommitModuleConfig, PBS_MODULE_NAME, }, pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent}, + signature::{ + verify_proposer_commitment_signature_bls, verify_proposer_commitment_signature_ecdsa, + }, signer::{BlsPublicKey, BlsSignature, EcdsaSignature}, types::Chain, utils::{initialize_tracing_log, utcnow_ms, utcnow_ns, utcnow_sec, utcnow_us}, diff --git a/config.example.toml b/config.example.toml index 8ed5b139..f4612081 100644 --- a/config.example.toml +++ b/config.example.toml @@ -152,10 +152,10 @@ url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09f # - Dirk: a remote Dirk instance # - Local: a local Signer module # More details on the docs (https://commit-boost.github.io/commit-boost-client/get_started/configuration/#signer-module) -# [signer] +[signer] # Docker image to use for the Signer module. # OPTIONAL, DEFAULT: ghcr.io/commit-boost/signer:latest -# docker_image = "ghcr.io/commit-boost/signer:latest" +docker_image = "ghcr.io/commit-boost/signer:latest" # Host to bind the Signer API server to # OPTIONAL, DEFAULT: 127.0.0.1 host = "127.0.0.1" @@ -249,6 +249,8 @@ proxy_dir = "./proxies" [[modules]] # Unique ID of the module id = "DA_COMMIT" +# Unique hash that the Signer service will combine with the incoming data in signing requests to generate a signature specific to this module +signing_id = "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" # Type of the module. Supported values: commit, events type = "commit" # Docker image of the module diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 9a67dcc2..5bc3a14b 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -6,7 +6,7 @@ use std::{ use alloy::{ hex, - primitives::{Address, B256}, + primitives::{aliases::B32, Address, B256}, rpc::types::beacon::BlsSignature, }; use derive_more::derive::From; @@ -62,7 +62,8 @@ impl SignedProxyDelegation { &self.message.delegator, &self.message, &self.signature, - COMMIT_BOOST_DOMAIN, + None, + &B32::from(COMMIT_BOOST_DOMAIN), ) } } diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 16b089ca..71c4891b 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use alloy::primitives::B256; use eyre::{ContextCompat, Result}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use toml::Table; @@ -37,6 +38,8 @@ pub struct StaticModuleConfig { /// Type of the module #[serde(rename = "type")] pub kind: ModuleKind, + /// Signing ID for the module to use when requesting signatures + pub signing_id: B256, } /// Runtime config to start a module diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 9e5f2b46..a397d696 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -4,25 +4,59 @@ use std::{ path::PathBuf, }; +use alloy::primitives::B256; use docker_image::DockerImage; -use eyre::{bail, ensure, OptionExt, Result}; +use eyre::{bail, ensure, Context, OptionExt, Result}; use serde::{Deserialize, Serialize}; use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - load_jwt_secrets, load_optional_env_var, utils::load_env_var, CommitBoostConfig, - SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, - SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, - SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV, SIGNER_PORT_DEFAULT, + load_optional_env_var, utils::load_env_var, CommitBoostConfig, SIGNER_ENDPOINT_ENV, + SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, + SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV, + SIGNER_PORT_DEFAULT, }; use crate::{ - config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV}, + config::{ + load_jwt_secrets, DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV, + }, signer::{ProxyStore, SignerLoader}, types::{Chain, ModuleId}, utils::{default_host, default_u16, default_u32}, }; +/// The signing configuration for a commitment module. +#[derive(Clone, Debug, PartialEq)] +pub struct ModuleSigningConfig { + /// Human-readable name of the module. + pub module_name: ModuleId, + + /// The JWT secret for the module to communicate with the signer module. + pub jwt_secret: String, + + /// A unique identifier for the module, which is used when signing requests + /// to generate signatures for this module. Must be a 32-byte hex string. + /// A leading 0x prefix is optional. + pub signing_id: B256, +} + +impl ModuleSigningConfig { + pub fn validate(&self) -> Result<()> { + // Ensure the JWT secret is not empty + if self.jwt_secret.is_empty() { + bail!("JWT secret cannot be empty"); + } + + // Ensure the signing ID is a valid B256 + if self.signing_id.is_zero() { + bail!("Signing ID cannot be zero"); + } + + Ok(()) + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct SignerConfig { @@ -130,7 +164,7 @@ pub struct StartSignerConfig { pub loader: Option, pub store: Option, pub endpoint: SocketAddr, - pub jwts: HashMap, + pub mod_signing_configs: HashMap, pub admin_secret: String, pub jwt_auth_fail_limit: u32, pub jwt_auth_fail_timeout_seconds: u32, @@ -141,7 +175,11 @@ impl StartSignerConfig { pub fn load_from_env() -> Result { let config = CommitBoostConfig::from_env_path()?; - let (admin_secret, jwts) = load_jwt_secrets()?; + let (admin_secret, jwt_secrets) = load_jwt_secrets()?; + + // Load the module signing configs + let mod_signing_configs = load_module_signing_configs(&config, &jwt_secrets) + .wrap_err("Failed to load module signing configs")?; let signer_config = config.signer.ok_or_eyre("Signer config is missing")?; @@ -175,7 +213,7 @@ impl StartSignerConfig { chain: config.chain, loader: Some(loader), endpoint, - jwts, + mod_signing_configs, admin_secret, jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds, @@ -206,7 +244,7 @@ impl StartSignerConfig { Ok(StartSignerConfig { chain: config.chain, endpoint, - jwts, + mod_signing_configs, admin_secret, jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds, @@ -235,3 +273,341 @@ impl StartSignerConfig { } } } + +/// Loads the signing configurations for each module defined in the Commit Boost +/// config, coupling them with their JWT secrets and handling any potential +/// duplicates or missing values. +pub fn load_module_signing_configs( + config: &CommitBoostConfig, + jwt_secrets: &HashMap, +) -> Result> { + let mut mod_signing_configs = HashMap::new(); + let modules = config.modules.as_ref().ok_or_eyre("No modules defined in the config")?; + + let mut seen_jwt_secrets = HashMap::new(); + let mut seen_signing_ids = HashMap::new(); + for module in modules { + // Validate the module ID + ensure!(!module.id.is_empty(), "Module ID cannot be empty"); + + // Make sure it hasn't been used yet + ensure!( + !mod_signing_configs.contains_key(&module.id), + "Duplicate module config detected: ID {} is already used", + module.id + ); + + // Make sure the JWT secret is present + let jwt_secret = match jwt_secrets.get(&module.id) { + Some(secret) => secret.clone(), + None => bail!("JWT secret for module {} is missing", module.id), + }; + // Create the module signing config and validate it + let module_signing_config = ModuleSigningConfig { + module_name: module.id.clone(), + jwt_secret, + signing_id: module.signing_id, + }; + module_signing_config + .validate() + .wrap_err(format!("Invalid signing config for module {}", module.id))?; + + // Check for duplicates in JWT secrets and signing IDs + if let Some(existing_module) = + seen_jwt_secrets.insert(module_signing_config.jwt_secret.clone(), &module.id) + { + bail!("Duplicate JWT secret detected for modules {} and {}", existing_module, module.id) + }; + if let Some(existing_module) = + seen_signing_ids.insert(module_signing_config.signing_id, &module.id) + { + bail!("Duplicate signing ID detected for modules {} and {}", existing_module, module.id) + }; + + mod_signing_configs.insert(module.id.clone(), module_signing_config); + } + + Ok(mod_signing_configs) +} + +#[cfg(test)] +mod tests { + use alloy::primitives::{b256, Uint}; + + use super::*; + use crate::config::{LogsSettings, ModuleKind, PbsConfig, StaticModuleConfig, StaticPbsConfig}; + + async fn get_base_config() -> CommitBoostConfig { + CommitBoostConfig { + chain: Chain::Hoodi, + relays: vec![], + pbs: StaticPbsConfig { + docker_image: String::from(""), + pbs_config: PbsConfig { + host: Ipv4Addr::new(127, 0, 0, 1), + port: 0, + relay_check: false, + wait_all_registrations: false, + timeout_get_header_ms: 0, + timeout_get_payload_ms: 0, + timeout_register_validator_ms: 0, + skip_sigverify: false, + min_bid_wei: Uint::<256, 4>::from(0), + late_in_slot_time_ms: 0, + extra_validation_enabled: false, + rpc_url: None, + http_timeout_seconds: 30, + register_validator_retry_limit: 3, + }, + with_signer: true, + }, + muxes: None, + modules: Some(vec![]), + signer: None, + metrics: None, + logs: LogsSettings::default(), + } + } + + async fn create_module_config(id: ModuleId, signing_id: B256) -> StaticModuleConfig { + StaticModuleConfig { + id: id.clone(), + signing_id, + docker_image: String::from(""), + env: None, + env_file: None, + kind: ModuleKind::Commit, + } + } + + #[tokio::test] + async fn test_good_config() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let second_module_id = ModuleId("2nd_test_module".to_string()); + let second_signing_id = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + cfg.modules = Some(vec![ + create_module_config(first_module_id.clone(), first_signing_id).await, + create_module_config(second_module_id.clone(), second_signing_id).await, + ]); + + let jwts = HashMap::from([ + (first_module_id.clone(), "supersecret".to_string()), + (second_module_id.clone(), "another-secret".to_string()), + ]); + + // Load the mod signing configuration + let mod_signing_configs = load_module_signing_configs(&cfg, &jwts) + .wrap_err("Failed to load module signing configs")?; + assert!(mod_signing_configs.len() == 2, "Expected 2 mod signing configurations"); + + // Check the first module + let module_1 = mod_signing_configs + .get(&first_module_id) + .unwrap_or_else(|| panic!("Missing '{first_module_id}' in mod signing configs")); + assert_eq!(module_1.module_name, first_module_id, "Module name mismatch for 'test_module'"); + assert_eq!( + module_1.jwt_secret, jwts[&first_module_id], + "JWT secret mismatch for '{first_module_id}'" + ); + assert_eq!( + module_1.signing_id, first_signing_id, + "Signing ID mismatch for '{first_module_id}'" + ); + + // Check the second module + let module_2 = mod_signing_configs + .get(&second_module_id) + .unwrap_or_else(|| panic!("Missing '{second_module_id}' in mod signing configs")); + assert_eq!( + module_2.module_name, second_module_id, + "Module name mismatch for '{second_module_id}'" + ); + assert_eq!( + module_2.jwt_secret, jwts[&second_module_id], + "JWT secret mismatch for '{second_module_id}'" + ); + assert_eq!( + module_2.signing_id, second_signing_id, + "Signing ID mismatch for '{second_module_id}'" + ); + + Ok(()) + } + + #[tokio::test] + async fn test_duplicate_module_names() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let second_module_id = ModuleId("2nd_test_module".to_string()); + let second_signing_id = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + cfg.modules = Some(vec![ + create_module_config(first_module_id.clone(), first_signing_id).await, + create_module_config(first_module_id.clone(), second_signing_id).await, /* Duplicate + * module + * name */ + ]); + + let jwts = HashMap::from([ + (first_module_id.clone(), "supersecret".to_string()), + (second_module_id.clone(), "another-secret".to_string()), + ]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to duplicate module names"); + if let Err(e) = result { + assert_eq!( + e.to_string(), + format!("Duplicate module config detected: ID {first_module_id} is already used") + ); + } + Ok(()) + } + + #[tokio::test] + async fn test_duplicate_jwt_secrets() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let second_module_id = ModuleId("2nd_test_module".to_string()); + let second_signing_id = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + cfg.modules = Some(vec![ + create_module_config(first_module_id.clone(), first_signing_id).await, + create_module_config(second_module_id.clone(), second_signing_id).await, + ]); + + let jwts = HashMap::from([ + (first_module_id.clone(), "supersecret".to_string()), + (second_module_id.clone(), "supersecret".to_string()), /* Duplicate JWT secret */ + ]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to duplicate JWT secrets"); + if let Err(e) = result { + assert_eq!( + e.to_string(), + format!( + "Duplicate JWT secret detected for modules {first_module_id} and {second_module_id}", + ) + ); + } + Ok(()) + } + + #[tokio::test] + async fn test_duplicate_signing_ids() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let second_module_id = ModuleId("2nd_test_module".to_string()); + + cfg.modules = Some(vec![ + create_module_config(first_module_id.clone(), first_signing_id).await, + create_module_config(second_module_id.clone(), first_signing_id).await, /* Duplicate signing ID */ + ]); + + let jwts = HashMap::from([ + (first_module_id.clone(), "supersecret".to_string()), + (second_module_id.clone(), "another-secret".to_string()), + ]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to duplicate signing IDs"); + if let Err(e) = result { + assert_eq!( + e.to_string(), + format!( + "Duplicate signing ID detected for modules {first_module_id} and {second_module_id}", + ) + ); + } + Ok(()) + } + + #[tokio::test] + async fn test_missing_jwt_secret() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let second_module_id = ModuleId("2nd_test_module".to_string()); + let second_signing_id = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + cfg.modules = Some(vec![ + create_module_config(first_module_id.clone(), first_signing_id).await, + create_module_config(second_module_id.clone(), second_signing_id).await, + ]); + + let jwts = HashMap::from([(second_module_id.clone(), "another-secret".to_string())]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to missing JWT secret"); + if let Err(e) = result { + assert_eq!( + e.to_string(), + format!("JWT secret for module {first_module_id} is missing") + ); + } + Ok(()) + } + + #[tokio::test] + async fn test_empty_jwt_secret() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + cfg.modules = + Some(vec![create_module_config(first_module_id.clone(), first_signing_id).await]); + + let jwts = HashMap::from([(first_module_id.clone(), "".to_string())]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to empty JWT secret"); + if let Err(e) = result { + assert!(format!("{:?}", e).contains("JWT secret cannot be empty")); + } + + Ok(()) + } + + #[tokio::test] + async fn test_zero_signing_id() -> Result<()> { + let mut cfg = get_base_config().await; + let first_module_id = ModuleId("test_module".to_string()); + let first_signing_id = + b256!("0000000000000000000000000000000000000000000000000000000000000000"); + + cfg.modules = + Some(vec![create_module_config(first_module_id.clone(), first_signing_id).await]); + + let jwts = HashMap::from([(first_module_id.clone(), "supersecret".to_string())]); + + // Make sure there was an error + let result = load_module_signing_configs(&cfg, &jwts); + assert!(result.is_err(), "Expected error due to zero signing ID"); + if let Err(e) = result { + assert!(format!("{:?}", e).contains("Signing ID cannot be zero")); + } + Ok(()) + } +} diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index 7ab346f1..5e8e3a65 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -4,8 +4,11 @@ use alloy::rpc::types::beacon::BlsPublicKey; use eyre::{bail, Context, Result}; use serde::de::DeserializeOwned; -use super::{ADMIN_JWT_ENV, JWTS_ENV}; -use crate::{config::MUXER_HTTP_MAX_LENGTH, types::ModuleId, utils::read_chunked_body_with_max}; +use crate::{ + config::{ADMIN_JWT_ENV, JWTS_ENV, MUXER_HTTP_MAX_LENGTH}, + types::ModuleId, + utils::read_chunked_body_with_max, +}; pub fn load_env_var(env: &str) -> Result { std::env::var(env).wrap_err(format!("{env} is not set")) @@ -90,6 +93,7 @@ pub fn decode_string_to_map(raw: &str) -> Result> { mod tests { use super::*; + /// TODO: This was only used by the old JWT loader, can it be removed now? #[test] fn test_decode_string_to_map() { let raw = " KEY=VALUE , KEY2=value2 "; diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index 18d5361f..c5e40a21 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -94,7 +94,7 @@ pub struct ExecutionPayloadHeaderMessageElectra { #[cfg(test)] mod tests { - use alloy::primitives::U256; + use alloy::primitives::{aliases::B32, U256}; use super::*; use crate::{ @@ -176,7 +176,8 @@ mod tests { &parsed.message.pubkey, &parsed.message, &parsed.signature, - APPLICATION_BUILDER_DOMAIN + None, + &B32::from(APPLICATION_BUILDER_DOMAIN) ) .is_ok()) } diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index e51e2291..cd960031 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -1,12 +1,15 @@ -use alloy::rpc::types::beacon::{constants::BLS_DST_SIG, BlsPublicKey, BlsSignature}; +use alloy::{ + primitives::{aliases::B32, Address, B256}, + rpc::types::beacon::{constants::BLS_DST_SIG, BlsPublicKey, BlsSignature}, +}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ constants::{COMMIT_BOOST_DOMAIN, GENESIS_VALIDATORS_ROOT}, error::BlstErrorWrapper, - signer::{verify_bls_signature, BlsSecretKey}, - types::Chain, + signer::{verify_bls_signature, verify_ecdsa_signature, BlsSecretKey, EcdsaSignature}, + types::{self, Chain}, }; pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { @@ -14,21 +17,29 @@ pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { BlsSignature::from_slice(&signature) } -pub fn compute_signing_root(object_root: [u8; 32], signing_domain: [u8; 32]) -> [u8; 32] { - #[derive(Default, Debug, TreeHash)] - struct SigningData { - object_root: [u8; 32], - signing_domain: [u8; 32], +pub fn compute_prop_commit_signing_root( + chain: Chain, + object_root: &B256, + module_signing_id: Option<&B256>, + domain_mask: &B32, +) -> B256 { + let domain = compute_domain(chain, domain_mask); + match module_signing_id { + Some(id) => { + let object_root = + types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + .tree_hash_root(); + types::SigningData { object_root, signing_domain: domain }.tree_hash_root() + } + None => types::SigningData { object_root: *object_root, signing_domain: domain } + .tree_hash_root(), } - - let signing_data = SigningData { object_root, signing_domain }; - signing_data.tree_hash_root().0 } // NOTE: this currently works only for builder domain signatures and // verifications // ref: https://github.com/ralexstokes/ethereum-consensus/blob/cf3c404043230559660810bc0c9d6d5a8498d819/ethereum-consensus/src/builder/mod.rs#L26-L29 -pub fn compute_domain(chain: Chain, domain_mask: [u8; 4]) -> [u8; 32] { +pub fn compute_domain(chain: Chain, domain_mask: &B32) -> B256 { #[derive(Debug, TreeHash)] struct ForkData { fork_version: [u8; 4], @@ -36,7 +47,7 @@ pub fn compute_domain(chain: Chain, domain_mask: [u8; 4]) -> [u8; 32] { } let mut domain = [0u8; 32]; - domain[..4].copy_from_slice(&domain_mask); + domain[..4].copy_from_slice(&domain_mask.0); let fork_version = chain.genesis_fork_version(); let fd = ForkData { fork_version, genesis_validators_root: GENESIS_VALIDATORS_ROOT }; @@ -44,7 +55,7 @@ pub fn compute_domain(chain: Chain, domain_mask: [u8; 4]) -> [u8; 32] { domain[4..].copy_from_slice(&fork_data_root[..28]); - domain + B256::from(domain) } pub fn verify_signed_message( @@ -52,69 +63,114 @@ pub fn verify_signed_message( pubkey: &BlsPublicKey, msg: &T, signature: &BlsSignature, - domain_mask: [u8; 4], + module_signing_id: Option<&B256>, + domain_mask: &B32, ) -> Result<(), BlstErrorWrapper> { - let domain = compute_domain(chain, domain_mask); - let signing_root = compute_signing_root(msg.tree_hash_root().0, domain); - - verify_bls_signature(pubkey, &signing_root, signature) + let signing_root = compute_prop_commit_signing_root( + chain, + &msg.tree_hash_root(), + module_signing_id, + domain_mask, + ); + verify_bls_signature(pubkey, signing_root.as_slice(), signature) } +/// Signs a message with the Beacon builder domain. pub fn sign_builder_message( chain: Chain, secret_key: &BlsSecretKey, msg: &impl TreeHash, ) -> BlsSignature { - sign_builder_root(chain, secret_key, msg.tree_hash_root().0) + sign_builder_root(chain, secret_key, &msg.tree_hash_root()) } pub fn sign_builder_root( chain: Chain, secret_key: &BlsSecretKey, - object_root: [u8; 32], + object_root: &B256, ) -> BlsSignature { - let domain = chain.builder_domain(); - let signing_root = compute_signing_root(object_root, domain); - sign_message(secret_key, &signing_root) + let signing_domain = chain.builder_domain(); + let signing_data = + types::SigningData { object_root: object_root.tree_hash_root(), signing_domain }; + let signing_root = signing_data.tree_hash_root(); + sign_message(secret_key, signing_root.as_slice()) } pub fn sign_commit_boost_root( chain: Chain, secret_key: &BlsSecretKey, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> BlsSignature { - let domain = compute_domain(chain, COMMIT_BOOST_DOMAIN); - let signing_root = compute_signing_root(object_root, domain); - sign_message(secret_key, &signing_root) + let signing_root = compute_prop_commit_signing_root( + chain, + object_root, + module_signing_id, + &B32::from(COMMIT_BOOST_DOMAIN), + ); + sign_message(secret_key, signing_root.as_slice()) } +// ============================== +// === Signature Verification === +// ============================== + +/// Verifies that a proposer commitment signature was generated by the given BLS +/// key for the provided message, chain ID, and module signing ID. +pub fn verify_proposer_commitment_signature_bls( + chain: Chain, + pubkey: &BlsPublicKey, + msg: &impl TreeHash, + signature: &BlsSignature, + module_signing_id: &B256, +) -> Result<(), BlstErrorWrapper> { + let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = types::PropCommitSigningInfo { + data: msg.tree_hash_root(), + module_signing_id: *module_signing_id, + } + .tree_hash_root(); + let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); + verify_bls_signature(pubkey, signing_root.as_slice(), signature) +} + +/// Verifies that a proposer commitment signature was generated by the given +/// ECDSA key for the provided message, chain ID, and module signing ID. +pub fn verify_proposer_commitment_signature_ecdsa( + chain: Chain, + address: &Address, + msg: &impl TreeHash, + signature: &EcdsaSignature, + module_signing_id: &B256, +) -> Result<(), eyre::Report> { + let object_root = msg.tree_hash_root(); + let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = + types::PropCommitSigningInfo { data: object_root, module_signing_id: *module_signing_id } + .tree_hash_root(); + let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); + verify_ecdsa_signature(address, &signing_root, signature) +} + +// =============== +// === Testing === +// =============== + #[cfg(test)] mod tests { + use alloy::primitives::aliases::B32; + use super::compute_domain; use crate::{constants::APPLICATION_BUILDER_DOMAIN, types::Chain}; #[test] fn test_builder_domains() { - assert_eq!( - compute_domain(Chain::Mainnet, APPLICATION_BUILDER_DOMAIN), - Chain::Mainnet.builder_domain() - ); - assert_eq!( - compute_domain(Chain::Holesky, APPLICATION_BUILDER_DOMAIN), - Chain::Holesky.builder_domain() - ); - assert_eq!( - compute_domain(Chain::Sepolia, APPLICATION_BUILDER_DOMAIN), - Chain::Sepolia.builder_domain() - ); - assert_eq!( - compute_domain(Chain::Helder, APPLICATION_BUILDER_DOMAIN), - Chain::Helder.builder_domain() - ); - assert_eq!( - compute_domain(Chain::Hoodi, APPLICATION_BUILDER_DOMAIN), - Chain::Hoodi.builder_domain() - ); + let domain = &B32::from(APPLICATION_BUILDER_DOMAIN); + assert_eq!(compute_domain(Chain::Mainnet, domain), Chain::Mainnet.builder_domain()); + assert_eq!(compute_domain(Chain::Holesky, domain), Chain::Holesky.builder_domain()); + assert_eq!(compute_domain(Chain::Sepolia, domain), Chain::Sepolia.builder_domain()); + assert_eq!(compute_domain(Chain::Helder, domain), Chain::Helder.builder_domain()); + assert_eq!(compute_domain(Chain::Hoodi, domain), Chain::Hoodi.builder_domain()); } } diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs index f133b2bc..15367f36 100644 --- a/crates/common/src/signer/schemes/bls.rs +++ b/crates/common/src/signer/schemes/bls.rs @@ -1,5 +1,5 @@ -use alloy::rpc::types::beacon::constants::BLS_DST_SIG; pub use alloy::rpc::types::beacon::BlsSignature; +use alloy::{primitives::B256, rpc::types::beacon::constants::BLS_DST_SIG}; use blst::BLST_ERROR; use tree_hash::TreeHash; @@ -32,20 +32,32 @@ impl BlsSigner { } } - pub fn secret(&self) -> [u8; 32] { + pub fn secret(&self) -> B256 { match self { - BlsSigner::Local(secret) => secret.clone().to_bytes(), + BlsSigner::Local(secret) => B256::from(secret.clone().to_bytes()), } } - pub async fn sign(&self, chain: Chain, object_root: [u8; 32]) -> BlsSignature { + pub async fn sign( + &self, + chain: Chain, + object_root: &B256, + module_signing_id: Option<&B256>, + ) -> BlsSignature { match self { - BlsSigner::Local(sk) => sign_commit_boost_root(chain, sk, object_root), + BlsSigner::Local(sk) => { + sign_commit_boost_root(chain, sk, object_root, module_signing_id) + } } } - pub async fn sign_msg(&self, chain: Chain, msg: &impl TreeHash) -> BlsSignature { - self.sign(chain, msg.tree_hash_root().0).await + pub async fn sign_msg( + &self, + chain: Chain, + msg: &impl TreeHash, + module_signing_id: Option<&B256>, + ) -> BlsSignature { + self.sign(chain, &msg.tree_hash_root(), module_signing_id).await } } diff --git a/crates/common/src/signer/schemes/ecdsa.rs b/crates/common/src/signer/schemes/ecdsa.rs index 612df5e3..907340f1 100644 --- a/crates/common/src/signer/schemes/ecdsa.rs +++ b/crates/common/src/signer/schemes/ecdsa.rs @@ -1,7 +1,7 @@ use std::{ops::Deref, str::FromStr}; use alloy::{ - primitives::{Address, PrimitiveSignature}, + primitives::{aliases::B32, Address, PrimitiveSignature, B256}, signers::{local::PrivateKeySigner, SignerSync}, }; use eyre::ensure; @@ -9,8 +9,8 @@ use tree_hash::TreeHash; use crate::{ constants::COMMIT_BOOST_DOMAIN, - signature::{compute_domain, compute_signing_root}, - types::Chain, + signature::compute_domain, + types::{self, Chain}, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -86,32 +86,44 @@ impl EcdsaSigner { pub async fn sign( &self, chain: Chain, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { match self { EcdsaSigner::Local(sk) => { - let domain = compute_domain(chain, COMMIT_BOOST_DOMAIN); - let signing_root = compute_signing_root(object_root, domain).into(); + let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); + let signing_root = match module_signing_id { + Some(id) => { + let object_root = types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *id, + } + .tree_hash_root(); + types::SigningData { object_root, signing_domain }.tree_hash_root() + } + None => types::SigningData { object_root: *object_root, signing_domain } + .tree_hash_root(), + }; sk.sign_hash_sync(&signing_root).map(EcdsaSignature::from) } } } - pub async fn sign_msg( &self, chain: Chain, msg: &impl TreeHash, + module_signing_id: Option<&B256>, ) -> Result { - self.sign(chain, msg.tree_hash_root().0).await + self.sign(chain, &msg.tree_hash_root(), module_signing_id).await } } pub fn verify_ecdsa_signature( address: &Address, - msg: &[u8; 32], + msg: &B256, signature: &EcdsaSignature, ) -> eyre::Result<()> { - let recovered = signature.recover_address_from_prehash(msg.into())?; + let recovered = signature.recover_address_from_prehash(msg)?; ensure!(recovered == *address, "invalid signature"); Ok(()) } @@ -124,15 +136,16 @@ mod test { use super::*; #[tokio::test] - async fn test_ecdsa_signer() { + async fn test_ecdsa_signer_noncommit() { let pk = bytes!("88bcd6672d95bcba0d52a3146494ed4d37675af4ed2206905eb161aa99a6c0d1"); let signer = EcdsaSigner::new_from_bytes(&pk).unwrap(); - let object_root = [1; 32]; - let signature = signer.sign(Chain::Holesky, object_root).await.unwrap(); + let object_root = B256::from([1; 32]); + let signature = signer.sign(Chain::Holesky, &object_root, None).await.unwrap(); - let domain = compute_domain(Chain::Holesky, COMMIT_BOOST_DOMAIN); - let msg = compute_signing_root(object_root, domain); + let domain = compute_domain(Chain::Holesky, &B32::from(COMMIT_BOOST_DOMAIN)); + let signing_data = types::SigningData { object_root, signing_domain: domain }; + let msg = signing_data.tree_hash_root(); assert_eq!(msg, hex!("219ca7a673b2cbbf67bec6c9f60f78bd051336d57b68d1540190f30667e86725")); @@ -140,4 +153,26 @@ mod test { let verified = verify_ecdsa_signature(&address, &msg, &signature); assert!(verified.is_ok()); } + + #[tokio::test] + async fn test_ecdsa_signer_prop_commit() { + let pk = bytes!("88bcd6672d95bcba0d52a3146494ed4d37675af4ed2206905eb161aa99a6c0d1"); + let signer = EcdsaSigner::new_from_bytes(&pk).unwrap(); + + let object_root = B256::from([1; 32]); + let module_signing_id = B256::from([2; 32]); + let signature = + signer.sign(Chain::Hoodi, &object_root, Some(&module_signing_id)).await.unwrap(); + + let signing_domain = compute_domain(Chain::Hoodi, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = + types::PropCommitSigningInfo { data: object_root, module_signing_id }.tree_hash_root(); + let msg = types::SigningData { object_root, signing_domain }.tree_hash_root(); + + assert_eq!(msg, hex!("8cd49ccf2f9b0297796ff96ce5f7c5d26e20a59d0032ee2ad6249dcd9682b808")); + + let address = signer.address(); + let verified = verify_ecdsa_signature(&address, &msg, &signature); + assert!(verified.is_ok()); + } } diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index 479a4016..834f4bd8 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -532,7 +532,8 @@ mod test { delegator: consensus_signer.pubkey(), proxy: proxy_signer.pubkey(), }; - let signature = consensus_signer.sign(Chain::Mainnet, message.tree_hash_root().0).await; + let signature = + consensus_signer.sign(Chain::Mainnet, &message.tree_hash_root(), None).await; let delegation = SignedProxyDelegationBls { signature, message }; let proxy_signer = BlsProxySigner { signer: proxy_signer, delegation }; @@ -645,7 +646,8 @@ mod test { delegator: consensus_signer.pubkey(), proxy: proxy_signer.pubkey(), }; - let signature = consensus_signer.sign(Chain::Mainnet, message.tree_hash_root().0).await; + let signature = + consensus_signer.sign(Chain::Mainnet, &message.tree_hash_root(), None).await; let delegation = SignedProxyDelegationBls { signature, message }; let proxy_signer = BlsProxySigner { signer: proxy_signer, delegation }; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 3d07e89c..c747815b 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -1,9 +1,10 @@ use std::path::PathBuf; -use alloy::primitives::{hex, Bytes}; +use alloy::primitives::{aliases::B32, hex, Bytes, B256}; use derive_more::{Deref, Display, From, Into}; use eyre::{bail, Context}; use serde::{Deserialize, Serialize}; +use tree_hash_derive::TreeHash; use crate::{constants::APPLICATION_BUILDER_DOMAIN, signature::compute_domain}; @@ -84,14 +85,14 @@ impl Chain { } } - pub fn builder_domain(&self) -> [u8; 32] { + pub fn builder_domain(&self) -> B256 { match self { Chain::Mainnet => KnownChain::Mainnet.builder_domain(), Chain::Holesky => KnownChain::Holesky.builder_domain(), Chain::Sepolia => KnownChain::Sepolia.builder_domain(), Chain::Helder => KnownChain::Helder.builder_domain(), Chain::Hoodi => KnownChain::Hoodi.builder_domain(), - Chain::Custom { .. } => compute_domain(*self, APPLICATION_BUILDER_DOMAIN), + Chain::Custom { .. } => compute_domain(*self, &B32::from(APPLICATION_BUILDER_DOMAIN)), } } @@ -155,28 +156,28 @@ impl KnownChain { } } - pub fn builder_domain(&self) -> [u8; 32] { + pub fn builder_domain(&self) -> B256 { match self { - KnownChain::Mainnet => [ + KnownChain::Mainnet => B256::from([ 0, 0, 0, 1, 245, 165, 253, 66, 209, 106, 32, 48, 39, 152, 239, 110, 211, 9, 151, 155, 67, 0, 61, 35, 32, 217, 240, 232, 234, 152, 49, 169, - ], - KnownChain::Holesky => [ + ]), + KnownChain::Holesky => B256::from([ 0, 0, 0, 1, 91, 131, 162, 55, 89, 197, 96, 178, 208, 198, 69, 118, 225, 220, 252, 52, 234, 148, 196, 152, 143, 62, 13, 159, 119, 240, 83, 135, - ], - KnownChain::Sepolia => [ + ]), + KnownChain::Sepolia => B256::from([ 0, 0, 0, 1, 211, 1, 7, 120, 205, 8, 238, 81, 75, 8, 254, 103, 182, 197, 3, 181, 16, 152, 122, 76, 228, 63, 66, 48, 109, 151, 198, 124, - ], - KnownChain::Helder => [ + ]), + KnownChain::Helder => B256::from([ 0, 0, 0, 1, 148, 196, 26, 244, 132, 255, 247, 150, 73, 105, 224, 189, 217, 34, 248, 45, 255, 15, 75, 232, 122, 96, 208, 102, 76, 201, 209, 255, - ], - KnownChain::Hoodi => [ + ]), + KnownChain::Hoodi => B256::from([ 0, 0, 0, 1, 113, 145, 3, 81, 30, 250, 79, 19, 98, 255, 42, 80, 153, 108, 204, 243, 41, 204, 132, 203, 65, 12, 94, 92, 125, 53, 29, 3, - ], + ]), } } @@ -289,6 +290,22 @@ impl<'de> Deserialize<'de> for Chain { } } +/// Structure for signatures used in Beacon chain operations +#[derive(Default, Debug, TreeHash)] +pub struct SigningData { + pub object_root: B256, + pub signing_domain: B256, +} + +/// Structure for signatures used for proposer commitments in Commit Boost. +/// The signing root of this struct must be used as the object_root of a +/// SigningData for signatures. +#[derive(Default, Debug, TreeHash)] +pub struct PropCommitSigningInfo { + pub data: B256, + pub module_signing_id: B256, +} + /// Returns seconds_per_slot and genesis_fork_version from a spec, such as /// returned by /eth/v1/config/spec ref: https://ethereum.github.io/beacon-APIs/#/Config/getSpec /// Try to load two formats: diff --git a/crates/pbs/src/mev_boost/get_header.rs b/crates/pbs/src/mev_boost/get_header.rs index 613815ce..1adcb74a 100644 --- a/crates/pbs/src/mev_boost/get_header.rs +++ b/crates/pbs/src/mev_boost/get_header.rs @@ -4,7 +4,7 @@ use std::{ }; use alloy::{ - primitives::{utils::format_ether, B256, U256}, + primitives::{aliases::B32, utils::format_ether, B256, U256}, providers::Provider, rpc::types::{beacon::BlsPublicKey, Block}, }; @@ -475,7 +475,8 @@ fn validate_signature( &received_relay_pubkey, &message, signature, - APPLICATION_BUILDER_DOMAIN, + None, + &B32::from(APPLICATION_BUILDER_DOMAIN), ) .map_err(ValidationError::Sigverify)?; diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index b0fc88de..64a3e5b8 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -33,6 +33,9 @@ pub enum SignerModuleError { #[error("rate limited for {0} more seconds")] RateLimited(f64), + + #[error("request error: {0}")] + RequestError(String), } impl IntoResponse for SignerModuleError { @@ -55,6 +58,9 @@ impl IntoResponse for SignerModuleError { SignerModuleError::RateLimited(duration) => { (StatusCode::TOO_MANY_REQUESTS, format!("rate limited for {duration:?}")) } + SignerModuleError::RequestError(err) => { + (StatusCode::BAD_REQUEST, format!("bad request: {err}")) + } } .into_response() } diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 4c2d909f..add9e3a2 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -1,6 +1,10 @@ use std::{collections::HashMap, io::Write, path::PathBuf}; -use alloy::{hex, rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN}; +use alloy::{ + hex, + primitives::{aliases::B32, B256}, + rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, +}; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ commit::request::{ConsensusProxyMap, ProxyDelegation, SignedProxyDelegation}, @@ -8,7 +12,7 @@ use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, signer::{BlsPublicKey, BlsSignature, ProxyStore}, - types::{Chain, ModuleId}, + types::{self, Chain, ModuleId}, }; use eyre::{bail, OptionExt}; use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; @@ -192,14 +196,15 @@ impl DirkManager { pub async fn request_consensus_signature( &self, pubkey: &BlsPublicKey, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { match self.consensus_accounts.get(pubkey) { Some(Account::Simple(account)) => { - self.request_simple_signature(account, object_root).await + self.request_simple_signature(account, object_root, module_signing_id).await } Some(Account::Distributed(account)) => { - self.request_distributed_signature(account, object_root).await + self.request_distributed_signature(account, object_root, module_signing_id).await } None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec())), } @@ -209,14 +214,15 @@ impl DirkManager { pub async fn request_proxy_signature( &self, pubkey: &BlsPublicKey, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { match self.proxy_accounts.get(pubkey) { Some(ProxyAccount { inner: Account::Simple(account), .. }) => { - self.request_simple_signature(account, object_root).await + self.request_simple_signature(account, object_root, module_signing_id).await } Some(ProxyAccount { inner: Account::Distributed(account), .. }) => { - self.request_distributed_signature(account, object_root).await + self.request_distributed_signature(account, object_root, module_signing_id).await } None => Err(SignerModuleError::UnknownProxySigner(pubkey.to_vec())), } @@ -226,13 +232,21 @@ impl DirkManager { async fn request_simple_signature( &self, account: &SimpleAccount, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { - let domain = compute_domain(self.chain, COMMIT_BOOST_DOMAIN); + let domain = compute_domain(self.chain, &B32::from(COMMIT_BOOST_DOMAIN)); + + let data = match module_signing_id { + Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + .tree_hash_root() + .to_vec(), + None => object_root.to_vec(), + }; let response = SignerClient::new(account.connection.clone()) .sign(SignRequest { - data: object_root.to_vec(), + data, domain: domain.to_vec(), id: Some(sign_request::Id::PublicKey(account.public_key.to_vec())), }) @@ -256,17 +270,27 @@ impl DirkManager { async fn request_distributed_signature( &self, account: &DistributedAccount, - object_root: [u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { let mut partials = Vec::with_capacity(account.participants.len()); let mut requests = Vec::with_capacity(account.participants.len()); + let data = match module_signing_id { + Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + .tree_hash_root() + .to_vec(), + None => object_root.to_vec(), + }; + for (id, channel) in account.participants.iter() { + let data_copy = data.clone(); let request = async move { SignerClient::new(channel.clone()) .sign(SignRequest { - data: object_root.to_vec(), - domain: compute_domain(self.chain, COMMIT_BOOST_DOMAIN).to_vec(), + data: data_copy, + domain: compute_domain(self.chain, &B32::from(COMMIT_BOOST_DOMAIN)) + .to_vec(), id: Some(sign_request::Id::Account(account.name.clone())), }) .map(|res| (res, *id)) @@ -336,7 +360,7 @@ impl DirkManager { let message = ProxyDelegation { delegator: consensus, proxy: proxy_account.inner.public_key() }; let delegation_signature = - self.request_consensus_signature(&consensus, message.tree_hash_root().0).await?; + self.request_consensus_signature(&consensus, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegation { message, signature: delegation_signature }; diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index a613df0a..a13695e5 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use alloy::{primitives::Address, rpc::types::beacon::BlsSignature}; +use alloy::{ + primitives::{Address, B256}, + rpc::types::beacon::BlsSignature, +}; use cb_common::{ commit::request::{ ConsensusProxyMap, ProxyDelegationBls, ProxyDelegationEcdsa, SignedProxyDelegationBls, @@ -95,7 +98,7 @@ impl LocalSigningManager { let proxy_pubkey = signer.pubkey(); let message = ProxyDelegationBls { delegator, proxy: proxy_pubkey }; - let signature = self.sign_consensus(&delegator, &message.tree_hash_root().0).await?; + let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationBls { signature, message }; let proxy_signer = BlsProxySigner { signer, delegation }; @@ -114,7 +117,7 @@ impl LocalSigningManager { let proxy_address = signer.address(); let message = ProxyDelegationEcdsa { delegator, proxy: proxy_address }; - let signature = self.sign_consensus(&delegator, &message.tree_hash_root().0).await?; + let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationEcdsa { signature, message }; let proxy_signer = EcdsaProxySigner { signer, delegation }; @@ -129,13 +132,14 @@ impl LocalSigningManager { pub async fn sign_consensus( &self, pubkey: &BlsPublicKey, - object_root: &[u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { let signer = self .consensus_signers .get(pubkey) .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; - let signature = signer.sign(self.chain, *object_root).await; + let signature = signer.sign(self.chain, object_root, module_signing_id).await; Ok(signature) } @@ -143,28 +147,30 @@ impl LocalSigningManager { pub async fn sign_proxy_bls( &self, pubkey: &BlsPublicKey, - object_root: &[u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { let bls_proxy = self .proxy_signers .bls_signers .get(pubkey) .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; - let signature = bls_proxy.sign(self.chain, *object_root).await; + let signature = bls_proxy.sign(self.chain, object_root, module_signing_id).await; Ok(signature) } pub async fn sign_proxy_ecdsa( &self, address: &Address, - object_root: &[u8; 32], + object_root: &B256, + module_signing_id: Option<&B256>, ) -> Result { let ecdsa_proxy = self .proxy_signers .ecdsa_signers .get(address) .ok_or(SignerModuleError::UnknownProxySigner(address.to_vec()))?; - let signature = ecdsa_proxy.sign(self.chain, *object_root).await?; + let signature = ecdsa_proxy.sign(self.chain, object_root, module_signing_id).await?; Ok(signature) } @@ -265,7 +271,6 @@ impl LocalSigningManager { #[cfg(test)] mod tests { use alloy::primitives::B256; - use cb_common::signature::compute_signing_root; use lazy_static::lazy_static; use super::*; @@ -287,9 +292,48 @@ mod tests { (signing_manager, consensus_pk) } + mod test_bls { + use alloy::primitives::aliases::B32; + use cb_common::{ + constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, + signer::verify_bls_signature, types, + }; + + use super::*; + + #[tokio::test] + async fn test_key_signs_message() { + let (signing_manager, consensus_pk) = init_signing_manager(); + + let data_root = B256::random(); + let module_signing_id = B256::random(); + + let sig = signing_manager + .sign_consensus(&consensus_pk, &data_root, Some(&module_signing_id)) + .await + .unwrap(); + + // Verify signature + let signing_domain = compute_domain(CHAIN, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = types::PropCommitSigningInfo { + data: data_root.tree_hash_root(), + module_signing_id, + } + .tree_hash_root(); + let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); + + let validation_result = + verify_bls_signature(&consensus_pk, signing_root.as_slice(), &sig); + + assert!(validation_result.is_ok(), "Keypair must produce valid signatures of messages.") + } + } + mod test_proxy_bls { + use alloy::primitives::aliases::B32; use cb_common::{ - constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, signer::verify_bls_signature, + constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, + signer::verify_bls_signature, types, }; use super::*; @@ -339,14 +383,23 @@ mod tests { let proxy_pk = signed_delegation.message.proxy; let data_root = B256::random(); + let module_signing_id = B256::random(); - let sig = signing_manager.sign_proxy_bls(&proxy_pk, &data_root).await.unwrap(); + let sig = signing_manager + .sign_proxy_bls(&proxy_pk, &data_root, Some(&module_signing_id)) + .await + .unwrap(); // Verify signature - let domain = compute_domain(CHAIN, COMMIT_BOOST_DOMAIN); - let signing_root = compute_signing_root(data_root.tree_hash_root().0, domain); + let signing_domain = compute_domain(CHAIN, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = types::PropCommitSigningInfo { + data: data_root.tree_hash_root(), + module_signing_id, + } + .tree_hash_root(); + let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); - let validation_result = verify_bls_signature(&proxy_pk, &signing_root, &sig); + let validation_result = verify_bls_signature(&proxy_pk, signing_root.as_slice(), &sig); assert!( validation_result.is_ok(), @@ -356,9 +409,10 @@ mod tests { } mod test_proxy_ecdsa { + use alloy::primitives::aliases::B32; use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - signer::verify_ecdsa_signature, + signer::verify_ecdsa_signature, types, }; use super::*; @@ -408,12 +462,21 @@ mod tests { let proxy_pk = signed_delegation.message.proxy; let data_root = B256::random(); + let module_signing_id = B256::random(); - let sig = signing_manager.sign_proxy_ecdsa(&proxy_pk, &data_root).await.unwrap(); + let sig = signing_manager + .sign_proxy_ecdsa(&proxy_pk, &data_root, Some(&module_signing_id)) + .await + .unwrap(); // Verify signature - let domain = compute_domain(CHAIN, COMMIT_BOOST_DOMAIN); - let signing_root = compute_signing_root(data_root.tree_hash_root().0, domain); + let signing_domain = compute_domain(CHAIN, &B32::from(COMMIT_BOOST_DOMAIN)); + let object_root = types::PropCommitSigningInfo { + data: data_root.tree_hash_root(), + module_signing_id, + } + .tree_hash_root(); + let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); let validation_result = verify_ecdsa_signature(&proxy_pk, &signing_root, &sig); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 59da3c3d..4ecf5e75 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -25,7 +25,7 @@ use cb_common::{ RevokeModuleRequest, SignConsensusRequest, SignProxyRequest, SignRequest, }, }, - config::StartSignerConfig, + config::{ModuleSigningConfig, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{Chain, Jwt, ModuleId}, utils::{decode_jwt, validate_admin_jwt, validate_jwt}, @@ -61,9 +61,10 @@ struct SigningState { /// Manager handling different signing methods manager: Arc>, - /// Map of modules ids to JWT secrets. This also acts as registry of all - /// modules running - jwts: Arc>>, + /// Map of modules ids to JWT configurations. This also acts as registry of + /// all modules running + jwts: Arc>>, + /// Secret for the admin JWT admin_secret: Arc>, @@ -77,16 +78,17 @@ struct SigningState { impl SigningService { pub async fn run(config: StartSignerConfig) -> eyre::Result<()> { - if config.jwts.is_empty() { + if config.mod_signing_configs.is_empty() { warn!("Signing service was started but no module is registered. Exiting"); return Ok(()); } - let module_ids: Vec = config.jwts.keys().cloned().map(Into::into).collect(); + let module_ids: Vec = + config.mod_signing_configs.keys().cloned().map(Into::into).collect(); let state = SigningState { manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)), - jwts: Arc::new(ParkingRwLock::new(config.jwts)), + jwts: Arc::new(ParkingRwLock::new(config.mod_signing_configs)), admin_secret: Arc::new(ParkingRwLock::new(config.admin_secret)), jwt_auth_failures: Arc::new(ParkingRwLock::new(HashMap::new())), jwt_auth_fail_limit: config.jwt_auth_fail_limit, @@ -228,12 +230,12 @@ fn check_jwt_auth( })?; let guard = state.jwts.read(); - let jwt_secret = guard.get(&module_id).ok_or_else(|| { + let jwt_config = guard.get(&module_id).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; - validate_jwt(jwt, jwt_secret).map_err(|e| { + validate_jwt(jwt, &jwt_config.jwt_secret).map_err(|e| { error!("Unauthorized request. Invalid JWT: {e}"); SignerModuleError::Unauthorized })?; @@ -298,37 +300,48 @@ async fn handle_request_signature( ) -> Result { let req_id = Uuid::new_v4(); + let Some(signing_id) = state.jwts.read().get(&module_id).map(|m| m.signing_id) else { + error!(event = "request_signature", ?module_id, ?req_id, "Module signing ID not found"); + return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); + }; + debug!(event = "request_signature", ?module_id, %request, ?req_id, "New request"); let manager = state.manager.read().await; let res = match &*manager { SigningManager::Local(local_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => local_manager - .sign_consensus(&pubkey, &object_root) - .await - .map(|sig| Json(sig).into_response()), - SignRequest::ProxyBls(SignProxyRequest { object_root, proxy: bls_key }) => { + SignRequest::Consensus(SignConsensusRequest { ref object_root, ref pubkey }) => { + local_manager + .sign_consensus(pubkey, object_root, Some(&signing_id)) + .await + .map(|sig| Json(sig).into_response()) + } + SignRequest::ProxyBls(SignProxyRequest { ref object_root, proxy: ref bls_key }) => { local_manager - .sign_proxy_bls(&bls_key, &object_root) + .sign_proxy_bls(bls_key, object_root, Some(&signing_id)) .await .map(|sig| Json(sig).into_response()) } - SignRequest::ProxyEcdsa(SignProxyRequest { object_root, proxy: ecdsa_key }) => { + SignRequest::ProxyEcdsa(SignProxyRequest { ref object_root, proxy: ref ecdsa_key }) => { local_manager - .sign_proxy_ecdsa(&ecdsa_key, &object_root) + .sign_proxy_ecdsa(ecdsa_key, object_root, Some(&signing_id)) .await .map(|sig| Json(sig).into_response()) } }, SigningManager::Dirk(dirk_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { object_root, pubkey }) => dirk_manager - .request_consensus_signature(&pubkey, *object_root) - .await - .map(|sig| Json(sig).into_response()), - SignRequest::ProxyBls(SignProxyRequest { object_root, proxy: bls_key }) => dirk_manager - .request_proxy_signature(&bls_key, *object_root) - .await - .map(|sig| Json(sig).into_response()), + SignRequest::Consensus(SignConsensusRequest { ref object_root, ref pubkey }) => { + dirk_manager + .request_consensus_signature(pubkey, object_root, Some(&signing_id)) + .await + .map(|sig| Json(sig).into_response()) + } + SignRequest::ProxyBls(SignProxyRequest { ref object_root, proxy: ref bls_key }) => { + dirk_manager + .request_proxy_signature(bls_key, object_root, Some(&signing_id)) + .await + .map(|sig| Json(sig).into_response()) + } SignRequest::ProxyEcdsa(_) => { error!( event = "request_signature", @@ -405,7 +418,24 @@ async fn handle_reload( }; if let Some(jwt_secrets) = request.jwt_secrets { - *state.jwts.write() = jwt_secrets; + let mut jwt_configs = state.jwts.write(); + let mut new_configs = HashMap::new(); + for (module_id, jwt_secret) in jwt_secrets { + if let Some(signing_id) = jwt_configs.get(&module_id).map(|cfg| cfg.signing_id) { + new_configs.insert(module_id.clone(), ModuleSigningConfig { + module_name: module_id, + jwt_secret, + signing_id, + }); + } else { + let error_message = format!( + "Module {module_id} signing ID not found in commit-boost config, cannot reload" + ); + error!(event = "reload", ?req_id, module_id = %module_id, error = %error_message); + return Err(SignerModuleError::RequestError(error_message)); + } + } + *jwt_configs = new_configs; } if let Some(admin_secret) = request.admin_secret { diff --git a/docs/docs/developing/prop-commit-signing.md b/docs/docs/developing/prop-commit-signing.md new file mode 100644 index 00000000..fd19fafc --- /dev/null +++ b/docs/docs/developing/prop-commit-signing.md @@ -0,0 +1,60 @@ +# Requesting Proposer Commitment Signatures with Commit-Boost + +When you create a new validator on the Ethereum network, one of the steps is the generation of a new BLS private key (commonly known as the "validator key" or the "signer key") and its corresponding BLS public key (the "validator pubkey", used as an identifier). Typically this private key will be used by an Ethereum consensus client to sign things such as attestations and blocks for publication on the Beacon chain. These signatures prove that you, as the owner of that private key, approve of the data being signed. However, as general-purpose private keys, they can also be used to sign *other* arbitrary messages not destined for the Beacon chain. + +Commit-Boost takes advantage of this by offering a standard known as **proposer commitments**. These are arbitrary messages (albeit with some important rules), similar to the kind used on the Beacon chain, that have been signed by one of the owner's private keys. Modules interested in leveraging Commit-Boost's proposer commitments can construct their own data in whatever format they like and request that Commit-Boost's **signer service** generate a signature for it with a particular private key. The module can then use that signature to verify the data was signed by that user. + +Commit-Boost supports proposer commitment signatures for both BLS private keys (identified by their public key) and ECDSA private keys (identified by their Ethereum address). + + +## Rules of Proposer Commitment Signatures + +Proposer commitment signatures produced by Commit-Boost's signer service conform to the following rules: + +- Signatures are **unique** to a given EVM chain (identified by its [chain ID](https://chainlist.org/)). Signatures generated for one chain will not work on a different chain. +- Signatures are **unique** to Commit-Boost proposer commitments. The signer service **cannot** be used to create signatures that could be used for other applications, such as for attestations on the Beacon chain. While the signer service has access to the same validator private keys used to attest on the Beacon chain, it cannot create signatures that would get you slashed on the Beacon chain. +- Signatures are **unique** to a particular module. One module cannot, for example, request an identical payload as another module and effectively "forge" a signature for the second module; identical payloads from two separate modules will result in two separate signatures. +- The data payload being signed must be a **32-byte array**, typically serializd as a 64-character hex string with an optional `0x` prefix. The value itself is arbitrary, as long as it has meaning to the requester - though it is typically the 256-bit hash of some kind of data. +- If requesting a signature from a BLS key, the resulting signature will be a standard BLS signature (96 bytes in length). +- If requesting a signature from an ECDSA key, the resulting signature will be a standard Ethereum RSV signature (65 bytes in length). + + +## Configuring a Module for Proposer Commitments + +Commit-Boost's signer service must be configured prior to launching to expect requests from your module. There are two main parts: + +1. An entry for your module into [Commit-Boost's configuration file](../get_started/configuration.md#custom-module). This must include a unique ID for your module, the line `type = "commit"`, and include a unique [signing ID](#the-signing-id) for your module. Generally you should provide values for these in your documentation, so your users can reference it when configuring their own Commit-Boost node. + +2. A JWT secret used by your module to authenticate with the signer in HTTP requests. This must be a string that both the Commit-Boost signer can read and your module can read, but no other modules should be allowed to access it. The user should be responsible for determining an appropriate secret and providing it to the Commit-Boost signer service securely; your module will need some way to accept this, typically via a command line argument that accepts a path to a file with the secret or as an environment variable. + +Once the user has configured both Commit-Boost and your module with these settings, your module will be able to authenticate with the signer service and request signatures. + + +## The Signing ID + +Your module's signing ID is a 32-byte value that is used as a unique identifier within the signing process. Proposer commitment signatures incorporate this value along with the data being signed as a way to create signatures that are exclusive to your module, so other modules can't maliciously construct signatures that appear to be from your module. Your module must have this ID incorporated into itself ahead of time, and the user must include this same ID within their Commit-Boost configuration file section for your module. Commit-Boost does not maintain a global registry of signing IDs, so this is a value you should provide to your users in your documentation. + +The Signing ID is decoupled from your module's human-readable name (the `module_id` field in the Commit-Boost configuration file) so that any changes to your module name will not invalidate signatures from previous versions. Similarly, if you don't change the module ID but *want* to invalidate previous signatures, you can modify the signing ID and it will do so. Just ensure your users are made aware of the change, so they can update it in their Commit-Boost configuration files accordingly. + + +## Structure of a Signature + +The form proposer commitment signatures take depends on the type of signature being requested. BLS signatures take the [standard form](https://eth2book.info/latest/part2/building_blocks/signatures/) (96-byte values). ECDSA (Ethereum EL) signatures take the [standard Ethereum ECDSA `r,s,v` signature form](https://forum.openzeppelin.com/t/sign-it-like-you-mean-it-creating-and-verifying-ethereum-signatures/697). In both cases, the data being signed is a 32-byte hash - the root hash of an SSZ Merkle tree, described below: + +
+ + + +
+ +where: + +- `Request Data` is a 32-byte array that serves as the data you want to sign. This is typically a hash of some more complex data on its own that your module constructs. + +- `Signing ID` is your module's 32-byte signing ID. The signer service will load this for your module from its configuration file. + +- `Domain` is the 32-byte output of the [compute_domain()](https://eth2book.info/capella/part2/building_blocks/signatures/#domain-separation-and-forks) function in the Beacon specification. The 4-byte domain type in this case is not a standard Beacon domain type, but rather Commit-Boost's own domain type: `0x6D6D6F43`. + +The data signed in a proposer commitment is the 32-byte root of this tree (the green `Root` box). Note that calculating this will involve calculating the Merkle Root of two separate trees: first the blue data subtree (with the original request data and the signing ID) to establish the blue `Root` value, and then again with a tree created from that value and the `Domain`. + +Many languages provide libraries for computing the root of an SSZ Merkle tree, such as [fastssz for Go](https://github.com/ferranbt/fastssz) or [tree_hash for Rust](https://docs.rs/tree_hash/latest/tree_hash/). When verifying proposer commitment signatures, use a library that supports Merkle tree root hashing, the `compute_domain()` operation, and validation for signatures generated by your key of choice. diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index 4448cd89..ed2ffa6e 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -341,6 +341,7 @@ Delegation signatures will be stored in files with the format `/deleg A full example of a config file with Dirk can be found [here](https://github.com/Commit-Boost/commit-boost-client/blob/main/examples/configs/dirk_signer.toml). + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: ```bash @@ -375,6 +376,7 @@ enabled = true id = "DA_COMMIT" type = "commit" docker_image = "test_da_commit" +signing_id = "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" sleep_secs = 5 [[modules]] @@ -385,10 +387,11 @@ docker_image = "test_builder_log" A few things to note: - We now added a `signer` section which will be used to create the Signer module. -- There is now a `[[modules]]` section which at a minimum needs to specify the module `id`, `type` and `docker_image`. Additional parameters needed for the business logic of the module will also be here, +- There is now a `[[modules]]` section which 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 propser commitment documentation](../developing/prop-commit-signing.md)). Additional parameters needed for the business logic of the module will also be here. To learn more about developing modules, check out [here](/category/developing). + ## Vouch [Vouch](https://github.com/attestantio/vouch) is a multi-node validator client built by [Attestant](https://www.attestant.io/). Vouch is particular in that it also integrates an MEV-Boost client to interact with relays. The Commit-Boost PBS module 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`. diff --git a/docs/docs/res/img/prop_commit_tree.png b/docs/docs/res/img/prop_commit_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..1e36f4b4d3fcac21e74255fa6d617a206ad2a323 GIT binary patch literal 57442 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 SRYXcexWKAfD&_Z}f&T$W;}+`x literal 0 HcmV?d00001 diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 71b61c53..c73b2191 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use alloy::primitives::Address; +use alloy::primitives::{b256, Address, B256}; use commit_boost::prelude::*; use eyre::{OptionExt, Result}; use lazy_static::lazy_static; @@ -9,6 +9,13 @@ use serde::Deserialize; use tokio::time::sleep; use tracing::{error, info}; +// This is the signing ID used for the DA Commit module. +// Signatures produced by the signer service will incorporate this ID as part of +// the signature, preventing other modules from using the same signature for +// different purposes. +pub const DA_COMMIT_SIGNING_ID: B256 = + b256!("0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b"); + // You can define custom metrics and a custom registry for the business logic of // your module. These will be automatically scaped by the Prometheus server lazy_static! { @@ -83,17 +90,38 @@ impl DaCommitService { ) -> Result<()> { let datagram = Datagram { data }; + // Request a signature directly from a BLS key let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); let signature = self.config.signer_client.request_consensus_signature(request).await?; - info!("Proposer commitment (consensus): {}", signature); + match verify_proposer_commitment_signature_bls( + self.config.chain, + &pubkey, + &datagram, + &signature, + &DA_COMMIT_SIGNING_ID, + ) { + Ok(_) => info!("Signature verified successfully"), + Err(err) => error!(%err, "Signature verification failed"), + }; + // Request a signature from a proxy BLS key let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); let proxy_signature_bls = self.config.signer_client.request_proxy_signature_bls(proxy_request_bls).await?; - info!("Proposer commitment (proxy BLS): {}", proxy_signature_bls); + match verify_proposer_commitment_signature_bls( + self.config.chain, + &proxy_bls, + &datagram, + &proxy_signature_bls, + &DA_COMMIT_SIGNING_ID, + ) { + Ok(_) => info!("Signature verified successfully"), + Err(err) => error!(%err, "Signature verification failed"), + }; + // If ECDSA keys are enabled, request a signature from a proxy ECDSA key if let Some(proxy_ecdsa) = proxy_ecdsa { let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); let proxy_signature_ecdsa = self @@ -102,6 +130,16 @@ impl DaCommitService { .request_proxy_signature_ecdsa(proxy_request_ecdsa) .await?; info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); + match verify_proposer_commitment_signature_ecdsa( + self.config.chain, + &proxy_ecdsa, + &datagram, + &proxy_signature_ecdsa, + &DA_COMMIT_SIGNING_ID, + ) { + Ok(_) => info!("Signature verified successfully"), + Err(err) => error!(%err, "Signature verification failed"), + }; } SIG_RECEIVED_COUNTER.inc(); diff --git a/tests/data/configs/signer.happy.toml b/tests/data/configs/signer.happy.toml new file mode 100644 index 00000000..6fb76445 --- /dev/null +++ b/tests/data/configs/signer.happy.toml @@ -0,0 +1,52 @@ +chain = "Hoodi" + +[pbs] +docker_image = "ghcr.io/commit-boost/pbs:latest" +with_signer = true +host = "127.0.0.1" +port = 18550 +relay_check = true +wait_all_registrations = true +timeout_get_header_ms = 950 +timeout_get_payload_ms = 4000 +timeout_register_validator_ms = 3000 +skip_sigverify = false +min_bid_eth = 0.5 +late_in_slot_time_ms = 2000 +extra_validation_enabled = false +rpc_url = "https://ethereum-holesky-rpc.publicnode.com" + +[[relays]] +id = "example-relay" +url = "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz" +headers = { X-MyCustomHeader = "MyCustomHeader" } +enable_timing_games = false +target_first_request_ms = 200 +frequency_get_header_ms = 300 + +[signer] +docker_image = "ghcr.io/commit-boost/signer:latest" +host = "127.0.0.1" +port = 20000 +jwt_auth_fail_limit = 3 +jwt_auth_fail_timeout_seconds = 300 + +[signer.local.loader] +key_path = "./tests/data/keys.example.json" + +[signer.local.store] +proxy_dir = "./proxies" + +[[modules]] +id = "test-module" +signing_id = "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" +type = "commit" +docker_image = "test_da_commit" +env_file = ".cb.env" + +[[modules]] +id = "another-module" +signing_id = "0x61fe00135d7b4912a8c63ada215ac2e62326e6e7b30f49a29fcf9779d7ad800d" +type = "commit" +docker_image = "test_da_commit" +env_file = ".cb.env" diff --git a/tests/src/lib.rs b/tests/src/lib.rs index a4fbbb6a..54aedc46 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,3 +1,4 @@ pub mod mock_relay; pub mod mock_validator; +pub mod signer_service; pub mod utils; diff --git a/tests/src/mock_relay.rs b/tests/src/mock_relay.rs index 04ebfc24..414b82fe 100644 --- a/tests/src/mock_relay.rs +++ b/tests/src/mock_relay.rs @@ -121,8 +121,8 @@ async fn handle_get_header( response.message.pubkey = blst_pubkey_to_alloy(&state.signer.sk_to_pk()); response.message.header.timestamp = timestamp_of_slot_start_sec(0, state.chain); - let object_root = response.message.tree_hash_root().0; - response.signature = sign_builder_root(state.chain, &state.signer, object_root); + let object_root = response.message.tree_hash_root(); + response.signature = sign_builder_root(state.chain, &state.signer, &object_root); let response = GetHeaderResponse::Electra(response); (StatusCode::OK, Json(response)).into_response() diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs new file mode 100644 index 00000000..5270e2a8 --- /dev/null +++ b/tests/src/signer_service.rs @@ -0,0 +1,87 @@ +use std::{collections::HashMap, time::Duration}; + +use alloy::{hex, primitives::FixedBytes}; +use cb_common::{ + commit::request::GetPubkeysResponse, + config::{ModuleSigningConfig, StartSignerConfig}, + constants::SIGNER_JWT_EXPIRATION, + signer::{SignerLoader, ValidatorKeysFormat}, + types::{Chain, Jwt, JwtAdmin, ModuleId}, +}; +use cb_signer::service::SigningService; +use eyre::Result; +use reqwest::{Response, StatusCode}; +use tracing::info; + +use crate::utils::{get_signer_config, get_start_signer_config}; + +// Starts the signer moduler server on a separate task and returns its +// configuration +pub async fn start_server( + port: u16, + mod_signing_configs: &HashMap, + admin_secret: String, +) -> Result { + let chain = Chain::Hoodi; + + // Create a signer config + let loader = SignerLoader::ValidatorsDir { + keys_path: "data/keystores/keys".into(), + secrets_path: "data/keystores/secrets".into(), + format: ValidatorKeysFormat::Lighthouse, + }; + let mut config = get_signer_config(loader); + config.port = port; + config.jwt_auth_fail_limit = 3; // Set a low fail limit for testing + config.jwt_auth_fail_timeout_seconds = 3; // Set a short timeout for testing + let start_config = get_start_signer_config(config, chain, mod_signing_configs, admin_secret); + + // Run the Signer + let server_handle = tokio::spawn(SigningService::run(start_config.clone())); + + // Make sure the server is running + tokio::time::sleep(Duration::from_millis(100)).await; + if server_handle.is_finished() { + return Err(eyre::eyre!( + "Signer service failed to start: {}", + server_handle.await.unwrap_err() + )); + } + Ok(start_config) +} + +// Verifies that the pubkeys returned by the server match the pubkeys in the +// test data +pub async fn verify_pubkeys(response: Response) -> Result<()> { + // Verify the expected pubkeys are returned + assert!(response.status() == StatusCode::OK); + let pubkey_json = response.json::().await?; + assert_eq!(pubkey_json.keys.len(), 2); + let expected_pubkeys = vec![ + FixedBytes::new(hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4")), + FixedBytes::new(hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9")), + ]; + for expected in expected_pubkeys { + assert!( + pubkey_json.keys.iter().any(|k| k.consensus == expected), + "Expected pubkey not found: {:?}", + expected + ); + info!("Server returned expected pubkey: {:?}", expected); + } + Ok(()) +} + +// Creates a JWT for module administration +pub fn create_admin_jwt(admin_secret: String) -> Result { + jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &JwtAdmin { + admin: true, + exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + }, + &jsonwebtoken::EncodingKey::from_secret(admin_secret.as_ref()), + ) + .map_err(Into::into) + .map(Jwt::from) +} diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 30835ebb..0493040c 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -4,12 +4,17 @@ use std::{ sync::{Arc, Once}, }; -use alloy::{primitives::U256, rpc::types::beacon::BlsPublicKey}; +use alloy::{ + primitives::{B256, U256}, + rpc::types::beacon::BlsPublicKey, +}; use cb_common::{ config::{ - PbsConfig, PbsModuleConfig, RelayConfig, SignerConfig, SignerType, StartSignerConfig, - SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, - SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_PORT_DEFAULT, + CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, + PbsModuleConfig, RelayConfig, SignerConfig, SignerType, StartSignerConfig, + StaticModuleConfig, StaticPbsConfig, SIGNER_IMAGE_DEFAULT, + SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, + SIGNER_PORT_DEFAULT, }, pbs::{RelayClient, RelayEntry}, signer::SignerLoader, @@ -65,7 +70,7 @@ pub fn generate_mock_relay_with_batch_size( RelayClient::new(config) } -pub fn get_pbs_static_config(port: u16) -> PbsConfig { +pub fn get_pbs_config(port: u16) -> PbsConfig { PbsConfig { host: Ipv4Addr::UNSPECIFIED, port, @@ -84,6 +89,23 @@ pub fn get_pbs_static_config(port: u16) -> PbsConfig { } } +pub fn get_pbs_static_config(pbs_config: PbsConfig) -> StaticPbsConfig { + StaticPbsConfig { docker_image: String::from(""), pbs_config, with_signer: true } +} + +pub fn get_commit_boost_config(pbs_static_config: StaticPbsConfig) -> CommitBoostConfig { + CommitBoostConfig { + chain: Chain::Hoodi, + relays: vec![], + pbs: pbs_static_config, + muxes: None, + modules: Some(vec![]), + signer: None, + metrics: None, + logs: LogsSettings::default(), + } +} + pub fn to_pbs_config( chain: Chain, pbs_config: PbsConfig, @@ -115,7 +137,7 @@ pub fn get_signer_config(loader: SignerLoader) -> SignerConfig { pub fn get_start_signer_config( signer_config: SignerConfig, chain: Chain, - jwts: HashMap, + mod_signing_configs: &HashMap, admin_secret: String, ) -> StartSignerConfig { match signer_config.inner { @@ -124,7 +146,7 @@ pub fn get_start_signer_config( loader: Some(loader), store: None, endpoint: SocketAddr::new(signer_config.host.into(), signer_config.port), - jwts, + mod_signing_configs: mod_signing_configs.clone(), admin_secret, jwt_auth_fail_limit: signer_config.jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds: signer_config.jwt_auth_fail_timeout_seconds, @@ -133,3 +155,14 @@ pub fn get_start_signer_config( _ => panic!("Only local signers are supported in tests"), } } + +pub fn create_module_config(id: ModuleId, signing_id: B256) -> StaticModuleConfig { + StaticModuleConfig { + id, + signing_id, + docker_image: String::from(""), + env: None, + env_file: None, + kind: ModuleKind::Commit, + } +} diff --git a/tests/tests/pbs_get_header.rs b/tests/tests/pbs_get_header.rs index 10f30b6a..088fedb2 100644 --- a/tests/tests/pbs_get_header.rs +++ b/tests/tests/pbs_get_header.rs @@ -12,7 +12,7 @@ use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, mock_validator::MockValidator, - utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, + utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; use reqwest::StatusCode; @@ -35,7 +35,7 @@ async fn test_get_header() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), vec![mock_relay.clone()]); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -58,7 +58,7 @@ async fn test_get_header() -> Result<()> { assert_eq!(res.message.header.timestamp, timestamp_of_slot_start_sec(0, chain)); assert_eq!( res.signature, - sign_builder_root(chain, &mock_state.signer, res.message.tree_hash_root().0) + sign_builder_root(chain, &mock_state.signer, &res.message.tree_hash_root()) ); Ok(()) } @@ -81,7 +81,7 @@ async fn test_get_header_returns_204_if_relay_down() -> Result<()> { // tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), vec![mock_relay.clone()]); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -113,7 +113,7 @@ async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), vec![mock_relay.clone()]); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), vec![mock_relay.clone()]); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); diff --git a/tests/tests/pbs_get_status.rs b/tests/tests/pbs_get_status.rs index 3e913dc5..629bea69 100644 --- a/tests/tests/pbs_get_status.rs +++ b/tests/tests/pbs_get_status.rs @@ -9,7 +9,7 @@ use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, mock_validator::MockValidator, - utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, + utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; use reqwest::StatusCode; @@ -34,7 +34,7 @@ async fn test_get_status() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_0_port)); tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_1_port)); - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays.clone()); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -67,7 +67,7 @@ async fn test_get_status_returns_502_if_relay_down() -> Result<()> { // Don't start the relay // tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays.clone()); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays.clone()); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs index 4d830e20..84fa1a3d 100644 --- a/tests/tests/pbs_mux.rs +++ b/tests/tests/pbs_mux.rs @@ -10,7 +10,7 @@ use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, mock_validator::MockValidator, - utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, + utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; use reqwest::StatusCode; @@ -37,7 +37,7 @@ async fn test_mux() -> Result<()> { // Register all relays in PBS config let relays = vec![default_relay.clone()]; - let mut config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let mut config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); config.all_relays = vec![mux_relay_1.clone(), mux_relay_2.clone(), default_relay.clone()]; // Configure mux for two relays diff --git a/tests/tests/pbs_post_blinded_blocks.rs b/tests/tests/pbs_post_blinded_blocks.rs index 9e91dfa9..24b7e66b 100644 --- a/tests/tests/pbs_post_blinded_blocks.rs +++ b/tests/tests/pbs_post_blinded_blocks.rs @@ -10,7 +10,7 @@ use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, mock_validator::MockValidator, - utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, + utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; use reqwest::{Response, StatusCode}; @@ -47,7 +47,7 @@ async fn test_submit_block_too_large() -> Result<()> { let mock_state = Arc::new(MockRelayState::new(chain, signer).with_large_body()); tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -77,7 +77,7 @@ async fn submit_block_impl(pbs_port: u16, api_version: &BuilderApiVersion) -> Re tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs index f2480ac1..1ab3a786 100644 --- a/tests/tests/pbs_post_validators.rs +++ b/tests/tests/pbs_post_validators.rs @@ -10,7 +10,7 @@ use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, mock_validator::MockValidator, - utils::{generate_mock_relay, get_pbs_static_config, setup_test_env, to_pbs_config}, + utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; use reqwest::StatusCode; @@ -31,7 +31,7 @@ async fn test_register_validators() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -77,7 +77,7 @@ async fn test_register_validators_returns_422_if_request_is_malformed() -> Resul tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); @@ -220,7 +220,7 @@ async fn test_register_validators_does_not_retry_on_429() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); // Run the PBS service - let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays); + let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); let state = PbsState::new(config); tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state.clone())); @@ -272,7 +272,7 @@ async fn test_register_validators_retries_on_500() -> Result<()> { tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); // Set retry limit to 3 - let mut pbs_config = get_pbs_static_config(pbs_port); + let mut pbs_config = get_pbs_config(pbs_port); pbs_config.register_validator_retry_limit = 3; let config = to_pbs_config(chain, pbs_config, relays); diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index 820afbcc..63f0783f 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -1,35 +1,48 @@ use std::{collections::HashMap, time::Duration}; -use alloy::{hex, primitives::FixedBytes}; +use alloy::primitives::b256; use cb_common::{ - commit::{ - constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, - request::GetPubkeysResponse, - }, - config::StartSignerConfig, - constants::SIGNER_JWT_EXPIRATION, - signer::{SignerLoader, ValidatorKeysFormat}, - types::{Chain, Jwt, JwtAdmin, ModuleId}, + commit::constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, + config::{load_module_signing_configs, ModuleSigningConfig}, + types::ModuleId, utils::create_jwt, }; -use cb_signer::service::SigningService; -use cb_tests::utils::{get_signer_config, get_start_signer_config, setup_test_env}; +use cb_tests::{ + signer_service::{create_admin_jwt, start_server, verify_pubkeys}, + utils::{self, setup_test_env}, +}; use eyre::Result; -use reqwest::{Response, StatusCode}; +use reqwest::StatusCode; use tracing::info; const JWT_MODULE: &str = "test-module"; const JWT_SECRET: &str = "test-jwt-secret"; const ADMIN_SECRET: &str = "test-admin-secret"; +async fn create_mod_signing_configs() -> HashMap { + let mut cfg = + utils::get_commit_boost_config(utils::get_pbs_static_config(utils::get_pbs_config(0))); + + let module_id = ModuleId(JWT_MODULE.to_string()); + let signing_id = b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + cfg.modules = Some(vec![utils::create_module_config(module_id.clone(), signing_id)]); + + let jwts = HashMap::from([(module_id.clone(), JWT_SECRET.to_string())]); + + load_module_signing_configs(&cfg, &jwts).unwrap() +} + #[tokio::test] async fn test_signer_jwt_auth_success() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); - let start_config = start_server(20100).await?; + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request - let jwt = create_jwt(&module_id, JWT_SECRET)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); let response = client.get(&url).bearer_auth(&jwt).send().await?; @@ -44,7 +57,8 @@ async fn test_signer_jwt_auth_success() -> Result<()> { async fn test_signer_jwt_auth_fail() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); - let start_config = start_server(20200).await?; + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string()).await?; // Run a pubkeys request - this should fail due to invalid JWT let jwt = create_jwt(&module_id, "incorrect secret")?; @@ -64,7 +78,9 @@ async fn test_signer_jwt_auth_fail() -> Result<()> { async fn test_signer_jwt_rate_limit() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); - let start_config = start_server(20300).await?; + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run as many pubkeys requests as the fail limit let jwt = create_jwt(&module_id, "incorrect secret")?; @@ -76,7 +92,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { } // Run another request - this should fail due to rate limiting now - let jwt = create_jwt(&module_id, JWT_SECRET)?; + let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret)?; let response = client.get(&url).bearer_auth(&jwt).send().await?; assert!(response.status() == StatusCode::TOO_MANY_REQUESTS); @@ -94,12 +110,14 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { #[tokio::test] async fn test_signer_revoked_jwt_fail() -> Result<()> { setup_test_env(); + let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); - let start_config = start_server(20400).await?; + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20400, &mod_cfgs, admin_secret.clone()).await?; // Run as many pubkeys requests as the fail limit let jwt = create_jwt(&module_id, JWT_SECRET)?; - let admin_jwt = create_admin_jwt()?; + let admin_jwt = create_admin_jwt(admin_secret)?; let client = reqwest::Client::new(); // At first, test module should be allowed to request pubkeys @@ -127,12 +145,14 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { #[tokio::test] async fn test_signer_only_admin_can_revoke() -> Result<()> { setup_test_env(); + let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); - let start_config = start_server(20500).await?; + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20500, &mod_cfgs, admin_secret.clone()).await?; // Run as many pubkeys requests as the fail limit let jwt = create_jwt(&module_id, JWT_SECRET)?; - let admin_jwt = create_admin_jwt()?; + let admin_jwt = create_admin_jwt(admin_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); @@ -158,75 +178,3 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { Ok(()) } - -// Starts the signer moduler server on a separate task and returns its -// configuration -async fn start_server(port: u16) -> Result { - setup_test_env(); - let chain = Chain::Hoodi; - - // Mock JWT secrets - let module_id = ModuleId(JWT_MODULE.to_string()); - let mut jwts = HashMap::new(); - jwts.insert(module_id.clone(), JWT_SECRET.to_string()); - - // Create a signer config - let loader = SignerLoader::ValidatorsDir { - keys_path: "data/keystores/keys".into(), - secrets_path: "data/keystores/secrets".into(), - format: ValidatorKeysFormat::Lighthouse, - }; - let mut config = get_signer_config(loader); - config.port = port; - config.jwt_auth_fail_limit = 3; // Set a low fail limit for testing - config.jwt_auth_fail_timeout_seconds = 3; // Set a short timeout for testing - let start_config = get_start_signer_config(config, chain, jwts, ADMIN_SECRET.to_string()); - - // Run the Signer - let server_handle = tokio::spawn(SigningService::run(start_config.clone())); - - // Make sure the server is running - tokio::time::sleep(Duration::from_millis(100)).await; - if server_handle.is_finished() { - return Err(eyre::eyre!( - "Signer service failed to start: {}", - server_handle.await.unwrap_err() - )); - } - Ok(start_config) -} - -// Verifies that the pubkeys returned by the server match the pubkeys in the -// test data -async fn verify_pubkeys(response: Response) -> Result<()> { - // Verify the expected pubkeys are returned - assert!(response.status() == StatusCode::OK); - let pubkey_json = response.json::().await?; - assert_eq!(pubkey_json.keys.len(), 2); - let expected_pubkeys = vec![ - FixedBytes::new(hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4")), - FixedBytes::new(hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9")), - ]; - for expected in expected_pubkeys { - assert!( - pubkey_json.keys.iter().any(|k| k.consensus == expected), - "Expected pubkey not found: {:?}", - expected - ); - info!("Server returned expected pubkey: {:?}", expected); - } - Ok(()) -} - -fn create_admin_jwt() -> Result { - jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &JwtAdmin { - admin: true, - exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, - }, - &jsonwebtoken::EncodingKey::from_secret(ADMIN_SECRET.as_ref()), - ) - .map_err(Into::into) - .map(Jwt::from) -} diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs new file mode 100644 index 00000000..868a1f71 --- /dev/null +++ b/tests/tests/signer_request_sig.rs @@ -0,0 +1,113 @@ +use std::collections::HashMap; + +use alloy::{ + hex, + primitives::{b256, FixedBytes}, +}; +use cb_common::{ + commit::{ + constants::REQUEST_SIGNATURE_PATH, + request::{SignConsensusRequest, SignRequest}, + }, + config::{load_module_signing_configs, ModuleSigningConfig}, + types::ModuleId, + utils::create_jwt, +}; +use cb_tests::{ + signer_service::start_server, + utils::{self, setup_test_env}, +}; +use eyre::Result; +use reqwest::StatusCode; + +const MODULE_ID_1: &str = "test-module"; +const MODULE_ID_2: &str = "another-module"; +const PUBKEY_1: [u8; 48] = + hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4"); +const ADMIN_SECRET: &str = "test-admin-secret"; + +async fn create_mod_signing_configs() -> HashMap { + let mut cfg = + utils::get_commit_boost_config(utils::get_pbs_static_config(utils::get_pbs_config(0))); + + let module_id_1 = ModuleId(MODULE_ID_1.to_string()); + let signing_id_1 = b256!("0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b"); + let module_id_2 = ModuleId(MODULE_ID_2.to_string()); + let signing_id_2 = b256!("0x61fe00135d7b4912a8c63ada215ac2e62326e6e7b30f49a29fcf9779d7ad800d"); + + cfg.modules = Some(vec![ + utils::create_module_config(module_id_1.clone(), signing_id_1), + utils::create_module_config(module_id_2.clone(), signing_id_2), + ]); + + let jwts = HashMap::from([ + (module_id_1.clone(), "supersecret".to_string()), + (module_id_2.clone(), "anothersecret".to_string()), + ]); + + load_module_signing_configs(&cfg, &jwts).unwrap() +} + +/// Makes sure the signer service signs requests correctly, using the module's +/// signing ID +#[tokio::test] +async fn test_signer_sign_request_good() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(MODULE_ID_1.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20200, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); + + // Send a signing request + let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); + let request = + SignRequest::Consensus(SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }); + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_PATH); + let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; + + // Verify the response is successful + assert!(response.status() == StatusCode::OK); + + // Verify the signature is returned + let signature = response.text().await?; + assert!(!signature.is_empty(), "Signature should not be empty"); + + let expected_signature = "\"0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115\""; + assert_eq!(signature, expected_signature, "Signature does not match expected value"); + + Ok(()) +} + +/// Makes sure the signer service returns a signature that is different for each +/// module +#[tokio::test] +async fn test_signer_sign_request_different_module() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(MODULE_ID_2.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20201, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); + + // Send a signing request + let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); + let request = + SignRequest::Consensus(SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }); + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_PATH); + let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; + + // Verify the response is successful + assert!(response.status() == StatusCode::OK); + + // Verify the signature is returned + let signature = response.text().await?; + assert!(!signature.is_empty(), "Signature should not be empty"); + + let incorrect_signature = "\"0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115\""; + assert_ne!(signature, incorrect_signature, "Signature does not match expected value"); + + Ok(()) +} From af1308961e2e72e63d0dcba72f0959bbe65142d3 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 19 Aug 2025 14:41:38 -0400 Subject: [PATCH 03/30] Split `request_signature` into separate paths that return JSON (#350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: eltitanb Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao --- Cargo.lock | 1 + Cargo.toml | 1 + api/signer-api.yml | 373 +++++++++++++++--- crates/common/Cargo.toml | 1 + crates/common/src/commit/client.rs | 38 +- crates/common/src/commit/constants.rs | 9 +- crates/common/src/commit/mod.rs | 1 + crates/common/src/commit/request.rs | 98 ++--- crates/common/src/commit/response.rs | 45 +++ crates/signer/src/constants.rs | 4 +- crates/signer/src/metrics.rs | 10 +- crates/signer/src/service.rs | 177 ++++++--- examples/da_commit/src/main.rs | 18 +- .../grafana/signer_public_dashboard.json | 24 +- tests/tests/signer_request_sig.rs | 53 ++- 15 files changed, 637 insertions(+), 216 deletions(-) create mode 100644 crates/common/src/commit/response.rs diff --git a/Cargo.lock b/Cargo.lock index 3afd9e63..539d458b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,6 +1487,7 @@ dependencies = [ "blst", "bytes", "cipher 0.4.4", + "const_format", "ctr 0.9.2", "derive_more 2.0.1", "docker-image", diff --git a/Cargo.toml b/Cargo.toml index 3955e9cc..d3a6d7d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ cb-signer = { path = "crates/signer" } cipher = "0.4" clap = { version = "4.5.4", features = ["derive", "env"] } color-eyre = "0.6.3" +const_format = "0.2.34" ctr = "0.9.2" derive_more = { version = "2.0.1", features = ["deref", "display", "from", "into"] } docker-compose-types = "0.16.0" diff --git a/api/signer-api.yml b/api/signer-api.yml index 69239e38..a6c427a4 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -1,7 +1,7 @@ -openapi: "3.0.2" +openapi: "3.1.1" info: title: Signer API - version: "0.1.0" + version: "0.2.0" description: API that allows commit modules to request generic signatures from validators tags: - name: Signer @@ -58,9 +58,9 @@ paths: type: string example: "Internal error" - /signer/v1/request_signature: + /signer/v1/request_signature/bls: post: - summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the requested BLS or ECDSA key. + summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested public key. tags: - Signer security: @@ -71,65 +71,309 @@ paths: application/json: schema: type: object - required: [type, object_root] - oneOf: - - required: [pubkey] - - required: [proxy] + required: [pubkey, object_root] properties: - type: - description: Type of the sign request - type: string - enum: [consensus, proxy_bls, proxy_ecdsa] pubkey: description: The 48-byte BLS public key, with optional `0x` prefix, of the proposer key that you want to request a signature from. $ref: "#/components/schemas/BlsPubkey" + object_root: + description: The 32-byte data you want to sign, with optional `0x` prefix. + $ref: "#/components/schemas/B256" + example: + pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + responses: + "200": + description: A successful signature response. + content: + application/json: + schema: + $ref: "#/components/schemas/BlsSignatureResponse" + example: + pubkey: "0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" + object_root: "0x0123456789012345678901234567890123456789012345678901234567890123" + module_signing_id: "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" + signature: "0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115" + "400": + description: | + This can occur in several scenarios: + - The Commit-Boost configuration file does not contain a signing ID for the module that made the request. + - You requested an operation while using the Dirk signer mode instead of locally-managed signer mode, but Dirk doesn't support that operation. + - Something went wrong while preparing your request; the error text will provide more information. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 400 + message: + type: string + example: "Bad request: Invalid pubkey format" + "401": + description: The requesting module did not provide a JWT string in the request's authorization header, or the JWT string was not configured in the signer service's configuration file as belonging to the module. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 401 + message: + type: string + example: "Unauthorized" + + "404": + description: You either requested a route that doesn't exist, or you requested a signature from a key that does not exist. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 404 + message: + type: string + example: "Unknown pubkey" + "429": + description: Your module attempted and failed JWT authentication too many times recently, and is currently timed out. It cannot make any more requests until the timeout ends. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 429 + message: + type: string + example: "Too many requests" + "500": + description: Your request was valid, but something went wrong internally that prevented it from being fulfilled. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 500 + message: + type: string + example: "Internal error" + "502": + description: The signer service is running in Dirk signer mode, but Dirk could not be reached. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 502 + message: + type: string + example: "Bad gateway: Dirk signer service is unreachable" + + /signer/v1/request_signature/proxy-bls: + post: + summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested proxy public key. + tags: + - Signer + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [proxy, object_root] + 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. - oneOf: - - $ref: "#/components/schemas/BlsPubkey" - - $ref: "#/components/schemas/EcdsaAddress" + $ref: "#/components/schemas/BlsPubkey" object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. - type: string - format: hex - pattern: "^0x[a-fA-F0-9]{64}$" - example: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" - examples: - Consensus: - value: - type: "consensus" - pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" - ProxyBls: - value: - type: "proxy_bls" - proxy: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" - ProxyEcdsa: - value: - type: "proxy_ecdsa" - proxy: "0x71f65e9f6336770e22d148bd5e89b391a1c3b0bb" - object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + $ref: "#/components/schemas/B256" + example: + pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" responses: "200": - description: A successful signature response. The returned signature is the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID as specified in the Commit-Boost configuration. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). + description: A successful signature response. content: application/json: schema: - oneOf: - - $ref: "#/components/schemas/BlsSignature" - - $ref: "#/components/schemas/EcdsaSignature" - examples: - Consensus: - value: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - ProxyBls: - value: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" - ProxyEcdsa: - value: "0x985b495f49d1b96db3bba3f6c5dd1810950317c10d4c2042bd316f338cdbe74359072e209b85e56ac492092d7860063dd096ca31b4e164ef27e3f8d508e656801c" + $ref: "#/components/schemas/BlsSignatureResponse" + example: + pubkey: "0x883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" + object_root: "0x0123456789012345678901234567890123456789012345678901234567890123" + module_signing_id: "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" + signature: "0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115" + "400": + description: | + This can occur in several scenarios: + - The Commit-Boost configuration file does not contain a signing ID for the module that made the request. + - You requested an operation while using the Dirk signer mode instead of locally-managed signer mode, but Dirk doesn't support that operation. + - Something went wrong while preparing your request; the error text will provide more information. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 400 + message: + type: string + example: "Bad request: Invalid pubkey format" + "401": + description: The requesting module did not provide a JWT string in the request's authorization header, or the JWT string was not configured in the signer service's configuration file as belonging to the module. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 401 + message: + type: string + example: "Unauthorized" + + "404": + description: You either requested a route that doesn't exist, or you requested a signature from a key that does not exist. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 404 + message: + type: string + example: "Unknown pubkey" + "429": + description: Your module attempted and failed JWT authentication too many times recently, and is currently timed out. It cannot make any more requests until the timeout ends. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 429 + message: + type: string + example: "Too many requests" + "500": + description: Your request was valid, but something went wrong internally that prevented it from being fulfilled. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 500 + message: + type: string + example: "Internal error" + "502": + description: The signer service is running in Dirk signer mode, but Dirk could not be reached. + content: + application/json: + schema: + type: object + required: + - code + - message + properties: + code: + type: number + example: 502 + message: + type: string + example: "Bad gateway: Dirk signer service is unreachable" + + /signer/v1/request_signature/proxy-ecdsa: + post: + summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the ECDSA private key for the requested proxy Ethereum address. + tags: + - Signer + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [proxy, object_root] + properties: + proxy: + description: The 20-byte Ethereum address, with optional `0x` prefix, of the proxy key that you want to request a signature from. + $ref: "#/components/schemas/EcdsaAddress" + object_root: + description: The 32-byte data you want to sign, with optional `0x` prefix. + $ref: "#/components/schemas/B256" + example: + proxy: "0x71f65e9f6336770e22d148bd5e89b391a1c3b0bb" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + responses: + "200": + description: A successful signature response. + content: + application/json: + schema: + $ref: "#/components/schemas/EcdsaSignatureResponse" + example: + address: "0x71f65e9f6336770e22d148bd5e89b391a1c3b0bb" + object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" + module_signing_id: "0x6a33a23ef26a4836979edff86c493a69b26ccf0b4a16491a815a13787657431b" + signature: "0x985b495f49d1b96db3bba3f6c5dd1810950317c10d4c2042bd316f338cdbe74359072e209b85e56ac492092d7860063dd096ca31b4e164ef27e3f8d508e656801c" "400": description: | This can occur in several scenarios: - + - The Commit-Boost configuration file does not contain a signing ID for the module that made the request. - You requested an operation while using the Dirk signer mode instead of locally-managed signer mode, but Dirk doesn't support that operation. - Something went wrong while preparing your request; the error text will provide more information. content: @@ -351,6 +595,11 @@ components: scheme: bearer bearerFormat: JWT schemas: + B256: + type: string + format: hex + pattern: "^0x[a-fA-F0-9]{64}$" + example: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" BlsPubkey: type: string format: hex @@ -371,3 +620,33 @@ components: format: hex pattern: "^0x[a-fA-F0-9]{130}$" example: "0x985b495f49d1b96db3bba3f6c5dd1810950317c10d4c2042bd316f338cdbe74359072e209b85e56ac492092d7860063dd096ca31b4e164ef27e3f8d508e656801c" + BlsSignatureResponse: + type: object + properties: + pubkey: + description: The BLS public key corresponding to the private key that was used to sign the request + $ref: "#/components/schemas/BlsPubkey" + object_root: + description: The 32-byte data that was signed, with `0x` prefix + $ref: "#/components/schemas/B256" + signing_id: + description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration + $ref: "#/components/schemas/B256" + signature: + description: The BLS signature of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). + $ref: "#/components/schemas/BlsSignature" + EcdsaSignatureResponse: + type: object + properties: + address: + description: The ECDSA address corresponding to the private key that was used to sign the request + $ref: "#/components/schemas/EcdsaAddress" + object_root: + description: The 32-byte data that was signed, with `0x` prefix + $ref: "#/components/schemas/B256" + module_signing_id: + description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration + $ref: "#/components/schemas/B256" + signature: + description: The ECDSA signature (in Ethereum RSV format) of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). + $ref: "#/components/schemas/EcdsaSignature" \ No newline at end of file diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index fe2f9aec..8653ea44 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -15,6 +15,7 @@ bimap.workspace = true blst.workspace = true bytes.workspace = true cipher.workspace = true +const_format.workspace = true ctr.workspace = true derive_more.workspace = true docker-image.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 34413b65..6945b4d2 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,22 +1,29 @@ use std::time::{Duration, Instant}; -use alloy::{primitives::Address, rpc::types::beacon::BlsSignature}; +use alloy::primitives::Address; use eyre::WrapErr; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use url::Url; use super::{ - constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH}, + constants::{GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH}, error::SignerClientError, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, ProxyId, SignConsensusRequest, - SignProxyRequest, SignRequest, SignedProxyDelegation, + SignProxyRequest, SignedProxyDelegation, }, }; use crate::{ + commit::{ + constants::{ + REQUEST_SIGNATURE_BLS_PATH, REQUEST_SIGNATURE_PROXY_BLS_PATH, + REQUEST_SIGNATURE_PROXY_ECDSA_PATH, + }, + response::{BlsSignResponse, EcdsaSignResponse}, + }, constants::SIGNER_JWT_EXPIRATION, - signer::{BlsPublicKey, EcdsaSignature}, + signer::BlsPublicKey, types::{Jwt, ModuleId}, utils::create_jwt, DEFAULT_REQUEST_TIMEOUT, @@ -99,13 +106,18 @@ impl SignerClient { } /// Send a signature request - async fn request_signature(&mut self, request: &SignRequest) -> Result + async fn request_signature( + &mut self, + route: &str, + request: &Q, + ) -> Result where + Q: Serialize, T: for<'de> Deserialize<'de>, { self.refresh_jwt()?; - let url = self.url.join(REQUEST_SIGNATURE_PATH)?; + let url = self.url.join(route)?; let res = self.client.post(url).json(&request).send().await?; let status = res.status(); @@ -126,22 +138,22 @@ impl SignerClient { pub async fn request_consensus_signature( &mut self, request: SignConsensusRequest, - ) -> Result { - self.request_signature(&request.into()).await + ) -> Result { + self.request_signature(REQUEST_SIGNATURE_BLS_PATH, &request).await } pub async fn request_proxy_signature_ecdsa( &mut self, request: SignProxyRequest
, - ) -> Result { - self.request_signature(&request.into()).await + ) -> Result { + self.request_signature(REQUEST_SIGNATURE_PROXY_ECDSA_PATH, &request).await } pub async fn request_proxy_signature_bls( &mut self, request: SignProxyRequest, - ) -> Result { - self.request_signature(&request.into()).await + ) -> Result { + self.request_signature(REQUEST_SIGNATURE_PROXY_BLS_PATH, &request).await } async fn generate_proxy_key( diff --git a/crates/common/src/commit/constants.rs b/crates/common/src/commit/constants.rs index ea9cd9bb..f2d5e94c 100644 --- a/crates/common/src/commit/constants.rs +++ b/crates/common/src/commit/constants.rs @@ -1,5 +1,12 @@ +use const_format::concatcp; + pub const GET_PUBKEYS_PATH: &str = "/signer/v1/get_pubkeys"; -pub const REQUEST_SIGNATURE_PATH: &str = "/signer/v1/request_signature"; +pub const REQUEST_SIGNATURE_BASE_PATH: &str = "/signer/v1/request_signature"; +pub const REQUEST_SIGNATURE_BLS_PATH: &str = concatcp!(REQUEST_SIGNATURE_BASE_PATH, "/bls"); +pub const REQUEST_SIGNATURE_PROXY_BLS_PATH: &str = + concatcp!(REQUEST_SIGNATURE_BASE_PATH, "/proxy-bls"); +pub const REQUEST_SIGNATURE_PROXY_ECDSA_PATH: &str = + concatcp!(REQUEST_SIGNATURE_BASE_PATH, "/proxy-ecdsa"); pub const GENERATE_PROXY_KEY_PATH: &str = "/signer/v1/generate_proxy_key"; pub const STATUS_PATH: &str = "/status"; pub const RELOAD_PATH: &str = "/reload"; diff --git a/crates/common/src/commit/mod.rs b/crates/common/src/commit/mod.rs index 205785ff..193db630 100644 --- a/crates/common/src/commit/mod.rs +++ b/crates/common/src/commit/mod.rs @@ -2,3 +2,4 @@ pub mod client; pub mod constants; pub mod error; pub mod request; +pub mod response; diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 5bc3a14b..5e101092 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -9,7 +9,6 @@ use alloy::{ primitives::{aliases::B32, Address, B256}, rpc::types::beacon::BlsSignature, }; -use derive_more::derive::From; use serde::{Deserialize, Deserializer, Serialize}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; @@ -74,40 +73,6 @@ impl fmt::Display for SignedProxyDelegation { } } -// TODO(David): This struct shouldn't be visible to module authors -#[derive(Debug, Clone, Serialize, Deserialize, From)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum SignRequest { - Consensus(SignConsensusRequest), - ProxyBls(SignProxyRequest), - ProxyEcdsa(SignProxyRequest
), -} - -impl Display for SignRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SignRequest::Consensus(req) => write!( - f, - "Consensus(pubkey: {}, object_root: {})", - req.pubkey, - hex::encode_prefixed(req.object_root) - ), - SignRequest::ProxyBls(req) => write!( - f, - "BLS(proxy: {}, object_root: {})", - req.proxy, - hex::encode_prefixed(req.object_root) - ), - SignRequest::ProxyEcdsa(req) => write!( - f, - "ECDSA(proxy: {}, object_root: {})", - req.proxy, - hex::encode_prefixed(req.object_root) - ), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, @@ -132,6 +97,17 @@ impl SignConsensusRequest { } } +impl Display for SignConsensusRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Consensus(pubkey: {}, object_root: {})", + self.pubkey, + hex::encode_prefixed(self.object_root) + ) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignProxyRequest { pub proxy: T, @@ -156,6 +132,28 @@ impl SignProxyRequest { } } +impl Display for SignProxyRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "BLS(proxy: {}, object_root: {})", + self.proxy, + hex::encode_prefixed(self.object_root) + ) + } +} + +impl Display for SignProxyRequest
{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ECDSA(proxy: {}, object_root: {})", + self.proxy, + hex::encode_prefixed(self.object_root) + ) + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum EncryptionScheme { #[serde(rename = "bls")] @@ -249,36 +247,6 @@ mod tests { use super::*; use crate::signer::EcdsaSignature; - #[test] - fn test_decode_request_signature() { - let data = r#"{ - "type": "consensus", - "pubkey": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050", - "object_root": "0x5c89913beafa0472168e0ec05e349b4ceb9985d25ab9fa8de53a60208c85b3a5" - }"#; - - let request: SignRequest = serde_json::from_str(data).unwrap(); - assert!(matches!(request, SignRequest::Consensus(..))); - - let data = r#"{ - "type": "proxy_bls", - "proxy": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050", - "object_root": "0x5c89913beafa0472168e0ec05e349b4ceb9985d25ab9fa8de53a60208c85b3a5" - }"#; - - let request: SignRequest = serde_json::from_str(data).unwrap(); - assert!(matches!(request, SignRequest::ProxyBls(..))); - - let data = r#"{ - "type": "proxy_ecdsa", - "proxy": "0x4ca9939a8311a7cab3dde201b70157285fa81a9d", - "object_root": "0x5c89913beafa0472168e0ec05e349b4ceb9985d25ab9fa8de53a60208c85b3a5" - }"#; - - let request: SignRequest = serde_json::from_str(data).unwrap(); - assert!(matches!(request, SignRequest::ProxyEcdsa(..))); - } - #[test] fn test_decode_response_signature() { let data = r#""0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989""#; diff --git a/crates/common/src/commit/response.rs b/crates/common/src/commit/response.rs new file mode 100644 index 00000000..543fb6fc --- /dev/null +++ b/crates/common/src/commit/response.rs @@ -0,0 +1,45 @@ +use alloy::{ + primitives::{Address, B256}, + rpc::types::beacon::BlsSignature, +}; +use serde::{Deserialize, Serialize}; + +use crate::signer::{BlsPublicKey, EcdsaSignature}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct BlsSignResponse { + pub pubkey: BlsPublicKey, + pub object_root: B256, + pub module_signing_id: B256, + pub signature: BlsSignature, +} + +impl BlsSignResponse { + pub fn new( + pubkey: BlsPublicKey, + object_root: B256, + module_signing_id: B256, + signature: BlsSignature, + ) -> Self { + Self { pubkey, object_root, module_signing_id, signature } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct EcdsaSignResponse { + pub address: Address, + pub object_root: B256, + pub module_signing_id: B256, + pub signature: EcdsaSignature, +} + +impl EcdsaSignResponse { + pub fn new( + address: Address, + object_root: B256, + module_signing_id: B256, + signature: EcdsaSignature, + ) -> Self { + Self { address, object_root, module_signing_id, signature } + } +} diff --git a/crates/signer/src/constants.rs b/crates/signer/src/constants.rs index 268cd2e2..e5884d27 100644 --- a/crates/signer/src/constants.rs +++ b/crates/signer/src/constants.rs @@ -1,3 +1,5 @@ pub const GET_PUBKEYS_ENDPOINT_TAG: &str = "get_pubkeys"; pub const GENERATE_PROXY_KEY_ENDPOINT_TAG: &str = "generate_proxy_key"; -pub const REQUEST_SIGNATURE_ENDPOINT_TAG: &str = "request_signature"; +pub const REQUEST_SIGNATURE_BLS_ENDPOINT_TAG: &str = "request_signature_bls"; +pub const REQUEST_SIGNATURE_PROXY_BLS_ENDPOINT_TAG: &str = "request_signature_proxy_bls"; +pub const REQUEST_SIGNATURE_PROXY_ECDSA_ENDPOINT_TAG: &str = "request_signature_proxy_ecdsa"; diff --git a/crates/signer/src/metrics.rs b/crates/signer/src/metrics.rs index 85fa02b2..f7711d69 100644 --- a/crates/signer/src/metrics.rs +++ b/crates/signer/src/metrics.rs @@ -2,13 +2,15 @@ use axum::http::Uri; use cb_common::commit::constants::{ - GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_PATH, + GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, REQUEST_SIGNATURE_BLS_PATH, + REQUEST_SIGNATURE_PROXY_BLS_PATH, REQUEST_SIGNATURE_PROXY_ECDSA_PATH, }; use lazy_static::lazy_static; use prometheus::{register_int_counter_vec_with_registry, IntCounterVec, Registry}; use crate::constants::{ - GENERATE_PROXY_KEY_ENDPOINT_TAG, GET_PUBKEYS_ENDPOINT_TAG, REQUEST_SIGNATURE_ENDPOINT_TAG, + GENERATE_PROXY_KEY_ENDPOINT_TAG, GET_PUBKEYS_ENDPOINT_TAG, REQUEST_SIGNATURE_BLS_ENDPOINT_TAG, + REQUEST_SIGNATURE_PROXY_BLS_ENDPOINT_TAG, REQUEST_SIGNATURE_PROXY_ECDSA_ENDPOINT_TAG, }; lazy_static! { @@ -28,7 +30,9 @@ pub fn uri_to_tag(uri: &Uri) -> &str { match uri.path() { GET_PUBKEYS_PATH => GET_PUBKEYS_ENDPOINT_TAG, GENERATE_PROXY_KEY_PATH => GENERATE_PROXY_KEY_ENDPOINT_TAG, - REQUEST_SIGNATURE_PATH => REQUEST_SIGNATURE_ENDPOINT_TAG, + REQUEST_SIGNATURE_BLS_PATH => REQUEST_SIGNATURE_BLS_ENDPOINT_TAG, + REQUEST_SIGNATURE_PROXY_BLS_PATH => REQUEST_SIGNATURE_PROXY_BLS_ENDPOINT_TAG, + REQUEST_SIGNATURE_PROXY_ECDSA_PATH => REQUEST_SIGNATURE_PROXY_ECDSA_ENDPOINT_TAG, _ => "unknown endpoint", } } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 4ecf5e75..f69eb650 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -5,6 +5,10 @@ use std::{ time::{Duration, Instant}, }; +use alloy::{ + primitives::{Address, B256}, + rpc::types::beacon::BlsPublicKey, +}; use axum::{ extract::{ConnectInfo, Request, State}, http::StatusCode, @@ -17,13 +21,15 @@ use axum_extra::TypedHeader; use cb_common::{ commit::{ constants::{ - GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, RELOAD_PATH, REQUEST_SIGNATURE_PATH, + GENERATE_PROXY_KEY_PATH, GET_PUBKEYS_PATH, RELOAD_PATH, REQUEST_SIGNATURE_BLS_PATH, + REQUEST_SIGNATURE_PROXY_BLS_PATH, REQUEST_SIGNATURE_PROXY_ECDSA_PATH, REVOKE_MODULE_PATH, STATUS_PATH, }, request::{ EncryptionScheme, GenerateProxyRequest, GetPubkeysResponse, ReloadRequest, - RevokeModuleRequest, SignConsensusRequest, SignProxyRequest, SignRequest, + RevokeModuleRequest, SignConsensusRequest, SignProxyRequest, }, + response::{BlsSignResponse, EcdsaSignResponse}, }, config::{ModuleSigningConfig, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, @@ -119,7 +125,9 @@ impl SigningService { SigningService::init_metrics(config.chain)?; let signer_app = axum::Router::new() - .route(REQUEST_SIGNATURE_PATH, post(handle_request_signature)) + .route(REQUEST_SIGNATURE_BLS_PATH, post(handle_request_signature_bls)) + .route(REQUEST_SIGNATURE_PROXY_BLS_PATH, post(handle_request_signature_proxy_bls)) + .route(REQUEST_SIGNATURE_PROXY_ECDSA_PATH, post(handle_request_signature_proxy_ecdsa)) .route(GET_PUBKEYS_PATH, get(handle_get_pubkeys)) .route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy)) .route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth)) @@ -292,73 +300,136 @@ async fn handle_get_pubkeys( Ok((StatusCode::OK, Json(res)).into_response()) } -/// Implements request_signature from the Signer API -async fn handle_request_signature( +/// Validates a BLS key signature request and returns the signature +async fn handle_request_signature_bls( Extension(module_id): Extension, State(state): State, - Json(request): Json, + Json(request): Json, ) -> Result { let req_id = Uuid::new_v4(); + debug!(event = "bls_request_signature", ?module_id, %request, ?req_id, "New request"); + handle_request_signature_bls_impl( + &module_id, + &state, + &req_id, + false, + &request.pubkey, + &request.object_root, + ) + .await +} - let Some(signing_id) = state.jwts.read().get(&module_id).map(|m| m.signing_id) else { - error!(event = "request_signature", ?module_id, ?req_id, "Module signing ID not found"); +/// Validates a BLS key signature request using a proxy key and returns the +/// signature +async fn handle_request_signature_proxy_bls( + Extension(module_id): Extension, + State(state): State, + Json(request): Json>, +) -> Result { + let req_id = Uuid::new_v4(); + debug!(event = "proxy_bls_request_signature", ?module_id, %request, ?req_id, "New request"); + handle_request_signature_bls_impl( + &module_id, + &state, + &req_id, + true, + &request.proxy, + &request.object_root, + ) + .await +} + +/// Implementation for handling a BLS signature request +async fn handle_request_signature_bls_impl( + module_id: &ModuleId, + state: &SigningState, + req_id: &Uuid, + is_proxy: bool, + signing_pubkey: &BlsPublicKey, + object_root: &B256, +) -> Result { + let Some(signing_id) = state.jwts.read().get(module_id).map(|m| m.signing_id) else { + error!( + event = "proxy_bls_request_signature", + ?module_id, + ?req_id, + "Module signing ID not found" + ); return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; - debug!(event = "request_signature", ?module_id, %request, ?req_id, "New request"); - - let manager = state.manager.read().await; - let res = match &*manager { - SigningManager::Local(local_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { ref object_root, ref pubkey }) => { - local_manager - .sign_consensus(pubkey, object_root, Some(&signing_id)) - .await - .map(|sig| Json(sig).into_response()) - } - SignRequest::ProxyBls(SignProxyRequest { ref object_root, proxy: ref bls_key }) => { - local_manager - .sign_proxy_bls(bls_key, object_root, Some(&signing_id)) - .await - .map(|sig| Json(sig).into_response()) - } - SignRequest::ProxyEcdsa(SignProxyRequest { ref object_root, proxy: ref ecdsa_key }) => { - local_manager - .sign_proxy_ecdsa(ecdsa_key, object_root, Some(&signing_id)) - .await - .map(|sig| Json(sig).into_response()) + match &*state.manager.read().await { + SigningManager::Local(local_manager) => { + if is_proxy { + local_manager.sign_proxy_bls(signing_pubkey, object_root, Some(&signing_id)).await + } else { + local_manager.sign_consensus(signing_pubkey, object_root, Some(&signing_id)).await } - }, - SigningManager::Dirk(dirk_manager) => match request { - SignRequest::Consensus(SignConsensusRequest { ref object_root, ref pubkey }) => { + } + SigningManager::Dirk(dirk_manager) => { + if is_proxy { dirk_manager - .request_consensus_signature(pubkey, object_root, Some(&signing_id)) + .request_proxy_signature(signing_pubkey, object_root, Some(&signing_id)) .await - .map(|sig| Json(sig).into_response()) - } - SignRequest::ProxyBls(SignProxyRequest { ref object_root, proxy: ref bls_key }) => { + } else { dirk_manager - .request_proxy_signature(bls_key, object_root, Some(&signing_id)) + .request_consensus_signature(signing_pubkey, object_root, Some(&signing_id)) .await - .map(|sig| Json(sig).into_response()) - } - SignRequest::ProxyEcdsa(_) => { - error!( - event = "request_signature", - ?module_id, - ?req_id, - "ECDSA proxy sign request not supported with Dirk" - ); - Err(SignerModuleError::DirkNotSupported) } - }, + } + } + .map(|sig| { + Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, sig)).into_response() + }) + .map_err(|err| { + error!(event = "request_signature", ?module_id, ?req_id, "{err}"); + err + }) +} + +/// Validates an ECDSA key signature request using a proxy key and returns the +/// signature +async fn handle_request_signature_proxy_ecdsa( + Extension(module_id): Extension, + State(state): State, + Json(request): Json>, +) -> Result { + let req_id = Uuid::new_v4(); + let Some(signing_id) = state.jwts.read().get(&module_id).map(|m| m.signing_id) else { + error!( + event = "proxy_ecdsa_request_signature", + ?module_id, + ?req_id, + "Module signing ID not found" + ); + return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; + debug!(event = "proxy_ecdsa_request_signature", ?module_id, %request, ?req_id, "New request"); - if let Err(err) = &res { - error!(event = "request_signature", ?module_id, ?req_id, "{err}"); + match &*state.manager.read().await { + SigningManager::Local(local_manager) => { + local_manager + .sign_proxy_ecdsa(&request.proxy, &request.object_root, Some(&signing_id)) + .await + } + SigningManager::Dirk(_) => { + error!( + event = "request_signature", + ?module_id, + ?req_id, + "ECDSA proxy sign request not supported with Dirk" + ); + Err(SignerModuleError::DirkNotSupported) + } } - - res + .map(|sig| { + Json(EcdsaSignResponse::new(request.proxy, request.object_root, signing_id, sig)) + .into_response() + }) + .map_err(|err| { + error!(event = "request_signature", ?module_id, ?req_id, "{err}"); + err + }) } async fn handle_generate_proxy( diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index c73b2191..8360d07d 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -92,13 +92,13 @@ impl DaCommitService { // Request a signature directly from a BLS key let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); - let signature = self.config.signer_client.request_consensus_signature(request).await?; - info!("Proposer commitment (consensus): {}", signature); + let response = self.config.signer_client.request_consensus_signature(request).await?; + info!("Proposer commitment (consensus): {}", response.signature); match verify_proposer_commitment_signature_bls( self.config.chain, &pubkey, &datagram, - &signature, + &response.signature, &DA_COMMIT_SIGNING_ID, ) { Ok(_) => info!("Signature verified successfully"), @@ -107,14 +107,14 @@ impl DaCommitService { // Request a signature from a proxy BLS key let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); - let proxy_signature_bls = + let proxy_response_bls = self.config.signer_client.request_proxy_signature_bls(proxy_request_bls).await?; - info!("Proposer commitment (proxy BLS): {}", proxy_signature_bls); + info!("Proposer commitment (proxy BLS): {}", proxy_response_bls.signature); match verify_proposer_commitment_signature_bls( self.config.chain, &proxy_bls, &datagram, - &proxy_signature_bls, + &proxy_response_bls.signature, &DA_COMMIT_SIGNING_ID, ) { Ok(_) => info!("Signature verified successfully"), @@ -124,17 +124,17 @@ impl DaCommitService { // If ECDSA keys are enabled, request a signature from a proxy ECDSA key if let Some(proxy_ecdsa) = proxy_ecdsa { let proxy_request_ecdsa = SignProxyRequest::builder(proxy_ecdsa).with_msg(&datagram); - let proxy_signature_ecdsa = self + let proxy_response_ecdsa = self .config .signer_client .request_proxy_signature_ecdsa(proxy_request_ecdsa) .await?; - info!("Proposer commitment (proxy ECDSA): {}", proxy_signature_ecdsa); + info!("Proposer commitment (proxy ECDSA): {}", proxy_response_ecdsa.signature); match verify_proposer_commitment_signature_ecdsa( self.config.chain, &proxy_ecdsa, &datagram, - &proxy_signature_ecdsa, + &proxy_response_ecdsa.signature, &DA_COMMIT_SIGNING_ID, ) { Ok(_) => info!("Signature verified successfully"), diff --git a/provisioning/grafana/signer_public_dashboard.json b/provisioning/grafana/signer_public_dashboard.json index 327d48bb..4b904b1a 100644 --- a/provisioning/grafana/signer_public_dashboard.json +++ b/provisioning/grafana/signer_public_dashboard.json @@ -539,13 +539,19 @@ "list": [ { "current": { - "text": "$__all", + "selected": true, + "text": "All", "value": "$__all" }, "description": "SignerAPI endpoint", "includeAll": true, "name": "endpoint", "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, { "selected": false, "text": "get_pubkeys", @@ -558,11 +564,21 @@ }, { "selected": false, - "text": "request_signature", - "value": "request_signature" + "text": "request_signature_bls", + "value": "request_signature_bls" + }, + { + "selected": false, + "text": "request_signature_proxy_bls", + "value": "request_signature_proxy_bls" + }, + { + "selected": false, + "text": "request_signature_proxy_ecdsa", + "value": "request_signature_proxy_ecdsa" } ], - "query": "get_pubkeys, generate_proxy_key, request_signature", + "query": "get_pubkeys, generate_proxy_key, request_signature_bls, request_signature_proxy_bls, request_signature_proxy_ecdsa", "type": "custom" } ] diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index 868a1f71..f5c4e6c1 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -1,15 +1,17 @@ use std::collections::HashMap; use alloy::{ - hex, - primitives::{b256, FixedBytes}, + hex::FromHex, + primitives::{b256, hex, FixedBytes}, + rpc::types::beacon::BlsPublicKey, }; use cb_common::{ commit::{ - constants::REQUEST_SIGNATURE_PATH, - request::{SignConsensusRequest, SignRequest}, + constants::REQUEST_SIGNATURE_BLS_PATH, request::SignConsensusRequest, + response::BlsSignResponse, }, config::{load_module_signing_configs, ModuleSigningConfig}, + signer::BlsSignature, types::ModuleId, utils::create_jwt, }; @@ -60,22 +62,23 @@ async fn test_signer_sign_request_good() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = - SignRequest::Consensus(SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }); + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); - let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_PATH); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; // Verify the response is successful assert!(response.status() == StatusCode::OK); // Verify the signature is returned - let signature = response.text().await?; - assert!(!signature.is_empty(), "Signature should not be empty"); - - let expected_signature = "\"0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115\""; - assert_eq!(signature, expected_signature, "Signature does not match expected value"); + let sig_response = response.json::().await?; + let expected = BlsSignResponse::new( + BlsPublicKey::from(PUBKEY_1), + object_root, + mod_cfgs.get(&module_id).unwrap().signing_id, + BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap()); + assert_eq!(sig_response, expected, "Signature response does not match expected value"); Ok(()) } @@ -92,22 +95,32 @@ async fn test_signer_sign_request_different_module() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = - SignRequest::Consensus(SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }); + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); - let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_PATH); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; // Verify the response is successful assert!(response.status() == StatusCode::OK); // Verify the signature is returned - let signature = response.text().await?; - assert!(!signature.is_empty(), "Signature should not be empty"); - - let incorrect_signature = "\"0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115\""; - assert_ne!(signature, incorrect_signature, "Signature does not match expected value"); + let sig_response = response.json::().await?; + assert_eq!( + sig_response.pubkey, + BlsPublicKey::from(PUBKEY_1), + "Public key does not match expected value" + ); + assert_eq!(sig_response.object_root, object_root, "Object root does not match expected value"); + assert_eq!( + sig_response.module_signing_id, + mod_cfgs.get(&module_id).unwrap().signing_id, + "Module signing ID does not match expected value" + ); + assert_ne!( + sig_response.signature, BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap(), + "Signature matches the reference signature, which should not happen" + ); Ok(()) } From 0ef87871acfd74f3c7194c71b385f616c9bbe000 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 19 Aug 2025 15:11:51 -0400 Subject: [PATCH 04/30] Add nonce and chain ID to signature requests (#354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: eltitanb Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao --- api/signer-api.yml | 28 ++++++++- crates/common/src/commit/request.rs | 37 +++++++---- crates/common/src/commit/response.rs | 14 ++++- crates/common/src/config/pbs.rs | 5 +- crates/common/src/pbs/types/get_header.rs | 2 +- crates/common/src/signature.rs | 41 ++++++++----- crates/common/src/signer/schemes/bls.rs | 12 ++-- crates/common/src/signer/schemes/ecdsa.rs | 46 ++++++++++---- crates/common/src/types.rs | 27 ++++++--- crates/signer/src/manager/dirk.rs | 51 +++++++++++----- crates/signer/src/manager/local.rs | 46 +++++++++++--- crates/signer/src/service.rs | 64 +++++++++++++++++--- docs/docs/developing/prop-commit-signing.md | 24 ++++++-- docs/docs/res/img/prop_commit_tree.png | Bin 57442 -> 96528 bytes examples/da_commit/src/main.rs | 9 ++- tests/tests/signer_request_sig.rs | 14 +++-- 16 files changed, 314 insertions(+), 106 deletions(-) diff --git a/api/signer-api.yml b/api/signer-api.yml index a6c427a4..a6edd6bb 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -79,6 +79,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -217,6 +219,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: pubkey: "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -355,6 +359,8 @@ paths: object_root: description: The 32-byte data you want to sign, with optional `0x` prefix. $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" example: proxy: "0x71f65e9f6336770e22d148bd5e89b391a1c3b0bb" object_root: "0x3e9f4a78b5c21d64f0b8e3d9a7f5c02b4d1e67a3c8f29b5d6e4a3b1c8f72e6d9" @@ -629,9 +635,15 @@ components: object_root: description: The 32-byte data that was signed, with `0x` prefix $ref: "#/components/schemas/B256" - signing_id: + module_signing_id: description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" + chain_id: + description: The chain ID that the signature is valid for, as specified in the Commit-Boost configuration + type: integer + example: 1 signature: description: The BLS signature of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). $ref: "#/components/schemas/BlsSignature" @@ -647,6 +659,18 @@ components: module_signing_id: description: The signing ID of the module that requested the signature, as specified in the Commit-Boost configuration $ref: "#/components/schemas/B256" + nonce: + $ref: "#/components/schemas/Nonce" + chain_id: + description: The chain ID that the signature is valid for, as specified in the Commit-Boost configuration + type: integer + example: 1 signature: description: The ECDSA signature (in Ethereum RSV format) of the Merkle root hash of the provided `object_root` field and the requesting module's Signing ID. For details on this signature, see the [signature structure documentation](https://commit-boost.github.io/commit-boost-client/developing/prop-commit-signing.md#structure-of-a-signature). - $ref: "#/components/schemas/EcdsaSignature" \ No newline at end of file + $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. + minimum: 0 + maximum: 18446744073709551614 // 2^64-2 + example: 1 diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 5e101092..81edd5fe 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -77,15 +77,16 @@ impl fmt::Display for SignedProxyDelegation { pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, pub object_root: B256, + pub nonce: u64, } impl SignConsensusRequest { - pub fn new(pubkey: BlsPublicKey, object_root: B256) -> Self { - Self { pubkey, object_root } + pub fn new(pubkey: BlsPublicKey, object_root: B256, nonce: u64) -> Self { + Self { pubkey, object_root, nonce } } pub fn builder(pubkey: BlsPublicKey) -> Self { - Self::new(pubkey, B256::ZERO) + Self::new(pubkey, B256::ZERO, u64::MAX - 1) } pub fn with_root>(self, object_root: R) -> Self { @@ -95,15 +96,20 @@ impl SignConsensusRequest { pub fn with_msg(self, msg: &impl TreeHash) -> Self { self.with_root(msg.tree_hash_root().0) } + + pub fn with_nonce(self, nonce: u64) -> Self { + Self { nonce, ..self } + } } impl Display for SignConsensusRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "Consensus(pubkey: {}, object_root: {})", + "Consensus(pubkey: {}, object_root: {}, nonce: {})", self.pubkey, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } @@ -112,15 +118,16 @@ impl Display for SignConsensusRequest { pub struct SignProxyRequest { pub proxy: T, pub object_root: B256, + pub nonce: u64, } impl SignProxyRequest { - pub fn new(proxy: T, object_root: B256) -> Self { - Self { proxy, object_root } + pub fn new(proxy: T, object_root: B256, nonce: u64) -> Self { + Self { proxy, object_root, nonce } } pub fn builder(proxy: T) -> Self { - Self::new(proxy, B256::ZERO) + Self::new(proxy, B256::ZERO, u64::MAX - 1) } pub fn with_root>(self, object_root: R) -> Self { @@ -130,15 +137,20 @@ impl SignProxyRequest { pub fn with_msg(self, msg: &impl TreeHash) -> Self { self.with_root(msg.tree_hash_root().0) } + + pub fn with_nonce(self, nonce: u64) -> Self { + Self { nonce, ..self } + } } impl Display for SignProxyRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "BLS(proxy: {}, object_root: {})", + "BLS(proxy: {}, object_root: {}, nonce: {})", self.proxy, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } @@ -147,9 +159,10 @@ impl Display for SignProxyRequest
{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "ECDSA(proxy: {}, object_root: {})", + "ECDSA(proxy: {}, object_root: {}, nonce: {})", self.proxy, - hex::encode_prefixed(self.object_root) + hex::encode_prefixed(self.object_root), + self.nonce ) } } diff --git a/crates/common/src/commit/response.rs b/crates/common/src/commit/response.rs index 543fb6fc..a5eb9434 100644 --- a/crates/common/src/commit/response.rs +++ b/crates/common/src/commit/response.rs @@ -1,5 +1,5 @@ use alloy::{ - primitives::{Address, B256}, + primitives::{Address, B256, U256}, rpc::types::beacon::BlsSignature, }; use serde::{Deserialize, Serialize}; @@ -11,6 +11,8 @@ pub struct BlsSignResponse { pub pubkey: BlsPublicKey, pub object_root: B256, pub module_signing_id: B256, + pub nonce: u64, + pub chain_id: U256, pub signature: BlsSignature, } @@ -19,9 +21,11 @@ impl BlsSignResponse { pubkey: BlsPublicKey, object_root: B256, module_signing_id: B256, + nonce: u64, + chain_id: U256, signature: BlsSignature, ) -> Self { - Self { pubkey, object_root, module_signing_id, signature } + Self { pubkey, object_root, module_signing_id, nonce, chain_id, signature } } } @@ -30,6 +34,8 @@ pub struct EcdsaSignResponse { pub address: Address, pub object_root: B256, pub module_signing_id: B256, + pub nonce: u64, + pub chain_id: U256, pub signature: EcdsaSignature, } @@ -38,8 +44,10 @@ impl EcdsaSignResponse { address: Address, object_root: B256, module_signing_id: B256, + nonce: u64, + chain_id: U256, signature: EcdsaSignature, ) -> Self { - Self { address, object_root, module_signing_id, signature } + Self { address, object_root, module_signing_id, nonce, chain_id, signature } } } diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index d04b3394..e12493d4 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -169,12 +169,13 @@ impl PbsConfig { if !matches!(chain, Chain::Custom { .. }) { let provider = ProviderBuilder::new().on_http(rpc_url.clone()); let chain_id = provider.get_chain_id().await?; + let chain_id_big = U256::from(chain_id); ensure!( - chain_id == chain.id(), + chain_id_big == chain.id(), "Rpc url is for the wrong chain, expected: {} ({:?}) got {}", chain.id(), chain, - chain_id + chain_id_big ); } } diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index c5e40a21..ebb91e11 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -177,7 +177,7 @@ mod tests { &parsed.message, &parsed.signature, None, - &B32::from(APPLICATION_BUILDER_DOMAIN) + &B32::from(APPLICATION_BUILDER_DOMAIN), ) .is_ok()) } diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index cd960031..8d034077 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -9,7 +9,7 @@ use crate::{ constants::{COMMIT_BOOST_DOMAIN, GENESIS_VALIDATORS_ROOT}, error::BlstErrorWrapper, signer::{verify_bls_signature, verify_ecdsa_signature, BlsSecretKey, EcdsaSignature}, - types::{self, Chain}, + types::{self, Chain, SignatureRequestInfo}, }; pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { @@ -20,15 +20,19 @@ pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { pub fn compute_prop_commit_signing_root( chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, domain_mask: &B32, ) -> B256 { let domain = compute_domain(chain, domain_mask); - match module_signing_id { - Some(id) => { - let object_root = - types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } - .tree_hash_root(); + match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + let object_root = types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: chain.id(), + } + .tree_hash_root(); types::SigningData { object_root, signing_domain: domain }.tree_hash_root() } None => types::SigningData { object_root: *object_root, signing_domain: domain } @@ -63,13 +67,13 @@ pub fn verify_signed_message( pubkey: &BlsPublicKey, msg: &T, signature: &BlsSignature, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, domain_mask: &B32, ) -> Result<(), BlstErrorWrapper> { let signing_root = compute_prop_commit_signing_root( chain, &msg.tree_hash_root(), - module_signing_id, + signature_request_info, domain_mask, ); verify_bls_signature(pubkey, signing_root.as_slice(), signature) @@ -100,12 +104,12 @@ pub fn sign_commit_boost_root( chain: Chain, secret_key: &BlsSecretKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { let signing_root = compute_prop_commit_signing_root( chain, object_root, - module_signing_id, + signature_request_info, &B32::from(COMMIT_BOOST_DOMAIN), ); sign_message(secret_key, signing_root.as_slice()) @@ -123,11 +127,14 @@ pub fn verify_proposer_commitment_signature_bls( msg: &impl TreeHash, signature: &BlsSignature, module_signing_id: &B256, + nonce: u64, ) -> Result<(), BlstErrorWrapper> { let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); let object_root = types::PropCommitSigningInfo { data: msg.tree_hash_root(), module_signing_id: *module_signing_id, + nonce, + chain_id: chain.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -142,12 +149,16 @@ pub fn verify_proposer_commitment_signature_ecdsa( msg: &impl TreeHash, signature: &EcdsaSignature, module_signing_id: &B256, + nonce: u64, ) -> Result<(), eyre::Report> { - let object_root = msg.tree_hash_root(); let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let object_root = - types::PropCommitSigningInfo { data: object_root, module_signing_id: *module_signing_id } - .tree_hash_root(); + let object_root = types::PropCommitSigningInfo { + data: msg.tree_hash_root(), + module_signing_id: *module_signing_id, + nonce, + chain_id: chain.id(), + } + .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); verify_ecdsa_signature(address, &signing_root, signature) } diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs index 15367f36..c5b75ce6 100644 --- a/crates/common/src/signer/schemes/bls.rs +++ b/crates/common/src/signer/schemes/bls.rs @@ -4,7 +4,9 @@ use blst::BLST_ERROR; use tree_hash::TreeHash; use crate::{ - error::BlstErrorWrapper, signature::sign_commit_boost_root, types::Chain, + error::BlstErrorWrapper, + signature::sign_commit_boost_root, + types::{Chain, SignatureRequestInfo}, utils::blst_pubkey_to_alloy, }; @@ -42,11 +44,11 @@ impl BlsSigner { &self, chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { match self { BlsSigner::Local(sk) => { - sign_commit_boost_root(chain, sk, object_root, module_signing_id) + sign_commit_boost_root(chain, sk, object_root, signature_request_info) } } } @@ -55,9 +57,9 @@ impl BlsSigner { &self, chain: Chain, msg: &impl TreeHash, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> BlsSignature { - self.sign(chain, &msg.tree_hash_root(), module_signing_id).await + self.sign(chain, &msg.tree_hash_root(), signature_request_info).await } } diff --git a/crates/common/src/signer/schemes/ecdsa.rs b/crates/common/src/signer/schemes/ecdsa.rs index 907340f1..c01cf85f 100644 --- a/crates/common/src/signer/schemes/ecdsa.rs +++ b/crates/common/src/signer/schemes/ecdsa.rs @@ -10,7 +10,7 @@ use tree_hash::TreeHash; use crate::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - types::{self, Chain}, + types::{self, Chain, SignatureRequestInfo}, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -87,16 +87,18 @@ impl EcdsaSigner { &self, chain: Chain, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self { EcdsaSigner::Local(sk) => { let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let signing_root = match module_signing_id { - Some(id) => { + let signing_root = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { let object_root = types::PropCommitSigningInfo { data: *object_root, - module_signing_id: *id, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: chain.id(), } .tree_hash_root(); types::SigningData { object_root, signing_domain }.tree_hash_root() @@ -112,9 +114,9 @@ impl EcdsaSigner { &self, chain: Chain, msg: &impl TreeHash, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { - self.sign(chain, &msg.tree_hash_root(), module_signing_id).await + self.sign(chain, &msg.tree_hash_root(), signature_request_info).await } } @@ -131,7 +133,10 @@ pub fn verify_ecdsa_signature( #[cfg(test)] mod test { - use alloy::{hex, primitives::bytes}; + use alloy::{ + hex, + primitives::{b256, bytes}, + }; use super::*; @@ -161,15 +166,30 @@ mod test { let object_root = B256::from([1; 32]); let module_signing_id = B256::from([2; 32]); - let signature = - signer.sign(Chain::Hoodi, &object_root, Some(&module_signing_id)).await.unwrap(); + let nonce = 42; + let signature = signer + .sign( + Chain::Hoodi, + &object_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) + .await + .unwrap(); let signing_domain = compute_domain(Chain::Hoodi, &B32::from(COMMIT_BOOST_DOMAIN)); - let object_root = - types::PropCommitSigningInfo { data: object_root, module_signing_id }.tree_hash_root(); + let object_root = types::PropCommitSigningInfo { + data: object_root, + module_signing_id, + nonce, + chain_id: Chain::Hoodi.id(), + } + .tree_hash_root(); let msg = types::SigningData { object_root, signing_domain }.tree_hash_root(); - assert_eq!(msg, hex!("8cd49ccf2f9b0297796ff96ce5f7c5d26e20a59d0032ee2ad6249dcd9682b808")); + assert_eq!( + msg, + b256!("0x0b95fcdb3f003fc6f0fd3238d906f359809e97fe7ec71f56771cb05bee4150bd") + ); let address = signer.address(); let verified = verify_ecdsa_signature(&address, &msg, &signature); diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index c747815b..d650f6be 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use alloy::primitives::{aliases::B32, hex, Bytes, B256}; +use alloy::primitives::{aliases::B32, hex, Bytes, B256, U256}; use derive_more::{Deref, Display, From, Into}; use eyre::{bail, Context}; use serde::{Deserialize, Serialize}; @@ -72,7 +72,9 @@ impl std::fmt::Debug for Chain { } impl Chain { - pub fn id(&self) -> u64 { + // Chain IDs are 256-bit unsigned integers because they need to support + // Keccak256 hashes + pub fn id(&self) -> U256 { match self { Chain::Mainnet => KnownChain::Mainnet.id(), Chain::Holesky => KnownChain::Holesky.id(), @@ -146,13 +148,13 @@ pub enum KnownChain { // Constants impl KnownChain { - pub fn id(&self) -> u64 { + pub fn id(&self) -> U256 { match self { - KnownChain::Mainnet => 1, - KnownChain::Holesky => 17000, - KnownChain::Sepolia => 11155111, - KnownChain::Helder => 167000, - KnownChain::Hoodi => 560048, + KnownChain::Mainnet => U256::from(1), + KnownChain::Holesky => U256::from(17000), + KnownChain::Sepolia => U256::from(11155111), + KnownChain::Helder => U256::from(167000), + KnownChain::Hoodi => U256::from(560048), } } @@ -304,6 +306,15 @@ pub struct SigningData { pub struct PropCommitSigningInfo { pub data: B256, pub module_signing_id: B256, + pub nonce: u64, // As per https://eips.ethereum.org/EIPS/eip-2681 + pub chain_id: U256, +} + +/// Information about a signature request, including the module signing ID and +/// nonce. +pub struct SignatureRequestInfo { + pub module_signing_id: B256, + pub nonce: u64, } /// Returns seconds_per_slot and genesis_fork_version from a spec, such as diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index add9e3a2..3067c8a1 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -12,7 +12,7 @@ use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, signer::{BlsPublicKey, BlsSignature, ProxyStore}, - types::{self, Chain, ModuleId}, + types::{self, Chain, ModuleId, SignatureRequestInfo}, }; use eyre::{bail, OptionExt}; use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; @@ -148,6 +148,11 @@ impl DirkManager { }) } + /// Get the chain config for the manager + pub fn get_chain(&self) -> Chain { + self.chain + } + /// Set the proxy store to use for storing proxy delegations pub fn with_proxy_store(self, store: ProxyStore) -> eyre::Result { if let ProxyStore::ERC2335 { .. } = store { @@ -197,14 +202,15 @@ impl DirkManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self.consensus_accounts.get(pubkey) { Some(Account::Simple(account)) => { - self.request_simple_signature(account, object_root, module_signing_id).await + self.request_simple_signature(account, object_root, signature_request_info).await } Some(Account::Distributed(account)) => { - self.request_distributed_signature(account, object_root, module_signing_id).await + self.request_distributed_signature(account, object_root, signature_request_info) + .await } None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec())), } @@ -215,14 +221,15 @@ impl DirkManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { match self.proxy_accounts.get(pubkey) { Some(ProxyAccount { inner: Account::Simple(account), .. }) => { - self.request_simple_signature(account, object_root, module_signing_id).await + self.request_simple_signature(account, object_root, signature_request_info).await } Some(ProxyAccount { inner: Account::Distributed(account), .. }) => { - self.request_distributed_signature(account, object_root, module_signing_id).await + self.request_distributed_signature(account, object_root, signature_request_info) + .await } None => Err(SignerModuleError::UnknownProxySigner(pubkey.to_vec())), } @@ -233,14 +240,21 @@ impl DirkManager { &self, account: &SimpleAccount, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let domain = compute_domain(self.chain, &B32::from(COMMIT_BOOST_DOMAIN)); - let data = match module_signing_id { - Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + let data = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: self.chain.id(), + } .tree_hash_root() - .to_vec(), + .to_vec() + } None => object_root.to_vec(), }; @@ -271,15 +285,22 @@ impl DirkManager { &self, account: &DistributedAccount, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let mut partials = Vec::with_capacity(account.participants.len()); let mut requests = Vec::with_capacity(account.participants.len()); - let data = match module_signing_id { - Some(id) => types::PropCommitSigningInfo { data: *object_root, module_signing_id: *id } + let data = match signature_request_info { + Some(SignatureRequestInfo { module_signing_id, nonce }) => { + types::PropCommitSigningInfo { + data: *object_root, + module_signing_id: *module_signing_id, + nonce: *nonce, + chain_id: self.chain.id(), + } .tree_hash_root() - .to_vec(), + .to_vec() + } None => object_root.to_vec(), }; diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index a13695e5..632a01b6 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -13,7 +13,7 @@ use cb_common::{ BlsProxySigner, BlsPublicKey, BlsSigner, ConsensusSigner, EcdsaProxySigner, EcdsaSignature, EcdsaSigner, ProxySigners, ProxyStore, }, - types::{Chain, ModuleId}, + types::{Chain, ModuleId, SignatureRequestInfo}, }; use tree_hash::TreeHash; @@ -53,6 +53,11 @@ impl LocalSigningManager { Ok(manager) } + /// Get the chain config for the manager + pub fn get_chain(&self) -> Chain { + self.chain + } + pub fn add_consensus_signer(&mut self, signer: ConsensusSigner) { self.consensus_signers.insert(signer.pubkey(), signer); } @@ -133,13 +138,13 @@ impl LocalSigningManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let signer = self .consensus_signers .get(pubkey) .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; - let signature = signer.sign(self.chain, object_root, module_signing_id).await; + let signature = signer.sign(self.chain, object_root, signature_request_info).await; Ok(signature) } @@ -148,14 +153,14 @@ impl LocalSigningManager { &self, pubkey: &BlsPublicKey, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let bls_proxy = self .proxy_signers .bls_signers .get(pubkey) .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; - let signature = bls_proxy.sign(self.chain, object_root, module_signing_id).await; + let signature = bls_proxy.sign(self.chain, object_root, signature_request_info).await; Ok(signature) } @@ -163,14 +168,14 @@ impl LocalSigningManager { &self, address: &Address, object_root: &B256, - module_signing_id: Option<&B256>, + signature_request_info: Option<&SignatureRequestInfo>, ) -> Result { let ecdsa_proxy = self .proxy_signers .ecdsa_signers .get(address) .ok_or(SignerModuleError::UnknownProxySigner(address.to_vec()))?; - let signature = ecdsa_proxy.sign(self.chain, object_root, module_signing_id).await?; + let signature = ecdsa_proxy.sign(self.chain, object_root, signature_request_info).await?; Ok(signature) } @@ -307,9 +312,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 43; let sig = signing_manager - .sign_consensus(&consensus_pk, &data_root, Some(&module_signing_id)) + .sign_consensus( + &consensus_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -318,6 +328,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -384,9 +396,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 44; let sig = signing_manager - .sign_proxy_bls(&proxy_pk, &data_root, Some(&module_signing_id)) + .sign_proxy_bls( + &proxy_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -395,6 +412,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); @@ -463,9 +482,14 @@ mod tests { let data_root = B256::random(); let module_signing_id = B256::random(); + let nonce = 45; let sig = signing_manager - .sign_proxy_ecdsa(&proxy_pk, &data_root, Some(&module_signing_id)) + .sign_proxy_ecdsa( + &proxy_pk, + &data_root, + Some(&SignatureRequestInfo { module_signing_id, nonce }), + ) .await .unwrap(); @@ -474,6 +498,8 @@ mod tests { let object_root = types::PropCommitSigningInfo { data: data_root.tree_hash_root(), module_signing_id, + nonce, + chain_id: CHAIN.id(), } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index f69eb650..bb168cf4 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -6,7 +6,7 @@ use std::{ }; use alloy::{ - primitives::{Address, B256}, + primitives::{Address, B256, U256}, rpc::types::beacon::BlsPublicKey, }; use axum::{ @@ -33,7 +33,7 @@ use cb_common::{ }, config::{ModuleSigningConfig, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, - types::{Chain, Jwt, ModuleId}, + types::{Chain, Jwt, ModuleId, SignatureRequestInfo}, utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; @@ -315,6 +315,7 @@ async fn handle_request_signature_bls( false, &request.pubkey, &request.object_root, + request.nonce, ) .await } @@ -335,6 +336,7 @@ async fn handle_request_signature_proxy_bls( true, &request.proxy, &request.object_root, + request.nonce, ) .await } @@ -347,6 +349,7 @@ async fn handle_request_signature_bls_impl( is_proxy: bool, signing_pubkey: &BlsPublicKey, object_root: &B256, + nonce: u64, ) -> Result { let Some(signing_id) = state.jwts.read().get(module_id).map(|m| m.signing_id) else { error!( @@ -358,28 +361,52 @@ async fn handle_request_signature_bls_impl( return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; + let chain_id: U256; match &*state.manager.read().await { SigningManager::Local(local_manager) => { + chain_id = local_manager.get_chain().id(); if is_proxy { - local_manager.sign_proxy_bls(signing_pubkey, object_root, Some(&signing_id)).await + local_manager + .sign_proxy_bls( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) + .await } else { - local_manager.sign_consensus(signing_pubkey, object_root, Some(&signing_id)).await + local_manager + .sign_consensus( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) + .await } } SigningManager::Dirk(dirk_manager) => { + chain_id = dirk_manager.get_chain().id(); if is_proxy { dirk_manager - .request_proxy_signature(signing_pubkey, object_root, Some(&signing_id)) + .request_proxy_signature( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) .await } else { dirk_manager - .request_consensus_signature(signing_pubkey, object_root, Some(&signing_id)) + .request_consensus_signature( + signing_pubkey, + object_root, + Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), + ) .await } } } .map(|sig| { - Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, sig)).into_response() + Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, nonce, chain_id, sig)) + .into_response() }) .map_err(|err| { error!(event = "request_signature", ?module_id, ?req_id, "{err}"); @@ -406,13 +433,23 @@ async fn handle_request_signature_proxy_ecdsa( }; debug!(event = "proxy_ecdsa_request_signature", ?module_id, %request, ?req_id, "New request"); + let chain_id: U256; match &*state.manager.read().await { SigningManager::Local(local_manager) => { + chain_id = local_manager.get_chain().id(); local_manager - .sign_proxy_ecdsa(&request.proxy, &request.object_root, Some(&signing_id)) + .sign_proxy_ecdsa( + &request.proxy, + &request.object_root, + Some(&SignatureRequestInfo { + module_signing_id: signing_id, + nonce: request.nonce, + }), + ) .await } SigningManager::Dirk(_) => { + chain_id = U256::ZERO; // Dirk does not support ECDSA proxy signing error!( event = "request_signature", ?module_id, @@ -423,8 +460,15 @@ async fn handle_request_signature_proxy_ecdsa( } } .map(|sig| { - Json(EcdsaSignResponse::new(request.proxy, request.object_root, signing_id, sig)) - .into_response() + Json(EcdsaSignResponse::new( + request.proxy, + request.object_root, + signing_id, + request.nonce, + chain_id, + sig, + )) + .into_response() }) .map_err(|err| { error!(event = "request_signature", ?module_id, ?req_id, "{err}"); diff --git a/docs/docs/developing/prop-commit-signing.md b/docs/docs/developing/prop-commit-signing.md index fd19fafc..1e8bd249 100644 --- a/docs/docs/developing/prop-commit-signing.md +++ b/docs/docs/developing/prop-commit-signing.md @@ -17,6 +17,7 @@ Proposer commitment signatures produced by Commit-Boost's signer service conform - The data payload being signed must be a **32-byte array**, typically serializd as a 64-character hex string with an optional `0x` prefix. The value itself is arbitrary, as long as it has meaning to the requester - though it is typically the 256-bit hash of some kind of data. - If requesting a signature from a BLS key, the resulting signature will be a standard BLS signature (96 bytes in length). - If requesting a signature from an ECDSA key, the resulting signature will be a standard Ethereum RSV signature (65 bytes in length). +- Signatures **may** be **unique** per request, using the optional `nonce` field in their requests to indicate a unique sequence that this signature belongs to. ## Configuring a Module for Proposer Commitments @@ -37,9 +38,18 @@ Your module's signing ID is a 32-byte value that is used as a unique identifier The Signing ID is decoupled from your module's human-readable name (the `module_id` field in the Commit-Boost configuration file) so that any changes to your module name will not invalidate signatures from previous versions. Similarly, if you don't change the module ID but *want* to invalidate previous signatures, you can modify the signing ID and it will do so. Just ensure your users are made aware of the change, so they can update it in their Commit-Boost configuration files accordingly. +## Nonces + +Your module has the option of using **Nonces** for each of its signature requests. Nonces are intended to be unique values that establish a sequence of signature requests, distinguishing one signature from another - even if all of their other payload information is identical. When making a request for a signature, you may include a unique nonce as part of the request; the signature will include it in its data, ensuring that things like replay attacks cannot be used for that signature. + +If you want to use them within your module, your module (or whatever remote backend system it connects to) **will be responsible** for storing, comparing, validating, and otherwise using the nonces. Commit-Boost's signer service by itself **does not** store nonces or track which ones have already been used by a given module. + +In terms of implementation, the nonce format conforms to the specification in [EIP-2681](https://eips.ethereum.org/EIPS/eip-2681). It is an unsigned 64-bit big-endian integer, with a minimum value of 0 and a maximum value of `2^64-2`. We recommend using `2^64-1` as a signifier indicating that your module doesn't use nonces, rather than using 0 for such a purpose. + + ## Structure of a Signature -The form proposer commitment signatures take depends on the type of signature being requested. BLS signatures take the [standard form](https://eth2book.info/latest/part2/building_blocks/signatures/) (96-byte values). ECDSA (Ethereum EL) signatures take the [standard Ethereum ECDSA `r,s,v` signature form](https://forum.openzeppelin.com/t/sign-it-like-you-mean-it-creating-and-verifying-ethereum-signatures/697). In both cases, the data being signed is a 32-byte hash - the root hash of an SSZ Merkle tree, described below: +The form proposer commitment signatures take depends on the type of signature being requested. BLS signatures take the [standard form](https://eth2book.info/latest/part2/building_blocks/signatures/) (96-byte values). ECDSA (Ethereum EL) signatures take the [standard Ethereum ECDSA `r,s,v` signature form](https://forum.openzeppelin.com/t/sign-it-like-you-mean-it-creating-and-verifying-ethereum-signatures/697). In both cases, the data being signed is a 32-byte hash - the root hash of a composite two-stage [SSZ Merkle tree](https://thogiti.github.io/2024/05/02/Merkleization.html), described below:
@@ -47,14 +57,20 @@ The form proposer commitment signatures take depends on the type of signature be
-where: +where, for the sub-tree in blue: - `Request Data` is a 32-byte array that serves as the data you want to sign. This is typically a hash of some more complex data on its own that your module constructs. - `Signing ID` is your module's 32-byte signing ID. The signer service will load this for your module from its configuration file. -- `Domain` is the 32-byte output of the [compute_domain()](https://eth2book.info/capella/part2/building_blocks/signatures/#domain-separation-and-forks) function in the Beacon specification. The 4-byte domain type in this case is not a standard Beacon domain type, but rather Commit-Boost's own domain type: `0x6D6D6F43`. +- `Nonce` is the nonce value for the signature request. While this value must be present, it can be effectively ignored by setting it to some arbitrary value if your module does not track nonces. Conforming with the tree specification, it must be added as a 256-bit unsigned little-endian integer. Most libraries will be able to do this conversion automatically if you specify the field as the language's primitive for 64-bit unsigned integers (e.g., `uint64`, `u64`, `ulong`, etc.). + +- `Chain ID` is the ID of the chain that the Signer service is currently configured to use, as indicated by the [Commit-Boost configuration file](../get_started/configuration.md). This must also be a 256-bit unsigned little-endian integer. + +A Merkle tree must be constructed from these four leaf nodes, and its root hash calculated according to the standard SSZ hash computation rules. This result will be called the "sub-tree root". With this, a second Merkle tree is created using this sub-tree root and a value called the Domain: + +- `Domain` is the 32-byte output of the [compute_domain()](https://eth2book.info/capella/part2/building_blocks/signatures/#domain-separation-and-forks) function in the Beacon specification. The 4-byte domain type in this case is not a standard Beacon domain type, but rather Commit Boost's own domain type: `0x6D6D6F43`. -The data signed in a proposer commitment is the 32-byte root of this tree (the green `Root` box). Note that calculating this will involve calculating the Merkle Root of two separate trees: first the blue data subtree (with the original request data and the signing ID) to establish the blue `Root` value, and then again with a tree created from that value and the `Domain`. +The data signed in a proposer commitment is the 32-byte hash root of this new tree (the green `Root` box). Many languages provide libraries for computing the root of an SSZ Merkle tree, such as [fastssz for Go](https://github.com/ferranbt/fastssz) or [tree_hash for Rust](https://docs.rs/tree_hash/latest/tree_hash/). When verifying proposer commitment signatures, use a library that supports Merkle tree root hashing, the `compute_domain()` operation, and validation for signatures generated by your key of choice. diff --git a/docs/docs/res/img/prop_commit_tree.png b/docs/docs/res/img/prop_commit_tree.png index 1e36f4b4d3fcac21e74255fa6d617a206ad2a323..2c0b181578513700d3e51c1c77f40aa5ec17e47f 100644 GIT binary patch literal 96528 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 literal 57442 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 SRYXcexWKAfD&_Z}f&T$W;}+`x diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 8360d07d..3edd6bc2 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -32,6 +32,7 @@ struct Datagram { struct DaCommitService { config: StartCommitModuleConfig, + nonce: u64, } // Extra configurations parameters can be set here and will be automatically @@ -100,10 +101,12 @@ impl DaCommitService { &datagram, &response.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; + self.nonce += 1; // Request a signature from a proxy BLS key let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); @@ -116,10 +119,12 @@ impl DaCommitService { &datagram, &proxy_response_bls.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; + self.nonce += 1; // If ECDSA keys are enabled, request a signature from a proxy ECDSA key if let Some(proxy_ecdsa) = proxy_ecdsa { @@ -136,11 +141,13 @@ impl DaCommitService { &datagram, &proxy_response_ecdsa.signature, &DA_COMMIT_SIGNING_ID, + self.nonce, ) { Ok(_) => info!("Signature verified successfully"), Err(err) => error!(%err, "Signature verification failed"), }; } + self.nonce += 1; SIG_RECEIVED_COUNTER.inc(); @@ -168,7 +175,7 @@ async fn main() -> Result<()> { "Starting module with custom data" ); - let mut service = DaCommitService { config }; + let mut service = DaCommitService { config, nonce: 0 }; if let Err(err) = service.run().await { error!(%err, "Service failed"); diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index f5c4e6c1..deb3bd39 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -12,7 +12,7 @@ use cb_common::{ }, config::{load_module_signing_configs, ModuleSigningConfig}, signer::BlsSignature, - types::ModuleId, + types::{Chain, ModuleId}, utils::create_jwt, }; use cb_tests::{ @@ -62,7 +62,8 @@ async fn test_signer_sign_request_good() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; + let nonce: u64 = 101; + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -77,7 +78,9 @@ async fn test_signer_sign_request_good() -> Result<()> { BlsPublicKey::from(PUBKEY_1), object_root, mod_cfgs.get(&module_id).unwrap().signing_id, - BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap()); + nonce, + Chain::Hoodi.id(), + BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap()); assert_eq!(sig_response, expected, "Signature response does not match expected value"); Ok(()) @@ -95,7 +98,8 @@ async fn test_signer_sign_request_different_module() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root }; + let nonce: u64 = 101; + let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -118,7 +122,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { "Module signing ID does not match expected value" ); assert_ne!( - sig_response.signature, BlsSignature::from_hex("0xa43e623f009e615faa3987368f64d6286a4103de70e9a81d82562c50c91eae2d5d6fb9db9fe943aa8ee42fd92d8210c1149f25ed6aa72a557d74a0ed5646fdd0e8255ec58e3e2931695fe913863ba0cdf90d29f651bce0a34169a6f6ce5b3115").unwrap(), + sig_response.signature, BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap(), "Signature matches the reference signature, which should not happen" ); From 6988444c6269f83dc221b93b5defdac1cb03ed8f Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 26 Aug 2025 04:21:12 -0400 Subject: [PATCH 05/30] Merge `main` to `sigp-audit-fixes` (#361) Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> --- Cargo.lock | 607 ++++++++++++++++-- Cargo.toml | 4 +- benches/pbs/src/main.rs | 20 +- bin/src/lib.rs | 4 +- crates/common/Cargo.toml | 4 +- crates/common/src/commit/client.rs | 3 +- crates/common/src/commit/request.rs | 33 +- crates/common/src/commit/response.rs | 10 +- crates/common/src/config/mux.rs | 73 ++- crates/common/src/config/pbs.rs | 3 +- crates/common/src/config/utils.rs | 12 +- crates/common/src/error.rs | 58 -- crates/common/src/lib.rs | 1 - crates/common/src/pbs/error.rs | 15 +- crates/common/src/pbs/relay.rs | 41 +- crates/common/src/pbs/types/beacon_block.rs | 16 +- .../src/pbs/types/blinded_block_body.rs | 28 +- .../src/pbs/types/execution_requests.rs | 12 +- crates/common/src/pbs/types/get_header.rs | 42 +- .../types/testdata/get-header-response.json | 14 +- .../types/testdata/get-header-response.ssz | Bin 2448 -> 1224 bytes ...signed-blinded-beacon-block-electra-2.json | 34 +- .../signed-blinded-beacon-block-electra-2.ssz | Bin 8618 -> 4309 bytes crates/common/src/signature.rs | 31 +- crates/common/src/signer/loader.rs | 50 +- crates/common/src/signer/schemes/bls.rs | 48 +- crates/common/src/signer/store.rs | 187 +++--- crates/common/src/signer/types.rs | 3 +- crates/common/src/types.rs | 41 +- crates/common/src/utils.rs | 51 +- crates/pbs/Cargo.toml | 1 - crates/pbs/src/mev_boost/get_header.rs | 72 +-- crates/pbs/src/routes/get_header.rs | 4 +- crates/pbs/src/state.rs | 2 +- crates/signer/src/manager/dirk.rs | 90 +-- crates/signer/src/manager/local.rs | 75 +-- crates/signer/src/service.rs | 18 +- examples/da_commit/src/main.rs | 31 +- tests/data/signed_blinded_block_holesky.json | 10 +- tests/data/ssv_valid.json | 6 +- tests/src/mock_relay.rs | 55 +- tests/src/mock_validator.rs | 34 +- tests/src/signer_service.rs | 6 +- tests/src/utils.rs | 7 +- tests/tests/pbs_get_header.rs | 21 +- tests/tests/pbs_get_status.rs | 12 +- tests/tests/pbs_mux.rs | 19 +- tests/tests/pbs_post_blinded_blocks.rs | 20 +- tests/tests/pbs_post_validators.rs | 13 +- tests/tests/signer_request_sig.rs | 27 +- 50 files changed, 1196 insertions(+), 772 deletions(-) delete mode 100644 crates/common/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 539d458b..29cf4d1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,18 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -234,7 +246,7 @@ dependencies = [ "c-kzg", "derive_more 2.0.1", "either", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "ethereum_ssz_derive", "once_cell", "serde", @@ -326,9 +338,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eacedba97e65cdc7ab592f2b22ef5d3ab8d60b2056bc3a6e6363577e8270ec6f" dependencies = [ "alloy-rlp", + "arbitrary", "bytes", "cfg-if", "const-hex", + "derive_arbitrary", "derive_more 2.0.1", "foldhash", "getrandom 0.2.15", @@ -339,6 +353,7 @@ dependencies = [ "keccak-asm", "paste", "proptest", + "proptest-derive", "rand 0.8.5", "ruint", "rustc-hash", @@ -507,12 +522,12 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rpc-types-engine", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "ethereum_ssz_derive", "serde", "serde_with", "thiserror 2.0.12", - "tree_hash 0.9.1", + "tree_hash", "tree_hash_derive", ] @@ -538,7 +553,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "derive_more 2.0.1", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "ethereum_ssz_derive", "rand 0.8.5", "serde", @@ -875,6 +890,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "archery" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a8da9bc4c4053ee067669762bcaeea6e241841295a2b6c948312dad6ef4cc02" +dependencies = [ + "static_assertions", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1279,6 +1303,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.0" @@ -1318,20 +1348,20 @@ dependencies = [ [[package]] name = "bls" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v7.0.1#e42406d7b79a85ad4622f3a7440ff6468ac4c9e1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" dependencies = [ "alloy-primitives", "arbitrary", "blst", "ethereum_hashing", "ethereum_serde_utils", - "ethereum_ssz 0.7.1", + "ethereum_ssz", "fixed_bytes", "hex", "rand 0.8.5", "safe_arith", "serde", - "tree_hash 0.8.0", + "tree_hash", "zeroize", ] @@ -1374,6 +1404,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "subtle", +] + [[package]] name = "blstrs_plus" version = "0.8.18" @@ -1484,7 +1530,6 @@ dependencies = [ "axum 0.8.1", "base64 0.22.1", "bimap", - "blst", "bytes", "cipher 0.4.4", "const_format", @@ -1493,7 +1538,7 @@ dependencies = [ "docker-image", "eth2_keystore", "ethereum_serde_utils", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "ethereum_ssz_derive", "eyre", "futures", @@ -1514,8 +1559,9 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", - "tree_hash 0.9.1", + "tree_hash", "tree_hash_derive", + "types", "unicode-normalization", "url", ] @@ -1541,7 +1587,6 @@ dependencies = [ "async-trait", "axum 0.8.1", "axum-extra", - "blst", "cb-common", "cb-metrics", "eyre", @@ -1553,7 +1598,7 @@ dependencies = [ "serde_json", "tokio", "tracing", - "tree_hash 0.9.1", + "tree_hash", "url", "uuid 1.16.0", ] @@ -1583,7 +1628,7 @@ dependencies = [ "tonic", "tonic-build", "tracing", - "tree_hash 0.9.1", + "tree_hash", "uuid 1.16.0", ] @@ -1604,7 +1649,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "tree_hash 0.9.1", + "tree_hash", "url", ] @@ -1674,7 +1719,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -1753,10 +1798,27 @@ dependencies = [ "eyre", "tokio", "tracing", - "tree_hash 0.9.1", + "tree_hash", "tree_hash_derive", ] +[[package]] +name = "compare_fields" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "itertools 0.10.5", +] + +[[package]] +name = "compare_fields_derive" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "const-hex" version = "1.14.0" @@ -1796,6 +1858,25 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "context_deserialize" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "milhouse", + "serde", + "ssz_types", +] + +[[package]] +name = "context_deserialize_derive" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1830,6 +1911,58 @@ dependencies = [ "libc", ] +[[package]] +name = "crate_crypto_internal_eth_kzg_bls12_381" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f9cdad245e39a3659bc4c8958e93de34bd31ba3131ead14ccfb4b2cd60e52d" +dependencies = [ + "blst", + "blstrs", + "ff", + "group", + "pairing", + "subtle", +] + +[[package]] +name = "crate_crypto_internal_eth_kzg_erasure_codes" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581d28bcc93eecd97a04cebc5293271e0f41650f03c102f24d6cd784cbedb9f2" +dependencies = [ + "crate_crypto_internal_eth_kzg_bls12_381", + "crate_crypto_internal_eth_kzg_polynomial", +] + +[[package]] +name = "crate_crypto_internal_eth_kzg_maybe_rayon" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06fc0f984e585ea984a766c5b58d6bf6c51e463b0a0835b0dd4652d358b506b3" + +[[package]] +name = "crate_crypto_internal_eth_kzg_polynomial" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dff7a45e2d80308b21abdbc5520ec23c3ebfb3a94fafc02edfa7f356af6d7f" +dependencies = [ + "crate_crypto_internal_eth_kzg_bls12_381", +] + +[[package]] +name = "crate_crypto_kzg_multi_open_fk20" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0c2f82695a88809e713e1ff9534cb90ceffab0a08f4bd33245db711f9d356f" +dependencies = [ + "crate_crypto_internal_eth_kzg_bls12_381", + "crate_crypto_internal_eth_kzg_maybe_rayon", + "crate_crypto_internal_eth_kzg_polynomial", + "hex", + "sha2 0.10.8", +] + [[package]] name = "crc" version = "3.2.1" @@ -1885,7 +2018,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.9.0", "crossterm_winapi", "parking_lot", "rustix 0.38.44", @@ -1973,14 +2106,38 @@ dependencies = [ "tracing", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] @@ -1993,17 +2150,28 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.100", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", "quote", "syn 2.0.100", ] @@ -2085,7 +2253,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.100", @@ -2223,6 +2391,18 @@ dependencies = [ "spki", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "either" version = "1.15.0" @@ -2262,6 +2442,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2278,10 +2478,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "eth2_interop_keypairs" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "bls", + "ethereum_hashing", + "hex", + "num-bigint", + "serde", + "serde_yaml", +] + [[package]] name = "eth2_key_derivation" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v7.0.1#e42406d7b79a85ad4622f3a7440ff6468ac4c9e1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" dependencies = [ "bls", "num-bigint-dig", @@ -2293,7 +2506,7 @@ dependencies = [ [[package]] name = "eth2_keystore" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v7.0.1#e42406d7b79a85ad4622f3a7440ff6468ac4c9e1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" dependencies = [ "aes 0.7.5", "bls", @@ -2336,17 +2549,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "ethereum_ssz" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e999563461faea0ab9bc0024e5e66adcee35881f3d5062f52f31a4070fe1522" -dependencies = [ - "alloy-primitives", - "itertools 0.13.0", - "smallvec", -] - [[package]] name = "ethereum_ssz" version = "0.8.3" @@ -2354,6 +2556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86da3096d1304f5f28476ce383005385459afeaf0eea08592b65ddbc9b258d16" dependencies = [ "alloy-primitives", + "arbitrary", "ethereum_serde_utils", "itertools 0.13.0", "serde", @@ -2368,7 +2571,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d832a5c38eba0e7ad92592f7a22d693954637fbb332b4f669590d66a5c3183e5" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.100", @@ -2384,6 +2587,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -2438,7 +2653,7 @@ dependencies = [ [[package]] name = "fixed_bytes" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v7.0.1#e42406d7b79a85ad4622f3a7440ff6468ac4c9e1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" dependencies = [ "alloy-primitives", "safe_arith", @@ -2687,6 +2902,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -2700,6 +2919,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "headers" version = "0.4.0" @@ -3125,6 +3353,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ + "arbitrary", "equivalent", "hashbrown 0.15.2", "serde", @@ -3139,6 +3368,14 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "int_to_bytes" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "bytes", +] + [[package]] name = "interprocess" version = "2.2.3" @@ -3255,6 +3492,25 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "kzg" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "arbitrary", + "c-kzg", + "derivative", + "ethereum_hashing", + "ethereum_serde_utils", + "ethereum_ssz", + "ethereum_ssz_derive", + "hex", + "rust_eth_kzg", + "serde", + "serde_json", + "tree_hash", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3276,6 +3532,17 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libsqlite3-sys" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3330,6 +3597,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.1.0" @@ -3357,6 +3630,17 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "merkle_proof" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "alloy-primitives", + "ethereum_hashing", + "fixed_bytes", + "safe_arith", +] + [[package]] name = "merlin" version = "3.0.0" @@ -3369,6 +3653,52 @@ dependencies = [ "zeroize", ] +[[package]] +name = "metastruct" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d74f54f231f9a18d77393ecc5cc7ab96709b2a61ee326c2b2b291009b0cc5a07" +dependencies = [ + "metastruct_macro", +] + +[[package]] +name = "metastruct_macro" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7225f3a4dfbec47a0c6a730a874185fda840d365d7bbd6ba199dd81796d5" +dependencies = [ + "darling 0.13.4", + "itertools 0.10.5", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "milhouse" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1ada1f56cc1c79f40517fdcbf57e19f60424a3a1ce372c3fe9b22e4fdd83eb" +dependencies = [ + "alloy-primitives", + "arbitrary", + "educe", + "ethereum_hashing", + "ethereum_ssz", + "ethereum_ssz_derive", + "itertools 0.13.0", + "parking_lot", + "rayon", + "serde", + "smallvec", + "tree_hash", + "triomphe", + "typenum", + "vec_map", +] + [[package]] name = "mime" version = "0.3.17" @@ -3562,7 +3892,7 @@ version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -3881,7 +4211,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.9.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -3893,6 +4223,17 @@ dependencies = [ "unarray", ] +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "prost" version = "0.13.5" @@ -4074,7 +4415,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -4201,6 +4542,15 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rpds" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ef5140bcb576bfd6d56cd2de709a7d17851ac1f3805e67fe9d99e42a11821f" +dependencies = [ + "archery", +] + [[package]] name = "ruint" version = "1.13.1" @@ -4208,6 +4558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "825df406ec217a8116bd7b06897c6cc8f65ffefc15d030ae2c9540acc9ed50b6" dependencies = [ "alloy-rlp", + "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", "bytes", @@ -4233,6 +4584,34 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rusqlite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +dependencies = [ + "bitflags 1.3.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust_eth_kzg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f83b5559e1dcd3f7721838909288faf4500fb466eff98eac99b67ac04335b93" +dependencies = [ + "crate_crypto_internal_eth_kzg_bls12_381", + "crate_crypto_internal_eth_kzg_erasure_codes", + "crate_crypto_kzg_multi_open_fk20", + "hex", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4275,7 +4654,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4288,7 +4667,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.9.3", @@ -4363,7 +4742,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "safe_arith" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v7.0.1#e42406d7b79a85ad4622f3a7440ff6468ac4c9e1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" [[package]] name = "salsa20" @@ -4422,7 +4801,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.9.0", "core-foundation", "core-foundation-sys", "libc", @@ -4576,7 +4955,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.100", @@ -4709,6 +5088,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" dependencies = [ + "arbitrary", "serde", ] @@ -4744,13 +5124,14 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad0fa7e9a85c06d0a6ba5100d733fff72e231eb6db2d86078225cf716fd2d95" dependencies = [ + "arbitrary", "ethereum_serde_utils", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "itertools 0.13.0", "serde", "serde_derive", "smallvec", - "tree_hash 0.9.1", + "tree_hash", "typenum", ] @@ -4783,6 +5164,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -4817,6 +5204,30 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "superstruct" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" +dependencies = [ + "darling 0.13.4", + "itertools 0.10.5", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "swap_or_not_shuffle" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "alloy-primitives", + "ethereum_hashing", + "fixed_bytes", +] + [[package]] name = "syn" version = "1.0.109" @@ -4877,7 +5288,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.0", "core-foundation", "system-configuration-sys", ] @@ -4911,6 +5322,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test_random_derive" +version = "0.2.0" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5370,17 +5790,6 @@ dependencies = [ "tracing-serde", ] -[[package]] -name = "tree_hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373495c23db675a5192de8b610395e1bec324d596f9e6111192ce903dc11403a" -dependencies = [ - "alloy-primitives", - "ethereum_hashing", - "smallvec", -] - [[package]] name = "tree_hash" version = "0.9.1" @@ -5389,7 +5798,7 @@ checksum = "6c58eb0f518840670270d90d97ffee702d8662d9c5494870c9e1e9e0fa00f668" dependencies = [ "alloy-primitives", "ethereum_hashing", - "ethereum_ssz 0.8.3", + "ethereum_ssz", "smallvec", "typenum", ] @@ -5400,12 +5809,22 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699e7fb6b3fdfe0c809916f251cf5132d64966858601695c3736630a87e7166a" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", "syn 2.0.100", ] +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "serde", + "stable_deref_trait", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5437,6 +5856,56 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "types" +version = "0.2.1" +source = "git+https://github.com/sigp/lighthouse?tag=v7.1.0#cfb1f7331064b758c6786e4e1dc15507af5ff5d1" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arbitrary", + "bls", + "compare_fields", + "compare_fields_derive", + "context_deserialize", + "context_deserialize_derive", + "derivative", + "eth2_interop_keypairs", + "ethereum_hashing", + "ethereum_serde_utils", + "ethereum_ssz", + "ethereum_ssz_derive", + "fixed_bytes", + "hex", + "int_to_bytes", + "itertools 0.10.5", + "kzg", + "maplit", + "merkle_proof", + "metastruct", + "milhouse", + "parking_lot", + "rand 0.8.5", + "rand_xorshift", + "rayon", + "regex", + "rpds", + "rusqlite", + "safe_arith", + "serde", + "serde_json", + "serde_yaml", + "smallvec", + "ssz_types", + "superstruct", + "swap_or_not_shuffle", + "tempfile", + "test_random_derive", + "tracing", + "tree_hash", + "tree_hash_derive", +] + [[package]] name = "ucd-trie" version = "0.1.7" @@ -5584,6 +6053,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.5" @@ -5989,7 +6464,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -6058,18 +6533,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d3a6d7d2..78ee7ac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ axum-extra = { version = "0.10.0", features = ["typed-header"] } base64 = "0.22.1" bimap = { version = "0.6.3", features = ["serde"] } blsful = "2.5" -blst = "0.3.11" bytes = "1.10.1" cb-cli = { path = "crates/cli" } cb-common = { path = "crates/common" } @@ -39,7 +38,6 @@ ctr = "0.9.2" derive_more = { version = "2.0.1", features = ["deref", "display", "from", "into"] } docker-compose-types = "0.16.0" docker-image = "0.2.1" -eth2_keystore = { git = "https://github.com/sigp/lighthouse", tag = "v7.0.1" } ethereum_serde_utils = "0.7.0" ethereum_ssz = "0.8" ethereum_ssz_derive = "0.8" @@ -49,6 +47,8 @@ headers = "0.4.0" indexmap = "2.2.6" jsonwebtoken = { version = "9.3.1", default-features = false } lazy_static = "1.5.0" +lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v7.1.0" } +lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v7.1.0" } parking_lot = "0.12.3" pbkdf2 = "0.12.2" prometheus = "0.13.4" diff --git a/benches/pbs/src/main.rs b/benches/pbs/src/main.rs index c013fd61..7b852a43 100644 --- a/benches/pbs/src/main.rs +++ b/benches/pbs/src/main.rs @@ -1,12 +1,11 @@ use std::time::{Duration, Instant}; -use alloy::{primitives::B256, rpc::types::beacon::BlsPublicKey}; +use alloy::primitives::B256; use cb_common::{ config::RelayConfig, pbs::{GetHeaderResponse, RelayClient, RelayEntry}, - signer::BlsSecretKey, - types::Chain, - utils::blst_pubkey_to_alloy, + types::{BlsPublicKey, BlsSecretKey, Chain}, + utils::TestRandomSeed, }; use cb_tests::mock_relay::{start_mock_relay_service, MockRelayState}; use comfy_table::Table; @@ -18,9 +17,6 @@ mod config; fn get_random_hash() -> B256 { B256::from(rand::random::<[u8; 32]>()) } -fn get_random_pubkey() -> BlsPublicKey { - BlsPublicKey::ZERO -} #[tokio::main] async fn main() { @@ -46,8 +42,8 @@ async fn main() { // bench for slot in 0..config.benchmark.n_slots { let parent_hash = get_random_hash(); - let validator_pubkey = get_random_pubkey(); - let url = mock_validator.get_header_url(slot, parent_hash, validator_pubkey).unwrap(); + let validator_pubkey = BlsPublicKey::test_random(); + let url = mock_validator.get_header_url(slot, &parent_hash, &validator_pubkey).unwrap(); for _ in 0..config.benchmark.headers_per_slot { let url = url.clone(); @@ -138,8 +134,8 @@ const MOCK_RELAY_SECRET: [u8; 32] = [ 152, 98, 59, 240, 181, 131, 47, 1, 180, 255, 245, ]; async fn start_mock_relay(chain: Chain, relay_config: RelayConfig) { - let signer = BlsSecretKey::key_gen(&MOCK_RELAY_SECRET, &[]).unwrap(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let signer = BlsSecretKey::deserialize(&MOCK_RELAY_SECRET).unwrap(); + let pubkey: BlsPublicKey = signer.public_key(); assert_eq!(relay_config.entry.pubkey, pubkey, "Expected relay pubkey to be 0xb060572f535ba5615b874ebfef757fbe6825352ad257e31d724e57fe25a067a13cfddd0f00cb17bf3a3d2e901a380c17"); @@ -152,7 +148,7 @@ async fn start_mock_relay(chain: Chain, relay_config: RelayConfig) { } fn get_mock_validator(bench: BenchConfig) -> RelayClient { - let entry = RelayEntry { id: bench.id, pubkey: BlsPublicKey::default(), url: bench.url }; + let entry = RelayEntry { id: bench.id, pubkey: BlsPublicKey::test_random(), url: bench.url }; let config = RelayConfig { entry, id: None, diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 122a35fc..7359fea3 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -13,8 +13,8 @@ pub mod prelude { signature::{ verify_proposer_commitment_signature_bls, verify_proposer_commitment_signature_ecdsa, }, - signer::{BlsPublicKey, BlsSignature, EcdsaSignature}, - types::Chain, + signer::EcdsaSignature, + types::{BlsPublicKey, BlsSignature, Chain}, utils::{initialize_tracing_log, utcnow_ms, utcnow_ns, utcnow_sec, utcnow_us}, }; pub use cb_metrics::provider::MetricsProvider; diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 8653ea44..3cf20988 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -12,20 +12,20 @@ async-trait.workspace = true axum.workspace = true base64.workspace = true bimap.workspace = true -blst.workspace = true bytes.workspace = true cipher.workspace = true const_format.workspace = true ctr.workspace = true derive_more.workspace = true docker-image.workspace = true -eth2_keystore.workspace = true ethereum_serde_utils.workspace = true ethereum_ssz.workspace = true ethereum_ssz_derive.workspace = true eyre.workspace = true futures.workspace = true jsonwebtoken.workspace = true +lh_eth2_keystore.workspace = true +lh_types.workspace = true pbkdf2.workspace = true rand.workspace = true rayon.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 6945b4d2..5fda0d23 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -23,8 +23,7 @@ use crate::{ response::{BlsSignResponse, EcdsaSignResponse}, }, constants::SIGNER_JWT_EXPIRATION, - signer::BlsPublicKey, - types::{Jwt, ModuleId}, + types::{BlsPublicKey, Jwt, ModuleId}, utils::create_jwt, DEFAULT_REQUEST_TIMEOUT, }; diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index 81edd5fe..e2654b5c 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -7,7 +7,6 @@ use std::{ use alloy::{ hex, primitives::{aliases::B32, Address, B256}, - rpc::types::beacon::BlsSignature, }; use serde::{Deserialize, Deserializer, Serialize}; use tree_hash::TreeHash; @@ -16,20 +15,28 @@ use tree_hash_derive::TreeHash; use crate::{ config::decode_string_to_map, constants::COMMIT_BOOST_DOMAIN, - error::BlstErrorWrapper, signature::verify_signed_message, - signer::BlsPublicKey, - types::{Chain, ModuleId}, + types::{BlsPublicKey, BlsSignature, Chain, ModuleId}, }; -pub trait ProxyId: AsRef<[u8]> + Debug + Clone + Copy + TreeHash + Display {} +pub trait ProxyId: Debug + Clone + TreeHash + Display { + fn to_bytes(&self) -> Vec; +} -impl ProxyId for Address {} +impl ProxyId for Address { + fn to_bytes(&self) -> Vec { + self.0.as_slice().to_vec() + } +} -impl ProxyId for BlsPublicKey {} +impl ProxyId for BlsPublicKey { + fn to_bytes(&self) -> Vec { + self.serialize().to_vec() + } +} // GENERIC PROXY DELEGATION -#[derive(Debug, Clone, Copy, Serialize, Deserialize, TreeHash)] +#[derive(Debug, Clone, Serialize, Deserialize, TreeHash)] pub struct ProxyDelegation { pub delegator: BlsPublicKey, pub proxy: T, @@ -44,7 +51,7 @@ impl fmt::Display for ProxyDelegation { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignedProxyDelegation { pub message: ProxyDelegation, /// Signature of message with the delegator keypair @@ -55,7 +62,7 @@ pub type SignedProxyDelegationBls = SignedProxyDelegation; pub type SignedProxyDelegationEcdsa = SignedProxyDelegation
; impl SignedProxyDelegation { - pub fn validate(&self, chain: Chain) -> Result<(), BlstErrorWrapper> { + pub fn validate(&self, chain: Chain) -> bool { verify_signed_message( chain, &self.message.delegator, @@ -262,7 +269,7 @@ mod tests { #[test] fn test_decode_response_signature() { - let data = r#""0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989""#; + let data = r#""0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000""#; let _: BlsSignature = serde_json::from_str(data).unwrap(); let data = r#""0x985b495f49d1b96db3bba3f6c5dd1810950317c10d4c2042bd316f338cdbe74359072e209b85e56ac492092d7860063dd096ca31b4e164ef27e3f8d508e656801c""#; @@ -295,7 +302,7 @@ mod tests { "delegator": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050", "proxy": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050" }, - "signature": "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + "signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }"#; let _: SignedProxyDelegationBls = serde_json::from_str(data).unwrap(); @@ -305,7 +312,7 @@ mod tests { "delegator": "0xa3366b54f28e4bf1461926a3c70cdb0ec432b5c92554ecaae3742d33fb33873990cbed1761c68020e6d3c14d30a22050", "proxy": "0x4ca9939a8311a7cab3dde201b70157285fa81a9d" }, - "signature": "0xa3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989a3ffa9241f78279f1af04644cb8c79c2d8f02bcf0e28e2f186f6dcccac0a869c2be441fda50f0dea895cfce2e53f0989" + "signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }"#; let _: SignedProxyDelegationEcdsa = serde_json::from_str(data).unwrap(); diff --git a/crates/common/src/commit/response.rs b/crates/common/src/commit/response.rs index a5eb9434..0e984144 100644 --- a/crates/common/src/commit/response.rs +++ b/crates/common/src/commit/response.rs @@ -1,10 +1,10 @@ -use alloy::{ - primitives::{Address, B256, U256}, - rpc::types::beacon::BlsSignature, -}; +use alloy::primitives::{Address, B256, U256}; use serde::{Deserialize, Serialize}; -use crate::signer::{BlsPublicKey, EcdsaSignature}; +use crate::{ + signer::EcdsaSignature, + types::{BlsPublicKey, BlsSignature}, +}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BlsSignResponse { diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 65be45fb..0c8f7011 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -8,13 +8,13 @@ use std::{ use alloy::{ primitives::{address, Address, U256}, providers::ProviderBuilder, - rpc::{client::RpcClient, types::beacon::BlsPublicKey}, + rpc::{client::RpcClient, types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN}, sol, transports::http::Http, }; use eyre::{bail, ensure, Context}; use reqwest::Client; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use tracing::{debug, info, warn}; use url::Url; @@ -22,7 +22,7 @@ use super::{load_optional_env_var, PbsConfig, RelayConfig, MUX_PATH_ENV}; use crate::{ config::{remove_duplicate_keys, safe_read_http_response}, pbs::RelayClient, - types::Chain, + types::{BlsPublicKey, Chain}, }; #[derive(Debug, Deserialize, Serialize)] @@ -103,8 +103,8 @@ impl PbsMuxes { let config = Arc::new(config); let runtime_config = RuntimeMuxConfig { id: mux.id, config, relays: relay_clients }; - for pubkey in mux.validator_pubkeys.iter() { - configs.insert(*pubkey, runtime_config.clone()); + for pubkey in mux.validator_pubkeys.into_iter() { + configs.insert(pubkey, runtime_config.clone()); } } @@ -296,7 +296,6 @@ async fn fetch_lido_registry_keys( debug!("fetching {total_keys} total keys"); const CALL_BATCH_SIZE: u64 = 250u64; - const BLS_PK_LEN: usize = BlsPublicKey::len_bytes(); let mut keys = vec![]; let mut offset = 0; @@ -311,13 +310,16 @@ async fn fetch_lido_registry_keys( .pubkeys; ensure!( - pubkeys.len() % BLS_PK_LEN == 0, - "unexpected number of keys in batch, expected multiple of {BLS_PK_LEN}, got {}", + pubkeys.len() % BLS_PUBLIC_KEY_BYTES_LEN == 0, + "unexpected number of keys in batch, expected multiple of {BLS_PUBLIC_KEY_BYTES_LEN}, got {}", pubkeys.len() ); - for chunk in pubkeys.chunks(BLS_PK_LEN) { - keys.push(BlsPublicKey::try_from(chunk)?); + for chunk in pubkeys.chunks(BLS_PUBLIC_KEY_BYTES_LEN) { + keys.push( + BlsPublicKey::deserialize(chunk) + .map_err(|_| eyre::eyre!("invalid BLS public key"))?, + ); } offset += limit; @@ -356,10 +358,13 @@ async fn fetch_ssv_pubkeys( ); let response = fetch_ssv_pubkeys_from_url(&url, http_timeout).await?; - pubkeys.extend(response.validators.iter().map(|v| v.pubkey).collect::>()); + let fetched = response.validators.len(); + pubkeys.extend( + response.validators.into_iter().map(|v| v.pubkey).collect::>(), + ); page += 1; - if response.validators.len() < MAX_PER_PAGE { + if fetched < MAX_PER_PAGE { ensure!( pubkeys.len() == response.pagination.total, "expected {} keys, got {}", @@ -397,12 +402,29 @@ struct SSVResponse { pagination: SSVPagination, } -#[derive(Deserialize)] struct SSVValidator { - #[serde(rename = "public_key")] pubkey: BlsPublicKey, } +impl<'de> Deserialize<'de> for SSVValidator { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct SSVValidator { + public_key: String, + } + + let s = SSVValidator::deserialize(deserializer)?; + let bytes = alloy::hex::decode(&s.public_key).map_err(serde::de::Error::custom)?; + let pubkey = BlsPublicKey::deserialize(&bytes) + .map_err(|e| serde::de::Error::custom(format!("invalid BLS public key: {e:?}")))?; + + Ok(Self { pubkey }) + } +} + #[derive(Deserialize)] struct SSVPagination { total: usize, @@ -412,7 +434,7 @@ struct SSVPagination { mod tests { use std::net::SocketAddr; - use alloy::{hex::FromHex, primitives::U256, providers::ProviderBuilder}; + use alloy::{primitives::U256, providers::ProviderBuilder}; use axum::{response::Response, routing::get}; use tokio::{net::TcpListener, task::JoinHandle}; use url::Url; @@ -420,7 +442,7 @@ mod tests { use super::*; use crate::{ config::{HTTP_TIMEOUT_SECONDS_DEFAULT, MUXER_HTTP_MAX_LENGTH}, - utils::{set_ignore_content_length, ResponseReadError}, + utils::{bls_pubkey_from_hex_unchecked, set_ignore_content_length, ResponseReadError}, }; const TEST_HTTP_TIMEOUT: u64 = 2; @@ -448,8 +470,11 @@ mod tests { .pubkeys; let mut vec = vec![]; - for chunk in pubkeys.chunks(BlsPublicKey::len_bytes()) { - vec.push(BlsPublicKey::try_from(chunk)?); + for chunk in pubkeys.chunks(BLS_PUBLIC_KEY_BYTES_LEN) { + vec.push( + BlsPublicKey::deserialize(chunk) + .map_err(|_| eyre::eyre!("invalid BLS public key"))?, + ); } assert_eq!(vec.len(), LIMIT); @@ -472,15 +497,9 @@ mod tests { // NOTE: requires that ssv_data.json dpesn't change assert_eq!(response.validators.len(), 3); let expected_pubkeys = [ - BlsPublicKey::from_hex( - "0x967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a", - )?, - BlsPublicKey::from_hex( - "0xac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c", - )?, - BlsPublicKey::from_hex( - "0x8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639", - )?, + bls_pubkey_from_hex_unchecked("967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a"), + bls_pubkey_from_hex_unchecked("ac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c"), + bls_pubkey_from_hex_unchecked("8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639"), ]; for (i, validator) in response.validators.iter().enumerate() { assert_eq!(validator.pubkey, expected_pubkeys[i]); diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index e12493d4..96a6fbda 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -9,7 +9,6 @@ use std::{ use alloy::{ primitives::{utils::format_ether, U256}, providers::{Provider, ProviderBuilder}, - rpc::types::beacon::BlsPublicKey, }; use eyre::{ensure, Result}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -29,7 +28,7 @@ use crate::{ BuilderEventPublisher, DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT, LATE_IN_SLOT_TIME_MS, REGISTER_VALIDATOR_RETRY_LIMIT, }, - types::{Chain, Jwt, ModuleId}, + types::{BlsPublicKey, Chain, Jwt, ModuleId}, utils::{ as_eth_str, default_bool, default_host, default_u16, default_u256, default_u32, default_u64, WEI_PER_ETH, diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index 5e8e3a65..d99ae2dd 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,12 +1,11 @@ use std::{collections::HashMap, path::Path}; -use alloy::rpc::types::beacon::BlsPublicKey; use eyre::{bail, Context, Result}; use serde::de::DeserializeOwned; use crate::{ config::{ADMIN_JWT_ENV, JWTS_ENV, MUXER_HTTP_MAX_LENGTH}, - types::ModuleId, + types::{BlsPublicKey, ModuleId}, utils::read_chunked_body_with_max, }; @@ -67,7 +66,7 @@ pub fn remove_duplicate_keys(keys: Vec) -> Vec { let mut key_set = std::collections::HashSet::new(); for key in keys { - if key_set.insert(key) { + if key_set.insert(key.clone()) { unique_keys.push(key); } } @@ -92,6 +91,7 @@ pub fn decode_string_to_map(raw: &str) -> Result> { #[cfg(test)] mod tests { use super::*; + use crate::utils::TestRandomSeed; /// TODO: This was only used by the old JWT loader, can it be removed now? #[test] @@ -106,9 +106,9 @@ mod tests { #[test] fn test_remove_duplicate_keys() { - let key1 = BlsPublicKey::from([1; 48]); - let key2 = BlsPublicKey::from([2; 48]); - let keys = vec![key1, key2, key1]; + let key1 = BlsPublicKey::test_random(); + let key2 = BlsPublicKey::test_random(); + let keys = vec![key1.clone(), key2.clone(), key1.clone()]; let unique_keys = remove_duplicate_keys(keys); assert_eq!(unique_keys.len(), 2); diff --git a/crates/common/src/error.rs b/crates/common/src/error.rs deleted file mode 100644 index d34949ca..00000000 --- a/crates/common/src/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use blst::BLST_ERROR; -use thiserror::Error; -#[derive(Debug, Error, PartialEq, Eq)] -pub enum BlstErrorWrapper { - BlstSuccess(BLST_ERROR), - BlstBadEncoding(BLST_ERROR), - BlstPointNotOnCurve(BLST_ERROR), - BlstPointNotInGroup(BLST_ERROR), - BlstAggrTypeMismatch(BLST_ERROR), - BlstVerifyFail(BLST_ERROR), - BlstPkIsInfinity(BLST_ERROR), - BlstBadScalar(BLST_ERROR), -} - -impl Display for BlstErrorWrapper { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - BlstErrorWrapper::BlstSuccess(_) => write!(f, "BLST_SUCCESS"), - BlstErrorWrapper::BlstBadEncoding(_) => write!(f, "BLST_BAD_ENCODING"), - BlstErrorWrapper::BlstPointNotOnCurve(_) => write!(f, "BLST_POINT_NOT_ON_CURVE"), - BlstErrorWrapper::BlstPointNotInGroup(_) => write!(f, "BLST_POINT_NOT_IN_GROUP"), - BlstErrorWrapper::BlstAggrTypeMismatch(_) => write!(f, "BLST_AGGR_TYPE_MISMATCH"), - BlstErrorWrapper::BlstVerifyFail(_) => write!(f, "BLST_VERIFY_FAIL"), - BlstErrorWrapper::BlstPkIsInfinity(_) => write!(f, "BLST_PK_IS_INFINITY"), - BlstErrorWrapper::BlstBadScalar(_) => write!(f, "BLST_BAD_SCALAR"), - } - } -} -impl From for BlstErrorWrapper { - fn from(value: BLST_ERROR) -> Self { - match value { - BLST_ERROR::BLST_SUCCESS => BlstErrorWrapper::BlstSuccess(BLST_ERROR::BLST_SUCCESS), - BLST_ERROR::BLST_BAD_ENCODING => { - BlstErrorWrapper::BlstBadEncoding(BLST_ERROR::BLST_BAD_ENCODING) - } - BLST_ERROR::BLST_POINT_NOT_ON_CURVE => { - BlstErrorWrapper::BlstPointNotOnCurve(BLST_ERROR::BLST_POINT_NOT_ON_CURVE) - } - BLST_ERROR::BLST_POINT_NOT_IN_GROUP => { - BlstErrorWrapper::BlstPointNotInGroup(BLST_ERROR::BLST_POINT_NOT_IN_GROUP) - } - BLST_ERROR::BLST_AGGR_TYPE_MISMATCH => { - BlstErrorWrapper::BlstAggrTypeMismatch(BLST_ERROR::BLST_AGGR_TYPE_MISMATCH) - } - BLST_ERROR::BLST_VERIFY_FAIL => { - BlstErrorWrapper::BlstVerifyFail(BLST_ERROR::BLST_VERIFY_FAIL) - } - BLST_ERROR::BLST_PK_IS_INFINITY => { - BlstErrorWrapper::BlstPkIsInfinity(BLST_ERROR::BLST_PK_IS_INFINITY) - } - BLST_ERROR::BLST_BAD_SCALAR => { - BlstErrorWrapper::BlstBadScalar(BLST_ERROR::BLST_BAD_SCALAR) - } - } - } -} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 5042061b..1fe1f26a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -3,7 +3,6 @@ use std::time::Duration; pub mod commit; pub mod config; pub mod constants; -pub mod error; pub mod pbs; pub mod signature; pub mod signer; diff --git a/crates/common/src/pbs/error.rs b/crates/common/src/pbs/error.rs index 9b42a626..dd91ec45 100644 --- a/crates/common/src/pbs/error.rs +++ b/crates/common/src/pbs/error.rs @@ -1,10 +1,7 @@ -use alloy::{ - primitives::{B256, U256}, - rpc::types::beacon::BlsPublicKey, -}; +use alloy::primitives::{B256, U256}; use thiserror::Error; -use crate::{error::BlstErrorWrapper, utils::ResponseReadError}; +use crate::{types::BlsPublicKey, utils::ResponseReadError}; #[derive(Debug, Error)] pub enum PbsError { @@ -58,7 +55,7 @@ pub enum ValidationError { EmptyBlockhash, #[error("pubkey mismatch: expected {expected} got {got}")] - PubkeyMismatch { expected: BlsPublicKey, got: BlsPublicKey }, + PubkeyMismatch { expected: Box, got: Box }, #[error("parent hash mismatch: expected {expected} got {got}")] ParentHashMismatch { expected: B256, got: B256 }, @@ -66,7 +63,7 @@ pub enum ValidationError { #[error("block hash mismatch: expected {expected} got {got}")] BlockHashMismatch { expected: B256, got: B256 }, - #[error("mismatch in KZG commitments: exepcted_blobs: {expected_blobs} got_blobs: {got_blobs} got_commitments: {got_commitments} got_proofs: {got_proofs}")] + #[error("mismatch in KZG commitments: expected_blobs: {expected_blobs} got_blobs: {got_blobs} got_commitments: {got_commitments} got_proofs: {got_proofs}")] KzgCommitments { expected_blobs: usize, got_blobs: usize, @@ -83,8 +80,8 @@ pub enum ValidationError { #[error("empty tx root")] EmptyTxRoot, - #[error("failed signature verification: {0:?}")] - Sigverify(#[from] BlstErrorWrapper), + #[error("failed signature verification")] + Sigverify, #[error("wrong timestamp: expected {expected} got {got}")] TimestampMismatch { expected: u64, got: u64 }, diff --git a/crates/common/src/pbs/relay.rs b/crates/common/src/pbs/relay.rs index 6f582a13..4d8296c2 100644 --- a/crates/common/src/pbs/relay.rs +++ b/crates/common/src/pbs/relay.rs @@ -1,9 +1,6 @@ use std::{str::FromStr, sync::Arc}; -use alloy::{ - primitives::{hex::FromHex, B256}, - rpc::types::beacon::BlsPublicKey, -}; +use alloy::primitives::B256; use eyre::WrapErr; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde::{Deserialize, Serialize}; @@ -14,7 +11,9 @@ use super::{ error::PbsError, HEADER_VERSION_KEY, HEADER_VERSION_VALUE, }; -use crate::{config::RelayConfig, pbs::BuilderApiVersion, DEFAULT_REQUEST_TIMEOUT}; +use crate::{ + config::RelayConfig, pbs::BuilderApiVersion, types::BlsPublicKey, DEFAULT_REQUEST_TIMEOUT, +}; /// A parsed entry of the relay url in the format: scheme://pubkey@host #[derive(Debug, Clone)] @@ -43,7 +42,7 @@ impl<'de> Deserialize<'de> for RelayEntry { { let url = Url::deserialize(deserializer)?; let id = url.host().ok_or(serde::de::Error::custom("missing host"))?.to_string(); - let pubkey = BlsPublicKey::from_hex(url.username()) + let pubkey = BlsPublicKey::from_str(url.username()) .map_err(|_| serde::de::Error::custom("invalid BLS pubkey"))?; Ok(RelayEntry { pubkey, url, id }) @@ -84,8 +83,8 @@ impl RelayClient { Ok(Self { id: Arc::new(config.id().to_owned()), client, config: Arc::new(config) }) } - pub fn pubkey(&self) -> BlsPublicKey { - self.config.entry.pubkey + pub fn pubkey(&self) -> &BlsPublicKey { + &self.config.entry.pubkey } // URL builders @@ -112,8 +111,8 @@ impl RelayClient { pub fn get_header_url( &self, slot: u64, - parent_hash: B256, - validator_pubkey: BlsPublicKey, + parent_hash: &B256, + validator_pubkey: &BlsPublicKey, ) -> Result { self.builder_api_url( &format!("/header/{slot}/{parent_hash}/{validator_pubkey}"), @@ -138,21 +137,19 @@ impl RelayClient { mod tests { use std::collections::HashMap; - use alloy::{ - primitives::{hex::FromHex, B256}, - rpc::types::beacon::BlsPublicKey, - }; + use alloy::primitives::B256; use super::{RelayClient, RelayEntry}; - use crate::config::RelayConfig; + use crate::{config::RelayConfig, utils::bls_pubkey_from_hex_unchecked}; #[test] fn test_relay_entry() { - let s = "http://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@abc.xyz/"; + let pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); + let s = format!("http://{pubkey}@abc.xyz/"); let parsed = serde_json::from_str::(&format!("\"{s}\"")).unwrap(); - assert_eq!(parsed.pubkey, BlsPublicKey::from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae").unwrap()); + assert_eq!(parsed.pubkey, pubkey); assert_eq!(parsed.url.as_str(), s); assert_eq!(parsed.id, "abc.xyz"); } @@ -161,7 +158,7 @@ mod tests { fn test_relay_url() { let slot = 0; let parent_hash = B256::ZERO; - let validator_pubkey = BlsPublicKey::ZERO; + let validator_pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); let expected = format!("http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}"); let relay_config = r#" @@ -173,7 +170,7 @@ mod tests { let relay = RelayClient::new(config).unwrap(); assert_eq!( - relay.get_header_url(slot, parent_hash, validator_pubkey).unwrap().to_string(), + relay.get_header_url(slot, &parent_hash, &validator_pubkey).unwrap().to_string(), expected ); @@ -186,7 +183,7 @@ mod tests { let relay = RelayClient::new(config).unwrap(); assert_eq!( - relay.get_header_url(slot, parent_hash, validator_pubkey).unwrap().to_string(), + relay.get_header_url(slot, &parent_hash, &validator_pubkey).unwrap().to_string(), expected ); } @@ -195,7 +192,7 @@ mod tests { fn test_relay_url_with_get_params() { let slot = 0; let parent_hash = B256::ZERO; - let validator_pubkey = BlsPublicKey::ZERO; + let validator_pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); // Note: HashMap iteration order is not guaranteed, so we can't predict the // exact order of parameters Instead of hard-coding the order, we'll // check that both parameters are present in the URL @@ -214,7 +211,7 @@ mod tests { config.get_params = Some(get_params); let relay = RelayClient::new(config).unwrap(); - let url = relay.get_header_url(slot, parent_hash, validator_pubkey).unwrap().to_string(); + let url = relay.get_header_url(slot, &parent_hash, &validator_pubkey).unwrap().to_string(); assert!(url.starts_with(&url_prefix)); assert!(url.contains("param1=value1")); assert!(url.contains("param2=value2")); diff --git a/crates/common/src/pbs/types/beacon_block.rs b/crates/common/src/pbs/types/beacon_block.rs index 485876e9..5d3b139c 100644 --- a/crates/common/src/pbs/types/beacon_block.rs +++ b/crates/common/src/pbs/types/beacon_block.rs @@ -1,4 +1,4 @@ -use alloy::{primitives::B256, rpc::types::beacon::BlsSignature}; +use alloy::primitives::B256; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -6,8 +6,9 @@ use super::{ blinded_block_body::BlindedBeaconBlockBodyElectra, blobs_bundle::BlobsBundle, execution_payload::ExecutionPayload, spec::ElectraSpec, utils::VersionedResponse, }; +use crate::types::BlsSignature; -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] /// Sent to relays in submit_block pub struct SignedBlindedBeaconBlock { pub message: BlindedBeaconBlock, @@ -47,13 +48,7 @@ pub enum BlindedBeaconBlock { Electra(BlindedBeaconBlockElectra), } -impl Default for BlindedBeaconBlock { - fn default() -> Self { - Self::Electra(BlindedBeaconBlockElectra::default()) - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct BlindedBeaconBlockElectra { #[serde(with = "serde_utils::quoted_u64")] pub slot: u64, @@ -170,8 +165,7 @@ mod tests { assert!(matches!(block_json.message, BlindedBeaconBlock::Electra(_))); let data_ssz = include_bytes!("testdata/signed-blinded-beacon-block-electra-2.ssz"); - let data_ssz = alloy::primitives::hex::decode(data_ssz).unwrap(); - let block_ssz = test_encode_decode_ssz::(&data_ssz); + let block_ssz = test_encode_decode_ssz::(data_ssz); assert!(matches!(block_ssz.message, BlindedBeaconBlock::Electra(_))); assert_eq!(block_json.as_ssz_bytes(), data_ssz); diff --git a/crates/common/src/pbs/types/blinded_block_body.rs b/crates/common/src/pbs/types/blinded_block_body.rs index 966aa1e5..4acd3dd1 100644 --- a/crates/common/src/pbs/types/blinded_block_body.rs +++ b/crates/common/src/pbs/types/blinded_block_body.rs @@ -1,7 +1,4 @@ -use alloy::{ - primitives::{Address, B256}, - rpc::types::beacon::{BlsPublicKey, BlsSignature}, -}; +use alloy::primitives::{Address, B256}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use ssz_types::{typenum, BitList, BitVector, FixedVector, VariableList}; @@ -10,8 +7,9 @@ use super::{ execution_payload::ExecutionPayloadHeader, execution_requests::ExecutionRequests, kzg::KzgCommitments, spec::EthSpec, utils::*, }; +use crate::types::{BlsPublicKey, BlsSignature}; -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(deny_unknown_fields)] pub struct BlindedBeaconBlockBodyElectra { pub randao_reveal: BlsSignature, @@ -49,13 +47,13 @@ pub struct BeaconBlockHeader { pub body_root: B256, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct SignedBeaconBlockHeader { pub message: BeaconBlockHeader, pub signature: BlsSignature, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct BlsToExecutionChange { #[serde(with = "serde_utils::quoted_u64")] pub validator_index: u64, @@ -63,25 +61,25 @@ pub struct BlsToExecutionChange { pub to_execution_address: Address, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct SignedBlsToExecutionChange { pub message: BlsToExecutionChange, pub signature: BlsSignature, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct ProposerSlashing { pub signed_header_1: SignedBeaconBlockHeader, pub signed_header_2: SignedBeaconBlockHeader, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct AttesterSlashingElectra { pub attestation_1: IndexedAttestationElectra, pub attestation_2: IndexedAttestationElectra, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "T: EthSpec")] pub struct IndexedAttestationElectra { /// Lists validator registry indices, not committee indices. @@ -120,13 +118,13 @@ pub struct AttestationElectra { pub committee_bits: BitVector, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct Deposit { pub proof: FixedVector, // put this in EthSpec? pub data: DepositData, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct DepositData { pub pubkey: BlsPublicKey, pub withdrawal_credentials: B256, @@ -135,7 +133,7 @@ pub struct DepositData { pub signature: BlsSignature, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct SignedVoluntaryExit { pub message: VoluntaryExit, pub signature: BlsSignature, @@ -150,7 +148,7 @@ pub struct VoluntaryExit { pub validator_index: u64, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "T: EthSpec")] pub struct SyncAggregate { pub sync_committee_bits: BitVector, diff --git a/crates/common/src/pbs/types/execution_requests.rs b/crates/common/src/pbs/types/execution_requests.rs index b84430bb..2c48f24a 100644 --- a/crates/common/src/pbs/types/execution_requests.rs +++ b/crates/common/src/pbs/types/execution_requests.rs @@ -1,13 +1,11 @@ -use alloy::{ - primitives::{Address, B256}, - rpc::types::beacon::{BlsPublicKey, BlsSignature}, -}; +use alloy::primitives::{Address, B256}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use ssz_types::VariableList; use tree_hash_derive::TreeHash; use super::spec::EthSpec; +use crate::types::{BlsPublicKey, BlsSignature}; #[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct ExecutionRequests { @@ -16,7 +14,7 @@ pub struct ExecutionRequests { pub consolidations: VariableList, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct DepositRequest { pub pubkey: BlsPublicKey, pub withdrawal_credentials: B256, @@ -27,7 +25,7 @@ pub struct DepositRequest { pub index: u64, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct WithdrawalRequest { pub source_address: Address, pub validator_pubkey: BlsPublicKey, @@ -35,7 +33,7 @@ pub struct WithdrawalRequest { pub amount: u64, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct ConsolidationRequest { pub source_address: Address, pub source_pubkey: BlsPublicKey, diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index ebb91e11..0f9c5330 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -1,7 +1,4 @@ -use alloy::{ - primitives::{B256, U256}, - rpc::types::beacon::{BlsPublicKey, BlsSignature}, -}; +use alloy::primitives::{B256, U256}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -11,8 +8,9 @@ use super::{ execution_payload::ExecutionPayloadHeader, execution_requests::ExecutionRequests, kzg::KzgCommitments, spec::ElectraSpec, utils::VersionedResponse, }; +use crate::types::{BlsPublicKey, BlsSignature}; -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct GetHeaderParams { /// The slot to request the header for pub slot: u64, @@ -45,9 +43,9 @@ impl GetHeaderResponse { } } - pub fn pubkey(&self) -> BlsPublicKey { + pub fn pubkey(&self) -> &BlsPublicKey { match self { - VersionedResponse::Electra(data) => data.message.pubkey, + VersionedResponse::Electra(data) => &data.message.pubkey, } } @@ -69,20 +67,20 @@ impl GetHeaderResponse { } } - pub fn signautre(&self) -> BlsSignature { + pub fn signautre(&self) -> &BlsSignature { match self { - GetHeaderResponse::Electra(data) => data.signature, + GetHeaderResponse::Electra(data) => &data.signature, } } } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] pub struct SignedExecutionPayloadHeader { pub message: T, pub signature: BlsSignature, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, TreeHash)] pub struct ExecutionPayloadHeaderMessageElectra { pub header: ExecutionPayloadHeader, pub blob_kzg_commitments: KzgCommitments, @@ -106,7 +104,7 @@ mod tests { }; #[test] - // from the builder api spec, the signature is a dummy so it's not checked + // from the builder api spec, with signature fixed to the correct pubkey fn test_get_header_electra() { let data = r#"{ "version": "electra", @@ -137,32 +135,32 @@ mod tests { "execution_requests": { "deposits": [ { - "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "pubkey": "0x911f24ad11078aad2b28ff9dcb4651a0b686e3972b2b4190273f35d416bf057dbd95553d7a0edb107b1a5e1b211da8c4", "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "amount": "1", - "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "signature": "0xb4f92cd90de8e4b67debeb0379f08d0e6d3046e67e824e6ed63cd841abc9999c8b123a780e34a480d4ef13466b6241e30000b047d27de43fcf475fc4e69da2d26929cec97742892346f53e78f973bbe8095285f05a8ea60b118cdd1e6a704c94", "index": "1" } ], "withdrawals": [ { "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", - "validator_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "validator_pubkey": "0x911f24ad11078aad2b28ff9dcb4651a0b686e3972b2b4190273f35d416bf057dbd95553d7a0edb107b1a5e1b211da8c4", "amount": "1" } ], "consolidations": [ { "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", - "source_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", - "target_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + "source_pubkey": "0x911f24ad11078aad2b28ff9dcb4651a0b686e3972b2b4190273f35d416bf057dbd95553d7a0edb107b1a5e1b211da8c4", + "target_pubkey": "0x911f24ad11078aad2b28ff9dcb4651a0b686e3972b2b4190273f35d416bf057dbd95553d7a0edb107b1a5e1b211da8c4" } ] }, "value": "1", - "pubkey": "0x86b1cea87eed94cad99244356abcd83995947670f0553a1d3fe83c4a9e8116f4891fb1c51db232e736be1cb3327164bc" + "pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5" }, - "signature": "0x8addecd35e0ffe27b74e41aff2836527e6fea0efdb46dbb0f7436f5087d0cd5665bd16d924f640fc928cdba0173971e400dc603dbd6310bfb6f249c1554b044fe06ae4cf5d5f452f3ff19d9d130809b34d3d3abdca3d192c839ba2ac91129c15" + "signature": "0x8aeb4642fb2982039a43fd6a6d9cc0ebf7598dbf02343c4617d9a68d799393c162492add63f31099a25eacc2782ba27a190e977a8c58760b6636dccb503d528b3be9e885c93d5b79699e68fcca870b0c790cdb00d67604d8b4a3025ae75efa2f" } }"#; @@ -178,8 +176,7 @@ mod tests { &parsed.signature, None, &B32::from(APPLICATION_BUILDER_DOMAIN), - ) - .is_ok()) + )) } #[test] @@ -191,9 +188,8 @@ mod tests { >(data_json); let data_ssz = include_bytes!("testdata/get-header-response.ssz"); - let data_ssz = alloy::primitives::hex::decode(data_ssz).unwrap(); test_encode_decode_ssz::>( - &data_ssz, + data_ssz, ); assert_eq!(block_json.as_ssz_bytes(), data_ssz); diff --git a/crates/common/src/pbs/types/testdata/get-header-response.json b/crates/common/src/pbs/types/testdata/get-header-response.json index 0198af6a..96ee57af 100644 --- a/crates/common/src/pbs/types/testdata/get-header-response.json +++ b/crates/common/src/pbs/types/testdata/get-header-response.json @@ -25,30 +25,30 @@ "execution_requests": { "deposits": [ { - "pubkey": "0x0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", "withdrawal_credentials": "0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f", "amount": "100", - "signature": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "0x8aeb4642fb2982039a43fd6a6d9cc0ebf7598dbf02343c4617d9a68d799393c162492add63f31099a25eacc2782ba27a190e977a8c58760b6636dccb503d528b3be9e885c93d5b79699e68fcca870b0c790cdb00d67604d8b4a3025ae75efa2f", "index": "1" } ], "withdrawals": [ { "source_address": "0x1100000000000000000000000000000000000000", - "validator_pubkey": "0x120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "validator_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", "amount": "1" } ], "consolidations": [ { "source_address": "0x1200000000000000000000000000000000000000", - "source_pubkey": "0x120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "target_pubkey": "0x110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "source_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", + "target_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5" } ] }, "value": "11", - "pubkey": "0x120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5" }, - "signature": "0x010203040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "signature": "0x8aeb4642fb2982039a43fd6a6d9cc0ebf7598dbf02343c4617d9a68d799393c162492add63f31099a25eacc2782ba27a190e977a8c58760b6636dccb503d528b3be9e885c93d5b79699e68fcca870b0c790cdb00d67604d8b4a3025ae75efa2f" } \ No newline at end of file diff --git a/crates/common/src/pbs/types/testdata/get-header-response.ssz b/crates/common/src/pbs/types/testdata/get-header-response.ssz index 59133c48265668b5513a9bc04f3cbefa6761ff40..7d9c12c554f1ab58f1ff8735db4fbdb0d9cf47bd 100644 GIT binary patch literal 1224 zcmYdcU|{Ha?dJ4bvx#|@^WUu8IR{>UkL=ygWMbncesfuG<>bi+lRUNVCVv)~xhQVU zp$hFqRg!$ut9l~JxYNw;oDQ%J>b8FQqV=S0bY_bxCv&y>~yzip<-X7ZN8ngJ;8J{wV zTLA()ZzoI%ST6Ko>dEg*8Sxnglml9c0hlpFPz6{}xfncF43Uu~z(&Bk4v(O*!f6Xu z+0Y^frY|u~^8_(GfRVxr7QsyrN+&$HwSr{`$)`Z|pMcUOsl_FXOw5F$9>rFW9cO@8 l0_a^*(=9&%0COL$^Cm3+2%`85!Xed9FtdcHU>2!%0|1Zx<^uo# literal 2448 zcmeHIZ4SdA2))u%(c0r|{kZ=TW>!qCi60GJlP%L91dt~W6&+z43K|AcW8?P-J8ii> zeBcqr%o0x-2lYmiVN|14)!TOV=4aYa)fLj4VXNqW=dV}5$p{h_jpJJx&AelJcv8!* z&l-AkG)4=>evrgO-zh2VC*VGHYR`1BFy3YFc=9PYFp#r@87S2Hv;P6}m{=;$r<UkL=ygWMbncesfuG<>bi+lRUNVCVv)~xhQVU zp$hFqRg!$ut9l~JxYNw;oDQ%J>b8FQqV=S0bY( z_A)u}x{qh|LbYFy-x^potNK5l-@9sAu~58&)0q&UF|>9w&zh?jC#FdNxEYOpi885p+m zGB70aF)&O7;%ca&CxCvT2cUJ_4>$j?03(G1h*^N~^8`qXLXs{6EV>w(m|0la*f}`4 zxOsT__yq)oghfQf#3dx9q-A8~$nP+-aQO)) z52JCZ8z{en(!d)awlsja3>HSj#yuf_!Q^4|fcc-1NWeq@FcSbQ1OO`mz(xSD6960p z04D*!MF4OU06YW$F9E z05SxCECC=#0LT*n3Iu>60kDQknfqo}S$3ae*oUONXO(v~c;7|yy*;$kHD>XxGd^V! zw*mxq-cFbjuw3ZF)RW(r5?fHxx`2l@PGAKrt?h%gL1Ffx(?9DZcv$(OHXw^zXGi+R z?5qEREQZWxxwmCsX6VDk$YP^xL{ut^nT>)Ts8X{$Mwhle)ehar|BIQ zyf)o3F)4=+<7h{(ZTCif!3*u%vA-Yv{yn(fSB8U(Ex=aC1Z;>*2#8S@L_|0X65^u; z8PU}OY#U9ei0l?LBfYJ z4Az2#{MUkv?AZd89!ywjS{5|q{T6iW8Y~#tb6B9*u~;zK1WV{$*P4tMbuDYgcb0Qm z>A!Y{ddBY?j~^>1Z~S-fPX1U2y~@BWsC#E*1c5ZvLmBW1F=?@d;z#IB(?nVQYV$*z zZyHMosQVwbMOTUa&i`o=$P(E#8M++h@Jv_VKS|im7;Y=^$xAPfbsa~3SMzSQp@+4vcE| zW;=`rPk@skoC2k-{`m|OzmM;iz%9Ptzy56HR=Mii6|v!%5u8!DZ`_)9 diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index 8d034077..33f79726 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -1,20 +1,15 @@ -use alloy::{ - primitives::{aliases::B32, Address, B256}, - rpc::types::beacon::{constants::BLS_DST_SIG, BlsPublicKey, BlsSignature}, -}; +use alloy::primitives::{aliases::B32, Address, B256}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ constants::{COMMIT_BOOST_DOMAIN, GENESIS_VALIDATORS_ROOT}, - error::BlstErrorWrapper, - signer::{verify_bls_signature, verify_ecdsa_signature, BlsSecretKey, EcdsaSignature}, - types::{self, Chain, SignatureRequestInfo}, + signer::{verify_bls_signature, verify_ecdsa_signature, EcdsaSignature}, + types::{self, BlsPublicKey, BlsSecretKey, BlsSignature, Chain, SignatureRequestInfo}, }; -pub fn sign_message(secret_key: &BlsSecretKey, msg: &[u8]) -> BlsSignature { - let signature = secret_key.sign(msg, BLS_DST_SIG, &[]).to_bytes(); - BlsSignature::from_slice(&signature) +pub fn sign_message(secret_key: &BlsSecretKey, msg: B256) -> BlsSignature { + secret_key.sign(msg) } pub fn compute_prop_commit_signing_root( @@ -47,14 +42,14 @@ pub fn compute_domain(chain: Chain, domain_mask: &B32) -> B256 { #[derive(Debug, TreeHash)] struct ForkData { fork_version: [u8; 4], - genesis_validators_root: [u8; 32], + genesis_validators_root: B256, } let mut domain = [0u8; 32]; domain[..4].copy_from_slice(&domain_mask.0); let fork_version = chain.genesis_fork_version(); - let fd = ForkData { fork_version, genesis_validators_root: GENESIS_VALIDATORS_ROOT }; + let fd = ForkData { fork_version, genesis_validators_root: GENESIS_VALIDATORS_ROOT.into() }; let fork_data_root = fd.tree_hash_root(); domain[4..].copy_from_slice(&fork_data_root[..28]); @@ -69,14 +64,14 @@ pub fn verify_signed_message( signature: &BlsSignature, signature_request_info: Option<&SignatureRequestInfo>, domain_mask: &B32, -) -> Result<(), BlstErrorWrapper> { +) -> bool { let signing_root = compute_prop_commit_signing_root( chain, &msg.tree_hash_root(), signature_request_info, domain_mask, ); - verify_bls_signature(pubkey, signing_root.as_slice(), signature) + verify_bls_signature(pubkey, signing_root, signature) } /// Signs a message with the Beacon builder domain. @@ -97,7 +92,7 @@ pub fn sign_builder_root( let signing_data = types::SigningData { object_root: object_root.tree_hash_root(), signing_domain }; let signing_root = signing_data.tree_hash_root(); - sign_message(secret_key, signing_root.as_slice()) + sign_message(secret_key, signing_root) } pub fn sign_commit_boost_root( @@ -112,7 +107,7 @@ pub fn sign_commit_boost_root( signature_request_info, &B32::from(COMMIT_BOOST_DOMAIN), ); - sign_message(secret_key, signing_root.as_slice()) + sign_message(secret_key, signing_root) } // ============================== @@ -128,7 +123,7 @@ pub fn verify_proposer_commitment_signature_bls( signature: &BlsSignature, module_signing_id: &B256, nonce: u64, -) -> Result<(), BlstErrorWrapper> { +) -> bool { let signing_domain = compute_domain(chain, &B32::from(COMMIT_BOOST_DOMAIN)); let object_root = types::PropCommitSigningInfo { data: msg.tree_hash_root(), @@ -138,7 +133,7 @@ pub fn verify_proposer_commitment_signature_bls( } .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); - verify_bls_signature(pubkey, signing_root.as_slice(), signature) + verify_bls_signature(pubkey, signing_root, signature) } /// Verifies that a proposer commitment signature was generated by the given diff --git a/crates/common/src/signer/loader.rs b/crates/common/src/signer/loader.rs index 4fb9adb1..853eba5c 100644 --- a/crates/common/src/signer/loader.rs +++ b/crates/common/src/signer/loader.rs @@ -8,9 +8,8 @@ use aes::{ cipher::{KeyIvInit, StreamCipher}, Aes128, }; -use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey}; -use eth2_keystore::{json_keystore::JsonKeystore, Keystore}; use eyre::{eyre, Context}; +use lh_eth2_keystore::{json_keystore::JsonKeystore, Keystore}; use pbkdf2::{hmac, pbkdf2}; use rayon::prelude::*; use serde::{de, Deserialize, Deserializer, Serialize}; @@ -21,6 +20,7 @@ use super::{BlsSigner, EcdsaSigner, PrysmDecryptedKeystore, PrysmKeystore}; use crate::{ config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV}, signer::ConsensusSigner, + utils::bls_pubkey_from_hex, }; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -127,9 +127,12 @@ fn load_from_lighthouse_format( } let maybe_pubkey = path.file_name().and_then(|d| d.to_str())?; - let Ok(pubkey) = BlsPublicKey::from_hex(maybe_pubkey) else { - warn!("Invalid pubkey: {}", maybe_pubkey); - return None + let pubkey = match bls_pubkey_from_hex(maybe_pubkey) { + Ok(pubkey) => pubkey, + Err(e) => { + warn!("Invalid pubkey: {}: {}", maybe_pubkey, e); + return None + } }; let ks_path = keys_path.join(maybe_pubkey).join("voting-keystore.json"); @@ -290,7 +293,7 @@ fn load_from_nimbus_format( } let maybe_pubkey = path.file_name().and_then(|d| d.to_str())?; - let Ok(pubkey) = BlsPublicKey::from_hex(maybe_pubkey) else { + let Ok(pubkey) = bls_pubkey_from_hex(maybe_pubkey) else { warn!("Invalid pubkey: {}", maybe_pubkey); return None }; @@ -329,7 +332,7 @@ pub fn load_ecdsa_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Res let key_reader = std::io::BufReader::new(key_file); let keystore: JsonKeystore = serde_json::from_reader(key_reader)?; let password = std::fs::read(secrets_path)?; - let decrypted_password = eth2_keystore::decrypt(&password, &keystore.crypto) + let decrypted_password = lh_eth2_keystore::decrypt(&password, &keystore.crypto) .map_err(|_| eyre::eyre!("Error decrypting ECDSA keystore"))?; EcdsaSigner::new_from_bytes(decrypted_password.as_bytes()) @@ -338,12 +341,13 @@ pub fn load_ecdsa_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Res #[cfg(test)] mod tests { - use alloy::{hex, primitives::FixedBytes}; - use super::{load_from_lighthouse_format, load_from_lodestar_format, FileKey}; - use crate::signer::{ - loader::{load_from_nimbus_format, load_from_prysm_format, load_from_teku_format}, - BlsPublicKey, BlsSigner, + use crate::{ + signer::{ + loader::{load_from_nimbus_format, load_from_prysm_format, load_from_teku_format}, + BlsSigner, + }, + utils::bls_pubkey_from_hex_unchecked, }; #[test] @@ -364,12 +368,10 @@ mod tests { fn test_correct_load(signers: Vec) { assert_eq!(signers.len(), 2); - assert!(signers.iter().any(|s| s.pubkey() == BlsPublicKey::from(FixedBytes::new( - hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4") - )))); - assert!(signers.iter().any(|s| s.pubkey() == BlsPublicKey::from(FixedBytes::new( - hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9") - )))); + assert!(signers.iter().any(|s| s.pubkey() == bls_pubkey_from_hex_unchecked("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4") + )); + assert!(signers.iter().any(|s| s.pubkey() == bls_pubkey_from_hex_unchecked("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9") + )); } #[test] @@ -420,9 +422,9 @@ mod tests { let signers = result.unwrap(); assert_eq!(signers.len(), 1); - assert!(signers[0].pubkey() == BlsPublicKey::from(FixedBytes::new( - hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4") - ))); + assert!(signers[0].pubkey() == bls_pubkey_from_hex_unchecked( + "883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" + )); let result = load_from_lodestar_format( "../../tests/data/keystores/teku-keys/".into(), @@ -434,9 +436,9 @@ mod tests { let signers = result.unwrap(); assert_eq!(signers.len(), 1); - assert!(signers[0].pubkey() == BlsPublicKey::from(FixedBytes::new( - hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9") - ))); + assert!(signers[0].pubkey() == bls_pubkey_from_hex_unchecked( + "b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9" + )); } #[test] diff --git a/crates/common/src/signer/schemes/bls.rs b/crates/common/src/signer/schemes/bls.rs index c5b75ce6..07f5e6dd 100644 --- a/crates/common/src/signer/schemes/bls.rs +++ b/crates/common/src/signer/schemes/bls.rs @@ -1,18 +1,11 @@ -pub use alloy::rpc::types::beacon::BlsSignature; -use alloy::{primitives::B256, rpc::types::beacon::constants::BLS_DST_SIG}; -use blst::BLST_ERROR; +use alloy::primitives::B256; use tree_hash::TreeHash; use crate::{ - error::BlstErrorWrapper, signature::sign_commit_boost_root, - types::{Chain, SignatureRequestInfo}, - utils::blst_pubkey_to_alloy, + types::{BlsPublicKey, BlsSecretKey, BlsSignature, Chain, SignatureRequestInfo}, }; -pub type BlsSecretKey = blst::min_pk::SecretKey; -pub type BlsPublicKey = alloy::rpc::types::beacon::BlsPublicKey; - #[derive(Clone)] pub enum BlsSigner { Local(BlsSecretKey), @@ -24,19 +17,20 @@ impl BlsSigner { } pub fn new_from_bytes(bytes: &[u8]) -> eyre::Result { - let secret = BlsSecretKey::from_bytes(bytes).map_err(BlstErrorWrapper::from)?; + let secret = + BlsSecretKey::deserialize(bytes).map_err(|_| eyre::eyre!("invalid secret key"))?; Ok(Self::Local(secret)) } pub fn pubkey(&self) -> BlsPublicKey { match self { - BlsSigner::Local(secret) => blst_pubkey_to_alloy(&secret.sk_to_pk()), + BlsSigner::Local(secret) => secret.public_key(), } } pub fn secret(&self) -> B256 { match self { - BlsSigner::Local(secret) => B256::from(secret.clone().to_bytes()), + BlsSigner::Local(secret) => secret.serialize().as_bytes().try_into().unwrap(), } } @@ -64,33 +58,9 @@ impl BlsSigner { } pub fn random_secret() -> BlsSecretKey { - use rand::RngCore; - - let mut rng = rand::rng(); - let mut ikm = [0u8; 32]; - rng.fill_bytes(&mut ikm); - - match BlsSecretKey::key_gen(&ikm, &[]) { - Ok(key) => key, - // Key material is always valid (32 `u8`s), so `key_gen` can't return Err. - Err(_) => unreachable!(), - } + BlsSecretKey::random() } -pub fn verify_bls_signature( - pubkey: &BlsPublicKey, - msg: &[u8], - signature: &BlsSignature, -) -> Result<(), BlstErrorWrapper> { - use crate::utils::{alloy_pubkey_to_blst, alloy_sig_to_blst}; - - let pubkey = alloy_pubkey_to_blst(pubkey)?; - let signature = alloy_sig_to_blst(signature)?; - - let res = signature.verify(true, msg, BLS_DST_SIG, &[], &pubkey, true); - if res == BLST_ERROR::BLST_SUCCESS { - Ok(()) - } else { - Err(res.into()) - } +pub fn verify_bls_signature(pubkey: &BlsPublicKey, msg: B256, signature: &BlsSignature) -> bool { + signature.verify(pubkey, msg) } diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index 834f4bd8..8ad00dcb 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -8,10 +8,10 @@ use std::{ use alloy::{ hex, - primitives::{Address, Bytes, FixedBytes}, - rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, + primitives::{Address, Bytes}, }; -use eth2_keystore::{ +use eyre::{Context, OptionExt}; +use lh_eth2_keystore::{ default_kdf, json_keystore::{ Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, JsonKeystore, KdfModule, @@ -19,7 +19,6 @@ use eth2_keystore::{ }, Uuid, IV_SIZE, SALT_SIZE, }; -use eyre::{Context, OptionExt}; use rand::Rng; use serde::{Deserialize, Serialize}; use tracing::{trace, warn}; @@ -28,10 +27,8 @@ use super::{load_bls_signer, load_ecdsa_signer}; use crate::{ commit::request::{EncryptionScheme, ProxyDelegation, ProxyId, SignedProxyDelegation}, config::{load_env_var, PROXY_DIR_ENV, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_ENV}, - signer::{ - BlsProxySigner, BlsPublicKey, BlsSigner, EcdsaProxySigner, EcdsaSigner, ProxySigners, - }, - types::ModuleId, + signer::{BlsProxySigner, BlsSigner, EcdsaProxySigner, EcdsaSigner, ProxySigners}, + types::{BlsPublicKey, BlsSignature, ModuleId}, }; #[derive(Debug, Serialize, Deserialize)] @@ -90,7 +87,7 @@ impl ProxyStore { .join("bls") .join(proxy.signer.pubkey().to_string()); let secret = Bytes::from(proxy.signer.secret()); - let to_store = KeyAndDelegation { secret, delegation: proxy.delegation }; + let to_store = KeyAndDelegation { secret, delegation: proxy.delegation.clone() }; let content = serde_json::to_vec(&to_store)?; if let Some(parent) = file_path.parent() { @@ -103,7 +100,7 @@ impl ProxyStore { ProxyStore::ERC2335 { keys_path, secrets_path } => { store_erc2335_key( module_id, - proxy.delegation, + proxy.delegation.clone(), proxy.secret().to_vec(), keys_path, secrets_path, @@ -127,7 +124,7 @@ impl ProxyStore { .join("ecdsa") .join(proxy.signer.address().to_string()); let secret = Bytes::from(proxy.signer.secret()); - let to_store = KeyAndDelegation { secret, delegation: proxy.delegation }; + let to_store = KeyAndDelegation { secret, delegation: proxy.delegation.clone() }; let content = serde_json::to_vec(&to_store)?; if let Some(parent) = file_path.parent() { @@ -140,7 +137,7 @@ impl ProxyStore { ProxyStore::ERC2335 { keys_path, secrets_path } => { store_erc2335_key( module_id, - proxy.delegation, + proxy.delegation.clone(), proxy.secret(), keys_path, secrets_path, @@ -230,7 +227,9 @@ impl ProxyStore { delegation: key_and_delegation.delegation, }; - proxy_signers.bls_signers.insert(pubkey, proxy_signer); + proxy_signers + .bls_signers + .insert(pubkey.clone(), proxy_signer); bls_map.entry(module_id.clone()).or_default().push(pubkey); } } @@ -277,20 +276,24 @@ impl ProxyStore { for entry in std::fs::read_dir(keys_path)? { let entry = entry?; let consensus_key_path = entry.path(); - let consensus_pubkey = - match FixedBytes::from_str(&entry.file_name().to_string_lossy()) { - Ok(bytes) => BlsPublicKey::from(bytes), - Err(e) => { - warn!("Failed to parse consensus pubkey: {e}"); - continue; - } - }; + let Ok(consensus_key_str) = + hex::decode(entry.file_name().to_string_lossy().as_ref()) + else { + warn!("Failed to parse consensus pubkey: {consensus_key_path:?}"); + continue; + }; + + let Ok(consensus_pubkey) = BlsPublicKey::deserialize(&consensus_key_str) else { + warn!("Failed to parse consensus pubkey: {consensus_key_path:?}"); + continue; + }; if !consensus_key_path.is_dir() { warn!("{consensus_key_path:?} is not a directory"); continue; } + let consensus_pubkey_str = consensus_pubkey.to_string(); for entry in std::fs::read_dir(&consensus_key_path)? { let entry = entry?; let module_path = entry.path(); @@ -319,30 +322,42 @@ impl ProxyStore { let signer = load_bls_signer( path, secrets_path - .join(consensus_pubkey.to_string()) + .join(consensus_pubkey_str.clone()) .join(&module_id) .join("bls") .join(name), ) .map_err(|e| eyre::eyre!("Error loading BLS signer: {e}"))?; - let delegation_signature = match std::fs::read_to_string( - bls_path.join(format!("{name}.sig")), - ) { - Ok(sig) => { - FixedBytes::::from_str(&sig)? - } - Err(e) => { - warn!("Failed to read delegation signature: {e}"); - continue; - } + let delegation_signature_path = + bls_path.join(format!("{name}.sig")); + + let Ok(delegation_signature) = + std::fs::read_to_string(&delegation_signature_path) + else { + warn!("Failed to read delegation signature: {delegation_signature_path:?}"); + continue; + }; + + let Ok(delegation_signature) = + alloy::primitives::hex::decode(delegation_signature) + else { + warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + continue; + }; + + let Ok(delegation_signature) = + BlsSignature::deserialize(&delegation_signature) + else { + warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + continue; }; let proxy_signer = BlsProxySigner { signer: signer.clone(), delegation: SignedProxyDelegation { message: ProxyDelegation { - delegator: consensus_pubkey, + delegator: consensus_pubkey.clone(), proxy: signer.pubkey(), }, signature: delegation_signature, @@ -375,28 +390,41 @@ impl ProxyStore { let signer = load_ecdsa_signer( path, secrets_path - .join(consensus_pubkey.to_string()) + .join(consensus_pubkey_str.clone()) .join(&module_id) .join("ecdsa") .join(name), )?; - let delegation_signature = match std::fs::read_to_string( - ecdsa_path.join(format!("{name}.sig")), - ) { - Ok(sig) => { - FixedBytes::::from_str(&sig)? - } - Err(e) => { - warn!("Failed to read delegation signature: {e}",); - continue; - } + + let delegation_signature_path = + ecdsa_path.join(format!("{name}.sig")); + + let Ok(delegation_signature) = + std::fs::read_to_string(&delegation_signature_path) + else { + warn!("Failed to read delegation signature: {delegation_signature_path:?}"); + continue; + }; + + let Ok(delegation_signature) = + alloy::primitives::hex::decode(delegation_signature) + else { + warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + continue; + }; + + let Ok(delegation_signature) = + BlsSignature::deserialize(&delegation_signature) + else { + warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + continue; }; let proxy_signer = EcdsaProxySigner { signer: signer.clone(), delegation: SignedProxyDelegation { message: ProxyDelegation { - delegator: consensus_pubkey, + delegator: consensus_pubkey.clone(), proxy: signer.address(), }, signature: delegation_signature, @@ -426,7 +454,7 @@ fn store_erc2335_key( secrets_path: &Path, scheme: EncryptionScheme, ) -> eyre::Result<()> { - let proxy_delegation = delegation.message.proxy; + let proxy_delegation = delegation.message.proxy.clone(); let password_bytes: [u8; 32] = rand::rng().random(); let password = hex::encode(password_bytes); @@ -455,7 +483,7 @@ fn store_erc2335_key( let kdf = default_kdf(salt.to_vec()); let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv: iv.to_vec().into() }); let (cipher_text, checksum) = - eth2_keystore::encrypt(&secret, password.as_bytes(), &kdf, &cipher) + lh_eth2_keystore::encrypt(&secret, password.as_bytes(), &kdf, &cipher) .map_err(|_| eyre::eyre!("Error encrypting key"))?; let keystore = JsonKeystore { @@ -463,11 +491,11 @@ fn store_erc2335_key( kdf: KdfModule { function: kdf.function(), params: kdf, - message: eth2_keystore::json_keystore::EmptyString, + message: lh_eth2_keystore::json_keystore::EmptyString, }, checksum: ChecksumModule { function: Sha256Checksum::function(), - params: eth2_keystore::json_keystore::EmptyMap, + params: lh_eth2_keystore::json_keystore::EmptyMap, message: checksum.to_vec().into(), }, cipher: CipherModule { @@ -478,8 +506,8 @@ fn store_erc2335_key( }, uuid: Uuid::new_v4(), path: None, - pubkey: alloy::hex::encode(delegation.message.proxy), - version: eth2_keystore::json_keystore::Version::V4, + pubkey: alloy::hex::encode(delegation.message.proxy.to_bytes()), + version: lh_eth2_keystore::json_keystore::Version::V4, description: Some(delegation.message.proxy.to_string()), name: None, }; @@ -498,7 +526,6 @@ fn store_erc2335_key( #[cfg(test)] mod test { - use hex::FromHex; use tree_hash::TreeHash; use super::*; @@ -506,6 +533,7 @@ mod test { commit::request::{ProxyDelegationBls, SignedProxyDelegationBls}, signer::ConsensusSigner, types::Chain, + utils::bls_pubkey_from_hex_unchecked, }; #[tokio::test] @@ -534,7 +562,7 @@ mod test { }; let signature = consensus_signer.sign(Chain::Mainnet, &message.tree_hash_root(), None).await; - let delegation = SignedProxyDelegationBls { signature, message }; + let delegation = SignedProxyDelegationBls { signature: signature.clone(), message }; let proxy_signer = BlsProxySigner { signer: proxy_signer, delegation }; store.store_proxy_bls(&module_id, &proxy_signer).unwrap(); @@ -564,9 +592,10 @@ mod test { assert_eq!(keystore.pubkey, proxy_signer.pubkey().to_string().trim_start_matches("0x")); - let sig = FixedBytes::from_hex(std::fs::read_to_string(sig_path).unwrap()); - assert!(sig.is_ok()); - assert_eq!(sig.unwrap(), signature); + let sig = hex::decode(std::fs::read_to_string(sig_path).unwrap()).unwrap(); + let sig = BlsSignature::deserialize(&sig).unwrap(); + + assert_eq!(sig, signature); } #[test] @@ -584,16 +613,8 @@ mod test { assert_eq!(proxy_signers.bls_signers.len(), 1); assert_eq!(proxy_signers.ecdsa_signers.len(), 0); - let proxy_key = BlsPublicKey::from( - FixedBytes::from_hex( - "a77084280678d9f1efe4ef47a3d62af27872ce82db19a35ee012c4fd5478e6b1123b8869032ba18b2383e8873294f0ba" - ).unwrap() - ); - let consensus_key = BlsPublicKey::from( - FixedBytes::from_hex( - "ac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118" - ).unwrap() - ); + let proxy_key = bls_pubkey_from_hex_unchecked("a77084280678d9f1efe4ef47a3d62af27872ce82db19a35ee012c4fd5478e6b1123b8869032ba18b2383e8873294f0ba"); + let consensus_key = bls_pubkey_from_hex_unchecked("ac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118"); let proxy_signer = proxy_signers.bls_signers.get(&proxy_key); @@ -602,13 +623,16 @@ mod test { assert_eq!( proxy_signer.delegation.signature, - FixedBytes::from_hex( - std::fs::read_to_string( - keys_path - .join(consensus_key.to_string()) - .join("TEST_MODULE") - .join("bls") - .join(format!("{proxy_key}.sig")) + BlsSignature::deserialize( + &hex::decode( + std::fs::read_to_string( + keys_path + .join(consensus_key.clone().to_string()) + .join("TEST_MODULE") + .join("bls") + .join(format!("{proxy_key}.sig")) + ) + .unwrap() ) .unwrap() ) @@ -670,13 +694,16 @@ mod test { assert_eq!( loaded_proxy_signer.delegation.signature, - FixedBytes::from_hex( - std::fs::read_to_string( - keys_path - .join(consensus_signer.pubkey().to_string()) - .join("TEST_MODULE") - .join("bls") - .join(format!("{}.sig", proxy_signer.pubkey())) + BlsSignature::deserialize( + &hex::decode( + std::fs::read_to_string( + keys_path + .join(consensus_signer.pubkey().to_string()) + .join("TEST_MODULE") + .join("bls") + .join(format!("{}.sig", proxy_signer.pubkey())) + ) + .unwrap() ) .unwrap() ) diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index da36af5d..60878c84 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -8,10 +8,11 @@ use serde::{ Deserialize, Deserializer, }; -use super::{BlsPublicKey, EcdsaSigner}; +use super::EcdsaSigner; use crate::{ commit::request::{SignedProxyDelegationBls, SignedProxyDelegationEcdsa}, signer::BlsSigner, + types::BlsPublicKey, }; // For extra safety and to avoid risking signing malicious messages, use a proxy diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index d650f6be..36c27fc7 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use alloy::primitives::{aliases::B32, hex, Bytes, B256, U256}; +use alloy::primitives::{aliases::B32, b256, hex, Bytes, B256, U256}; use derive_more::{Deref, Display, From, Into}; use eyre::{bail, Context}; use serde::{Deserialize, Serialize}; @@ -8,6 +8,10 @@ use tree_hash_derive::TreeHash; use crate::{constants::APPLICATION_BUILDER_DOMAIN, signature::compute_domain}; +pub type BlsPublicKey = lh_types::PublicKey; +pub type BlsSignature = lh_types::Signature; +pub type BlsSecretKey = lh_types::SecretKey; + #[derive(Clone, Debug, Display, PartialEq, Eq, Hash, Deref, From, Into, Serialize, Deserialize)] #[into(owned, ref, ref_mut)] #[serde(transparent)] @@ -160,26 +164,21 @@ impl KnownChain { pub fn builder_domain(&self) -> B256 { match self { - KnownChain::Mainnet => B256::from([ - 0, 0, 0, 1, 245, 165, 253, 66, 209, 106, 32, 48, 39, 152, 239, 110, 211, 9, 151, - 155, 67, 0, 61, 35, 32, 217, 240, 232, 234, 152, 49, 169, - ]), - KnownChain::Holesky => B256::from([ - 0, 0, 0, 1, 91, 131, 162, 55, 89, 197, 96, 178, 208, 198, 69, 118, 225, 220, 252, - 52, 234, 148, 196, 152, 143, 62, 13, 159, 119, 240, 83, 135, - ]), - KnownChain::Sepolia => B256::from([ - 0, 0, 0, 1, 211, 1, 7, 120, 205, 8, 238, 81, 75, 8, 254, 103, 182, 197, 3, 181, 16, - 152, 122, 76, 228, 63, 66, 48, 109, 151, 198, 124, - ]), - KnownChain::Helder => B256::from([ - 0, 0, 0, 1, 148, 196, 26, 244, 132, 255, 247, 150, 73, 105, 224, 189, 217, 34, 248, - 45, 255, 15, 75, 232, 122, 96, 208, 102, 76, 201, 209, 255, - ]), - KnownChain::Hoodi => B256::from([ - 0, 0, 0, 1, 113, 145, 3, 81, 30, 250, 79, 19, 98, 255, 42, 80, 153, 108, 204, 243, - 41, 204, 132, 203, 65, 12, 94, 92, 125, 53, 29, 3, - ]), + KnownChain::Mainnet => { + b256!("0x00000001f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a9") + } + KnownChain::Holesky => { + b256!("0x000000015b83a23759c560b2d0c64576e1dcfc34ea94c4988f3e0d9f77f05387") + } + KnownChain::Sepolia => { + b256!("0x00000001d3010778cd08ee514b08fe67b6c503b510987a4ce43f42306d97c67c") + } + KnownChain::Helder => { + b256!("0x0000000194c41af484fff7964969e0bdd922f82dff0f4be87a60d0664cc9d1ff") + } + KnownChain::Hoodi => { + b256!("0x00000001719103511efa4f1362ff2a50996cccf329cc84cb410c5e5c7d351d03") + } } } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 3f658c92..9c25656d 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -5,13 +5,10 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use alloy::{ - primitives::U256, - rpc::types::beacon::{BlsPublicKey, BlsSignature}, -}; +use alloy::{hex, primitives::U256}; use axum::http::HeaderValue; -use blst::min_pk::{PublicKey, Signature}; use futures::StreamExt; +use lh_types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; use rand::{distr::Alphanumeric, Rng}; use reqwest::{header::HeaderMap, Response}; use serde::{de::DeserializeOwned, Serialize}; @@ -30,7 +27,7 @@ use crate::{ config::LogsSettings, constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, - types::{Chain, Jwt, JwtAdmin, JwtClaims, ModuleId}, + types::{BlsPublicKey, Chain, Jwt, JwtAdmin, JwtClaims, ModuleId}, }; const MILLIS_PER_SECOND: u64 = 1_000; @@ -345,20 +342,6 @@ pub fn print_logo() { ) } -// Crypto conversions - -pub fn alloy_pubkey_to_blst(pubkey: &BlsPublicKey) -> Result { - PublicKey::key_validate(&pubkey.0) -} - -pub fn alloy_sig_to_blst(signature: &BlsSignature) -> Result { - Signature::from_bytes(&signature.0) -} - -pub fn blst_pubkey_to_alloy(pubkey: &PublicKey) -> BlsPublicKey { - BlsPublicKey::from_slice(&pubkey.to_bytes()) -} - /// Create a JWT for the given module id with expiration pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { jsonwebtoken::encode( @@ -464,6 +447,34 @@ pub async fn wait_for_signal() -> eyre::Result<()> { Ok(()) } +pub trait TestRandomSeed: TestRandom { + fn test_random() -> Self + where + Self: Sized, + { + let mut rng = XorShiftRng::from_entropy(); + Self::random_for_test(&mut rng) + } +} + +impl TestRandomSeed for T {} + +pub fn bls_pubkey_from_hex(hex: &str) -> eyre::Result { + let Ok(bytes) = hex::decode(hex) else { + eyre::bail!("invalid hex pubkey: {hex}"); + }; + + let pubkey = BlsPublicKey::deserialize(&bytes) + .map_err(|e| eyre::eyre!("invalid hex pubkey: {hex}: {e:?}"))?; + + Ok(pubkey) +} + +#[cfg(test)] +pub fn bls_pubkey_from_hex_unchecked(hex: &str) -> BlsPublicKey { + bls_pubkey_from_hex(hex).unwrap() +} + #[cfg(test)] mod test { use super::{create_jwt, decode_jwt, validate_jwt}; diff --git a/crates/pbs/Cargo.toml b/crates/pbs/Cargo.toml index ad747f9d..4cc3d2b2 100644 --- a/crates/pbs/Cargo.toml +++ b/crates/pbs/Cargo.toml @@ -10,7 +10,6 @@ alloy.workspace = true async-trait.workspace = true axum.workspace = true axum-extra.workspace = true -blst.workspace = true cb-common.workspace = true cb-metrics.workspace = true eyre.workspace = true diff --git a/crates/pbs/src/mev_boost/get_header.rs b/crates/pbs/src/mev_boost/get_header.rs index 1adcb74a..b5499891 100644 --- a/crates/pbs/src/mev_boost/get_header.rs +++ b/crates/pbs/src/mev_boost/get_header.rs @@ -6,7 +6,7 @@ use std::{ use alloy::{ primitives::{aliases::B32, utils::format_ether, B256, U256}, providers::Provider, - rpc::types::{beacon::BlsPublicKey, Block}, + rpc::types::Block, }; use axum::http::{HeaderMap, HeaderValue}; use cb_common::{ @@ -17,8 +17,7 @@ use cb_common::{ HEADER_START_TIME_UNIX_MS, }, signature::verify_signed_message, - signer::BlsSignature, - types::Chain, + types::{BlsPublicKey, BlsSignature, Chain}, utils::{ get_user_agent_with_version, ms_into_slot, read_chunked_body_with_max, timestamp_of_slot_start_sec, utcnow_ms, @@ -90,7 +89,7 @@ pub async fn get_header( for relay in relays.iter() { handles.push( send_timed_get_header( - params, + params.clone(), relay.clone(), state.config.chain, send_headers.clone(), @@ -166,7 +165,7 @@ async fn send_timed_get_header( mut timeout_left_ms: u64, validation: ValidationContext, ) -> Result, PbsError> { - let url = relay.get_header_url(params.slot, params.parent_hash, params.pubkey)?; + let url = relay.get_header_url(params.slot, ¶ms.parent_hash, ¶ms.pubkey)?; if relay.config.enable_timing_games { if let Some(target_ms) = relay.config.target_first_request_ms { @@ -197,6 +196,7 @@ async fn send_timed_get_header( ); loop { + let params = params.clone(); handles.push(tokio::spawn( send_one_get_header( params, @@ -388,7 +388,7 @@ async fn send_one_get_header( validate_signature( chain, relay.pubkey(), - res.message.pubkey, + &res.message.pubkey, &res.message, &res.signature, )?; @@ -458,27 +458,28 @@ fn validate_header_data( fn validate_signature( chain: Chain, - expected_relay_pubkey: BlsPublicKey, - received_relay_pubkey: BlsPublicKey, + expected_relay_pubkey: &BlsPublicKey, + received_relay_pubkey: &BlsPublicKey, message: &T, signature: &BlsSignature, ) -> Result<(), ValidationError> { if expected_relay_pubkey != received_relay_pubkey { return Err(ValidationError::PubkeyMismatch { - expected: expected_relay_pubkey, - got: received_relay_pubkey, + expected: Box::new(expected_relay_pubkey.clone()), + got: Box::new(received_relay_pubkey.clone()), }); } - verify_signed_message( + if !verify_signed_message( chain, - &received_relay_pubkey, + received_relay_pubkey, &message, signature, None, &B32::from(APPLICATION_BUILDER_DOMAIN), - ) - .map_err(ValidationError::Sigverify)?; + ) { + return Err(ValidationError::Sigverify); + } Ok(()) } @@ -506,16 +507,12 @@ fn extra_validation( #[cfg(test)] mod tests { - use alloy::{ - primitives::{B256, U256}, - rpc::types::beacon::BlsPublicKey, - }; - use blst::min_pk; + use alloy::primitives::{B256, U256}; use cb_common::{ - pbs::{error::ValidationError, ExecutionPayloadHeaderMessageElectra, EMPTY_TX_ROOT_HASH}, + pbs::{error::ValidationError, EMPTY_TX_ROOT_HASH}, signature::sign_builder_message, - types::Chain, - utils::timestamp_of_slot_start_sec, + types::{BlsSecretKey, Chain}, + utils::{timestamp_of_slot_start_sec, TestRandomSeed}, }; use super::{validate_header_data, *}; @@ -579,33 +576,28 @@ mod tests { #[test] fn test_validate_signature() { - let secret_key = min_pk::SecretKey::from_bytes(&[ - 0, 136, 227, 100, 165, 57, 106, 129, 181, 15, 235, 189, 200, 120, 70, 99, 251, 144, - 137, 181, 230, 124, 189, 193, 115, 153, 26, 0, 197, 135, 103, 63, - ]) - .unwrap(); - let pubkey = BlsPublicKey::from_slice(&secret_key.sk_to_pk().to_bytes()); + let secret_key = BlsSecretKey::test_random(); + let pubkey = secret_key.public_key(); + let wrong_pubkey = BlsPublicKey::test_random(); + let wrong_signature = BlsSignature::test_random(); - let message = ExecutionPayloadHeaderMessageElectra::default(); + let message = B256::random(); let signature = sign_builder_message(Chain::Holesky, &secret_key, &message); assert_eq!( - validate_signature( - Chain::Holesky, - BlsPublicKey::default(), - pubkey, - &message, - &BlsSignature::default() - ), - Err(ValidationError::PubkeyMismatch { expected: BlsPublicKey::default(), got: pubkey }) + validate_signature(Chain::Holesky, &wrong_pubkey, &pubkey, &message, &wrong_signature), + Err(ValidationError::PubkeyMismatch { + expected: Box::new(wrong_pubkey), + got: Box::new(pubkey.clone()) + }) ); assert!(matches!( - validate_signature(Chain::Holesky, pubkey, pubkey, &message, &BlsSignature::default()), - Err(ValidationError::Sigverify(_)) + validate_signature(Chain::Holesky, &pubkey, &pubkey, &message, &wrong_signature), + Err(ValidationError::Sigverify) )); - assert!(validate_signature(Chain::Holesky, pubkey, pubkey, &message, &signature).is_ok()); + assert!(validate_signature(Chain::Holesky, &pubkey, &pubkey, &message, &signature).is_ok()); } } diff --git a/crates/pbs/src/routes/get_header.rs b/crates/pbs/src/routes/get_header.rs index b6f55fa0..53aa194d 100644 --- a/crates/pbs/src/routes/get_header.rs +++ b/crates/pbs/src/routes/get_header.rs @@ -26,11 +26,11 @@ pub async fn handle_get_header>( ) -> Result { tracing::Span::current().record("slot", params.slot); tracing::Span::current().record("parent_hash", tracing::field::debug(params.parent_hash)); - tracing::Span::current().record("validator", tracing::field::debug(params.pubkey)); + tracing::Span::current().record("validator", tracing::field::debug(¶ms.pubkey)); let state = state.read().clone(); - state.publish_event(BuilderEvent::GetHeaderRequest(params)); + state.publish_event(BuilderEvent::GetHeaderRequest(params.clone())); let ua = get_user_agent(&req_headers); let ms_into_slot = ms_into_slot(params.slot, state.config.chain); diff --git a/crates/pbs/src/state.rs b/crates/pbs/src/state.rs index fbf211d3..f641c069 100644 --- a/crates/pbs/src/state.rs +++ b/crates/pbs/src/state.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use alloy::rpc::types::beacon::BlsPublicKey; use cb_common::{ config::{PbsConfig, PbsModuleConfig}, pbs::{BuilderEvent, RelayClient}, + types::BlsPublicKey, }; use parking_lot::RwLock; diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 3067c8a1..526abf49 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -3,7 +3,6 @@ use std::{collections::HashMap, io::Write, path::PathBuf}; use alloy::{ hex, primitives::{aliases::B32, B256}, - rpc::types::beacon::constants::BLS_SIGNATURE_BYTES_LEN, }; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ @@ -11,8 +10,8 @@ use cb_common::{ config::{DirkConfig, DirkHostConfig}, constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - signer::{BlsPublicKey, BlsSignature, ProxyStore}, - types::{self, Chain, ModuleId, SignatureRequestInfo}, + signer::ProxyStore, + types::{self, BlsPublicKey, BlsSignature, Chain, ModuleId, SignatureRequestInfo}, }; use eyre::{bail, OptionExt}; use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; @@ -61,10 +60,10 @@ struct DistributedAccount { } impl Account { - pub fn public_key(&self) -> BlsPublicKey { + pub fn public_key(&self) -> &BlsPublicKey { match self { - Account::Simple(account) => account.public_key, - Account::Distributed(account) => account.composite_public_key, + Account::Simple(account) => &account.public_key, + Account::Distributed(account) => &account.composite_public_key, } } } @@ -177,7 +176,7 @@ impl DirkManager { self.consensus_accounts .values() .map(|account| ConsensusProxyMap { - consensus: account.public_key(), + consensus: account.public_key().clone(), proxy_bls: self .proxy_accounts .values() @@ -185,7 +184,7 @@ impl DirkManager { if proxy.module == *module && proxy.consensus.public_key() == account.public_key() { - Some(proxy.inner.public_key()) + Some(proxy.inner.public_key().clone()) } else { None } @@ -212,7 +211,7 @@ impl DirkManager { self.request_distributed_signature(account, object_root, signature_request_info) .await } - None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec())), + None => Err(SignerModuleError::UnknownConsensusSigner(pubkey.serialize().to_vec())), } } @@ -231,7 +230,7 @@ impl DirkManager { self.request_distributed_signature(account, object_root, signature_request_info) .await } - None => Err(SignerModuleError::UnknownProxySigner(pubkey.to_vec())), + None => Err(SignerModuleError::UnknownProxySigner(pubkey.serialize().to_vec())), } } @@ -262,7 +261,7 @@ impl DirkManager { .sign(SignRequest { data, domain: domain.to_vec(), - id: Some(sign_request::Id::PublicKey(account.public_key.to_vec())), + id: Some(sign_request::Id::PublicKey(account.public_key.serialize().to_vec())), }) .await .map_err(|e| { @@ -275,7 +274,7 @@ impl DirkManager { )); } - BlsSignature::try_from(response.into_inner().signature.as_slice()).map_err(|_| { + BlsSignature::deserialize(response.into_inner().signature.as_slice()).map_err(|_| { SignerModuleError::DirkCommunicationError("Failed to parse signature".to_string()) }) } @@ -336,14 +335,14 @@ impl DirkManager { continue; } - let signature = match BlsSignature::try_from(response.into_inner().signature.as_slice()) - { - Ok(sig) => sig, - Err(e) => { - warn!("Failed to parse signature from participant {participant_id}: {e}"); - continue; - } - }; + let signature = + match BlsSignature::deserialize(response.into_inner().signature.as_slice()) { + Ok(sig) => sig, + Err(e) => { + warn!("Failed to parse signature from participant {participant_id}: {e:?}"); + continue; + } + }; partials.push((signature, participant_id)); @@ -375,11 +374,17 @@ impl DirkManager { Some(Account::Distributed(account)) => { self.generate_distributed_proxy_key(account, module).await? } - None => return Err(SignerModuleError::UnknownConsensusSigner(consensus.to_vec())), + None => { + return Err(SignerModuleError::UnknownConsensusSigner( + consensus.serialize().to_vec(), + )) + } }; - let message = - ProxyDelegation { delegator: consensus, proxy: proxy_account.inner.public_key() }; + let message = ProxyDelegation { + delegator: consensus.clone(), + proxy: proxy_account.inner.public_key().clone(), + }; let delegation_signature = self.request_consensus_signature(&consensus, &message.tree_hash_root(), None).await?; @@ -392,7 +397,7 @@ impl DirkManager { })?; } - self.proxy_accounts.insert(proxy_account.inner.public_key(), proxy_account.clone()); + self.proxy_accounts.insert(proxy_account.inner.public_key().clone(), proxy_account.clone()); Ok(delegation) } @@ -423,7 +428,7 @@ impl DirkManager { ))); } - let proxy_key = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + let proxy_key = BlsPublicKey::deserialize(response.into_inner().public_key.as_slice()) .map_err(|_| { SignerModuleError::DirkCommunicationError("Failed to parse proxy key".to_string()) })?; @@ -481,7 +486,8 @@ impl DirkManager { continue; } - let Ok(proxy_key) = BlsPublicKey::try_from(response.into_inner().public_key.as_slice()) + let Ok(proxy_key) = + BlsPublicKey::deserialize(response.into_inner().public_key.as_slice()) else { warn!("Failed to parse proxy key with participant {id}"); continue; @@ -610,10 +616,10 @@ fn load_simple_accounts( continue; } - match BlsPublicKey::try_from(account.public_key.as_slice()) { + match BlsPublicKey::deserialize(account.public_key.as_slice()) { Ok(public_key) => { consensus_accounts.insert( - public_key, + public_key.clone(), Account::Simple(SimpleAccount { public_key, connection: channel.clone(), @@ -648,7 +654,8 @@ fn load_distributed_accounts( continue; } - let Ok(public_key) = BlsPublicKey::try_from(account.composite_public_key.as_slice()) else { + let Ok(public_key) = BlsPublicKey::deserialize(account.composite_public_key.as_slice()) + else { warn!("Failed to parse composite public key for account {}", account.name); continue; }; @@ -682,7 +689,7 @@ fn load_distributed_accounts( participants.insert(participant_id as u32, channel.clone()); consensus_accounts.insert( - public_key, + public_key.clone(), Account::Distributed(DistributedAccount { composite_public_key: public_key, participants, @@ -705,10 +712,7 @@ fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Resul // Deserialize partial signatures into G2 points let mut shares: HashMap = HashMap::new(); for (signature, id) in partials { - if signature.len() != BLS_SIGNATURE_BYTES_LEN { - bail!("Invalid signature length") - } - let affine = G2Affine::from_compressed(signature) + let affine = G2Affine::from_compressed(&signature.serialize()) .into_option() .ok_or_eyre("Failed to deserialize signature")?; shares.insert(*id, G2Projective::from(&affine)); @@ -738,7 +742,10 @@ fn aggregate_partial_signatures(partials: &[(BlsSignature, u32)]) -> eyre::Resul // Serialize the recovered point back into a BlsSignature let bytes = recovered.to_compressed(); - Ok(bytes.into()) + let sig = BlsSignature::deserialize(&bytes) + .map_err(|_| eyre::eyre!("Failed to deserialize aggregated signature"))?; + + Ok(sig) } /// Generate a random password of 64 hex-characters @@ -755,21 +762,22 @@ fn name_matches_proxy(name: &str) -> bool { name.rsplit_once("/").is_some_and(|(_, name)| uuid::Uuid::parse_str(name).is_ok()) } -mod test { +#[cfg(test)] +mod tests { + use super::*; #[test] fn test_signature_aggregation() { use alloy::hex; - use cb_common::signer::BlsSignature; use super::aggregate_partial_signatures; let partials = vec![ - (BlsSignature::from_slice(&hex::decode("aa16233b9e65b596caf070122d564ad7a021dad4fc2ed8508fccecfab010da80892fad7336e9fbada607c50e2d0d78e00c9961f26618334ec9f0e7ea225212f3c0c7d66f73ff1c2e555712a3e31f517b8329bd0ad9e15a9aeaa91521ba83502c").unwrap()), 1), - (BlsSignature::from_slice(&hex::decode("b27dd4c088e386edc4d07b6b23c72ba87a34e04cffd4975e8cb679aa4640cec1d34ace3e2bf33ac0dffca023c82422840012bb6c92eab36ca7908a9f9519fa18b1ed2bdbc624a98e01ca217c318a021495cc6cc9c8b982d0afed2cd83dc8fe65").unwrap()), 2), - (BlsSignature::from_slice(&hex::decode("aca4a71373df2f76369e8b242b0e2b1f41fc384feee3abe605ee8d6723f5fb11de1c9bd2408f4a09be981342352c523801e3beea73893a329204dd67fe84cb520220af33f7fa027b6bcc3b7c8e78647f2aa372145e4d3aec7682d2605040a64a").unwrap()), 3) + (BlsSignature::deserialize(&hex::decode("aa16233b9e65b596caf070122d564ad7a021dad4fc2ed8508fccecfab010da80892fad7336e9fbada607c50e2d0d78e00c9961f26618334ec9f0e7ea225212f3c0c7d66f73ff1c2e555712a3e31f517b8329bd0ad9e15a9aeaa91521ba83502c").unwrap()).unwrap(), 1), + (BlsSignature::deserialize(&hex::decode("b27dd4c088e386edc4d07b6b23c72ba87a34e04cffd4975e8cb679aa4640cec1d34ace3e2bf33ac0dffca023c82422840012bb6c92eab36ca7908a9f9519fa18b1ed2bdbc624a98e01ca217c318a021495cc6cc9c8b982d0afed2cd83dc8fe65").unwrap()).unwrap(), 2), + (BlsSignature::deserialize(&hex::decode("aca4a71373df2f76369e8b242b0e2b1f41fc384feee3abe605ee8d6723f5fb11de1c9bd2408f4a09be981342352c523801e3beea73893a329204dd67fe84cb520220af33f7fa027b6bcc3b7c8e78647f2aa372145e4d3aec7682d2605040a64a").unwrap()).unwrap(), 3) ]; - let expected = BlsSignature::from_slice(&hex::decode("0x8e343f074f91d19fd5118d9301768e30cecb21fb96a1ad9539cbdeae8907e2e12a88c91fe1d7e1f6995dcde18fb0272b1512cd68800e14ebd1c7f189e7221ba238a0f196226385737157f4b72d348c1886ce18d0a9609ba0cd5503e41546286f").unwrap()); + let expected = BlsSignature::deserialize(&hex::decode("0x8e343f074f91d19fd5118d9301768e30cecb21fb96a1ad9539cbdeae8907e2e12a88c91fe1d7e1f6995dcde18fb0272b1512cd68800e14ebd1c7f189e7221ba238a0f196226385737157f4b72d348c1886ce18d0a9609ba0cd5503e41546286f").unwrap()).unwrap(); // With all signers let signature = aggregate_partial_signatures(&partials).unwrap(); diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index 632a01b6..c6590a21 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -1,19 +1,16 @@ use std::collections::HashMap; -use alloy::{ - primitives::{Address, B256}, - rpc::types::beacon::BlsSignature, -}; +use alloy::primitives::{Address, B256}; use cb_common::{ commit::request::{ ConsensusProxyMap, ProxyDelegationBls, ProxyDelegationEcdsa, SignedProxyDelegationBls, SignedProxyDelegationEcdsa, }, signer::{ - BlsProxySigner, BlsPublicKey, BlsSigner, ConsensusSigner, EcdsaProxySigner, EcdsaSignature, - EcdsaSigner, ProxySigners, ProxyStore, + BlsProxySigner, BlsSigner, ConsensusSigner, EcdsaProxySigner, EcdsaSignature, EcdsaSigner, + ProxySigners, ProxyStore, }, - types::{Chain, ModuleId, SignatureRequestInfo}, + types::{BlsPublicKey, BlsSignature, Chain, ModuleId, SignatureRequestInfo}, }; use tree_hash::TreeHash; @@ -102,10 +99,10 @@ impl LocalSigningManager { let signer = BlsSigner::new_random(); let proxy_pubkey = signer.pubkey(); - let message = ProxyDelegationBls { delegator, proxy: proxy_pubkey }; + let message = ProxyDelegationBls { delegator: delegator.clone(), proxy: proxy_pubkey }; let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationBls { signature, message }; - let proxy_signer = BlsProxySigner { signer, delegation }; + let proxy_signer = BlsProxySigner { signer, delegation: delegation.clone() }; self.add_proxy_signer_bls(proxy_signer, module_id) .map_err(|err| SignerModuleError::Internal(err.to_string()))?; @@ -121,10 +118,10 @@ impl LocalSigningManager { let signer = EcdsaSigner::new_random(); let proxy_address = signer.address(); - let message = ProxyDelegationEcdsa { delegator, proxy: proxy_address }; + let message = ProxyDelegationEcdsa { delegator: delegator.clone(), proxy: proxy_address }; let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationEcdsa { signature, message }; - let proxy_signer = EcdsaProxySigner { signer, delegation }; + let proxy_signer = EcdsaProxySigner { signer, delegation: delegation.clone() }; self.add_proxy_signer_ecdsa(proxy_signer, module_id) .map_err(|err| SignerModuleError::Internal(err.to_string()))?; @@ -143,7 +140,7 @@ impl LocalSigningManager { let signer = self .consensus_signers .get(pubkey) - .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.to_vec()))?; + .ok_or(SignerModuleError::UnknownConsensusSigner(pubkey.serialize().to_vec()))?; let signature = signer.sign(self.chain, object_root, signature_request_info).await; Ok(signature) @@ -159,7 +156,7 @@ impl LocalSigningManager { .proxy_signers .bls_signers .get(pubkey) - .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec()))?; + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.serialize().to_vec()))?; let signature = bls_proxy.sign(self.chain, object_root, signature_request_info).await; Ok(signature) } @@ -216,22 +213,22 @@ impl LocalSigningManager { pub fn get_delegation_bls( &self, pubkey: &BlsPublicKey, - ) -> Result { + ) -> Result<&SignedProxyDelegationBls, SignerModuleError> { self.proxy_signers .bls_signers .get(pubkey) - .map(|x| x.delegation) - .ok_or(SignerModuleError::UnknownProxySigner(pubkey.to_vec())) + .map(|x| &x.delegation) + .ok_or(SignerModuleError::UnknownProxySigner(pubkey.serialize().to_vec())) } pub fn get_delegation_ecdsa( &self, address: &Address, - ) -> Result { + ) -> Result<&SignedProxyDelegationEcdsa, SignerModuleError> { self.proxy_signers .ecdsa_signers .get(address) - .map(|x| x.delegation) + .map(|x| &x.delegation) .ok_or(SignerModuleError::UnknownProxySigner(address.to_vec())) } @@ -246,21 +243,21 @@ impl LocalSigningManager { let mut keys: Vec<_> = consensus.into_iter().map(ConsensusProxyMap::new).collect(); for bls in proxy_bls { - let delegator = self.get_delegation_bls(&bls)?.message.delegator; + let delegator = &self.get_delegation_bls(&bls)?.message.delegator; let entry = keys .iter_mut() - .find(|x| x.consensus == delegator) - .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.0.to_vec()))?; + .find(|x| &x.consensus == delegator) + .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.serialize().to_vec()))?; entry.proxy_bls.push(bls); } for ecdsa in proxy_ecdsa { - let delegator = self.get_delegation_ecdsa(&ecdsa)?.message.delegator; + let delegator = &self.get_delegation_ecdsa(&ecdsa)?.message.delegator; let entry = keys .iter_mut() - .find(|x| x.consensus == delegator) - .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.0.to_vec()))?; + .find(|x| &x.consensus == delegator) + .ok_or(SignerModuleError::UnknownConsensusSigner(delegator.serialize().to_vec()))?; entry.proxy_ecdsa.push(ecdsa); } @@ -334,10 +331,9 @@ mod tests { .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); - let validation_result = - verify_bls_signature(&consensus_pk, signing_root.as_slice(), &sig); + let validation_result = verify_bls_signature(&consensus_pk, signing_root, &sig); - assert!(validation_result.is_ok(), "Keypair must produce valid signatures of messages.") + assert!(validation_result, "Keypair must produce valid signatures of messages.") } } @@ -345,7 +341,7 @@ mod tests { use alloy::primitives::aliases::B32; use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - signer::verify_bls_signature, types, + signer::verify_bls_signature, types, utils::TestRandomSeed, }; use super::*; @@ -360,7 +356,7 @@ mod tests { let validation_result = signed_delegation.validate(CHAIN); assert!( - validation_result.is_ok(), + validation_result, "Proxy delegation signature must be valid for consensus key." ); @@ -378,12 +374,11 @@ mod tests { let mut signed_delegation = signing_manager.create_proxy_bls(MODULE_ID.clone(), consensus_pk).await.unwrap(); - let m = &mut signed_delegation.signature.0[0]; - (*m, _) = m.overflowing_add(1); + signed_delegation.signature = BlsSignature::test_random(); let validation_result = signed_delegation.validate(CHAIN); - assert!(validation_result.is_err(), "Tampered proxy key must be invalid."); + assert!(!validation_result, "Tampered proxy key must be invalid."); } #[tokio::test] @@ -418,12 +413,9 @@ mod tests { .tree_hash_root(); let signing_root = types::SigningData { object_root, signing_domain }.tree_hash_root(); - let validation_result = verify_bls_signature(&proxy_pk, signing_root.as_slice(), &sig); + let validation_result = verify_bls_signature(&proxy_pk, signing_root, &sig); - assert!( - validation_result.is_ok(), - "Proxy keypair must produce valid signatures of messages." - ) + assert!(validation_result, "Proxy keypair must produce valid signatures of messages.") } } @@ -431,7 +423,7 @@ mod tests { use alloy::primitives::aliases::B32; use cb_common::{ constants::COMMIT_BOOST_DOMAIN, signature::compute_domain, - signer::verify_ecdsa_signature, types, + signer::verify_ecdsa_signature, types, utils::TestRandomSeed, }; use super::*; @@ -446,7 +438,7 @@ mod tests { let validation_result = signed_delegation.validate(CHAIN); assert!( - validation_result.is_ok(), + validation_result, "Proxy delegation signature must be valid for consensus key." ); @@ -464,12 +456,11 @@ mod tests { let mut signed_delegation = signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk).await.unwrap(); - let m = &mut signed_delegation.signature.0[0]; - (*m, _) = m.overflowing_add(1); + signed_delegation.signature = BlsSignature::test_random(); let validation_result = signed_delegation.validate(CHAIN); - assert!(validation_result.is_err(), "Tampered proxy key must be invalid."); + assert!(!validation_result, "Tampered proxy key must be invalid."); } #[tokio::test] diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index bb168cf4..f7ce3f0b 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -5,10 +5,7 @@ use std::{ time::{Duration, Instant}, }; -use alloy::{ - primitives::{Address, B256, U256}, - rpc::types::beacon::BlsPublicKey, -}; +use alloy::primitives::{Address, B256, U256}; use axum::{ extract::{ConnectInfo, Request, State}, http::StatusCode, @@ -33,7 +30,7 @@ use cb_common::{ }, config::{ModuleSigningConfig, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, - types::{Chain, Jwt, ModuleId, SignatureRequestInfo}, + types::{BlsPublicKey, Chain, Jwt, ModuleId, SignatureRequestInfo}, utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; @@ -405,8 +402,15 @@ async fn handle_request_signature_bls_impl( } } .map(|sig| { - Json(BlsSignResponse::new(*signing_pubkey, *object_root, signing_id, nonce, chain_id, sig)) - .into_response() + Json(BlsSignResponse::new( + signing_pubkey.clone(), + *object_root, + signing_id, + nonce, + chain_id, + sig, + )) + .into_response() }) .map_err(|err| { error!(event = "request_signature", ?module_id, ?req_id, "{err}"); diff --git a/examples/da_commit/src/main.rs b/examples/da_commit/src/main.rs index 3edd6bc2..1af4c84d 100644 --- a/examples/da_commit/src/main.rs +++ b/examples/da_commit/src/main.rs @@ -57,16 +57,17 @@ impl DaCommitService { let pubkeys = self.config.signer_client.get_pubkeys().await?.keys; info!(pubkeys = %serde_json::to_string_pretty(&pubkeys).unwrap(), "Received pubkeys"); - let pubkey = pubkeys.first().ok_or_eyre("no key available")?.consensus; + let pubkey = pubkeys.first().ok_or_eyre("no key available")?.consensus.clone(); info!("Registered validator {pubkey}"); - let proxy_delegation_bls = self.config.signer_client.generate_proxy_key_bls(pubkey).await?; + let proxy_delegation_bls = + self.config.signer_client.generate_proxy_key_bls(pubkey.clone()).await?; info!("Obtained a BLS proxy delegation:\n{proxy_delegation_bls}"); let proxy_bls = proxy_delegation_bls.message.proxy; let proxy_ecdsa = if self.config.extra.use_ecdsa_keys { let proxy_delegation_ecdsa = - self.config.signer_client.generate_proxy_key_ecdsa(pubkey).await?; + self.config.signer_client.generate_proxy_key_ecdsa(pubkey.clone()).await?; info!("Obtained an ECDSA proxy delegation:\n{proxy_delegation_ecdsa}"); Some(proxy_delegation_ecdsa.message.proxy) } else { @@ -76,7 +77,7 @@ impl DaCommitService { let mut data = 0; loop { - self.send_request(data, pubkey, proxy_bls, proxy_ecdsa).await?; + self.send_request(data, pubkey.clone(), proxy_bls.clone(), proxy_ecdsa).await?; sleep(Duration::from_secs(self.config.extra.sleep_secs)).await; data += 1; } @@ -92,10 +93,10 @@ impl DaCommitService { let datagram = Datagram { data }; // Request a signature directly from a BLS key - let request = SignConsensusRequest::builder(pubkey).with_msg(&datagram); + let request = SignConsensusRequest::builder(pubkey.clone()).with_msg(&datagram); let response = self.config.signer_client.request_consensus_signature(request).await?; info!("Proposer commitment (consensus): {}", response.signature); - match verify_proposer_commitment_signature_bls( + if verify_proposer_commitment_signature_bls( self.config.chain, &pubkey, &datagram, @@ -103,17 +104,18 @@ impl DaCommitService { &DA_COMMIT_SIGNING_ID, self.nonce, ) { - Ok(_) => info!("Signature verified successfully"), - Err(err) => error!(%err, "Signature verification failed"), - }; + info!("Signature verified successfully"); + } else { + error!("Signature verification failed"); + } self.nonce += 1; // Request a signature from a proxy BLS key - let proxy_request_bls = SignProxyRequest::builder(proxy_bls).with_msg(&datagram); + let proxy_request_bls = SignProxyRequest::builder(proxy_bls.clone()).with_msg(&datagram); let proxy_response_bls = self.config.signer_client.request_proxy_signature_bls(proxy_request_bls).await?; info!("Proposer commitment (proxy BLS): {}", proxy_response_bls.signature); - match verify_proposer_commitment_signature_bls( + if verify_proposer_commitment_signature_bls( self.config.chain, &proxy_bls, &datagram, @@ -121,9 +123,10 @@ impl DaCommitService { &DA_COMMIT_SIGNING_ID, self.nonce, ) { - Ok(_) => info!("Signature verified successfully"), - Err(err) => error!(%err, "Signature verification failed"), - }; + info!("Signature verified successfully"); + } else { + error!("Signature verification failed"); + } self.nonce += 1; // If ECDSA keys are enabled, request a signature from a proxy ECDSA key diff --git a/tests/data/signed_blinded_block_holesky.json b/tests/data/signed_blinded_block_holesky.json index f70a69ad..14aeb814 100644 --- a/tests/data/signed_blinded_block_holesky.json +++ b/tests/data/signed_blinded_block_holesky.json @@ -197,25 +197,25 @@ "execution_requests": { "deposits": [ { - "pubkey": "0x0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", "withdrawal_credentials": "0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f", "amount": "100", - "signature": "0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "signature": "0x8aeb4642fb2982039a43fd6a6d9cc0ebf7598dbf02343c4617d9a68d799393c162492add63f31099a25eacc2782ba27a190e977a8c58760b6636dccb503d528b3be9e885c93d5b79699e68fcca870b0c790cdb00d67604d8b4a3025ae75efa2f", "index": "1" } ], "withdrawals": [ { "source_address": "0x1100000000000000000000000000000000000000", - "validator_pubkey": "0x120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "validator_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", "amount": "1" } ], "consolidations": [ { "source_address": "0x1200000000000000000000000000000000000000", - "source_pubkey": "0x120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "target_pubkey": "0x110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "source_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5", + "target_pubkey": "0xac0a230bd98a766b8e4156f0626ee679dd280dee5b0eedc2b9455ca3dacc4c7618da5010b9db609450a712f095c9f7a5" } ] } diff --git a/tests/data/ssv_valid.json b/tests/data/ssv_valid.json index e19b13e6..20fa9baa 100644 --- a/tests/data/ssv_valid.json +++ b/tests/data/ssv_valid.json @@ -2,7 +2,7 @@ "validators": [ { "id": 554991, - "public_key": "967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a", + "public_key": "0x967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a", "cluster": "0xf7c1283eb0c0f76b5fa84c7541d8d4d27751b4083a5e8dcb8ac9e72bb7f559b8", "owner_address": "0xB2EE025B1d129c61E77223bAb42fc65b29B16243", "status": "Inactive", @@ -31,7 +31,7 @@ }, { "id": 554992, - "public_key": "ac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c", + "public_key": "0xac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c", "cluster": "0xf7c1283eb0c0f76b5fa84c7541d8d4d27751b4083a5e8dcb8ac9e72bb7f559b8", "owner_address": "0xB2EE025B1d129c61E77223bAb42fc65b29B16243", "status": "Inactive", @@ -60,7 +60,7 @@ }, { "id": 554994, - "public_key": "8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639", + "public_key": "0x8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639", "cluster": "0xf7c1283eb0c0f76b5fa84c7541d8d4d27751b4083a5e8dcb8ac9e72bb7f559b8", "owner_address": "0xB2EE025B1d129c61E77223bAb42fc65b29B16243", "status": "Inactive", diff --git a/tests/src/mock_relay.rs b/tests/src/mock_relay.rs index 414b82fe..8794df96 100644 --- a/tests/src/mock_relay.rs +++ b/tests/src/mock_relay.rs @@ -16,15 +16,15 @@ use axum::{ }; use cb_common::{ pbs::{ - ExecutionPayloadHeaderMessageElectra, GetHeaderParams, GetHeaderResponse, - SignedExecutionPayloadHeader, SubmitBlindedBlockResponse, BUILDER_V1_API_PATH, - BUILDER_V2_API_PATH, GET_HEADER_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, - SUBMIT_BLOCK_PATH, + BlindedBeaconBlock, ExecutionPayloadHeaderMessageElectra, ExecutionRequests, + GetHeaderParams, GetHeaderResponse, KzgProof, SignedBlindedBeaconBlock, + SignedExecutionPayloadHeader, SubmitBlindedBlockResponse, VersionedResponse, + BUILDER_V1_API_PATH, BUILDER_V2_API_PATH, GET_HEADER_PATH, GET_STATUS_PATH, + REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH, }, signature::sign_builder_root, - signer::BlsSecretKey, - types::Chain, - utils::{blst_pubkey_to_alloy, timestamp_of_slot_start_sec}, + types::{BlsSecretKey, Chain}, + utils::timestamp_of_slot_start_sec, }; use cb_pbs::MAX_SIZE_SUBMIT_BLOCK_RESPONSE; use tokio::net::TcpListener; @@ -112,17 +112,23 @@ async fn handle_get_header( ) -> Response { state.received_get_header.fetch_add(1, Ordering::Relaxed); - let mut response: SignedExecutionPayloadHeader = - SignedExecutionPayloadHeader::default(); + let mut message = ExecutionPayloadHeaderMessageElectra { + header: Default::default(), + blob_kzg_commitments: Default::default(), + execution_requests: ExecutionRequests::default(), + value: Default::default(), + pubkey: state.signer.public_key(), + }; - response.message.header.parent_hash = parent_hash; - response.message.header.block_hash.0[0] = 1; - response.message.value = U256::from(10); - response.message.pubkey = blst_pubkey_to_alloy(&state.signer.sk_to_pk()); - response.message.header.timestamp = timestamp_of_slot_start_sec(0, state.chain); + message.header.parent_hash = parent_hash; + message.header.block_hash.0[0] = 1; + message.value = U256::from(10); + message.pubkey = state.signer.public_key(); + message.header.timestamp = timestamp_of_slot_start_sec(0, state.chain); - let object_root = response.message.tree_hash_root(); - response.signature = sign_builder_root(state.chain, &state.signer, &object_root); + let object_root = message.tree_hash_root(); + let signature = sign_builder_root(state.chain, &state.signer, &object_root); + let response = SignedExecutionPayloadHeader { message, signature }; let response = GetHeaderResponse::Electra(response); (StatusCode::OK, Json(response)).into_response() @@ -147,12 +153,25 @@ async fn handle_register_validator( StatusCode::OK.into_response() } -async fn handle_submit_block_v1(State(state): State>) -> Response { +async fn handle_submit_block_v1( + State(state): State>, + Json(submit_block): Json, +) -> Response { state.received_submit_block.fetch_add(1, Ordering::Relaxed); if state.large_body() { (StatusCode::OK, Json(vec![1u8; 1 + MAX_SIZE_SUBMIT_BLOCK_RESPONSE])).into_response() } else { - let response = SubmitBlindedBlockResponse::default(); + let VersionedResponse::Electra(mut response) = SubmitBlindedBlockResponse::default(); + response.execution_payload.block_hash = submit_block.block_hash(); + + let BlindedBeaconBlock::Electra(body) = submit_block.message; + + response.blobs_bundle.blobs.push(Default::default()).unwrap(); + response.blobs_bundle.commitments = body.body.blob_kzg_commitments; + response.blobs_bundle.proofs.push(KzgProof([0; 48])).unwrap(); + + let response = VersionedResponse::Electra(response); + (StatusCode::OK, Json(response)).into_response() } } diff --git a/tests/src/mock_validator.rs b/tests/src/mock_validator.rs index ee860189..3a2f68c0 100644 --- a/tests/src/mock_validator.rs +++ b/tests/src/mock_validator.rs @@ -1,8 +1,9 @@ -use alloy::{ - primitives::B256, - rpc::types::beacon::{relay::ValidatorRegistration, BlsPublicKey}, +use alloy::{primitives::B256, rpc::types::beacon::relay::ValidatorRegistration}; +use cb_common::{ + pbs::{BuilderApiVersion, RelayClient, SignedBlindedBeaconBlock}, + types::BlsPublicKey, + utils::bls_pubkey_from_hex, }; -use cb_common::pbs::{BuilderApiVersion, RelayClient, SignedBlindedBeaconBlock}; use reqwest::Response; use crate::utils::generate_mock_relay; @@ -13,11 +14,14 @@ pub struct MockValidator { impl MockValidator { pub fn new(port: u16) -> eyre::Result { - Ok(Self { comm_boost: generate_mock_relay(port, BlsPublicKey::default())? }) + let pubkey = bls_pubkey_from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae")?; + Ok(Self { comm_boost: generate_mock_relay(port, pubkey)? }) } pub async fn do_get_header(&self, pubkey: Option) -> eyre::Result { - let url = self.comm_boost.get_header_url(0, B256::ZERO, pubkey.unwrap_or_default())?; + let default_pubkey = bls_pubkey_from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae")?; + let url = + self.comm_boost.get_header_url(0, &B256::ZERO, &pubkey.unwrap_or(default_pubkey))?; Ok(self.comm_boost.client.get(url).send().await?) } @@ -60,12 +64,16 @@ impl MockValidator { ) -> eyre::Result { let url = self.comm_boost.submit_block_url(api_version).unwrap(); - Ok(self - .comm_boost - .client - .post(url) - .json(&signed_blinded_block.unwrap_or_default()) - .send() - .await?) + let signed_blinded_block = + signed_blinded_block.unwrap_or_else(load_test_signed_blinded_block); + + Ok(self.comm_boost.client.post(url).json(&signed_blinded_block).send().await?) } } + +pub fn load_test_signed_blinded_block() -> SignedBlindedBeaconBlock { + let data_json = include_str!( + "../../crates/common/src/pbs/types/testdata/signed-blinded-beacon-block-electra-2.json" + ); + serde_json::from_str(data_json).unwrap() +} diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs index 5270e2a8..c2528cd9 100644 --- a/tests/src/signer_service.rs +++ b/tests/src/signer_service.rs @@ -1,12 +1,12 @@ use std::{collections::HashMap, time::Duration}; -use alloy::{hex, primitives::FixedBytes}; use cb_common::{ commit::request::GetPubkeysResponse, config::{ModuleSigningConfig, StartSignerConfig}, constants::SIGNER_JWT_EXPIRATION, signer::{SignerLoader, ValidatorKeysFormat}, types::{Chain, Jwt, JwtAdmin, ModuleId}, + utils::bls_pubkey_from_hex, }; use cb_signer::service::SigningService; use eyre::Result; @@ -58,8 +58,8 @@ pub async fn verify_pubkeys(response: Response) -> Result<()> { let pubkey_json = response.json::().await?; assert_eq!(pubkey_json.keys.len(), 2); let expected_pubkeys = vec![ - FixedBytes::new(hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4")), - FixedBytes::new(hex!("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9")), + bls_pubkey_from_hex("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4")?, + bls_pubkey_from_hex("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9")?, ]; for expected in expected_pubkeys { assert!( diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 0493040c..f068f5d8 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -4,10 +4,7 @@ use std::{ sync::{Arc, Once}, }; -use alloy::{ - primitives::{B256, U256}, - rpc::types::beacon::BlsPublicKey, -}; +use alloy::primitives::{B256, U256}; use cb_common::{ config::{ CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, @@ -18,7 +15,7 @@ use cb_common::{ }, pbs::{RelayClient, RelayEntry}, signer::SignerLoader, - types::{Chain, ModuleId}, + types::{BlsPublicKey, Chain, ModuleId}, utils::default_host, }; use eyre::Result; diff --git a/tests/tests/pbs_get_header.rs b/tests/tests/pbs_get_header.rs index 088fedb2..268c13cd 100644 --- a/tests/tests/pbs_get_header.rs +++ b/tests/tests/pbs_get_header.rs @@ -2,11 +2,8 @@ use std::{sync::Arc, time::Duration}; use alloy::primitives::{B256, U256}; use cb_common::{ - pbs::GetHeaderResponse, - signature::sign_builder_root, - signer::{random_secret, BlsPublicKey}, - types::Chain, - utils::{blst_pubkey_to_alloy, timestamp_of_slot_start_sec}, + pbs::GetHeaderResponse, signature::sign_builder_root, signer::random_secret, types::Chain, + utils::timestamp_of_slot_start_sec, }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ @@ -23,7 +20,7 @@ use tree_hash::TreeHash; async fn test_get_header() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3200; @@ -54,7 +51,7 @@ async fn test_get_header() -> Result<()> { assert_eq!(res.message.header.block_hash.0[0], 1); assert_eq!(res.message.header.parent_hash, B256::ZERO); assert_eq!(res.message.value, U256::from(10)); - assert_eq!(res.message.pubkey, blst_pubkey_to_alloy(&mock_state.signer.sk_to_pk())); + assert_eq!(res.message.pubkey, mock_state.signer.public_key()); assert_eq!(res.message.header.timestamp, timestamp_of_slot_start_sec(0, chain)); assert_eq!( res.signature, @@ -67,7 +64,7 @@ async fn test_get_header() -> Result<()> { async fn test_get_header_returns_204_if_relay_down() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3300; @@ -101,7 +98,7 @@ async fn test_get_header_returns_204_if_relay_down() -> Result<()> { async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3400; @@ -109,7 +106,7 @@ async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { // Run a mock relay let mock_state = Arc::new(MockRelayState::new(chain, signer)); - let mock_relay = generate_mock_relay(relay_port, pubkey)?; + let mock_relay = generate_mock_relay(relay_port, pubkey.clone())?; tokio::spawn(start_mock_relay_service(mock_state.clone(), relay_port)); // Run the PBS service @@ -121,7 +118,7 @@ async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { tokio::time::sleep(Duration::from_millis(100)).await; // Create an invalid URL by truncating the pubkey - let mut bad_url = mock_relay.get_header_url(0, B256::ZERO, pubkey).unwrap(); + let mut bad_url = mock_relay.get_header_url(0, &B256::ZERO, &pubkey).unwrap(); bad_url.set_path(&bad_url.path().replace(&pubkey.to_string(), &pubkey.to_string()[..10])); let mock_validator = MockValidator::new(pbs_port)?; @@ -131,7 +128,7 @@ async fn test_get_header_returns_400_if_request_is_invalid() -> Result<()> { assert_eq!(res.status(), StatusCode::BAD_REQUEST); // Attempt again by truncating the parent hash - let mut bad_url = mock_relay.get_header_url(0, B256::ZERO, pubkey).unwrap(); + let mut bad_url = mock_relay.get_header_url(0, &B256::ZERO, &pubkey).unwrap(); bad_url .set_path(&bad_url.path().replace(&B256::ZERO.to_string(), &B256::ZERO.to_string()[..10])); let res = mock_validator.comm_boost.client.get(bad_url).send().await?; diff --git a/tests/tests/pbs_get_status.rs b/tests/tests/pbs_get_status.rs index 629bea69..9cd55f34 100644 --- a/tests/tests/pbs_get_status.rs +++ b/tests/tests/pbs_get_status.rs @@ -1,10 +1,6 @@ use std::{sync::Arc, time::Duration}; -use cb_common::{ - signer::{random_secret, BlsPublicKey}, - types::Chain, - utils::blst_pubkey_to_alloy, -}; +use cb_common::{signer::random_secret, types::Chain}; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, @@ -19,7 +15,7 @@ use tracing::info; async fn test_get_status() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3500; @@ -27,7 +23,7 @@ async fn test_get_status() -> Result<()> { let relay_1_port = pbs_port + 2; let relays = vec![ - generate_mock_relay(relay_0_port, pubkey)?, + generate_mock_relay(relay_0_port, pubkey.clone())?, generate_mock_relay(relay_1_port, pubkey)?, ]; let mock_state = Arc::new(MockRelayState::new(chain, signer)); @@ -55,7 +51,7 @@ async fn test_get_status() -> Result<()> { async fn test_get_status_returns_502_if_relay_down() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3600; diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs index 84fa1a3d..434e35a4 100644 --- a/tests/tests/pbs_mux.rs +++ b/tests/tests/pbs_mux.rs @@ -1,11 +1,6 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; -use cb_common::{ - config::RuntimeMuxConfig, - signer::{random_secret, BlsPublicKey}, - types::Chain, - utils::blst_pubkey_to_alloy, -}; +use cb_common::{config::RuntimeMuxConfig, signer::random_secret, types::Chain}; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, @@ -20,14 +15,14 @@ use tracing::info; async fn test_mux() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3700; - let mux_relay_1 = generate_mock_relay(pbs_port + 1, pubkey)?; - let mux_relay_2 = generate_mock_relay(pbs_port + 2, pubkey)?; - let default_relay = generate_mock_relay(pbs_port + 3, pubkey)?; + let mux_relay_1 = generate_mock_relay(pbs_port + 1, pubkey.clone())?; + let mux_relay_2 = generate_mock_relay(pbs_port + 2, pubkey.clone())?; + let default_relay = generate_mock_relay(pbs_port + 3, pubkey.clone())?; // Run 3 mock relays let mock_state = Arc::new(MockRelayState::new(chain, signer)); @@ -48,8 +43,8 @@ async fn test_mux() -> Result<()> { }; // Bind mux to a specific validator key - let validator_pubkey = blst_pubkey_to_alloy(&random_secret().sk_to_pk()); - config.muxes = Some(HashMap::from([(validator_pubkey, mux)])); + let validator_pubkey = random_secret().public_key(); + config.muxes = Some(HashMap::from([(validator_pubkey.clone(), mux)])); // Run PBS service let state = PbsState::new(config); diff --git a/tests/tests/pbs_post_blinded_blocks.rs b/tests/tests/pbs_post_blinded_blocks.rs index 24b7e66b..3b3c663a 100644 --- a/tests/tests/pbs_post_blinded_blocks.rs +++ b/tests/tests/pbs_post_blinded_blocks.rs @@ -1,15 +1,14 @@ use std::{sync::Arc, time::Duration}; use cb_common::{ - pbs::{BuilderApiVersion, SignedBlindedBeaconBlock, SubmitBlindedBlockResponse}, - signer::{random_secret, BlsPublicKey}, + pbs::{BuilderApiVersion, SubmitBlindedBlockResponse}, + signer::random_secret, types::Chain, - utils::blst_pubkey_to_alloy, }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ mock_relay::{start_mock_relay_service, MockRelayState}, - mock_validator::MockValidator, + mock_validator::{load_test_signed_blinded_block, MockValidator}, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; @@ -21,8 +20,10 @@ async fn test_submit_block_v1() -> Result<()> { let res = submit_block_impl(3800, &BuilderApiVersion::V1).await?; assert_eq!(res.status(), StatusCode::OK); + let signed_blinded_block = load_test_signed_blinded_block(); + let response_body = serde_json::from_slice::(&res.bytes().await?)?; - assert_eq!(response_body.block_hash(), SubmitBlindedBlockResponse::default().block_hash()); + assert_eq!(response_body.block_hash(), signed_blinded_block.block_hash()); Ok(()) } @@ -38,7 +39,7 @@ async fn test_submit_block_v2() -> Result<()> { async fn test_submit_block_too_large() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 3900; @@ -67,7 +68,7 @@ async fn test_submit_block_too_large() -> Result<()> { async fn submit_block_impl(pbs_port: u16, api_version: &BuilderApiVersion) -> Result { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey = signer.public_key(); let chain = Chain::Holesky; @@ -84,14 +85,15 @@ async fn submit_block_impl(pbs_port: u16, api_version: &BuilderApiVersion) -> Re // leave some time to start servers tokio::time::sleep(Duration::from_millis(100)).await; + let signed_blinded_block = load_test_signed_blinded_block(); let mock_validator = MockValidator::new(pbs_port)?; info!("Sending submit block"); let res = match api_version { BuilderApiVersion::V1 => { - mock_validator.do_submit_block_v1(Some(SignedBlindedBeaconBlock::default())).await? + mock_validator.do_submit_block_v1(Some(signed_blinded_block)).await? } BuilderApiVersion::V2 => { - mock_validator.do_submit_block_v2(Some(SignedBlindedBeaconBlock::default())).await? + mock_validator.do_submit_block_v2(Some(signed_blinded_block)).await? } }; assert_eq!(mock_state.received_submit_block(), 1); diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs index 1ab3a786..1b4619b2 100644 --- a/tests/tests/pbs_post_validators.rs +++ b/tests/tests/pbs_post_validators.rs @@ -2,9 +2,8 @@ use std::{sync::Arc, time::Duration}; use alloy::rpc::types::beacon::relay::ValidatorRegistration; use cb_common::{ - signer::{random_secret, BlsPublicKey}, - types::Chain, - utils::blst_pubkey_to_alloy, + signer::random_secret, + types::{BlsPublicKey, Chain}, }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ @@ -20,7 +19,7 @@ use tracing::info; async fn test_register_validators() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey: BlsPublicKey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 4000; @@ -66,7 +65,7 @@ async fn test_register_validators() -> Result<()> { async fn test_register_validators_returns_422_if_request_is_malformed() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey: BlsPublicKey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 4100; @@ -206,7 +205,7 @@ async fn test_register_validators_returns_422_if_request_is_malformed() -> Resul async fn test_register_validators_does_not_retry_on_429() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey: BlsPublicKey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 4200; @@ -259,7 +258,7 @@ async fn test_register_validators_does_not_retry_on_429() -> Result<()> { async fn test_register_validators_retries_on_500() -> Result<()> { setup_test_env(); let signer = random_secret(); - let pubkey: BlsPublicKey = blst_pubkey_to_alloy(&signer.sk_to_pk()); + let pubkey: BlsPublicKey = signer.public_key(); let chain = Chain::Holesky; let pbs_port = 4300; diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index deb3bd39..8a67d95b 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -1,18 +1,13 @@ use std::collections::HashMap; -use alloy::{ - hex::FromHex, - primitives::{b256, hex, FixedBytes}, - rpc::types::beacon::BlsPublicKey, -}; +use alloy::primitives::{b256, hex}; use cb_common::{ commit::{ constants::REQUEST_SIGNATURE_BLS_PATH, request::SignConsensusRequest, response::BlsSignResponse, }, config::{load_module_signing_configs, ModuleSigningConfig}, - signer::BlsSignature, - types::{Chain, ModuleId}, + types::{BlsPublicKey, BlsSignature, Chain, ModuleId}, utils::create_jwt, }; use cb_tests::{ @@ -63,7 +58,8 @@ async fn test_signer_sign_request_good() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); let nonce: u64 = 101; - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; + let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); + let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -75,12 +71,12 @@ async fn test_signer_sign_request_good() -> Result<()> { // Verify the signature is returned let sig_response = response.json::().await?; let expected = BlsSignResponse::new( - BlsPublicKey::from(PUBKEY_1), + pubkey, object_root, mod_cfgs.get(&module_id).unwrap().signing_id, nonce, Chain::Hoodi.id(), - BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap()); + BlsSignature::deserialize(&hex!("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c")).unwrap()); assert_eq!(sig_response, expected, "Signature response does not match expected value"); Ok(()) @@ -99,7 +95,8 @@ async fn test_signer_sign_request_different_module() -> Result<()> { // Send a signing request let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); let nonce: u64 = 101; - let request = SignConsensusRequest { pubkey: FixedBytes(PUBKEY_1), object_root, nonce }; + let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); + let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); @@ -110,11 +107,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { // Verify the signature is returned let sig_response = response.json::().await?; - assert_eq!( - sig_response.pubkey, - BlsPublicKey::from(PUBKEY_1), - "Public key does not match expected value" - ); + assert_eq!(sig_response.pubkey, pubkey, "Public key does not match expected value"); assert_eq!(sig_response.object_root, object_root, "Object root does not match expected value"); assert_eq!( sig_response.module_signing_id, @@ -122,7 +115,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { "Module signing ID does not match expected value" ); assert_ne!( - sig_response.signature, BlsSignature::from_hex("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c").unwrap(), + sig_response.signature, BlsSignature::deserialize(&hex!("0xb653034a6da0e516cb999d6bbcd5ddd8dde9695322a94aefcd3049e6235e0f4f63b13d81ddcd80d4e1e698c3f88c3b440ae696650ccef2f22329afb4ffecec85a34523e25920ceced54c5bc31168174a3b352977750c222c1c25f72672467e5c")).unwrap(), "Signature matches the reference signature, which should not happen" ); From 52aec57e9d24541adde0439fe378e59f444349be Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Mon, 1 Sep 2025 23:57:21 -0400 Subject: [PATCH 06/30] Add payload hash to signer JWT claims (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: eltitanb Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao --- api/signer-api.yml | 48 ++++++--- crates/common/src/commit/client.rs | 22 ++-- crates/common/src/types.rs | 6 +- crates/common/src/utils.rs | 156 +++++++++++++++++++++++------ crates/signer/src/service.rs | 80 ++++++++++++--- tests/src/signer_service.rs | 17 +--- tests/tests/signer_jwt_auth.rs | 56 +++++------ tests/tests/signer_request_sig.rs | 62 +++++++++++- 8 files changed, 332 insertions(+), 115 deletions(-) diff --git a/api/signer-api.yml b/api/signer-api.yml index a6edd6bb..9e11da34 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -10,6 +10,12 @@ paths: /signer/v1/get_pubkeys: get: summary: Get a list of public keys for which signatures may be requested + description: > + This endpoint requires a valid JWT Bearer token. + + The token **must include** the following claims: + - `exp` (integer): Expiration timestamp + - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. tags: - Signer security: @@ -61,6 +67,13 @@ paths: /signer/v1/request_signature/bls: post: summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested public key. + description: > + This endpoint requires a valid JWT Bearer token. + + The token **must include** the following claims: + - `exp` (integer): Expiration timestamp + - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer security: @@ -201,6 +214,13 @@ paths: /signer/v1/request_signature/proxy-bls: post: summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the BLS private key for the requested proxy public key. + description: > + This endpoint requires a valid JWT Bearer token. + + The token **must include** the following claims: + - `exp` (integer): Expiration timestamp + - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer security: @@ -341,6 +361,13 @@ paths: /signer/v1/request_signature/proxy-ecdsa: post: summary: Request a signature for a 32-byte blob of data (typically a hash), signed by the ECDSA private key for the requested proxy Ethereum address. + description: > + This endpoint requires a valid JWT Bearer token. + + The token **must include** the following claims: + - `exp` (integer): Expiration timestamp + - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer security: @@ -481,6 +508,13 @@ paths: /signer/v1/generate_proxy_key: post: summary: Request a proxy key be generated for a specific consensus pubkey + description: > + This endpoint requires a valid JWT Bearer token. + + The token **must include** the following claims: + - `exp` (integer): Expiration timestamp + - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer security: @@ -580,20 +614,6 @@ paths: type: string example: "Internal error" - /status: - get: - summary: Get the status of the Signer API module - tags: - - Management - responses: - "200": - description: Success - content: - text/plain: - schema: - type: string - example: "OK" - components: securitySchemes: BearerAuth: diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 5fda0d23..e7466d6c 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -42,7 +42,7 @@ pub struct SignerClient { impl SignerClient { /// Create a new SignerClient pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result { - let jwt = create_jwt(&module_id, &jwt_secret)?; + let jwt = create_jwt(&module_id, &jwt_secret, None)?; let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; @@ -67,7 +67,7 @@ impl SignerClient { fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) { - let jwt = create_jwt(&self.module_id, &self.jwt_secret)?; + let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; @@ -85,6 +85,16 @@ impl SignerClient { Ok(()) } + fn create_jwt_for_payload( + &mut self, + payload: &T, + ) -> Result { + let payload_vec = serde_json::to_vec(payload)?; + create_jwt(&self.module_id, &self.jwt_secret, Some(&payload_vec)) + .wrap_err("failed to create JWT for payload") + .map_err(SignerClientError::JWTError) + } + /// Request a list of validator pubkeys for which signatures can be /// requested. // TODO: add more docs on how proxy keys work @@ -114,10 +124,10 @@ impl SignerClient { Q: Serialize, T: for<'de> Deserialize<'de>, { - self.refresh_jwt()?; + let jwt = self.create_jwt_for_payload(request)?; let url = self.url.join(route)?; - let res = self.client.post(url).json(&request).send().await?; + let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?; let status = res.status(); let response_bytes = res.bytes().await?; @@ -162,10 +172,10 @@ impl SignerClient { where T: ProxyId + for<'de> Deserialize<'de>, { - self.refresh_jwt()?; + let jwt = self.create_jwt_for_payload(request)?; let url = self.url.join(GENERATE_PROXY_KEY_PATH)?; - let res = self.client.post(url).json(&request).send().await?; + let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?; let status = res.status(); let response_bytes = res.bytes().await?; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 36c27fc7..9b845c61 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -25,13 +25,15 @@ pub struct Jwt(pub String); #[derive(Debug, Serialize, Deserialize)] pub struct JwtClaims { pub exp: u64, - pub module: String, + pub module: ModuleId, + pub payload_hash: Option, } #[derive(Debug, Serialize, Deserialize)] -pub struct JwtAdmin { +pub struct JwtAdminClaims { pub exp: u64, pub admin: bool, + pub payload_hash: Option, } #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 9c25656d..35f68959 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -5,7 +5,10 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use alloy::{hex, primitives::U256}; +use alloy::{ + hex, + primitives::{keccak256, U256}, +}; use axum::http::HeaderValue; use futures::StreamExt; use lh_types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; @@ -27,7 +30,7 @@ use crate::{ config::LogsSettings, constants::SIGNER_JWT_EXPIRATION, pbs::HEADER_VERSION_VALUE, - types::{BlsPublicKey, Chain, Jwt, JwtAdmin, JwtClaims, ModuleId}, + types::{BlsPublicKey, Chain, Jwt, JwtAdminClaims, JwtClaims, ModuleId}, }; const MILLIS_PER_SECOND: u64 = 1_000; @@ -343,12 +346,13 @@ pub fn print_logo() { } /// Create a JWT for the given module id with expiration -pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { +pub fn create_jwt(module_id: &ModuleId, secret: &str, payload: Option<&[u8]>) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { - module: module_id.to_string(), + module: module_id.clone(), exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + payload_hash: payload.map(keccak256), }, &jsonwebtoken::EncodingKey::from_secret(secret.as_ref()), ) @@ -356,54 +360,111 @@ pub fn create_jwt(module_id: &ModuleId, secret: &str) -> eyre::Result { .map(Jwt::from) } -/// Decode a JWT and return the module id. IMPORTANT: This function does not -/// validate the JWT, it only obtains the module id from the claims. -pub fn decode_jwt(jwt: Jwt) -> eyre::Result { +// Creates a JWT for module administration +pub fn create_admin_jwt(admin_secret: String, payload: Option<&[u8]>) -> eyre::Result { + jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &JwtAdminClaims { + admin: true, + exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + payload_hash: payload.map(keccak256), + }, + &jsonwebtoken::EncodingKey::from_secret(admin_secret.as_ref()), + ) + .map_err(Into::into) + .map(Jwt::from) +} + +/// Decode a JWT and return the JWT claims. IMPORTANT: This function does not +/// validate the JWT, it only obtains the claims. +pub fn decode_jwt(jwt: Jwt) -> eyre::Result { + let mut validation = jsonwebtoken::Validation::default(); + validation.insecure_disable_signature_validation(); + + let claims = jsonwebtoken::decode::( + jwt.as_str(), + &jsonwebtoken::DecodingKey::from_secret(&[]), + &validation, + )? + .claims; + + Ok(claims) +} + +/// Decode an administrator JWT and return the JWT claims. IMPORTANT: This +/// function does not validate the JWT, it only obtains the claims. +pub fn decode_admin_jwt(jwt: Jwt) -> eyre::Result { let mut validation = jsonwebtoken::Validation::default(); validation.insecure_disable_signature_validation(); - let module = jsonwebtoken::decode::( + let claims = jsonwebtoken::decode::( jwt.as_str(), &jsonwebtoken::DecodingKey::from_secret(&[]), &validation, )? - .claims - .module - .into(); + .claims; - Ok(module) + Ok(claims) } /// Validate a JWT with the given secret -pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { +pub fn validate_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Result<()> { let mut validation = jsonwebtoken::Validation::default(); validation.leeway = 10; - jsonwebtoken::decode::( + let claims = jsonwebtoken::decode::( jwt.as_str(), &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), &validation, - ) - .map(|_| ()) - .map_err(From::from) + )? + .claims; + + // Validate the payload hash if provided + if let Some(payload_bytes) = payload { + if let Some(expected_hash) = claims.payload_hash { + let actual_hash = keccak256(payload_bytes); + if actual_hash != expected_hash { + eyre::bail!("Payload hash does not match"); + } + } else { + eyre::bail!("JWT does not contain a payload hash"); + } + } else if claims.payload_hash.is_some() { + eyre::bail!("JWT contains a payload hash but no payload was provided"); + } + Ok(()) } /// Validate an admin JWT with the given secret -pub fn validate_admin_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> { +pub fn validate_admin_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Result<()> { let mut validation = jsonwebtoken::Validation::default(); validation.leeway = 10; - let token = jsonwebtoken::decode::( + let claims = jsonwebtoken::decode::( jwt.as_str(), &jsonwebtoken::DecodingKey::from_secret(secret.as_ref()), &validation, - )?; + )? + .claims; - if token.claims.admin { - Ok(()) - } else { + if !claims.admin { eyre::bail!("Token is not admin") } + + // Validate the payload hash if provided + if let Some(payload_bytes) = payload { + if let Some(expected_hash) = claims.payload_hash { + let actual_hash = keccak256(payload_bytes); + if actual_hash != expected_hash { + eyre::bail!("Payload hash does not match"); + } + } else { + eyre::bail!("JWT does not contain a payload hash"); + } + } else if claims.payload_hash.is_some() { + eyre::bail!("JWT contains a payload hash but no payload was provided"); + } + Ok(()) } /// Generates a random string @@ -477,27 +538,64 @@ pub fn bls_pubkey_from_hex_unchecked(hex: &str) -> BlsPublicKey { #[cfg(test)] mod test { + use alloy::primitives::keccak256; + use super::{create_jwt, decode_jwt, validate_jwt}; use crate::types::{Jwt, ModuleId}; #[test] - fn test_jwt_validation() { + fn test_jwt_validation_no_payload_hash() { + // Check valid JWT + let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret", None).unwrap(); + let claims = decode_jwt(jwt.clone()).unwrap(); + let module_id = claims.module; + let payload_hash = claims.payload_hash; + assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); + assert!(payload_hash.is_none()); + let response = validate_jwt(jwt, "secret", None); + assert!(response.is_ok()); + + // Check expired JWT + let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); + let response = validate_jwt(expired_jwt, "secret", None); + assert!(response.is_err()); + assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); + + // Check invalid signature JWT + let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); + let response = validate_jwt(invalid_jwt, "secret", None); + assert!(response.is_err()); + assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); + } + + #[test] + fn test_jwt_validation_with_payload() { + // Pretend payload + let payload = serde_json::json!({ + "data": "test" + }); + let payload_bytes = serde_json::to_vec(&payload).unwrap(); + // Check valid JWT - let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret").unwrap(); - let module_id = decode_jwt(jwt.clone()).unwrap(); + let jwt = + create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret", Some(&payload_bytes)).unwrap(); + let claims = decode_jwt(jwt.clone()).unwrap(); + let module_id = claims.module; + let payload_hash = claims.payload_hash; assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); - let response = validate_jwt(jwt, "secret"); + assert_eq!(payload_hash, Some(keccak256(&payload_bytes))); + let response = validate_jwt(jwt, "secret", Some(&payload_bytes)); assert!(response.is_ok()); // Check expired JWT let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); - let response = validate_jwt(expired_jwt, "secret"); + let response = validate_jwt(expired_jwt, "secret", Some(&payload_bytes)); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); // Check invalid signature JWT let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); - let response = validate_jwt(invalid_jwt, "secret"); + let response = validate_jwt(invalid_jwt, "secret", Some(&payload_bytes)); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index f7ce3f0b..e471a6fb 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -5,8 +5,9 @@ use std::{ time::{Duration, Instant}, }; -use alloy::primitives::{Address, B256, U256}; +use alloy::primitives::{keccak256, Address, B256, U256}; use axum::{ + body::{to_bytes, Body}, extract::{ConnectInfo, Request, State}, http::StatusCode, middleware::{self, Next}, @@ -47,6 +48,8 @@ use crate::{ metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, }; +pub const REQUEST_MAX_BODY_LENGTH: usize = 1024 * 1024; // 1 MB + /// Implements the Signer API and provides a service for signing requests pub struct SigningService; @@ -159,16 +162,24 @@ async fn jwt_auth( State(state): State, TypedHeader(auth): TypedHeader>, addr: ConnectInfo, - mut req: Request, + req: Request, next: Next, ) -> Result { // Check if the request needs to be rate limited let client_ip = addr.ip(); check_jwt_rate_limit(&state, &client_ip)?; + // Clone the request so we can read the body + let (parts, body) = req.into_parts(); + let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { + error!("Failed to read request body: {e}"); + SignerModuleError::RequestError(e.to_string()) + })?; + // Process JWT authorization - match check_jwt_auth(&auth, &state) { + match check_jwt_auth(&auth, &state, &bytes) { Ok(module_id) => { + let mut req = Request::from_parts(parts, Body::from(bytes)); req.extensions_mut().insert(module_id); Ok(next.run(req).await) } @@ -224,42 +235,83 @@ fn check_jwt_rate_limit(state: &SigningState, client_ip: &IpAddr) -> Result<(), fn check_jwt_auth( auth: &Authorization, state: &SigningState, + body: &[u8], ) -> Result { let jwt: Jwt = auth.token().to_string().into(); // We first need to decode it to get the module id and then validate it // with the secret stored in the state - let module_id = decode_jwt(jwt.clone()).map_err(|e| { + let claims = decode_jwt(jwt.clone()).map_err(|e| { error!("Unauthorized request. Invalid JWT: {e}"); SignerModuleError::Unauthorized })?; let guard = state.jwts.read(); - let jwt_config = guard.get(&module_id).ok_or_else(|| { + let jwt_config = guard.get(&claims.module).ok_or_else(|| { error!("Unauthorized request. Was the module started correctly?"); SignerModuleError::Unauthorized })?; - validate_jwt(jwt, &jwt_config.jwt_secret).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })?; - Ok(module_id) + if body.is_empty() { + // Skip payload hash comparison for requests without a body + validate_jwt(jwt, &jwt_config.jwt_secret, None).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; + } else { + validate_jwt(jwt, &jwt_config.jwt_secret, Some(body)).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; + + // Make sure the request contains a hash of the payload in its claims + if !body.is_empty() { + let payload_hash = keccak256(body); + if claims.payload_hash.is_none() || claims.payload_hash != Some(payload_hash) { + error!("Unauthorized request. Invalid payload hash in JWT claims"); + return Err(SignerModuleError::Unauthorized); + } + } + } + + Ok(claims.module) } async fn admin_auth( State(state): State, TypedHeader(auth): TypedHeader>, + addr: ConnectInfo, req: Request, next: Next, ) -> Result { - let jwt: Jwt = auth.token().to_string().into(); + // Check if the request needs to be rate limited + let client_ip = addr.ip(); + check_jwt_rate_limit(&state, &client_ip)?; - validate_admin_jwt(jwt, &state.admin_secret.read()).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized + // Clone the request so we can read the body + let (parts, body) = req.into_parts(); + let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { + error!("Failed to read request body: {e}"); + SignerModuleError::RequestError(e.to_string()) })?; + let jwt: Jwt = auth.token().to_string().into(); + + // Validate the admin JWT + if bytes.is_empty() { + // Skip payload hash comparison for requests without a body + validate_admin_jwt(jwt, &state.admin_secret.read(), None).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; + } else { + validate_admin_jwt(jwt, &state.admin_secret.read(), Some(&bytes)).map_err(|e| { + error!("Unauthorized request. Invalid payload hash in JWT claims: {e}"); + SignerModuleError::Unauthorized + })?; + } + + let req = Request::from_parts(parts, Body::from(bytes)); Ok(next.run(req).await) } diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs index c2528cd9..e4cdd4e6 100644 --- a/tests/src/signer_service.rs +++ b/tests/src/signer_service.rs @@ -3,9 +3,8 @@ use std::{collections::HashMap, time::Duration}; use cb_common::{ commit::request::GetPubkeysResponse, config::{ModuleSigningConfig, StartSignerConfig}, - constants::SIGNER_JWT_EXPIRATION, signer::{SignerLoader, ValidatorKeysFormat}, - types::{Chain, Jwt, JwtAdmin, ModuleId}, + types::{Chain, ModuleId}, utils::bls_pubkey_from_hex, }; use cb_signer::service::SigningService; @@ -71,17 +70,3 @@ pub async fn verify_pubkeys(response: Response) -> Result<()> { } Ok(()) } - -// Creates a JWT for module administration -pub fn create_admin_jwt(admin_secret: String) -> Result { - jsonwebtoken::encode( - &jsonwebtoken::Header::default(), - &JwtAdmin { - admin: true, - exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, - }, - &jsonwebtoken::EncodingKey::from_secret(admin_secret.as_ref()), - ) - .map_err(Into::into) - .map(Jwt::from) -} diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index 63f0783f..a4510af0 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -2,13 +2,16 @@ use std::{collections::HashMap, time::Duration}; use alloy::primitives::b256; use cb_common::{ - commit::constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, + commit::{ + constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, + request::RevokeModuleRequest, + }, config::{load_module_signing_configs, ModuleSigningConfig}, types::ModuleId, - utils::create_jwt, + utils::{create_admin_jwt, create_jwt}, }; use cb_tests::{ - signer_service::{create_admin_jwt, start_server, verify_pubkeys}, + signer_service::{start_server, verify_pubkeys}, utils::{self, setup_test_env}, }; use eyre::Result; @@ -42,7 +45,7 @@ async fn test_signer_jwt_auth_success() -> Result<()> { let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); let response = client.get(&url).bearer_auth(&jwt).send().await?; @@ -61,7 +64,7 @@ async fn test_signer_jwt_auth_fail() -> Result<()> { let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string()).await?; // Run a pubkeys request - this should fail due to invalid JWT - let jwt = create_jwt(&module_id, "incorrect secret")?; + let jwt = create_jwt(&module_id, "incorrect secret", None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); let response = client.get(&url).bearer_auth(&jwt).send().await?; @@ -83,7 +86,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, "incorrect secret")?; + let jwt = create_jwt(&module_id, "incorrect secret", None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); for _ in 0..start_config.jwt_auth_fail_limit { @@ -92,7 +95,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { } // Run another request - this should fail due to rate limiting now - let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret)?; + let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret, None)?; let response = client.get(&url).bearer_auth(&jwt).send().await?; assert!(response.status() == StatusCode::TOO_MANY_REQUESTS); @@ -116,8 +119,7 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let start_config = start_server(20400, &mod_cfgs, admin_secret.clone()).await?; // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, JWT_SECRET)?; - let admin_jwt = create_admin_jwt(admin_secret)?; + let jwt = create_jwt(&module_id, JWT_SECRET, None)?; let client = reqwest::Client::new(); // At first, test module should be allowed to request pubkeys @@ -125,14 +127,13 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let response = client.get(&url).bearer_auth(&jwt).send().await?; assert!(response.status() == StatusCode::OK); + let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; + let body_bytes = serde_json::to_vec(&revoke_body)?; + let admin_jwt = create_admin_jwt(admin_secret, Some(&body_bytes))?; + let revoke_url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); - let response = client - .post(&revoke_url) - .header("content-type", "application/json") - .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) - .bearer_auth(&admin_jwt) - .send() - .await?; + let response = + client.post(&revoke_url).json(&revoke_body).bearer_auth(&admin_jwt).send().await?; assert!(response.status() == StatusCode::OK); // After revoke, test module shouldn't be allowed anymore @@ -150,30 +151,21 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { let mod_cfgs = create_mod_signing_configs().await; let start_config = start_server(20500, &mod_cfgs, admin_secret.clone()).await?; + let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; + let body_bytes = serde_json::to_vec(&revoke_body)?; + // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, JWT_SECRET)?; - let admin_jwt = create_admin_jwt(admin_secret)?; + let jwt = create_jwt(&module_id, JWT_SECRET, Some(&body_bytes))?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); // Module JWT shouldn't be able to revoke modules - let response = client - .post(&url) - .header("content-type", "application/json") - .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) - .bearer_auth(&jwt) - .send() - .await?; + let response = client.post(&url).json(&revoke_body).bearer_auth(&jwt).send().await?; assert!(response.status() == StatusCode::UNAUTHORIZED); // Admin should be able to revoke modules - let response = client - .post(&url) - .header("content-type", "application/json") - .body(reqwest::Body::wrap(format!("{{\"module_id\": \"{JWT_MODULE}\"}}"))) - .bearer_auth(&admin_jwt) - .send() - .await?; + let admin_jwt = create_admin_jwt(admin_secret, Some(&body_bytes))?; + let response = client.post(&url).json(&revoke_body).bearer_auth(&admin_jwt).send().await?; assert!(response.status() == StatusCode::OK); Ok(()) diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index 8a67d95b..fce8eaf7 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -60,7 +60,8 @@ async fn test_signer_sign_request_good() -> Result<()> { let nonce: u64 = 101; let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; + let payload_bytes = serde_json::to_vec(&request)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&payload_bytes))?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; @@ -97,7 +98,8 @@ async fn test_signer_sign_request_different_module() -> Result<()> { let nonce: u64 = 101; let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret)?; + let payload_bytes = serde_json::to_vec(&request)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&payload_bytes))?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; @@ -121,3 +123,59 @@ async fn test_signer_sign_request_different_module() -> Result<()> { Ok(()) } + +/// Makes sure the signer service does not allow requests for JWTs that do +/// not match the JWT hash +#[tokio::test] +async fn test_signer_sign_request_incorrect_hash() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(MODULE_ID_2.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20202, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); + + // Send a signing request + let fake_object_root = + b256!("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"); + let nonce: u64 = 101; + let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); + let fake_request = + SignConsensusRequest { pubkey: pubkey.clone(), object_root: fake_object_root, nonce }; + let fake_payload_bytes = serde_json::to_vec(&fake_request)?; + let true_object_root = + b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); + let true_request = SignConsensusRequest { pubkey, object_root: true_object_root, nonce }; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&fake_payload_bytes))?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); + let response = client.post(&url).json(&true_request).bearer_auth(&jwt).send().await?; + + // Verify that authorization failed + assert!(response.status() == StatusCode::UNAUTHORIZED); + Ok(()) +} + +/// Makes sure the signer service does not allow signer requests for JWTs that +/// do not include a payload hash +#[tokio::test] +async fn test_signer_sign_request_missing_hash() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(MODULE_ID_2.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20203, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); + + // Send a signing request + let nonce: u64 = 101; + let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); + let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); + let request = SignConsensusRequest { pubkey, object_root, nonce }; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); + let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; + + // Verify that authorization failed + assert!(response.status() == StatusCode::UNAUTHORIZED); + Ok(()) +} From 6b14d77040959c473dbfef4bf93bfa9edf62fe41 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Wed, 3 Sep 2025 14:55:41 -0400 Subject: [PATCH 07/30] Add TLS to signer (#357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: eltitanb Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> Co-authored-by: Manuel Iñaki Bilbao --- .gitignore | 1 + Cargo.lock | 311 +++++++++++++++++++++++- Cargo.toml | 5 +- config.example.toml | 8 + crates/cli/Cargo.toml | 1 + crates/cli/src/docker_init.rs | 115 ++++++++- crates/common/Cargo.toml | 1 + crates/common/src/commit/client.rs | 59 ++--- crates/common/src/config/constants.rs | 6 + crates/common/src/config/module.rs | 15 +- crates/common/src/config/pbs.rs | 17 +- crates/common/src/config/signer.rs | 42 +++- crates/signer/Cargo.toml | 2 + crates/signer/src/service.rs | 32 ++- docs/docs/get_started/configuration.md | 31 +++ docs/docs/get_started/running/binary.md | 1 + tests/Cargo.toml | 1 + tests/src/signer_service.rs | 45 +++- tests/src/utils.rs | 22 +- tests/tests/signer_jwt_auth.rs | 10 +- tests/tests/signer_request_sig.rs | 8 +- tests/tests/signer_tls.rs | 58 +++++ 22 files changed, 708 insertions(+), 83 deletions(-) create mode 100644 tests/tests/signer_tls.rs diff --git a/.gitignore b/.gitignore index e48792b4..2434366e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ targets.json .idea/ logs .vscode/ +certs/ diff --git a/Cargo.lock b/Cargo.lock index 3e224fc4..22400244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,7 +356,7 @@ dependencies = [ "proptest-derive", "rand 0.8.5", "ruint", - "rustc-hash", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -890,6 +890,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "archery" version = "0.4.0" @@ -1105,6 +1111,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-rs" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.7.9" @@ -1240,6 +1269,28 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "axum-server" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +dependencies = [ + "arc-swap", + "bytes", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -1288,6 +1339,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.100", + "which", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1506,6 +1580,7 @@ dependencies = [ "docker-compose-types", "eyre", "indexmap 2.8.0", + "rcgen", "serde_yaml", ] @@ -1535,6 +1610,7 @@ dependencies = [ "pbkdf2 0.12.2", "rand 0.9.0", "rayon", + "rcgen", "reqwest", "serde", "serde_json", @@ -1599,6 +1675,7 @@ dependencies = [ "alloy", "axum 0.8.1", "axum-extra", + "axum-server", "bimap", "blsful", "cb-common", @@ -1612,6 +1689,7 @@ dependencies = [ "prometheus", "prost", "rand 0.9.0", + "rustls", "thiserror 2.0.12", "tokio", "tonic", @@ -1632,6 +1710,7 @@ dependencies = [ "cb-signer", "eyre", "jsonwebtoken", + "rcgen", "reqwest", "serde_json", "tempfile", @@ -1648,15 +1727,32 @@ version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.40" @@ -1689,6 +1785,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.32" @@ -1729,6 +1836,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -2690,6 +2806,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2831,8 +2963,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -2999,6 +3133,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -3081,6 +3224,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -3425,6 +3569,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -3509,12 +3662,28 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.11" @@ -3694,6 +3863,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.4" @@ -3737,6 +3912,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4016,6 +4201,16 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4287,6 +4482,60 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "rand 0.9.0", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -4392,6 +4641,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" @@ -4478,7 +4740,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -4486,6 +4751,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower 0.5.2", "tower-service", @@ -4494,6 +4760,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "windows-registry", ] @@ -4607,6 +4874,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -4669,6 +4942,7 @@ version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4692,6 +4966,9 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4699,6 +4976,7 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -6211,6 +6489,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -6220,6 +6508,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "widestring" version = "1.2.0" @@ -6496,6 +6796,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 82ef3919..7559cca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ alloy = { version = "0.12", features = [ async-trait = "0.1.80" axum = { version = "0.8.1", features = ["macros"] } axum-extra = { version = "0.10.0", features = ["typed-header"] } +axum-server = { version = "0.7.2", features = ["tls-rustls"] } base64 = "0.22.1" bimap = { version = "0.6.3", features = ["serde"] } blsful = "2.5" @@ -55,7 +56,9 @@ prometheus = "0.13.4" prost = "0.13.4" rand = { version = "0.9", features = ["os_rng"] } rayon = "1.10.0" -reqwest = { version = "0.12.4", features = ["json", "stream"] } +rcgen = "0.13.2" +reqwest = { version = "0.12.4", features = ["json", "rustls-tls", "stream"] } +rustls = "0.23.23" serde = { version = "1.0.202", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.33" diff --git a/config.example.toml b/config.example.toml index 4aaf62d9..67085409 100644 --- a/config.example.toml +++ b/config.example.toml @@ -169,6 +169,14 @@ jwt_auth_fail_limit = 3 # OPTIONAL, DEFAULT: 300 jwt_auth_fail_timeout_seconds = 300 +# [signer.tls_mode] +# How to use TLS for the Signer's HTTP server; two modes are supported: +# - type = "insecure": disable TLS, so the server runs in HTTP mode (not recommended for production). +# - type = "certificate": Use TLS. Include a property named "path" below this with the provided path; `path` should be a directory containing `cert.pem` and `key.pem` files to use. If they don't exist, they'll be automatically generated in self-signed mode. +# OPTIONAL, DEFAULT: +# type = "certificate" +# path = "./certs" + # For Remote signer: # [signer.remote] # URL of the Web3Signer instance diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 2acc6a7b..ac07c46c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,4 +11,5 @@ clap.workspace = true docker-compose-types.workspace = true eyre.workspace = true indexmap.workspace = true +rcgen.workspace = true serde_yaml.workspace = true diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 0e1193e7..d06231ae 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,15 +6,18 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, ADMIN_JWT_ENV, - CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, - DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, - DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, - MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, - PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, - PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, - SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, - SIGNER_MODULE_NAME, SIGNER_PORT_DEFAULT, SIGNER_URL_ENV, + CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, TlsMode, + ADMIN_JWT_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, + DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, + DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, + LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, + PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, + PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, + SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, + SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, + SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_KEY_NAME, + SIGNER_URL_ENV, }, pbs::{BUILDER_V1_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -71,13 +74,19 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // targets to pass to prometheus let mut targets = Vec::new(); + let using_tls = cb_config + .signer + .as_ref() + .is_some_and(|signer_config| matches!(signer_config.tls_mode, TlsMode::Certificate(_))); + let signer_http_prefix = if using_tls { "https" } else { "http" }; + // address for signer API communication let signer_port = cb_config.signer.as_ref().map(|s| s.port).unwrap_or(SIGNER_PORT_DEFAULT); let signer_server = if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &cb_config.signer { url.to_string() } else { - format!("http://cb_signer:{signer_port}") + format!("{signer_http_prefix}://cb_signer:{signer_port}") }; let mut warnings = Vec::new(); @@ -87,6 +96,19 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re modules.iter().any(|module| matches!(module.kind, ModuleKind::Commit)) }); + // If signer config is not set, certs_path doesn't really matter + let certs_path = cb_config + .signer + .as_ref() + .map(|config| match &config.tls_mode { + TlsMode::Insecure => { + warnings.push("Signer TLS mode is set to Insecure, using HTTP instead of HTTPS for signer communication".to_string()); + None + }, + TlsMode::Certificate(path) => Some(path), + }) + .unwrap_or_default(); + // setup modules if let Some(modules_config) = cb_config.modules { for module in modules_config { @@ -107,6 +129,13 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re get_env_interp(MODULE_JWT_ENV, &jwt_name), get_env_val(SIGNER_URL_ENV, &signer_server), ]); + if using_tls { + let env_val = get_env_val( + SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + ); + module_envs.insert(env_val.0, env_val.1); + } // Pass on the env variables if let Some(envs) = module.env { @@ -154,6 +183,9 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut module_volumes = vec![config_volume.clone()]; module_volumes.extend(chain_spec_volume.clone()); module_volumes.extend(get_log_volume(&cb_config.logs, &module.id)); + if let Some(certs_path) = certs_path { + module_volumes.push(create_cert_binding(certs_path)); + } // depends_on let mut module_dependencies = IndexMap::new(); @@ -237,6 +269,14 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // volumes pbs_volumes.extend(chain_spec_volume.clone()); pbs_volumes.extend(get_log_volume(&cb_config.logs, PBS_MODULE_NAME)); + if needs_signer_module { + if let Some(certs_path) = certs_path { + pbs_volumes.push(create_cert_binding(certs_path)); + let (key, val) = + get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT); + pbs_envs.insert(key, val); + } + } let pbs_service = Service { container_name: Some("cb_pbs".to_owned()), @@ -263,7 +303,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // setup signer service if needs_signer_module { - let Some(signer_config) = cb_config.signer else { + let Some(signer_config) = cb_config.signer.clone() else { panic!("Signer module required but no signer config provided"); }; @@ -273,6 +313,10 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re 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, + ), ]); // Bind the signer API to 0.0.0.0 @@ -376,6 +420,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re volumes.extend(get_log_volume(&cb_config.logs, SIGNER_MODULE_NAME)); + // Add TLS support if needed + if let Some(certs_path) = certs_path { + add_tls_certs_volume(&mut volumes, certs_path)? + } + // networks let signer_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -388,7 +437,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re environment: Environment::KvPair(signer_envs), healthcheck: Some(Healthcheck { test: Some(HealthcheckTest::Single(format!( - "curl -f http://localhost:{signer_port}/status" + "curl -k -f {signer_server}/status" ))), interval: Some("30s".into()), timeout: Some("5s".into()), @@ -483,6 +532,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re None => {} } + // Add TLS support if needed + if let Some(certs_path) = certs_path { + add_tls_certs_volume(&mut volumes, certs_path)? + } + // networks let signer_networks = vec![SIGNER_NETWORK.to_owned()]; @@ -495,7 +549,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re environment: Environment::KvPair(signer_envs), healthcheck: Some(Healthcheck { test: Some(HealthcheckTest::Single(format!( - "curl -f http://localhost:{signer_port}/status" + "curl -k -f {signer_server}/status" ))), interval: Some("30s".into()), timeout: Some("5s".into()), @@ -626,3 +680,38 @@ fn get_log_volume(config: &LogsSettings, module_id: &str) -> Option { fn format_comma_separated(map: &IndexMap) -> String { map.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(",") } + +fn create_cert_binding(certs_path: &Path) -> Volumes { + Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).display(), + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + )) +} + +/// Adds the TLS cert and key bindings to the provided volumes list +fn add_tls_certs_volume(volumes: &mut Vec, certs_path: &Path) -> Result<()> { + if !certs_path.try_exists()? { + std::fs::create_dir(certs_path)?; + } + + if !certs_path.join(SIGNER_TLS_CERTIFICATE_NAME).try_exists()? || + !certs_path.join(SIGNER_TLS_KEY_NAME).try_exists()? + { + return Err(eyre::eyre!( + "Signer TLS certificate or key not found at {}, please provide a valid certificate and key or create them", + certs_path.display() + )); + } + + volumes.push(create_cert_binding(certs_path)); + volumes.push(Volumes::Simple(format!( + "{}:{}/{}:ro", + certs_path.join(SIGNER_TLS_KEY_NAME).display(), + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_KEY_NAME + ))); + + Ok(()) +} diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 3cf20988..a2fa792a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -29,6 +29,7 @@ lh_types.workspace = true pbkdf2.workspace = true rand.workspace = true rayon.workspace = true +rcgen.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index e7466d6c..9d6b87ae 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -1,8 +1,11 @@ -use std::time::{Duration, Instant}; +use std::path::PathBuf; use alloy::primitives::Address; use eyre::WrapErr; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION}, + Certificate, +}; use serde::{Deserialize, Serialize}; use url::Url; @@ -22,7 +25,6 @@ use crate::{ }, response::{BlsSignResponse, EcdsaSignResponse}, }, - constants::SIGNER_JWT_EXPIRATION, types::{BlsPublicKey, Jwt, ModuleId}, utils::create_jwt, DEFAULT_REQUEST_TIMEOUT, @@ -34,15 +36,32 @@ pub struct SignerClient { /// Url endpoint of the Signer Module url: Url, client: reqwest::Client, - last_jwt_refresh: Instant, module_id: ModuleId, jwt_secret: Jwt, } impl SignerClient { /// Create a new SignerClient - pub fn new(signer_server_url: Url, jwt_secret: Jwt, module_id: ModuleId) -> eyre::Result { - let jwt = create_jwt(&module_id, &jwt_secret, None)?; + pub fn new( + signer_server_url: Url, + cert_path: Option, + jwt_secret: Jwt, + module_id: ModuleId, + ) -> eyre::Result { + let mut builder = reqwest::Client::builder().timeout(DEFAULT_REQUEST_TIMEOUT); + + // If a certificate path is provided, use it + if let Some(cert_path) = cert_path { + builder = builder + .use_rustls_tls() + .add_root_certificate(Certificate::from_pem(&std::fs::read(cert_path)?)?); + } + + Ok(Self { url: signer_server_url, client: builder.build()?, module_id, jwt_secret }) + } + + fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { + let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; @@ -51,37 +70,11 @@ impl SignerClient { let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, auth_value); - let client = reqwest::Client::builder() + self.client = reqwest::Client::builder() .timeout(DEFAULT_REQUEST_TIMEOUT) .default_headers(headers) .build()?; - Ok(Self { - url: signer_server_url, - client, - last_jwt_refresh: Instant::now(), - module_id, - jwt_secret, - }) - } - - fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { - if self.last_jwt_refresh.elapsed() > Duration::from_secs(SIGNER_JWT_EXPIRATION) { - let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; - - let mut auth_value = - HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; - auth_value.set_sensitive(true); - - let mut headers = HeaderMap::new(); - headers.insert(AUTHORIZATION, auth_value); - - self.client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; - } - Ok(()) } diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index b3ba63e9..a4c54f8c 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -46,6 +46,12 @@ pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT: u32 = 5 * 60; pub const JWTS_ENV: &str = "CB_JWTS"; pub const ADMIN_JWT_ENV: &str = "CB_SIGNER_ADMIN_JWT"; +/// Path to the certificates folder where the cert.pem and key.pem files are +/// stored/generated +pub const SIGNER_TLS_CERTIFICATES_PATH_ENV: &str = "CB_SIGNER_TLS_CERTIFICATES"; +pub const SIGNER_TLS_CERTIFICATES_PATH_DEFAULT: &str = "/certs"; +pub const SIGNER_TLS_CERTIFICATE_NAME: &str = "cert.pem"; +pub const SIGNER_TLS_KEY_NAME: &str = "key.pem"; /// Path to json file with plaintext keys (testing only) pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_LOADER_FILE"; pub const SIGNER_DEFAULT: &str = "/keys.json"; diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index ff4f1dc0..b2f30ae8 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; use alloy::primitives::B256; use eyre::{ContextCompat, Result}; @@ -11,6 +11,7 @@ use crate::{ constants::{CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV}, load_env_var, utils::load_file_from_env, + SignerConfig, TlsMode, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, }, types::{Chain, Jwt, ModuleId}, }; @@ -82,6 +83,7 @@ pub fn load_commit_module_config() -> Result { chain: Chain, modules: Vec>, + signer: SignerConfig, } // load module config including the extra data (if any) @@ -104,7 +106,16 @@ pub fn load_commit_module_config() -> Result None, + TlsMode::Certificate(path) => Some( + load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path) + .join(SIGNER_TLS_CERTIFICATE_NAME), + ), + }; + let signer_client = SignerClient::new(signer_server_url, certs_path, module_jwt, module_id)?; Ok(StartCommitModuleConfig { id: module_config.static_config.id, diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 1889223a..b7187cec 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, + path::PathBuf, sync::Arc, }; @@ -21,8 +22,9 @@ use super::{ use crate::{ commit::client::SignerClient, config::{ - load_env_var, load_file_from_env, PbsMuxes, CONFIG_ENV, MODULE_JWT_ENV, PBS_MODULE_NAME, - SIGNER_URL_ENV, + load_env_var, load_file_from_env, PbsMuxes, SignerConfig, TlsMode, CONFIG_ENV, + MODULE_JWT_ENV, PBS_MODULE_NAME, SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_CERTIFICATE_NAME, SIGNER_URL_ENV, }, pbs::{ DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT, LATE_IN_SLOT_TIME_MS, @@ -298,6 +300,7 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC chain: Chain, relays: Vec, pbs: CustomPbsConfig, + signer: SignerConfig, muxes: Option, } @@ -350,8 +353,18 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC // if custom pbs requires a signer client, load jwt let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?); let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?; + let certs_path = match cb_config.signer.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(path) => Some( + load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path) + .join(SIGNER_TLS_CERTIFICATE_NAME), + ), + }; Some(SignerClient::new( signer_server_url, + certs_path, module_jwt, ModuleId(PBS_MODULE_NAME.to_string()), )?) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index a397d696..77eb425a 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -15,7 +15,8 @@ use super::{ load_optional_env_var, utils::load_env_var, CommitBoostConfig, SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV, - SIGNER_PORT_DEFAULT, + SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, + SIGNER_TLS_KEY_NAME, }; use crate::{ config::{ @@ -57,6 +58,17 @@ impl ModuleSigningConfig { } } +/// Mode to use for TLS support when starting the signer service +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", content = "path", rename_all = "snake_case")] +pub enum TlsMode { + /// Don't use TLS (regular HTTP) + Insecure, + + /// Use TLS with a certificate and key file in the provided directory + Certificate(PathBuf), +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct SignerConfig { @@ -80,6 +92,12 @@ pub struct SignerConfig { #[serde(default = "default_u32::")] pub jwt_auth_fail_timeout_seconds: u32, + /// Mode to use for TLS support. + /// If using Certificate mode, this must include a path to the TLS + /// certificates directory (with a `cert.pem` and a `key.pem` file). + #[serde(default = "default_tls_mode")] + pub tls_mode: TlsMode, + /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, @@ -106,6 +124,11 @@ fn default_signer_image() -> String { SIGNER_IMAGE_DEFAULT.to_string() } +fn default_tls_mode() -> TlsMode { + TlsMode::Insecure // To make the default use TLS, do + // TlsMode::Certificate(PathBuf::from("./certs")) +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct DirkHostConfig { @@ -169,6 +192,7 @@ pub struct StartSignerConfig { pub jwt_auth_fail_limit: u32, pub jwt_auth_fail_timeout_seconds: u32, pub dirk: Option, + pub tls_certificates: Option<(Vec, Vec)>, } impl StartSignerConfig { @@ -208,6 +232,20 @@ impl StartSignerConfig { signer_config.jwt_auth_fail_timeout_seconds }; + // Load the TLS certificates if requested, generating self-signed ones if + // necessary + let tls_certificates = match signer_config.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(path) => { + let certs_path = load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) + .map(PathBuf::from) + .unwrap_or(path); + let cert_path = certs_path.join(SIGNER_TLS_CERTIFICATE_NAME); + let key_path = certs_path.join(SIGNER_TLS_KEY_NAME); + Some((std::fs::read(cert_path)?, std::fs::read(key_path)?)) + } + }; + match signer_config.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, @@ -219,6 +257,7 @@ impl StartSignerConfig { jwt_auth_fail_timeout_seconds, store, dirk: None, + tls_certificates, }), SignerType::Dirk { @@ -264,6 +303,7 @@ impl StartSignerConfig { None => None, }, }), + tls_certificates, }) } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 569797ac..7c6e63fa 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true alloy.workspace = true axum.workspace = true axum-extra.workspace = true +axum-server.workspace = true bimap.workspace = true blsful.workspace = true cb-common.workspace = true @@ -22,6 +23,7 @@ parking_lot.workspace = true prometheus.workspace = true prost.workspace = true rand.workspace = true +rustls.workspace = true thiserror.workspace = true tokio.workspace = true tonic.workspace = true diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e471a6fb..2d510eeb 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -16,6 +16,7 @@ use axum::{ Extension, Json, }; use axum_extra::TypedHeader; +use axum_server::tls_rustls::RustlsConfig; use cb_common::{ commit::{ constants::{ @@ -38,7 +39,8 @@ use cb_metrics::provider::MetricsProvider; use eyre::Context; use headers::{authorization::Bearer, Authorization}; use parking_lot::RwLock as ParkingRwLock; -use tokio::{net::TcpListener, sync::RwLock}; +use rustls::crypto::{aws_lc_rs, CryptoProvider}; +use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; use uuid::Uuid; @@ -142,14 +144,28 @@ impl SigningService { .route_layer(middleware::from_fn(log_request)) .route(STATUS_PATH, get(handle_status)); - let listener = TcpListener::bind(config.endpoint).await?; + if CryptoProvider::get_default().is_none() { + aws_lc_rs::default_provider() + .install_default() + .map_err(|_| eyre::eyre!("Failed to install TLS provider"))?; + } - axum::serve( - listener, - signer_app.merge(admin_app).into_make_service_with_connect_info::(), - ) - .await - .wrap_err("signer server exited") + let server_result = if let Some(tls_config) = config.tls_certificates { + let tls_config = RustlsConfig::from_pem(tls_config.0, tls_config.1).await?; + axum_server::bind_rustls(config.endpoint, tls_config) + .serve( + signer_app.merge(admin_app).into_make_service_with_connect_info::(), + ) + .await + } else { + warn!("Running in insecure HTTP mode, no TLS certificates provided"); + axum_server::bind(config.endpoint) + .serve( + signer_app.merge(admin_app).into_make_service_with_connect_info::(), + ) + .await + }; + server_result.wrap_err("signer service exited") } fn init_metrics(network: Chain) -> eyre::Result<()> { diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index bb963690..acff09e7 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -358,6 +358,37 @@ Delegation signatures will be stored in files with the format `/deleg A full example of a config file with Dirk can be found [here](https://github.com/Commit-Boost/commit-boost-client/blob/main/examples/configs/dirk_signer.toml). +### TLS + +By default, the Signer service runs in **insecure** mode, so its API service uses HTTP without any TLS encryption. This is sufficient for testing or if you're running locally within your machine's isolated Docker network and only intend to access it within the confines of your machine. However, for larger production setups, it's recommended to enable TLS - especially for traffic that spans across multiple machines. + +The Signer service in TLS mode supports **TLS 1.2** and **TLS 1.3**. Older protocol versions are not supported. + +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, such as [Let's Encrypt](https://letsencrypt.org/getting-started/) (a free service) or [Bluehost](https://www.bluehost.com/help/article/how-to-set-up-an-ssl-certificate-for-website-security) (free but requires an account). We do not recommend using a self-signed ceriticate / key pair for production environments. + +When configuring TLS support, the Signer service expects a single folder (which you can specify) that contains the following two files: +- `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: + +```toml +[pbs] +... +with_signer = true + +[signer] +port = 20000 +... + +[signer.tls_mode] +type = "certificate" +path = "path/to/your/cert/folder" +``` + +Where `path` is the aforementioned folder. It defaults to `./certs` but can be replaced with whichever directory your certificate and private key file reside in, as long as they're readable by the Signer service (or its Docker container, if using Docker). + + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: diff --git a/docs/docs/get_started/running/binary.md b/docs/docs/get_started/running/binary.md index 0dfa9399..5d7265bf 100644 --- a/docs/docs/get_started/running/binary.md +++ b/docs/docs/get_started/running/binary.md @@ -30,6 +30,7 @@ Modules need some environment variables to work correctly. - `CB_SIGNER_ADMIN_JWT`: secret to use for admin JWT. - `CB_SIGNER_ENDPOINT`: optional, override to specify the `IP:port` endpoint to bind the signer server to. +- `CB_SIGNER_TLS_CERTIFICATES`: path to the TLS certificates for the server. - For loading keys we currently support: - `CB_SIGNER_LOADER_FILE`: path to a `.json` with plaintext keys (for testing purposes only). - `CB_SIGNER_LOADER_FORMAT`, `CB_SIGNER_LOADER_KEYS_DIR` and `CB_SIGNER_LOADER_SECRETS_DIR`: paths to the `keys` and `secrets` directories or files (ERC-2335 style keystores, see [Signer config](../configuration/#signer-module) for more info). diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 573cfa20..6cd2b829 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -12,6 +12,7 @@ cb-pbs.workspace = true cb-signer.workspace = true eyre.workspace = true jsonwebtoken.workspace = true +rcgen.workspace = true reqwest.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs index e4cdd4e6..10e158fa 100644 --- a/tests/src/signer_service.rs +++ b/tests/src/signer_service.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, time::Duration}; use cb_common::{ - commit::request::GetPubkeysResponse, + commit::{constants::STATUS_PATH, request::GetPubkeysResponse}, config::{ModuleSigningConfig, StartSignerConfig}, signer::{SignerLoader, ValidatorKeysFormat}, types::{Chain, ModuleId}, @@ -9,7 +9,7 @@ use cb_common::{ }; use cb_signer::service::SigningService; use eyre::Result; -use reqwest::{Response, StatusCode}; +use reqwest::{Certificate, Response, StatusCode}; use tracing::info; use crate::utils::{get_signer_config, get_start_signer_config}; @@ -20,6 +20,7 @@ pub async fn start_server( port: u16, mod_signing_configs: &HashMap, admin_secret: String, + use_tls: bool, ) -> Result { let chain = Chain::Hoodi; @@ -29,7 +30,7 @@ pub async fn start_server( secrets_path: "data/keystores/secrets".into(), format: ValidatorKeysFormat::Lighthouse, }; - let mut config = get_signer_config(loader); + let mut config = get_signer_config(loader, use_tls); config.port = port; config.jwt_auth_fail_limit = 3; // Set a low fail limit for testing config.jwt_auth_fail_timeout_seconds = 3; // Set a short timeout for testing @@ -38,15 +39,37 @@ pub async fn start_server( // Run the Signer let server_handle = tokio::spawn(SigningService::run(start_config.clone())); - // Make sure the server is running - tokio::time::sleep(Duration::from_millis(100)).await; - if server_handle.is_finished() { - return Err(eyre::eyre!( - "Signer service failed to start: {}", - server_handle.await.unwrap_err() - )); + // Wait for the server to start + let (url, client) = match start_config.tls_certificates { + Some(ref certificates) => { + let url = format!("https://{}{}", start_config.endpoint, STATUS_PATH); + let client = reqwest::Client::builder() + .add_root_certificate(Certificate::from_pem(&certificates.0)?) + .build()?; + (url, client) + } + None => { + let url = format!("http://{}{}", start_config.endpoint, STATUS_PATH); + (url, reqwest::Client::new()) + } + }; + + let sleep_duration = Duration::from_millis(100); + for i in 0..100 { + // 10 second max wait + if i > 0 { + tokio::time::sleep(sleep_duration).await; + } + match client.get(&url).send().await { + Ok(_) => { + return Ok(start_config); + } + Err(e) => { + info!("Waiting for signer service to start: {}", e); + } + } } - Ok(start_config) + Err(eyre::eyre!("Signer service failed to start: {}", server_handle.await.unwrap_err())) } // Verifies that the pubkeys returned by the server match the pubkeys in the diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 532496c4..55be8aa3 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, + path::PathBuf, sync::{Arc, Once}, }; @@ -9,7 +10,7 @@ use cb_common::{ config::{ CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, PbsModuleConfig, RelayConfig, SignerConfig, SignerType, StartSignerConfig, - StaticModuleConfig, StaticPbsConfig, SIGNER_IMAGE_DEFAULT, + StaticModuleConfig, StaticPbsConfig, TlsMode, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_PORT_DEFAULT, }, @@ -19,6 +20,7 @@ use cb_common::{ utils::default_host, }; use eyre::Result; +use rcgen::generate_simple_self_signed; pub fn get_local_address(port: u16) -> String { format!("http://0.0.0.0:{port}") @@ -119,7 +121,7 @@ pub fn to_pbs_config( } } -pub fn get_signer_config(loader: SignerLoader) -> SignerConfig { +pub fn get_signer_config(loader: SignerLoader, tls: bool) -> SignerConfig { SignerConfig { host: default_host(), port: SIGNER_PORT_DEFAULT, @@ -127,6 +129,7 @@ pub fn get_signer_config(loader: SignerLoader) -> SignerConfig { jwt_auth_fail_limit: SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, jwt_auth_fail_timeout_seconds: SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, inner: SignerType::Local { loader, store: None }, + tls_mode: if tls { TlsMode::Certificate(PathBuf::new()) } else { TlsMode::Insecure }, } } @@ -136,6 +139,20 @@ pub fn get_start_signer_config( mod_signing_configs: &HashMap, admin_secret: String, ) -> StartSignerConfig { + let tls_certificates = match signer_config.tls_mode { + TlsMode::Insecure => None, + TlsMode::Certificate(_) => Some( + generate_simple_self_signed(vec![signer_config.host.to_string()]) + .map(|x| { + ( + x.cert.pem().as_bytes().to_vec(), + x.key_pair.serialize_pem().as_bytes().to_vec(), + ) + }) + .expect("Failed to generate TLS certificate"), + ), + }; + match signer_config.inner { SignerType::Local { loader, .. } => StartSignerConfig { chain, @@ -147,6 +164,7 @@ pub fn get_start_signer_config( jwt_auth_fail_limit: signer_config.jwt_auth_fail_limit, jwt_auth_fail_timeout_seconds: signer_config.jwt_auth_fail_timeout_seconds, dirk: None, + tls_certificates, }, _ => panic!("Only local signers are supported in tests"), } diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index a4510af0..c18f5ea6 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -41,7 +41,7 @@ async fn test_signer_jwt_auth_success() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request @@ -61,7 +61,7 @@ async fn test_signer_jwt_auth_fail() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; // Run a pubkeys request - this should fail due to invalid JWT let jwt = create_jwt(&module_id, "incorrect secret", None)?; @@ -82,7 +82,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { setup_test_env(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run as many pubkeys requests as the fail limit @@ -116,7 +116,7 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20400, &mod_cfgs, admin_secret.clone()).await?; + let start_config = start_server(20400, &mod_cfgs, admin_secret.clone(), false).await?; // Run as many pubkeys requests as the fail limit let jwt = create_jwt(&module_id, JWT_SECRET, None)?; @@ -149,7 +149,7 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { let admin_secret = ADMIN_SECRET.to_string(); let module_id = ModuleId(JWT_MODULE.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20500, &mod_cfgs, admin_secret.clone()).await?; + let start_config = start_server(20500, &mod_cfgs, admin_secret.clone(), false).await?; let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; let body_bytes = serde_json::to_vec(&revoke_body)?; diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index fce8eaf7..c1325d17 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -52,7 +52,7 @@ async fn test_signer_sign_request_good() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_1.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20200, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20200, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Send a signing request @@ -90,7 +90,7 @@ async fn test_signer_sign_request_different_module() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20201, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20201, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request @@ -131,7 +131,7 @@ async fn test_signer_sign_request_incorrect_hash() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20202, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20202, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request @@ -162,7 +162,7 @@ async fn test_signer_sign_request_missing_hash() -> Result<()> { setup_test_env(); let module_id = ModuleId(MODULE_ID_2.to_string()); let mod_cfgs = create_mod_signing_configs().await; - let start_config = start_server(20203, &mod_cfgs, ADMIN_SECRET.to_string()).await?; + let start_config = start_server(20203, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for 2nd test module not found"); // Send a signing request diff --git a/tests/tests/signer_tls.rs b/tests/tests/signer_tls.rs new file mode 100644 index 00000000..787312df --- /dev/null +++ b/tests/tests/signer_tls.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use alloy::primitives::b256; +use cb_common::{ + commit::constants::GET_PUBKEYS_PATH, + config::{load_module_signing_configs, ModuleSigningConfig}, + types::ModuleId, + utils::create_jwt, +}; +use cb_tests::{ + signer_service::{start_server, verify_pubkeys}, + utils::{self, setup_test_env}, +}; +use eyre::{bail, Result}; +use reqwest::Certificate; + +const JWT_MODULE: &str = "test-module"; +const JWT_SECRET: &str = "test-jwt-secret"; +const ADMIN_SECRET: &str = "test-admin-secret"; + +async fn create_mod_signing_configs() -> HashMap { + let mut cfg = + utils::get_commit_boost_config(utils::get_pbs_static_config(utils::get_pbs_config(0))); + + let module_id = ModuleId(JWT_MODULE.to_string()); + let signing_id = b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + cfg.modules = Some(vec![utils::create_module_config(module_id.clone(), signing_id)]); + + let jwts = HashMap::from([(module_id.clone(), JWT_SECRET.to_string())]); + + load_module_signing_configs(&cfg, &jwts).unwrap() +} + +#[tokio::test] +async fn test_signer_tls() -> Result<()> { + setup_test_env(); + let module_id = ModuleId(JWT_MODULE.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20100, &mod_cfgs, ADMIN_SECRET.to_string(), true).await?; + let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); + + // Run a pubkeys request + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let cert = match start_config.tls_certificates { + Some(ref certificates) => &certificates.0, + None => bail!("TLS certificates not found in start config"), + }; + let client = + reqwest::Client::builder().add_root_certificate(Certificate::from_pem(cert)?).build()?; + let url = format!("https://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); + let response = client.get(&url).bearer_auth(&jwt).send().await?; + + // Verify the expected pubkeys are returned + verify_pubkeys(response).await?; + + Ok(()) +} From eb6166759d584d44a69e017d008975446d95335f Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Mon, 8 Sep 2025 14:43:26 -0400 Subject: [PATCH 08/30] Removed old dependency (#370) --- Cargo.lock | 2 -- crates/cli/Cargo.toml | 1 - crates/common/Cargo.toml | 1 - 3 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22400244..cdef774a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,7 +1580,6 @@ dependencies = [ "docker-compose-types", "eyre", "indexmap 2.8.0", - "rcgen", "serde_yaml", ] @@ -1610,7 +1609,6 @@ dependencies = [ "pbkdf2 0.12.2", "rand 0.9.0", "rayon", - "rcgen", "reqwest", "serde", "serde_json", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ac07c46c..2acc6a7b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,5 +11,4 @@ clap.workspace = true docker-compose-types.workspace = true eyre.workspace = true indexmap.workspace = true -rcgen.workspace = true serde_yaml.workspace = true diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index a2fa792a..3cf20988 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -29,7 +29,6 @@ lh_types.workspace = true pbkdf2.workspace = true rand.workspace = true rayon.workspace = true -rcgen.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true From 2dfe96b8d45d9c2bb37f71d56130a066dec16ec8 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 9 Sep 2025 00:05:07 -0400 Subject: [PATCH 09/30] Merge main to sigp audit fixes (#371) Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- Cargo.lock | 1588 +++++++++-------- Cargo.toml | 7 +- benches/pbs/src/config.rs | 2 +- benches/pbs/src/main.rs | 9 +- bin/cli.rs | 3 - bin/pbs.rs | 8 +- bin/signer.rs | 8 +- bin/src/lib.rs | 8 +- crates/cli/src/docker_init.rs | 150 +- crates/common/src/commit/client.rs | 6 +- crates/common/src/commit/request.rs | 2 +- crates/common/src/config/log.rs | 2 +- crates/common/src/config/mod.rs | 2 +- crates/common/src/config/module.rs | 4 +- crates/common/src/config/mux.rs | 23 +- crates/common/src/config/pbs.rs | 24 +- crates/common/src/config/signer.rs | 16 +- crates/common/src/config/utils.rs | 2 +- crates/common/src/pbs/builder.rs | 2 +- crates/common/src/pbs/error.rs | 4 +- crates/common/src/pbs/relay.rs | 24 +- .../src/pbs/types/blinded_block_body.rs | 2 +- .../common/src/pbs/types/execution_payload.rs | 4 +- crates/common/src/pbs/types/get_header.rs | 2 +- crates/common/src/pbs/types/mod.rs | 6 +- crates/common/src/pbs/types/utils.rs | 6 +- crates/common/src/signature.rs | 4 +- crates/common/src/signer/loader.rs | 32 +- crates/common/src/signer/schemes/ecdsa.rs | 4 +- crates/common/src/signer/store.rs | 164 +- crates/common/src/signer/types.rs | 4 +- crates/common/src/types.rs | 4 +- crates/common/src/utils.rs | 34 +- crates/metrics/src/provider.rs | 2 +- crates/pbs/Cargo.toml | 1 + crates/pbs/src/api.rs | 2 +- crates/pbs/src/metrics.rs | 4 +- crates/pbs/src/mev_boost/get_header.rs | 27 +- .../pbs/src/mev_boost/register_validator.rs | 4 +- crates/pbs/src/mev_boost/status.rs | 2 +- crates/pbs/src/mev_boost/submit_block.rs | 6 +- crates/pbs/src/routes/register_validator.rs | 2 +- crates/pbs/src/routes/reload.rs | 2 +- crates/pbs/src/routes/router.rs | 17 +- crates/pbs/src/routes/submit_block.rs | 2 +- crates/pbs/src/service.rs | 4 +- crates/signer/build.rs | 4 +- crates/signer/src/manager/dirk.rs | 12 +- crates/signer/src/metrics.rs | 2 +- crates/signer/src/service.rs | 76 +- docs/docs/get_started/building.md | 22 +- docs/docs/get_started/overview.md | 9 +- examples/da_commit/src/main.rs | 2 +- examples/status_api/src/main.rs | 6 +- justfile | 3 +- provisioning/build.Dockerfile | 2 +- rust-toolchain.toml | 2 +- tests/src/mock_relay.rs | 14 +- tests/src/mock_validator.rs | 8 +- tests/src/signer_service.rs | 11 +- tests/src/utils.rs | 7 +- tests/tests/config.rs | 51 +- tests/tests/payloads.rs | 11 +- tests/tests/pbs_get_header.rs | 2 +- tests/tests/pbs_get_status.rs | 2 +- tests/tests/pbs_mux.rs | 2 +- tests/tests/pbs_post_blinded_blocks.rs | 4 +- tests/tests/pbs_post_validators.rs | 2 +- tests/tests/signer_jwt_auth.rs | 2 +- tests/tests/signer_request_sig.rs | 7 +- tests/tests/signer_tls.rs | 4 +- 72 files changed, 1368 insertions(+), 1136 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b15367f..8a9b42ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2025-02-26 + toolchain: nightly-2025-06-26 components: clippy, rustfmt - name: Install protoc diff --git a/Cargo.lock b/Cargo.lock index cdef774a..0dd0e763 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -70,9 +70,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ec9b8795b2083585293bd3d19033e9d67e725917c95c44cb154e3400529ccd" +checksum = "2b4ae82946772d69f868b9ef81fc66acb1b149ef9b4601849bec4bcf5da6552e" dependencies = [ "alloy-consensus", "alloy-contract", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.64" +version = "0.1.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963fc7ac17f25d92c237448632330eb87b39ba8aa0209d4b517069a05b57db62" +checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" dependencies = [ "alloy-primitives", "num_enum", @@ -106,9 +106,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84efb7b8ddb9223346bfad9d8094e1a100c254037a3b5913243bfa8e04be266" +checksum = "6fbf458101ed6c389e9bb70a34ebc56039868ad10472540614816cdedc8f5265" dependencies = [ "alloy-eips", "alloy-primitives", @@ -124,14 +124,14 @@ dependencies = [ "rand 0.8.5", "serde", "serde_with", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-consensus-any" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fafded0c1ff8f0275c4a484239058e1c01c0c2589f8a16e03669ef7094a06f9b" +checksum = "fc982af629e511292310fe85b433427fd38cb3105147632b574abc997db44c91" dependencies = [ "alloy-consensus", "alloy-eips", @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0fa0584d13dd0c4e79288d411222c4d7c3411c71b7fa637cefda9dcf9bb1f9" +checksum = "cd0a0c1ddee20ecc14308aae21c2438c994df7b39010c26d70f86e1d8fdb8db0" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -160,14 +160,14 @@ dependencies = [ "alloy-transport", "futures", "futures-util", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-core" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca1380cc3c81b83d5234865779494970c83b5893b423c59cdd68c3cd1ed0b671" +checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7078bef2bc353c1d1a97b44981d0186198be320038fbfbb0b37d1dd822a555d3" +checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -203,7 +203,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] @@ -227,14 +227,14 @@ dependencies = [ "alloy-rlp", "k256", "serde", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-eips" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4bffedaddc627520eabdcbfe27a2d2c2f716e15295e2ed1010df3feae67040" +checksum = "6e86967eb559920e4b9102e4cb825fe30f2e9467988353ce4809f0d3f2c90cd4" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -250,14 +250,14 @@ dependencies = [ "ethereum_ssz_derive", "once_cell", "serde", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "alloy-genesis" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b11774716152a5204aff0e86a8c841df499ea81464e2b1f82b3f72d6a2ef32" +checksum = "a40de6f5b53ecf5fd7756072942f41335426d9a3704cd961f77d854739933bcf" dependencies = [ "alloy-eips", "alloy-primitives", @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec80745c33797e8baf547a8cfeb850e60d837fe9b9e67b3f579c1fcd26f527e9" +checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -280,23 +280,23 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6929e607b0a56803c69c68adc6e8aae1644c94e37ea458aa2d0713fc77490e70" +checksum = "27434beae2514d4a2aa90f53832cbdf6f23e4b5e2656d95eaf15f9276e2418b6" dependencies = [ "alloy-primitives", "alloy-sol-types", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tracing", ] [[package]] name = "alloy-network" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2b14524c3605ed5ee173b966333089474415416a8cfd80ceb003c18fd6d1736" +checksum = "26a33a38c7486b1945f8d093ff027add2f3a8f83c7300dbad6165cc49150085e" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -315,14 +315,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-network-primitives" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c06932646544ea341f0fda48d2c0fe4fda75bc132379cb84019cdfb6ddcb0fb" +checksum = "db973a7a23cbe96f2958e5687c51ce2d304b5c6d0dc5ccb3de8667ad8476f50b" dependencies = [ "alloy-consensus", "alloy-eips", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eacedba97e65cdc7ab592f2b22ef5d3ab8d60b2056bc3a6e6363577e8270ec6f" +checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" dependencies = [ "alloy-rlp", "arbitrary", @@ -345,9 +345,9 @@ dependencies = [ "derive_arbitrary", "derive_more 2.0.1", "foldhash", - "getrandom 0.2.15", - "hashbrown 0.15.2", - "indexmap 2.8.0", + "getrandom 0.2.16", + "hashbrown 0.15.5", + "indexmap 2.11.0", "itoa", "k256", "keccak-asm", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1ec2eabd9b3acc46e59247c35f75545960372431c68f7fdbfcfb970a486c30" +checksum = "8b03bde77ad73feae14aa593bcabb932c8098c0f0750ead973331cfc0003a4e1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -399,7 +399,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", "url", @@ -408,9 +408,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1cf194abddb88b034d22ab41449ed8532e5113e58699cd055bf21d98a0991ab" +checksum = "721aca709a9231815ad5903a2d284042cc77e7d9d382696451b30c9ee0950001" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -427,9 +427,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -438,20 +438,20 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "alloy-rpc-client" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6213829d8eabc239c2f9572452a5993ebdf78b04c020abc450ae48c54261d4ce" +checksum = "445a3298c14fae7afb5b9f2f735dead989f3dd83020c2ab8e48ed95d7b6d1acb" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -460,6 +460,7 @@ dependencies = [ "alloy-transport-http", "alloy-transport-ipc", "alloy-transport-ws", + "async-stream", "futures", "pin-project", "reqwest", @@ -469,15 +470,16 @@ dependencies = [ "tokio-stream", "tower 0.5.2", "tracing", + "tracing-futures", "url", "wasmtimer", ] [[package]] name = "alloy-rpc-types" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a153db94cf231b03238fe4da48f59dc6f36e01b5e4d5a2e30de33b95395380fa" +checksum = "9157deaec6ba2ad7854f16146e4cd60280e76593eed79fdcb06e0fa8b6c60f77" dependencies = [ "alloy-primitives", "alloy-rpc-types-anvil", @@ -492,9 +494,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5462937f088889c337c236c2509226e87a26301d2b01f9fafee246bd84cb0407" +checksum = "3a80ee83ef97e7ffd667a81ebdb6154558dfd5e8f20d8249a10a12a1671a04b3" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -504,9 +506,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd4ceea38ea27eeb26f021df34ed5b7b793704ad7a2a009f16137a19461e7ca" +checksum = "604dea1f00fd646debe8033abe8e767c732868bf8a5ae9df6321909ccbc99c56" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -515,9 +517,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c14f3c5747750f7373ec9f633230923ff149c2e31960513e31593bcfcf916be" +checksum = "645455186916281e0b3f063fd07d007711257cf90c3499ff3569a39ffdfc9d2f" dependencies = [ "alloy-eips", "alloy-primitives", @@ -526,16 +528,16 @@ dependencies = [ "ethereum_ssz_derive", "serde", "serde_with", - "thiserror 2.0.12", + "thiserror 2.0.16", "tree_hash", "tree_hash_derive", ] [[package]] name = "alloy-rpc-types-debug" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fa8f6e27d47b4c56c627cb03dc91624c26bd814f6609bb1d1a836148b76fc9b" +checksum = "08b113a0087d226291b9768ed331818fa0b0744cc1207ae7c150687cf3fde1bd" dependencies = [ "alloy-primitives", "serde", @@ -543,9 +545,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a1a0710dbfbab2b33200ef45c650963d63edf6a81b2c7399ede762b3586dfd" +checksum = "874ac9d1249ece0453e262d9ba72da9dbb3b7a2866220ded5940c2e47f1aa04d" dependencies = [ "alloy-consensus", "alloy-eips", @@ -562,9 +564,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e18d94b1036302720b987564560e4a5b85035a17553c53a50afa2bd8762b487" +checksum = "7e13d71eac04513a71af4b3df580f52f2b4dcbff9d971cc9a52519acf55514cb" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -577,28 +579,28 @@ dependencies = [ "itertools 0.14.0", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-rpc-types-trace" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef4bba67ec601730ceb23e542980d73ae9f718819604dfdd8289b13a506e762" +checksum = "4747763aee39c1b0f5face79bde9be8932be05b2db7d8bdcebb93490f32c889c" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", "alloy-serde", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-rpc-types-txpool" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8edc8512f919feb79dd30864ef7574d2877e71b73e30b5de4925ba9bc6bd4f96" +checksum = "70132ebdbea1eaa68c4d6f7a62c2fadf0bdce83b904f895ab90ca4ec96f63468" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -608,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9824e1bf92cd7848ca6fabb01c9aca15c9c5fb0ab96da5514ef0543f021c69f6" +checksum = "3a1cd73fc054de6353c7f22ff9b846b0f0f145cd0112da07d4119e41e9959207" dependencies = [ "alloy-primitives", "serde", @@ -619,9 +621,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81755ed6a6a33061302ac95e9bb7b40ebf7078e4568397168024242bc31a3e58" +checksum = "c96fbde54bee943cd94ebacc8a62c50b38c7dfd2552dcd79ff61aea778b1bfcc" dependencies = [ "alloy-primitives", "async-trait", @@ -629,14 +631,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-signer-local" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa857621a5c95c13e640e18bb9c4720f4338a666d6276f55446477a6bc3912ff" +checksum = "cc6e72002cc1801d8b41e9892165e3a6551b7bd382bd9d0414b21e90c0c62551" dependencies = [ "alloy-consensus", "alloy-network", @@ -645,47 +647,47 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.12", + "thiserror 2.0.16", ] [[package]] name = "alloy-sol-macro" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3637022e781bc73a9e300689cd91105a0e6be00391dd4e2110a71cc7e9f20a94" +checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9bd22d0bba90e40f40c625c33d39afb7d62b22192476a2ce1dcf8409dce880" +checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.8.0", + "indexmap 2.11.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae4646e8123ec2fd10f9c22e361ffe4365c42811431829c2eabae528546bcc" +checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" dependencies = [ "alloy-json-abi", "const-hex", @@ -695,15 +697,15 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.100", + "syn 2.0.106", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488a747fdcefeec5c1ed5aa9e08becd775106777fdeae2a35730729fc8a95910" +checksum = "6d162f8524adfdfb0e4bd0505c734c985f3e2474eb022af32eef0d52a4f3935c" dependencies = [ "serde", "winnow", @@ -711,9 +713,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767957235807b021126dca1598ac3ef477007eace07961607dc5f490550909c7" +checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -724,16 +726,19 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c74598eb65cefa886be6ba624c14a6856d9d84339ec720520f3efcc03311716" +checksum = "9aec325c2af8562ef355c02aeb527c755a07e9d8cf6a1e65dda8d0bf23e29b2c" dependencies = [ "alloy-json-rpc", - "base64 0.22.1", + "base64", + "derive_more 2.0.1", + "futures", "futures-utils-wasm", + "parking_lot", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tower 0.5.2", "tracing", @@ -743,9 +748,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcd2f8ab2f053cd848ead5d625cb1b63716562951101588c1fa49300e3c6418" +checksum = "a082c9473c6642cce8b02405a979496126a03b096997888e86229afad05db06c" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -758,9 +763,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e61e2b5cbf16f7588e4420848b61824f6514944773732534f4129ba6a251e059" +checksum = "45a78cfda2cac16fa83f6b5dd8b4643caec6161433b25b67e484ce05d2194513" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -778,9 +783,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ddcf4b98b3448eb998e057dc5a27345997863d6544ee7f0f79957616768dd3" +checksum = "ae865917bdabaae21f418010fe7e8837c6daa6611fde25f8d78a1778d6ecb523" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -827,9 +832,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -842,50 +847,50 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -1063,18 +1068,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1096,20 +1101,20 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" @@ -1163,11 +1168,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.0", + "axum-core 0.5.2", "axum-macros", "bytes", "form_urlencoded", @@ -1218,12 +1223,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", @@ -1238,12 +1243,12 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" dependencies = [ - "axum 0.8.1", - "axum-core 0.5.0", + "axum 0.8.4", + "axum-core 0.5.2", "bytes", "futures-util", "headers", @@ -1252,6 +1257,7 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "rustversion", "serde", "tower 0.5.2", "tower-layer", @@ -1266,7 +1272,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -1293,17 +1299,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -1312,12 +1318,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1326,9 +1326,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bimap" @@ -1345,7 +1345,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.10.5", @@ -1358,7 +1358,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.100", + "syn 2.0.106", "which", ] @@ -1385,9 +1385,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitvec" @@ -1457,7 +1457,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_bare", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "subtle", "thiserror 1.0.69", @@ -1514,9 +1514,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" @@ -1563,7 +1563,7 @@ dependencies = [ "cb-tests", "comfy-table", "histogram", - "rand 0.9.0", + "rand 0.9.2", "reqwest", "serde", "serde_json", @@ -1579,7 +1579,7 @@ dependencies = [ "clap", "docker-compose-types", "eyre", - "indexmap 2.8.0", + "indexmap 2.11.0", "serde_yaml", ] @@ -1590,8 +1590,8 @@ dependencies = [ "aes 0.8.4", "alloy", "async-trait", - "axum 0.8.1", - "base64 0.22.1", + "axum 0.8.4", + "base64", "bimap", "bytes", "cipher 0.4.4", @@ -1607,15 +1607,15 @@ dependencies = [ "futures", "jsonwebtoken", "pbkdf2 0.12.2", - "rand 0.9.0", + "rand 0.9.2", "rayon", "reqwest", "serde", "serde_json", "serde_yaml", - "sha2 0.10.8", + "sha2 0.10.9", "ssz_types", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "toml", "tonic", @@ -1633,11 +1633,11 @@ dependencies = [ name = "cb-metrics" version = "0.8.1-rc.1" dependencies = [ - "axum 0.8.1", + "axum 0.8.4", "cb-common", "eyre", "prometheus", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tracing", ] @@ -1648,7 +1648,7 @@ version = "0.8.1-rc.1" dependencies = [ "alloy", "async-trait", - "axum 0.8.1", + "axum 0.8.4", "axum-extra", "cb-common", "cb-metrics", @@ -1660,10 +1660,11 @@ dependencies = [ "reqwest", "serde_json", "tokio", + "tower-http", "tracing", "tree_hash", "url", - "uuid 1.16.0", + "uuid 1.18.1", ] [[package]] @@ -1671,7 +1672,7 @@ name = "cb-signer" version = "0.8.1-rc.1" dependencies = [ "alloy", - "axum 0.8.1", + "axum 0.8.4", "axum-extra", "axum-server", "bimap", @@ -1686,15 +1687,15 @@ dependencies = [ "parking_lot", "prometheus", "prost", - "rand 0.9.0", + "rand 0.9.2", "rustls", - "thiserror 2.0.12", + "thiserror 2.0.16", "tokio", "tonic", "tonic-build", "tracing", "tree_hash", - "uuid 1.16.0", + "uuid 1.18.1", ] [[package]] @@ -1702,7 +1703,7 @@ name = "cb-tests" version = "0.8.1-rc.1" dependencies = [ "alloy", - "axum 0.8.1", + "axum 0.8.4", "cb-common", "cb-pbs", "cb-signer", @@ -1721,10 +1722,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1741,9 +1743,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -1753,9 +1755,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1796,9 +1798,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -1806,9 +1808,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -1818,21 +1820,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -1845,9 +1847,9 @@ dependencies = [ [[package]] name = "color-eyre" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" dependencies = [ "backtrace", "color-spantrace", @@ -1860,9 +1862,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" dependencies = [ "once_cell", "owo-colors", @@ -1872,15 +1874,15 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comfy-table" -version = "7.1.4" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" +checksum = "3f8e18d0dca9578507f13f9803add0df13362b02c501c1c17734f0dbb52eaf0b" dependencies = [ "crossterm", "unicode-segmentation", @@ -1924,9 +1926,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" +checksum = "dccd746bf9b1038c0507b7cec21eb2b11222db96a2902c96e8c185d6d20fb9c4" dependencies = [ "cfg-if", "cpufeatures", @@ -2063,14 +2065,14 @@ dependencies = [ "crate_crypto_internal_eth_kzg_maybe_rayon", "crate_crypto_internal_eth_kzg_polynomial", "hex", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -2083,9 +2085,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -2117,14 +2119,15 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "crossterm_winapi", + "document-features", "parking_lot", - "rustix 0.38.44", + "rustix 1.0.8", "winapi", ] @@ -2139,9 +2142,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -2221,12 +2224,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -2245,16 +2248,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2270,13 +2273,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.20.11", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2295,15 +2298,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", @@ -2311,9 +2314,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", @@ -2332,13 +2335,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2356,10 +2359,10 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2369,7 +2372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2398,7 +2401,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2409,7 +2412,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "unicode-xid", ] @@ -2442,7 +2445,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2452,7 +2455,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f5e899a3da7a90647ef302f7e3050b00ed7f3f02c7b32683a04f3fbd9052541" dependencies = [ "derive_builder", - "indexmap 2.8.0", + "indexmap 2.11.0", "serde", "serde_yaml", ] @@ -2473,12 +2476,27 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" version = "0.16.9" @@ -2503,7 +2521,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2562,7 +2580,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2573,12 +2591,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2636,7 +2654,7 @@ checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" dependencies = [ "cpufeatures", "ring", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -2674,10 +2692,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d832a5c38eba0e7ad92592f7a22d693954637fbb332b4f669590d66a5c3183e5" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2741,6 +2759,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2797,9 +2821,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2882,7 +2906,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -2943,42 +2967,42 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", "wasm-bindgen", - "windows-targets 0.52.6", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" @@ -2989,15 +3013,15 @@ dependencies = [ "ff", "rand 0.8.5", "rand_core 0.6.4", - "rand_xorshift", + "rand_xorshift 0.3.0", "subtle", ] [[package]] name = "h2" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -3005,7 +3029,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.8.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -3030,9 +3054,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -3051,11 +3075,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", "http", @@ -3081,9 +3105,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -3188,13 +3212,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -3202,6 +3227,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3209,11 +3235,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -3222,7 +3247,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -3256,33 +3281,41 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -3298,21 +3331,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -3321,31 +3355,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -3353,67 +3367,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -3422,9 +3423,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3433,9 +3434,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -3458,14 +3459,14 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" @@ -3480,13 +3481,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "arbitrary", "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.5", "serde", ] @@ -3522,12 +3523,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -3578,9 +3600,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -3592,7 +3614,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.22.1", + "base64", "js-sys", "ring", "serde", @@ -3610,7 +3632,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -3668,9 +3690,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -3684,9 +3706,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libsqlite3-sys" @@ -3707,21 +3729,27 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -3729,9 +3757,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -3739,7 +3767,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.5", ] [[package]] @@ -3750,7 +3778,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -3761,11 +3789,11 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3782,9 +3810,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "merkle_proof" @@ -3869,29 +3897,29 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" @@ -3922,12 +3950,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3996,9 +4023,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", @@ -4006,22 +4033,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4039,18 +4067,24 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -4060,11 +4094,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -4081,7 +4115,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4092,9 +4126,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -4102,17 +4136,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" [[package]] name = "pairing" @@ -4125,9 +4153,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec", "bitvec", @@ -4141,21 +4169,21 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -4163,9 +4191,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -4205,24 +4233,24 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ - "base64 0.22.1", + "base64", "serde", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] @@ -4233,7 +4261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.8.0", + "indexmap 2.11.0", ] [[package]] @@ -4263,7 +4291,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4294,6 +4322,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4311,12 +4348,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4358,23 +4395,23 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "prometheus" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" dependencies = [ "cfg-if", "fnv", @@ -4382,24 +4419,24 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 1.0.69", + "thiserror 2.0.16", ] [[package]] name = "proptest" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.0", + "bitflags 2.9.4", "lazy_static", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_xorshift", - "regex-syntax 0.8.5", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -4413,7 +4450,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4442,7 +4479,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.100", + "syn 2.0.106", "tempfile", ] @@ -4456,7 +4493,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -4470,9 +4507,23 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.28.0" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] [[package]] name = "quick-error" @@ -4493,8 +4544,8 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", - "thiserror 2.0.12", + "socket2 0.5.10", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -4507,14 +4558,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" dependencies = [ "bytes", - "getrandom 0.3.1", - "rand 0.9.0", + "getrandom 0.3.3", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -4529,7 +4580,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -4543,6 +4594,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -4563,13 +4620,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy", ] [[package]] @@ -4598,7 +4654,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4607,7 +4663,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] @@ -4619,11 +4675,20 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -4631,9 +4696,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -4660,64 +4725,69 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] -name = "regex" -version = "1.11.1" +name = "ref-cast" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "ref-cast-impl", ] [[package]] -name = "regex-automata" -version = "0.1.10" +name = "ref-cast-impl" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ - "regex-syntax 0.6.29", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] -name = "regex-automata" -version = "0.4.9" +name = "regex" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.29" +name = "regex-automata" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.14" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -4730,36 +4800,32 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", "tokio-util", "tower 0.5.2", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.2", ] [[package]] @@ -4780,7 +4846,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4807,9 +4873,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825df406ec217a8116bd7b06897c6cc8f65ffefc15d030ae2c9540acc9ed50b6" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" dependencies = [ "alloy-rlp", "arbitrary", @@ -4825,6 +4891,7 @@ dependencies = [ "primitive-types", "proptest", "rand 0.8.5", + "rand 0.9.2", "rlp", "ruint-macro", "serde", @@ -4868,9 +4935,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -4914,7 +4981,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4923,22 +4990,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.2" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.3", - "windows-sys 0.59.0", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", @@ -4961,18 +5028,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -4982,9 +5050,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" @@ -5027,6 +5095,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -5066,7 +5158,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -5139,14 +5231,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -5172,14 +5264,14 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -5198,15 +5290,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ - "base64 0.22.1", + "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.11.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -5216,14 +5310,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5232,7 +5326,7 @@ version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.11.0", "itoa", "ryu", "serde", @@ -5275,9 +5369,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -5321,9 +5415,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -5340,18 +5434,15 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "arbitrary", "serde", @@ -5359,14 +5450,24 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -5417,7 +5518,7 @@ name = "status_api" version = "0.8.1-rc.1" dependencies = [ "async-trait", - "axum 0.8.1", + "axum 0.8.4", "color-eyre", "commit-boost", "eyre", @@ -5443,24 +5544,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5506,9 +5606,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -5517,14 +5617,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d975606bae72d8aad5b07d9342465e123a2cccf53a5a735aedf81ca92a709ecb" +checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5538,13 +5638,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5553,7 +5653,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -5576,15 +5676,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.2", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -5607,11 +5707,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -5622,18 +5722,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5658,12 +5758,11 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -5677,12 +5776,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -5692,15 +5790,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -5717,9 +5815,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -5727,9 +5825,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -5742,20 +5840,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5766,7 +5866,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5814,14 +5914,14 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -5832,9 +5932,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -5844,26 +5944,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.11.0", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tonic" version = "0.12.3" @@ -5873,7 +5980,7 @@ dependencies = [ "async-stream", "async-trait", "axum 0.7.9", - "base64 0.22.1", + "base64", "bytes", "h2", "http", @@ -5886,7 +5993,7 @@ dependencies = [ "pin-project", "prost", "rustls-pemfile", - "socket2", + "socket2 0.5.10", "tokio", "tokio-rustls", "tokio-stream", @@ -5907,7 +6014,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -5946,6 +6053,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -5984,20 +6110,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -6013,6 +6139,18 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "futures", + "futures-task", + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -6036,14 +6174,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -6074,10 +6212,10 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699e7fb6b3fdfe0c809916f251cf5132d64966858601695c3736630a87e7166a" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -6107,11 +6245,11 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.0", + "rand 0.9.2", "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.16", "utf-8", ] @@ -6151,7 +6289,7 @@ dependencies = [ "milhouse", "parking_lot", "rand 0.8.5", - "rand_xorshift", + "rand_xorshift 0.3.0", "rayon", "regex", "rpds", @@ -6227,9 +6365,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -6251,9 +6389,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -6267,12 +6405,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6291,19 +6423,21 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "serde", ] [[package]] name = "uuid" -version = "1.16.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.1", - "rand 0.9.0", + "getrandom 0.3.3", + "js-sys", + "rand 0.9.2", "serde", + "wasm-bindgen", ] [[package]] @@ -6366,50 +6500,51 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -6420,9 +6555,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6430,22 +6565,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] @@ -6465,9 +6600,9 @@ dependencies = [ [[package]] name = "wasmtimer" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ "futures", "js-sys", @@ -6479,9 +6614,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -6499,9 +6634,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -6548,44 +6692,70 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.53.0", ] [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -6608,6 +6778,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6626,10 +6805,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6738,39 +6918,30 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" dependencies = [ "async_io_stream", "futures", @@ -6779,7 +6950,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror 1.0.69", + "thiserror 2.0.16", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6805,9 +6976,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -6817,13 +6988,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "synstructure", ] @@ -6844,7 +7015,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] [[package]] @@ -6864,7 +7035,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", "synstructure", ] @@ -6886,14 +7057,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -6902,11 +7084,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.106", ] diff --git a/Cargo.toml b/Cargo.toml index 7559cca7..dc5ee88d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,8 @@ members = ["benches/*", "bin", "crates/*", "examples/da_commit", "examples/statu resolver = "2" [workspace.package] -edition = "2021" -rust-version = "1.83" +edition = "2024" +rust-version = "1.89" version = "0.8.1-rc.1" [workspace.dependencies] @@ -52,7 +52,7 @@ lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/l lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v7.1.0" } parking_lot = "0.12.3" pbkdf2 = "0.12.2" -prometheus = "0.13.4" +prometheus = "0.14.0" prost = "0.13.4" rand = { version = "0.9", features = ["os_rng"] } rayon = "1.10.0" @@ -70,6 +70,7 @@ tokio = { version = "1.37.0", features = ["full"] } toml = "0.8.13" tonic = { version = "0.12.3", features = ["channel", "prost", "tls"] } tonic-build = "0.12.3" +tower-http = { version = "0.6", features = ["trace"] } tracing = "0.1.40" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } diff --git a/benches/pbs/src/config.rs b/benches/pbs/src/config.rs index ea903b96..dd34786e 100644 --- a/benches/pbs/src/config.rs +++ b/benches/pbs/src/config.rs @@ -28,7 +28,7 @@ pub fn load_static_config() -> Config { let path = std::env::args().nth(1).expect("missing config path. Add config eg. `bench-config.toml'"); let config_file = fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Unable to find config file: '{}'", path)); + .unwrap_or_else(|_| panic!("Unable to find config file: '{path}'")); let config: Config = toml::from_str(&config_file).expect("failed to parse toml"); config diff --git a/benches/pbs/src/main.rs b/benches/pbs/src/main.rs index 7b852a43..82f72b5c 100644 --- a/benches/pbs/src/main.rs +++ b/benches/pbs/src/main.rs @@ -7,9 +7,9 @@ use cb_common::{ types::{BlsPublicKey, BlsSecretKey, Chain}, utils::TestRandomSeed, }; -use cb_tests::mock_relay::{start_mock_relay_service, MockRelayState}; +use cb_tests::mock_relay::{MockRelayState, start_mock_relay_service}; use comfy_table::Table; -use config::{load_static_config, BenchConfig}; +use config::{BenchConfig, load_static_config}; use histogram::Histogram; mod config; @@ -137,7 +137,10 @@ async fn start_mock_relay(chain: Chain, relay_config: RelayConfig) { let signer = BlsSecretKey::deserialize(&MOCK_RELAY_SECRET).unwrap(); let pubkey: BlsPublicKey = signer.public_key(); - assert_eq!(relay_config.entry.pubkey, pubkey, "Expected relay pubkey to be 0xb060572f535ba5615b874ebfef757fbe6825352ad257e31d724e57fe25a067a13cfddd0f00cb17bf3a3d2e901a380c17"); + assert_eq!( + relay_config.entry.pubkey, pubkey, + "Expected relay pubkey to be 0xb060572f535ba5615b874ebfef757fbe6825352ad257e31d724e57fe25a067a13cfddd0f00cb17bf3a3d2e901a380c17" + ); let relay_port = relay_config.entry.url.port().expect("missing port"); diff --git a/bin/cli.rs b/bin/cli.rs index da89ca39..d3fa736c 100644 --- a/bin/cli.rs +++ b/bin/cli.rs @@ -5,9 +5,6 @@ use clap::Parser; async fn main() -> eyre::Result<()> { color_eyre::install()?; // set default backtrace unless provided - if std::env::var_os("RUST_BACKTRACE").is_none() { - std::env::set_var("RUST_BACKTRACE", "1"); - } let args = cb_cli::Args::parse(); diff --git a/bin/pbs.rs b/bin/pbs.rs index 53714fb5..69945fe8 100644 --- a/bin/pbs.rs +++ b/bin/pbs.rs @@ -1,5 +1,5 @@ use cb_common::{ - config::{load_pbs_config, LogsSettings, PBS_MODULE_NAME}, + config::{LogsSettings, PBS_MODULE_NAME, load_pbs_config}, utils::{initialize_tracing_log, wait_for_signal}, }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; @@ -11,10 +11,6 @@ use tracing::{error, info}; async fn main() -> Result<()> { color_eyre::install()?; - // set default backtrace unless provided - if std::env::var_os("RUST_BACKTRACE").is_none() { - std::env::set_var("RUST_BACKTRACE", "1"); - } let _guard = initialize_tracing_log(PBS_MODULE_NAME, LogsSettings::from_env_config()?); let _args = cb_cli::PbsArgs::parse(); @@ -29,7 +25,7 @@ async fn main() -> Result<()> { maybe_err = server => { if let Err(err) = maybe_err { error!(%err, "PBS service unexpectedly stopped"); - eprintln!("PBS service unexpectedly stopped: {}", err); + eprintln!("PBS service unexpectedly stopped: {err}"); } }, _ = wait_for_signal() => { diff --git a/bin/signer.rs b/bin/signer.rs index e7c7da7c..2d9a60ad 100644 --- a/bin/signer.rs +++ b/bin/signer.rs @@ -1,5 +1,5 @@ use cb_common::{ - config::{LogsSettings, StartSignerConfig, SIGNER_MODULE_NAME}, + config::{LogsSettings, SIGNER_MODULE_NAME, StartSignerConfig}, utils::{initialize_tracing_log, wait_for_signal}, }; use cb_signer::service::SigningService; @@ -11,10 +11,6 @@ use tracing::{error, info}; async fn main() -> Result<()> { color_eyre::install()?; - // set default backtrace unless provided - if std::env::var_os("RUST_BACKTRACE").is_none() { - std::env::set_var("RUST_BACKTRACE", "1"); - } let _guard = initialize_tracing_log(SIGNER_MODULE_NAME, LogsSettings::from_env_config()?); let _args = cb_cli::SignerArgs::parse(); @@ -26,7 +22,7 @@ async fn main() -> Result<()> { maybe_err = server => { if let Err(err) = maybe_err { error!(%err, "signing server unexpectedly stopped"); - eprintln!("signing server unexpectedly stopped: {}", err); + eprintln!("signing server unexpectedly stopped: {err}"); } }, _ = wait_for_signal() => { diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 39a1c5c9..a673d61a 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -6,8 +6,8 @@ pub mod prelude { SignedProxyDelegationBls, SignedProxyDelegationEcdsa, }, config::{ - load_builder_module_config, load_commit_module_config, load_pbs_config, - load_pbs_custom_config, LogsSettings, StartCommitModuleConfig, PBS_MODULE_NAME, + LogsSettings, PBS_MODULE_NAME, StartCommitModuleConfig, load_builder_module_config, + load_commit_module_config, load_pbs_config, load_pbs_custom_config, }, signature::{ verify_proposer_commitment_signature_bls, verify_proposer_commitment_signature_ecdsa, @@ -18,8 +18,8 @@ pub mod prelude { }; pub use cb_metrics::provider::MetricsProvider; pub use cb_pbs::{ - get_header, get_status, register_validator, submit_block, BuilderApi, BuilderApiState, - DefaultBuilderApi, PbsService, PbsState, PbsStateGuard, + BuilderApi, BuilderApiState, DefaultBuilderApi, PbsService, PbsState, PbsStateGuard, + get_header, get_status, register_validator, submit_block, }; // The TreeHash derive macro requires tree_hash as import pub mod tree_hash { diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index d06231ae..6eaf554b 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -6,18 +6,17 @@ use std::{ use cb_common::{ config::{ - CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig, SignerType, TlsMode, - ADMIN_JWT_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, - DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, - DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, - LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PBS_ENDPOINT_ENV, - PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, - PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, - SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_DEFAULT, - SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, SIGNER_MODULE_NAME, - SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, - SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_KEY_NAME, - SIGNER_URL_ENV, + ADMIN_JWT_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV, CommitBoostConfig, + DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV, + DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV, + LOGS_DIR_DEFAULT, LOGS_DIR_ENV, LogsSettings, METRICS_PORT_ENV, MODULE_ID_ENV, + MODULE_JWT_ENV, ModuleKind, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, + PROXY_DIR_ENV, PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT, + PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV, + SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_ENDPOINT_ENV, SIGNER_KEYS_ENV, + SIGNER_MODULE_NAME, SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATE_NAME, + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_KEY_NAME, SIGNER_URL_ENV, SignerConfig, SignerType, TlsMode, }, pbs::{BUILDER_V1_API_PATH, GET_STATUS_PATH}, signer::{ProxyStore, SignerLoader}, @@ -151,21 +150,18 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re module_envs.insert(key, val); } - if let Some(metrics_config) = &cb_config.metrics { - if metrics_config.enabled { - let host_endpoint = - SocketAddr::from((metrics_config.host, metrics_port)); - ports.push(format!("{}:{}", host_endpoint, metrics_port)); - warnings.push(format!( - "{} has an exported port on {}", - module_cid, metrics_port - )); - targets.push(format!("{host_endpoint}")); - let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); - module_envs.insert(key, val); - - metrics_port += 1; - } + if let Some(metrics_config) = &cb_config.metrics && + metrics_config.enabled + { + let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); + ports.push(format!("{host_endpoint}:{metrics_port}")); + warnings + .push(format!("{module_cid} has an exported port on {metrics_port}")); + targets.push(format!("{host_endpoint}")); + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + module_envs.insert(key, val); + + metrics_port += 1; } if log_to_file { @@ -235,7 +231,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if let Some((env_name, actual_path, internal_path)) = mux.loader_env()? { let (key, val) = get_env_val(&env_name, &internal_path); pbs_envs.insert(key, val); - pbs_volumes.push(Volumes::Simple(format!("{}:{}:ro", actual_path, internal_path))); + pbs_volumes.push(Volumes::Simple(format!("{actual_path}:{internal_path}:ro"))); } } } @@ -243,17 +239,17 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if let Some((key, val)) = chain_spec_env.clone() { pbs_envs.insert(key, val); } - if let Some(metrics_config) = &cb_config.metrics { - if metrics_config.enabled { - let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); - ports.push(format!("{}:{}", host_endpoint, metrics_port)); - warnings.push(format!("cb_pbs has an exported port on {}", metrics_port)); - targets.push(format!("{host_endpoint}")); - let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); - pbs_envs.insert(key, val); - - metrics_port += 1; - } + if let Some(metrics_config) = &cb_config.metrics && + metrics_config.enabled + { + let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); + ports.push(format!("{host_endpoint}:{metrics_port}")); + warnings.push(format!("cb_pbs has an exported port on {metrics_port}")); + targets.push(format!("{host_endpoint}")); + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + pbs_envs.insert(key, val); + + metrics_port += 1; } if log_to_file { let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); @@ -269,13 +265,11 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re // volumes pbs_volumes.extend(chain_spec_volume.clone()); pbs_volumes.extend(get_log_volume(&cb_config.logs, PBS_MODULE_NAME)); - if needs_signer_module { - if let Some(certs_path) = certs_path { - pbs_volumes.push(create_cert_binding(certs_path)); - let (key, val) = - get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT); - pbs_envs.insert(key, val); - } + if needs_signer_module && let Some(certs_path) = certs_path { + pbs_volumes.push(create_cert_binding(certs_path)); + let (key, val) = + get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT); + pbs_envs.insert(key, val); } let pbs_service = Service { @@ -332,16 +326,15 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if let Some((key, val)) = chain_spec_env.clone() { signer_envs.insert(key, val); } - if let Some(metrics_config) = &cb_config.metrics { - if metrics_config.enabled { - let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); - ports.push(format!("{}:{}", host_endpoint, metrics_port)); - warnings - .push(format!("cb_signer has an exported port on {}", metrics_port)); - targets.push(format!("{host_endpoint}")); - let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); - signer_envs.insert(key, val); - } + if let Some(metrics_config) = &cb_config.metrics && + metrics_config.enabled + { + let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); + ports.push(format!("{host_endpoint}:{metrics_port}")); + warnings.push(format!("cb_signer has an exported port on {metrics_port}")); + targets.push(format!("{host_endpoint}")); + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + signer_envs.insert(key, val); } if log_to_file { let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); @@ -473,16 +466,15 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if let Some((key, val)) = chain_spec_env.clone() { signer_envs.insert(key, val); } - if let Some(metrics_config) = &cb_config.metrics { - if metrics_config.enabled { - let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); - ports.push(format!("{}:{}", host_endpoint, metrics_port)); - warnings - .push(format!("cb_signer has an exported port on {}", metrics_port)); - targets.push(format!("{host_endpoint}")); - let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); - signer_envs.insert(key, val); - } + if let Some(metrics_config) = &cb_config.metrics && + metrics_config.enabled + { + let host_endpoint = SocketAddr::from((metrics_config.host, metrics_port)); + ports.push(format!("{host_endpoint}:{metrics_port}")); + warnings.push(format!("cb_signer has an exported port on {metrics_port}")); + targets.push(format!("{host_endpoint}")); + let (key, val) = get_env_uval(METRICS_PORT_ENV, metrics_port as u64); + signer_envs.insert(key, val); } if log_to_file { let (key, val) = get_env_val(LOGS_DIR_ENV, LOGS_DIR_DEFAULT); @@ -590,7 +582,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re if !warnings.is_empty() { println!(); for exposed_port in warnings { - println!("Warning: {}", exposed_port); + println!("Warning: {exposed_port}"); } println!() } @@ -604,39 +596,35 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re println!() } - println!("Docker Compose file written to: {:?}", compose_path); + println!("Docker Compose file written to: {compose_path:?}"); // write prometheus targets to file if !targets.is_empty() { let targets = targets.join(", "); println!("Note: Make sure to add these targets for Prometheus to scrape: {targets}"); - println!("Check out the docs on how to configure Prometheus/Grafana/cAdvisor: https://commit-boost.github.io/commit-boost-client/get_started/running/metrics"); + println!( + "Check out the docs on how to configure Prometheus/Grafana/cAdvisor: https://commit-boost.github.io/commit-boost-client/get_started/running/metrics" + ); } if envs.is_empty() { - println!("Run with:\n\tdocker compose -f {:?} up -d", compose_path); + println!("Run with:\n\tdocker compose -f {compose_path:?} up -d"); } else { // write envs to .env file let envs_str = { let mut envs_str = String::new(); for (k, v) in envs { - envs_str.push_str(&format!("{}={}\n", k, v)); + envs_str.push_str(&format!("{k}={v}\n")); } envs_str }; let env_path = Path::new(&output_dir).join(CB_ENV_FILE); std::fs::write(&env_path, envs_str)?; - println!("Env file written to: {:?}", env_path); + println!("Env file written to: {env_path:?}"); println!(); - println!( - "Run with:\n\tdocker compose --env-file {:?} -f {:?} up -d", - env_path, compose_path - ); - println!( - "Stop with:\n\tdocker compose --env-file {:?} -f {:?} down", - env_path, compose_path - ); + println!("Run with:\n\tdocker compose --env-file {env_path:?} -f {compose_path:?} up -d"); + println!("Stop with:\n\tdocker compose --env-file {env_path:?} -f {compose_path:?} down"); } Ok(()) @@ -678,7 +666,7 @@ fn get_log_volume(config: &LogsSettings, module_id: &str) -> Option { /// Formats as a comma separated list of key=value fn format_comma_separated(map: &IndexMap) -> String { - map.iter().map(|(k, v)| format!("{}={}", k, v)).collect::>().join(",") + map.iter().map(|(k, v)| format!("{k}={v}")).collect::>().join(",") } fn create_cert_binding(certs_path: &Path) -> Volumes { diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 9d6b87ae..1151eb6f 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use alloy::primitives::Address; use eyre::WrapErr; use reqwest::{ - header::{HeaderMap, HeaderValue, AUTHORIZATION}, Certificate, + header::{AUTHORIZATION, HeaderMap, HeaderValue}, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -18,6 +18,7 @@ use super::{ }, }; use crate::{ + DEFAULT_REQUEST_TIMEOUT, commit::{ constants::{ REQUEST_SIGNATURE_BLS_PATH, REQUEST_SIGNATURE_PROXY_BLS_PATH, @@ -27,7 +28,6 @@ use crate::{ }, types::{BlsPublicKey, Jwt, ModuleId}, utils::create_jwt, - DEFAULT_REQUEST_TIMEOUT, }; /// Client used by commit modules to request signatures via the Signer API @@ -64,7 +64,7 @@ impl SignerClient { let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; let mut auth_value = - HeaderValue::from_str(&format!("Bearer {}", jwt)).wrap_err("invalid jwt")?; + HeaderValue::from_str(&format!("Bearer {jwt}")).wrap_err("invalid jwt")?; auth_value.set_sensitive(true); let mut headers = HeaderMap::new(); diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index e2654b5c..b1439e87 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -6,7 +6,7 @@ use std::{ use alloy::{ hex, - primitives::{aliases::B32, Address, B256}, + primitives::{Address, B256, aliases::B32}, }; use serde::{Deserialize, Deserializer, Serialize}; use tree_hash::TreeHash; diff --git a/crates/common/src/config/log.rs b/crates/common/src/config/log.rs index 1a1fa4e5..595a81a1 100644 --- a/crates/common/src/config/log.rs +++ b/crates/common/src/config/log.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use eyre::Result; use serde::{Deserialize, Serialize}; -use super::{load_optional_env_var, CommitBoostConfig, LOGS_DIR_DEFAULT, LOGS_DIR_ENV}; +use super::{CommitBoostConfig, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, load_optional_env_var}; use crate::utils::default_bool; #[derive(Clone, Default, Debug, Deserialize, Serialize)] diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 664fd13e..0880d5c8 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use eyre::Result; use serde::{Deserialize, Serialize}; -use crate::types::{load_chain_from_file, Chain, ChainLoader, ForkVersion}; +use crate::types::{Chain, ChainLoader, ForkVersion, load_chain_from_file}; mod constants; mod log; diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index b2f30ae8..d53c4bcd 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -2,16 +2,16 @@ use std::{collections::HashMap, path::PathBuf}; use alloy::primitives::B256; use eyre::{ContextCompat, Result}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use toml::Table; use crate::{ commit::client::SignerClient, config::{ + SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_CERTIFICATES_PATH_ENV, SignerConfig, TlsMode, constants::{CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_URL_ENV}, load_env_var, utils::load_file_from_env, - SignerConfig, TlsMode, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, }, types::{Chain, Jwt, ModuleId}, }; diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 0c8f7011..15bf9b7e 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -6,19 +6,19 @@ use std::{ }; use alloy::{ - primitives::{address, Address, U256}, + primitives::{Address, U256, address}, providers::ProviderBuilder, rpc::{client::RpcClient, types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN}, sol, transports::http::Http, }; -use eyre::{bail, ensure, Context}; +use eyre::{Context, bail, ensure}; use reqwest::Client; use serde::{Deserialize, Deserializer, Serialize}; use tracing::{debug, info, warn}; use url::Url; -use super::{load_optional_env_var, PbsConfig, RelayConfig, MUX_PATH_ENV}; +use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var}; use crate::{ config::{remove_duplicate_keys, safe_read_http_response}, pbs::RelayClient, @@ -353,8 +353,7 @@ async fn fetch_ssv_pubkeys( loop { let url = format!( - "https://api.ssv.network/api/v4/{}/validators/in_operator/{}?perPage={}&page={}", - chain_name, node_operator_id, MAX_PER_PAGE, page + "https://api.ssv.network/api/v4/{chain_name}/validators/in_operator/{node_operator_id}?perPage={MAX_PER_PAGE}&page={page}", ); let response = fetch_ssv_pubkeys_from_url(&url, http_timeout).await?; @@ -442,7 +441,7 @@ mod tests { use super::*; use crate::{ config::{HTTP_TIMEOUT_SECONDS_DEFAULT, MUXER_HTTP_MAX_LENGTH}, - utils::{bls_pubkey_from_hex_unchecked, set_ignore_content_length, ResponseReadError}, + utils::{ResponseReadError, bls_pubkey_from_hex_unchecked, set_ignore_content_length}, }; const TEST_HTTP_TIMEOUT: u64 = 2; @@ -497,9 +496,15 @@ mod tests { // NOTE: requires that ssv_data.json dpesn't change assert_eq!(response.validators.len(), 3); let expected_pubkeys = [ - bls_pubkey_from_hex_unchecked("967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a"), - bls_pubkey_from_hex_unchecked("ac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c"), - bls_pubkey_from_hex_unchecked("8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639"), + bls_pubkey_from_hex_unchecked( + "967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a", + ), + bls_pubkey_from_hex_unchecked( + "ac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c", + ), + bls_pubkey_from_hex_unchecked( + "8c866a5a05f3d45c49b457e29365259021a509c5daa82e124f9701a960ee87b8902e87175315ab638a3d8b1115b23639", + ), ]; for (i, validator) in response.validators.iter().enumerate() { assert_eq!(validator.pubkey, expected_pubkeys[i]); diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index b7187cec..fd9cf08d 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -8,32 +8,32 @@ use std::{ }; use alloy::{ - primitives::{utils::format_ether, U256}, + primitives::{U256, utils::format_ether}, providers::{Provider, ProviderBuilder}, }; -use eyre::{ensure, Result}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use eyre::{Result, ensure}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use url::Url; use super::{ - constants::PBS_IMAGE_DEFAULT, load_optional_env_var, CommitBoostConfig, RuntimeMuxConfig, - HTTP_TIMEOUT_SECONDS_DEFAULT, PBS_ENDPOINT_ENV, + CommitBoostConfig, HTTP_TIMEOUT_SECONDS_DEFAULT, PBS_ENDPOINT_ENV, RuntimeMuxConfig, + constants::PBS_IMAGE_DEFAULT, load_optional_env_var, }; use crate::{ commit::client::SignerClient, config::{ - load_env_var, load_file_from_env, PbsMuxes, SignerConfig, TlsMode, CONFIG_ENV, - MODULE_JWT_ENV, PBS_MODULE_NAME, SIGNER_TLS_CERTIFICATES_PATH_ENV, - SIGNER_TLS_CERTIFICATE_NAME, SIGNER_URL_ENV, + CONFIG_ENV, MODULE_JWT_ENV, PBS_MODULE_NAME, PbsMuxes, SIGNER_TLS_CERTIFICATE_NAME, + SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_URL_ENV, SignerConfig, TlsMode, load_env_var, + load_file_from_env, }, pbs::{ - DefaultTimeout, RelayClient, RelayEntry, DEFAULT_PBS_PORT, LATE_IN_SLOT_TIME_MS, - REGISTER_VALIDATOR_RETRY_LIMIT, + DEFAULT_PBS_PORT, DefaultTimeout, LATE_IN_SLOT_TIME_MS, REGISTER_VALIDATOR_RETRY_LIMIT, + RelayClient, RelayEntry, }, types::{BlsPublicKey, Chain, Jwt, ModuleId}, utils::{ - as_eth_str, default_bool, default_host, default_u16, default_u256, default_u32, - default_u64, WEI_PER_ETH, + WEI_PER_ETH, as_eth_str, default_bool, default_host, default_u16, default_u32, default_u64, + default_u256, }, }; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 77eb425a..4e040701 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -6,21 +6,21 @@ use std::{ use alloy::primitives::B256; use docker_image::DockerImage; -use eyre::{bail, ensure, Context, OptionExt, Result}; +use eyre::{Context, OptionExt, Result, bail, ensure}; use serde::{Deserialize, Serialize}; use tonic::transport::{Certificate, Identity}; use url::Url; use super::{ - load_optional_env_var, utils::load_env_var, CommitBoostConfig, SIGNER_ENDPOINT_ENV, - SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, + CommitBoostConfig, SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT, + SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV, - SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATE_NAME, - SIGNER_TLS_KEY_NAME, + SIGNER_PORT_DEFAULT, SIGNER_TLS_CERTIFICATE_NAME, SIGNER_TLS_CERTIFICATES_PATH_ENV, + SIGNER_TLS_KEY_NAME, load_optional_env_var, utils::load_env_var, }; use crate::{ config::{ - load_jwt_secrets, DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV, + DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV, load_jwt_secrets, }, signer::{ProxyStore, SignerLoader}, types::{Chain, ModuleId}, @@ -126,7 +126,7 @@ fn default_signer_image() -> String { fn default_tls_mode() -> TlsMode { TlsMode::Insecure // To make the default use TLS, do - // TlsMode::Certificate(PathBuf::from("./certs")) + // TlsMode::Certificate(PathBuf::from("./certs")) } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -372,7 +372,7 @@ pub fn load_module_signing_configs( #[cfg(test)] mod tests { - use alloy::primitives::{b256, Uint}; + use alloy::primitives::{Uint, b256}; use super::*; use crate::config::{LogsSettings, ModuleKind, PbsConfig, StaticModuleConfig, StaticPbsConfig}; diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index d99ae2dd..a9dab45b 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, path::Path}; -use eyre::{bail, Context, Result}; +use eyre::{Context, Result, bail}; use serde::de::DeserializeOwned; use crate::{ diff --git a/crates/common/src/pbs/builder.rs b/crates/common/src/pbs/builder.rs index 0656ac2d..782bffc5 100644 --- a/crates/common/src/pbs/builder.rs +++ b/crates/common/src/pbs/builder.rs @@ -25,6 +25,6 @@ impl Display for BuilderApiVersion { BuilderApiVersion::V1 => "v1", BuilderApiVersion::V2 => "v2", }; - write!(f, "{}", s) + write!(f, "{s}") } } diff --git a/crates/common/src/pbs/error.rs b/crates/common/src/pbs/error.rs index dd91ec45..8f350735 100644 --- a/crates/common/src/pbs/error.rs +++ b/crates/common/src/pbs/error.rs @@ -63,7 +63,9 @@ pub enum ValidationError { #[error("block hash mismatch: expected {expected} got {got}")] BlockHashMismatch { expected: B256, got: B256 }, - #[error("mismatch in KZG commitments: expected_blobs: {expected_blobs} got_blobs: {got_blobs} got_commitments: {got_commitments} got_proofs: {got_proofs}")] + #[error( + "mismatch in KZG commitments: expected_blobs: {expected_blobs} got_blobs: {got_blobs} got_commitments: {got_commitments} got_proofs: {got_proofs}" + )] KzgCommitments { expected_blobs: usize, got_blobs: usize, diff --git a/crates/common/src/pbs/relay.rs b/crates/common/src/pbs/relay.rs index 4d8296c2..59d963e1 100644 --- a/crates/common/src/pbs/relay.rs +++ b/crates/common/src/pbs/relay.rs @@ -7,12 +7,12 @@ use serde::{Deserialize, Serialize}; use url::Url; use super::{ + HEADER_VERSION_KEY, HEADER_VERSION_VALUE, constants::{GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH}, error::PbsError, - HEADER_VERSION_KEY, HEADER_VERSION_VALUE, }; use crate::{ - config::RelayConfig, pbs::BuilderApiVersion, types::BlsPublicKey, DEFAULT_REQUEST_TIMEOUT, + DEFAULT_REQUEST_TIMEOUT, config::RelayConfig, pbs::BuilderApiVersion, types::BlsPublicKey, }; /// A parsed entry of the relay url in the format: scheme://pubkey@host @@ -144,7 +144,9 @@ mod tests { #[test] fn test_relay_entry() { - let pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); + let pubkey = bls_pubkey_from_hex_unchecked( + "0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae", + ); let s = format!("http://{pubkey}@abc.xyz/"); let parsed = serde_json::from_str::(&format!("\"{s}\"")).unwrap(); @@ -158,8 +160,12 @@ mod tests { fn test_relay_url() { let slot = 0; let parent_hash = B256::ZERO; - let validator_pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); - let expected = format!("http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}"); + let validator_pubkey = bls_pubkey_from_hex_unchecked( + "0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae", + ); + let expected = format!( + "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}" + ); let relay_config = r#" { @@ -192,11 +198,15 @@ mod tests { fn test_relay_url_with_get_params() { let slot = 0; let parent_hash = B256::ZERO; - let validator_pubkey = bls_pubkey_from_hex_unchecked("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae"); + let validator_pubkey = bls_pubkey_from_hex_unchecked( + "0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae", + ); // Note: HashMap iteration order is not guaranteed, so we can't predict the // exact order of parameters Instead of hard-coding the order, we'll // check that both parameters are present in the URL - let url_prefix = format!("http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}?"); + let url_prefix = format!( + "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}?" + ); let mut get_params = HashMap::new(); get_params.insert("param1".to_string(), "value1".to_string()); diff --git a/crates/common/src/pbs/types/blinded_block_body.rs b/crates/common/src/pbs/types/blinded_block_body.rs index 4acd3dd1..754a8775 100644 --- a/crates/common/src/pbs/types/blinded_block_body.rs +++ b/crates/common/src/pbs/types/blinded_block_body.rs @@ -1,7 +1,7 @@ use alloy::primitives::{Address, B256}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; -use ssz_types::{typenum, BitList, BitVector, FixedVector, VariableList}; +use ssz_types::{BitList, BitVector, FixedVector, VariableList, typenum}; use super::{ execution_payload::ExecutionPayloadHeader, execution_requests::ExecutionRequests, diff --git a/crates/common/src/pbs/types/execution_payload.rs b/crates/common/src/pbs/types/execution_payload.rs index fcf4cdda..f4aff2c4 100644 --- a/crates/common/src/pbs/types/execution_payload.rs +++ b/crates/common/src/pbs/types/execution_payload.rs @@ -1,4 +1,4 @@ -use alloy::primitives::{b256, Address, B256, U256}; +use alloy::primitives::{Address, B256, U256, b256}; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use ssz_types::{FixedVector, VariableList}; @@ -95,7 +95,7 @@ mod tests { use super::*; use crate::{ - pbs::{types::execution_payload::Transactions, ElectraSpec}, + pbs::{ElectraSpec, types::execution_payload::Transactions}, utils::test_encode_decode, }; diff --git a/crates/common/src/pbs/types/get_header.rs b/crates/common/src/pbs/types/get_header.rs index 0f9c5330..2f0897b6 100644 --- a/crates/common/src/pbs/types/get_header.rs +++ b/crates/common/src/pbs/types/get_header.rs @@ -92,7 +92,7 @@ pub struct ExecutionPayloadHeaderMessageElectra { #[cfg(test)] mod tests { - use alloy::primitives::{aliases::B32, U256}; + use alloy::primitives::{U256, aliases::B32}; use super::*; use crate::{ diff --git a/crates/common/src/pbs/types/mod.rs b/crates/common/src/pbs/types/mod.rs index e22c4a2f..c5eaeed6 100644 --- a/crates/common/src/pbs/types/mod.rs +++ b/crates/common/src/pbs/types/mod.rs @@ -14,8 +14,8 @@ pub use beacon_block::{ }; pub use blobs_bundle::{Blob, BlobsBundle}; pub use execution_payload::{ - ExecutionPayload, ExecutionPayloadHeader, Transaction, Transactions, Withdrawal, - EMPTY_TX_ROOT_HASH, + EMPTY_TX_ROOT_HASH, ExecutionPayload, ExecutionPayloadHeader, Transaction, Transactions, + Withdrawal, }; pub use execution_requests::{ ConsolidationRequest, DepositRequest, ExecutionRequests, WithdrawalRequest, @@ -25,7 +25,7 @@ pub use get_header::{ SignedExecutionPayloadHeader, }; pub use kzg::{ - KzgCommitment, KzgCommitments, KzgProof, KzgProofs, BYTES_PER_COMMITMENT, BYTES_PER_PROOF, + BYTES_PER_COMMITMENT, BYTES_PER_PROOF, KzgCommitment, KzgCommitments, KzgProof, KzgProofs, }; pub use spec::{ElectraSpec, EthSpec}; pub use utils::VersionedResponse; diff --git a/crates/common/src/pbs/types/utils.rs b/crates/common/src/pbs/types/utils.rs index 8c66d357..0a8de5c0 100644 --- a/crates/common/src/pbs/types/utils.rs +++ b/crates/common/src/pbs/types/utils.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; pub mod quoted_variable_list_u64 { - use serde::{ser::SerializeSeq, Deserializer, Serializer}; + use serde::{Deserializer, Serializer, ser::SerializeSeq}; use serde_utils::quoted_u64_vec::{QuotedIntVecVisitor, QuotedIntWrapper}; - use ssz_types::{typenum::Unsigned, VariableList}; + use ssz_types::{VariableList, typenum::Unsigned}; pub fn serialize(value: &VariableList, serializer: S) -> Result where @@ -24,7 +24,7 @@ pub mod quoted_variable_list_u64 { { deserializer.deserialize_any(QuotedIntVecVisitor).and_then(|vec| { VariableList::new(vec) - .map_err(|e| serde::de::Error::custom(format!("invalid length: {:?}", e))) + .map_err(|e| serde::de::Error::custom(format!("invalid length: {e:?}"))) }) } } diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index 33f79726..4b0353a1 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -1,10 +1,10 @@ -use alloy::primitives::{aliases::B32, Address, B256}; +use alloy::primitives::{Address, B256, aliases::B32}; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ constants::{COMMIT_BOOST_DOMAIN, GENESIS_VALIDATORS_ROOT}, - signer::{verify_bls_signature, verify_ecdsa_signature, EcdsaSignature}, + signer::{EcdsaSignature, verify_bls_signature, verify_ecdsa_signature}, types::{self, BlsPublicKey, BlsSecretKey, BlsSignature, Chain, SignatureRequestInfo}, }; diff --git a/crates/common/src/signer/loader.rs b/crates/common/src/signer/loader.rs index 853eba5c..be5329b9 100644 --- a/crates/common/src/signer/loader.rs +++ b/crates/common/src/signer/loader.rs @@ -5,20 +5,20 @@ use std::{ }; use aes::{ - cipher::{KeyIvInit, StreamCipher}, Aes128, + cipher::{KeyIvInit, StreamCipher}, }; -use eyre::{eyre, Context}; -use lh_eth2_keystore::{json_keystore::JsonKeystore, Keystore}; +use eyre::{Context, eyre}; +use lh_eth2_keystore::{Keystore, json_keystore::JsonKeystore}; use pbkdf2::{hmac, pbkdf2}; use rayon::prelude::*; -use serde::{de, Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, de}; use tracing::warn; use unicode_normalization::UnicodeNormalization; use super::{BlsSigner, EcdsaSigner, PrysmDecryptedKeystore, PrysmKeystore}; use crate::{ - config::{load_env_var, SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV}, + config::{SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS_ENV, load_env_var}, signer::ConsensusSigner, utils::bls_pubkey_from_hex, }; @@ -341,11 +341,11 @@ pub fn load_ecdsa_signer(keys_path: PathBuf, secrets_path: PathBuf) -> eyre::Res #[cfg(test)] mod tests { - use super::{load_from_lighthouse_format, load_from_lodestar_format, FileKey}; + use super::{FileKey, load_from_lighthouse_format, load_from_lodestar_format}; use crate::{ signer::{ - loader::{load_from_nimbus_format, load_from_prysm_format, load_from_teku_format}, BlsSigner, + loader::{load_from_nimbus_format, load_from_prysm_format, load_from_teku_format}, }, utils::bls_pubkey_from_hex_unchecked, }; @@ -422,9 +422,12 @@ mod tests { let signers = result.unwrap(); assert_eq!(signers.len(), 1); - assert!(signers[0].pubkey() == bls_pubkey_from_hex_unchecked( - "883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" - )); + assert!( + signers[0].pubkey() == + bls_pubkey_from_hex_unchecked( + "883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" + ) + ); let result = load_from_lodestar_format( "../../tests/data/keystores/teku-keys/".into(), @@ -436,9 +439,12 @@ mod tests { let signers = result.unwrap(); assert_eq!(signers.len(), 1); - assert!(signers[0].pubkey() == bls_pubkey_from_hex_unchecked( - "b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9" - )); + assert!( + signers[0].pubkey() == + bls_pubkey_from_hex_unchecked( + "b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9" + ) + ); } #[test] diff --git a/crates/common/src/signer/schemes/ecdsa.rs b/crates/common/src/signer/schemes/ecdsa.rs index c01cf85f..fedac646 100644 --- a/crates/common/src/signer/schemes/ecdsa.rs +++ b/crates/common/src/signer/schemes/ecdsa.rs @@ -1,8 +1,8 @@ use std::{ops::Deref, str::FromStr}; use alloy::{ - primitives::{aliases::B32, Address, PrimitiveSignature, B256}, - signers::{local::PrivateKeySigner, SignerSync}, + primitives::{Address, B256, PrimitiveSignature, aliases::B32}, + signers::{SignerSync, local::PrivateKeySigner}, }; use eyre::ensure; use tree_hash::TreeHash; diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index 8ad00dcb..bd4aa103 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -12,12 +12,11 @@ use alloy::{ }; use eyre::{Context, OptionExt}; use lh_eth2_keystore::{ - default_kdf, + IV_SIZE, SALT_SIZE, Uuid, default_kdf, json_keystore::{ Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, JsonKeystore, KdfModule, Sha256Checksum, }, - Uuid, IV_SIZE, SALT_SIZE, }; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -26,7 +25,7 @@ use tracing::{trace, warn}; use super::{load_bls_signer, load_ecdsa_signer}; use crate::{ commit::request::{EncryptionScheme, ProxyDelegation, ProxyId, SignedProxyDelegation}, - config::{load_env_var, PROXY_DIR_ENV, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_ENV}, + config::{PROXY_DIR_ENV, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_ENV, load_env_var}, signer::{BlsProxySigner, BlsSigner, EcdsaProxySigner, EcdsaSigner, ProxySigners}, types::{BlsPublicKey, BlsSignature, ModuleId}, }; @@ -199,67 +198,60 @@ impl ProxyStore { let module_path = entry.path(); // Ensure that the entry is a directory - if module_path.is_dir() { - if let Some(module_id) = + if module_path.is_dir() && + let Some(module_id) = module_path.file_name().and_then(|name| name.to_str()) - { - let module_id = ModuleId(module_id.to_string()); - - // Paths to "bls" and "ecdsa" directories - let bls_path = module_path.join("bls"); - let ecdsa_path = module_path.join("ecdsa"); - - // Read "bls" directory files - if bls_path.is_dir() { - for entry in std::fs::read_dir(bls_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - let file_content = read_to_string(&path)?; - let key_and_delegation: KeyAndDelegation = - serde_json::from_str(&file_content)?; - let signer = - BlsSigner::new_from_bytes(&key_and_delegation.secret)?; - let pubkey = signer.pubkey(); - let proxy_signer = BlsProxySigner { - signer, - delegation: key_and_delegation.delegation, - }; - - proxy_signers - .bls_signers - .insert(pubkey.clone(), proxy_signer); - bls_map.entry(module_id.clone()).or_default().push(pubkey); - } + { + let module_id = ModuleId(module_id.to_string()); + + // Paths to "bls" and "ecdsa" directories + let bls_path = module_path.join("bls"); + let ecdsa_path = module_path.join("ecdsa"); + + // Read "bls" directory files + if bls_path.is_dir() { + for entry in std::fs::read_dir(bls_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let file_content = read_to_string(&path)?; + let key_and_delegation: KeyAndDelegation = + serde_json::from_str(&file_content)?; + let signer = + BlsSigner::new_from_bytes(&key_and_delegation.secret)?; + let pubkey = signer.pubkey(); + let proxy_signer = BlsProxySigner { + signer, + delegation: key_and_delegation.delegation, + }; + + proxy_signers.bls_signers.insert(pubkey.clone(), proxy_signer); + bls_map.entry(module_id.clone()).or_default().push(pubkey); } } + } - // Read "ecdsa" directory files - if ecdsa_path.is_dir() { - for entry in std::fs::read_dir(ecdsa_path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - let file_content = read_to_string(&path)?; - let key_and_delegation: KeyAndDelegation
= - serde_json::from_str(&file_content)?; - let signer = EcdsaSigner::new_from_bytes( - &key_and_delegation.secret, - )?; - let pubkey = signer.address(); - let proxy_signer = EcdsaProxySigner { - signer, - delegation: key_and_delegation.delegation, - }; - - proxy_signers.ecdsa_signers.insert(pubkey, proxy_signer); - ecdsa_map - .entry(module_id.clone()) - .or_default() - .push(pubkey); - } + // Read "ecdsa" directory files + if ecdsa_path.is_dir() { + for entry in std::fs::read_dir(ecdsa_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let file_content = read_to_string(&path)?; + let key_and_delegation: KeyAndDelegation
= + serde_json::from_str(&file_content)?; + let signer = + EcdsaSigner::new_from_bytes(&key_and_delegation.secret)?; + let pubkey = signer.address(); + let proxy_signer = EcdsaProxySigner { + signer, + delegation: key_and_delegation.delegation, + }; + + proxy_signers.ecdsa_signers.insert(pubkey, proxy_signer); + ecdsa_map.entry(module_id.clone()).or_default().push(pubkey); } } } @@ -335,21 +327,27 @@ impl ProxyStore { let Ok(delegation_signature) = std::fs::read_to_string(&delegation_signature_path) else { - warn!("Failed to read delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to read delegation signature: {delegation_signature_path:?}" + ); continue; }; let Ok(delegation_signature) = alloy::primitives::hex::decode(delegation_signature) else { - warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to parse delegation signature: {delegation_signature_path:?}" + ); continue; }; let Ok(delegation_signature) = BlsSignature::deserialize(&delegation_signature) else { - warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to parse delegation signature: {delegation_signature_path:?}" + ); continue; }; @@ -402,21 +400,27 @@ impl ProxyStore { let Ok(delegation_signature) = std::fs::read_to_string(&delegation_signature_path) else { - warn!("Failed to read delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to read delegation signature: {delegation_signature_path:?}" + ); continue; }; let Ok(delegation_signature) = alloy::primitives::hex::decode(delegation_signature) else { - warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to parse delegation signature: {delegation_signature_path:?}" + ); continue; }; let Ok(delegation_signature) = BlsSignature::deserialize(&delegation_signature) else { - warn!("Failed to parse delegation signature: {delegation_signature_path:?}"); + warn!( + "Failed to parse delegation signature: {delegation_signature_path:?}" + ); continue; }; @@ -473,7 +477,7 @@ fn store_erc2335_key( .join(&module_id.0) .join(scheme.to_string()); std::fs::create_dir_all(&sig_path)?; - let sig_path = sig_path.join(format!("{}.sig", proxy_delegation)); + let sig_path = sig_path.join(format!("{proxy_delegation}.sig")); let mut sig_file = std::fs::File::create(sig_path)?; sig_file.write_all(delegation.signature.to_string().as_bytes())?; @@ -517,7 +521,7 @@ fn store_erc2335_key( .join(&module_id.0) .join(scheme.to_string()); std::fs::create_dir_all(&json_path)?; - let json_path = json_path.join(format!("{}.json", proxy_delegation)); + let json_path = json_path.join(format!("{proxy_delegation}.json")); let mut json_file = std::fs::File::create(&json_path)?; json_file.write_all(serde_json::to_string(&keystore)?.as_bytes())?; @@ -613,8 +617,12 @@ mod test { assert_eq!(proxy_signers.bls_signers.len(), 1); assert_eq!(proxy_signers.ecdsa_signers.len(), 0); - let proxy_key = bls_pubkey_from_hex_unchecked("a77084280678d9f1efe4ef47a3d62af27872ce82db19a35ee012c4fd5478e6b1123b8869032ba18b2383e8873294f0ba"); - let consensus_key = bls_pubkey_from_hex_unchecked("ac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118"); + let proxy_key = bls_pubkey_from_hex_unchecked( + "a77084280678d9f1efe4ef47a3d62af27872ce82db19a35ee012c4fd5478e6b1123b8869032ba18b2383e8873294f0ba", + ); + let consensus_key = bls_pubkey_from_hex_unchecked( + "ac5e059177afc33263e95d0be0690138b9a1d79a6e19018086a0362e0c30a50bf9e05a08cb44785724d0b2718c5c7118", + ); let proxy_signer = proxy_signers.bls_signers.get(&proxy_key); @@ -641,9 +649,11 @@ mod test { assert_eq!(proxy_signer.delegation.message.delegator, consensus_key); assert_eq!(proxy_signer.delegation.message.proxy, proxy_key); - assert!(bls_keys - .get(&ModuleId("TEST_MODULE".into())) - .is_some_and(|keys| keys.contains(&proxy_key))); + assert!( + bls_keys + .get(&ModuleId("TEST_MODULE".into())) + .is_some_and(|keys| keys.contains(&proxy_key)) + ); } #[tokio::test] @@ -712,8 +722,10 @@ mod test { assert_eq!(loaded_proxy_signer.delegation.message.delegator, consensus_signer.pubkey()); assert_eq!(loaded_proxy_signer.delegation.message.proxy, proxy_signer.pubkey()); - assert!(bls_keys - .get(&ModuleId("TEST_MODULE".into())) - .is_some_and(|keys| keys.contains(&proxy_signer.pubkey()))); + assert!( + bls_keys + .get(&ModuleId("TEST_MODULE".into())) + .is_some_and(|keys| keys.contains(&proxy_signer.pubkey())) + ); } } diff --git a/crates/common/src/signer/types.rs b/crates/common/src/signer/types.rs index 60878c84..e3e21487 100644 --- a/crates/common/src/signer/types.rs +++ b/crates/common/src/signer/types.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use alloy::primitives::{Address, Bytes}; -use base64::{prelude::BASE64_STANDARD, Engine}; +use base64::{Engine, prelude::BASE64_STANDARD}; use derive_more::derive::Deref; use serde::{ - de::{Error as DeError, Unexpected}, Deserialize, Deserializer, + de::{Error as DeError, Unexpected}, }; use super::EcdsaSigner; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 9b845c61..13c6b501 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use alloy::primitives::{aliases::B32, b256, hex, Bytes, B256, U256}; +use alloy::primitives::{B256, Bytes, U256, aliases::B32, b256, hex}; use derive_more::{Deref, Display, From, Into}; -use eyre::{bail, Context}; +use eyre::{Context, bail}; use serde::{Deserialize, Serialize}; use tree_hash_derive::TreeHash; diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 35f68959..91c3b11a 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -7,23 +7,23 @@ use std::{ use alloy::{ hex, - primitives::{keccak256, U256}, + primitives::{U256, keccak256}, }; use axum::http::HeaderValue; use futures::StreamExt; use lh_types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; -use rand::{distr::Alphanumeric, Rng}; -use reqwest::{header::HeaderMap, Response}; -use serde::{de::DeserializeOwned, Serialize}; +use rand::{Rng, distr::Alphanumeric}; +use reqwest::{Response, header::HeaderMap}; +use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use ssz::{Decode, Encode}; use thiserror::Error; use tracing::Level; use tracing_appender::{non_blocking::WorkerGuard, rolling::Rotation}; use tracing_subscriber::{ - fmt::{format::Format, Layer}, - prelude::*, EnvFilter, + fmt::{Layer, format::Format}, + prelude::*, }; use crate::{ @@ -81,14 +81,14 @@ pub async fn read_chunked_body_with_max( } // Break if content length is provided but it's too big - if let Some(length) = content_length { - if length as usize > max_size { - return Err(ResponseReadError::PayloadTooLarge { - max: max_size, - content_length: length as usize, - raw: String::new(), // raw content is not available here - }); - } + if let Some(length) = content_length && + length as usize > max_size + { + return Err(ResponseReadError::PayloadTooLarge { + max: max_size, + content_length: length as usize, + raw: String::new(), // raw content is not available here + }); } let mut stream = res.bytes_stream(); @@ -176,8 +176,8 @@ pub fn test_encode_decode_ssz(d: &[u8]) -> T { pub mod as_eth_str { use alloy::primitives::{ - utils::{format_ether, parse_ether}, U256, + utils::{format_ether, parse_ether}, }; use serde::Deserialize; @@ -484,12 +484,12 @@ pub fn get_user_agent(req_headers: &HeaderMap) -> String { /// Adds the commit boost version to the existing user agent pub fn get_user_agent_with_version(req_headers: &HeaderMap) -> eyre::Result { let ua = get_user_agent(req_headers); - Ok(HeaderValue::from_str(&format!("commit-boost/{HEADER_VERSION_VALUE} {}", ua))?) + Ok(HeaderValue::from_str(&format!("commit-boost/{HEADER_VERSION_VALUE} {ua}"))?) } #[cfg(unix)] pub async fn wait_for_signal() -> eyre::Result<()> { - use tokio::signal::unix::{signal, SignalKind}; + use tokio::signal::unix::{SignalKind, signal}; let mut sigint = signal(SignalKind::interrupt())?; let mut sigterm = signal(SignalKind::terminate())?; diff --git a/crates/metrics/src/provider.rs b/crates/metrics/src/provider.rs index bb0ddebe..4751b8ac 100644 --- a/crates/metrics/src/provider.rs +++ b/crates/metrics/src/provider.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use axum::{ body::Body, extract::State, - http::{header::CONTENT_TYPE, StatusCode}, + http::{StatusCode, header::CONTENT_TYPE}, response::{IntoResponse, Response}, routing::get, }; diff --git a/crates/pbs/Cargo.toml b/crates/pbs/Cargo.toml index 4cc3d2b2..d3aaace8 100644 --- a/crates/pbs/Cargo.toml +++ b/crates/pbs/Cargo.toml @@ -20,6 +20,7 @@ prometheus.workspace = true reqwest.workspace = true serde_json.workspace = true tokio.workspace = true +tower-http.workspace = true tracing.workspace = true tree_hash.workspace = true url.workspace = true diff --git a/crates/pbs/src/api.rs b/crates/pbs/src/api.rs index 1f6e31d8..c388ddf1 100644 --- a/crates/pbs/src/api.rs +++ b/crates/pbs/src/api.rs @@ -1,6 +1,6 @@ use alloy::rpc::types::beacon::relay::ValidatorRegistration; use async_trait::async_trait; -use axum::{http::HeaderMap, Router}; +use axum::{Router, http::HeaderMap}; use cb_common::pbs::{ BuilderApiVersion, GetHeaderParams, GetHeaderResponse, SignedBlindedBeaconBlock, SubmitBlindedBlockResponse, diff --git a/crates/pbs/src/metrics.rs b/crates/pbs/src/metrics.rs index 814d00a1..1f91e47f 100644 --- a/crates/pbs/src/metrics.rs +++ b/crates/pbs/src/metrics.rs @@ -5,8 +5,8 @@ use lazy_static::lazy_static; use prometheus::{ - register_histogram_vec_with_registry, register_int_counter_vec_with_registry, - register_int_gauge_vec_with_registry, HistogramVec, IntCounterVec, IntGaugeVec, Registry, + HistogramVec, IntCounterVec, IntGaugeVec, Registry, register_histogram_vec_with_registry, + register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry, }; lazy_static! { diff --git a/crates/pbs/src/mev_boost/get_header.rs b/crates/pbs/src/mev_boost/get_header.rs index 22647621..3dc8ff10 100644 --- a/crates/pbs/src/mev_boost/get_header.rs +++ b/crates/pbs/src/mev_boost/get_header.rs @@ -4,7 +4,7 @@ use std::{ }; use alloy::{ - primitives::{aliases::B32, utils::format_ether, B256, U256}, + primitives::{B256, U256, aliases::B32, utils::format_ether}, providers::Provider, rpc::types::Block, }; @@ -12,9 +12,9 @@ use axum::http::{HeaderMap, HeaderValue}; use cb_common::{ constants::APPLICATION_BUILDER_DOMAIN, pbs::{ + EMPTY_TX_ROOT_HASH, GetHeaderParams, GetHeaderResponse, HEADER_START_TIME_UNIX_MS, + HEADER_TIMEOUT_MS, RelayClient, VersionedResponse, error::{PbsError, ValidationError}, - GetHeaderParams, GetHeaderResponse, RelayClient, VersionedResponse, EMPTY_TX_ROOT_HASH, - HEADER_START_TIME_UNIX_MS, HEADER_TIMEOUT_MS, }, signature::verify_signed_message, types::{BlsPublicKey, BlsSignature, Chain}, @@ -25,9 +25,9 @@ use cb_common::{ }; use futures::future::join_all; use parking_lot::RwLock; -use reqwest::{header::USER_AGENT, StatusCode}; +use reqwest::{StatusCode, header::USER_AGENT}; use tokio::time::sleep; -use tracing::{debug, error, warn, Instrument}; +use tracing::{Instrument, debug, error, warn}; use tree_hash::TreeHash; use url::Url; @@ -49,13 +49,12 @@ pub async fn get_header( state: PbsState, ) -> eyre::Result> { let parent_block = Arc::new(RwLock::new(None)); - if state.extra_validation_enabled() { - if let Some(rpc_url) = state.pbs_config().rpc_url.clone() { - tokio::spawn( - fetch_parent_block(rpc_url, params.parent_hash, parent_block.clone()) - .in_current_span(), - ); - } + if state.extra_validation_enabled() && + let Some(rpc_url) = state.pbs_config().rpc_url.clone() + { + tokio::spawn( + fetch_parent_block(rpc_url, params.parent_hash, parent_block.clone()).in_current_span(), + ); } let ms_into_slot = ms_into_slot(params.slot, state.config.chain); @@ -526,10 +525,10 @@ fn extra_validation( mod tests { use alloy::primitives::{B256, U256}; use cb_common::{ - pbs::{error::ValidationError, EMPTY_TX_ROOT_HASH}, + pbs::{EMPTY_TX_ROOT_HASH, error::ValidationError}, signature::sign_builder_message, types::{BlsSecretKey, Chain}, - utils::{timestamp_of_slot_start_sec, TestRandomSeed}, + utils::{TestRandomSeed, timestamp_of_slot_start_sec}, }; use super::{validate_header_data, *}; diff --git a/crates/pbs/src/mev_boost/register_validator.rs b/crates/pbs/src/mev_boost/register_validator.rs index 5d2b5f1e..eef2f398 100644 --- a/crates/pbs/src/mev_boost/register_validator.rs +++ b/crates/pbs/src/mev_boost/register_validator.rs @@ -3,13 +3,13 @@ use std::time::{Duration, Instant}; use alloy::rpc::types::beacon::relay::ValidatorRegistration; use axum::http::{HeaderMap, HeaderValue}; use cb_common::{ - pbs::{error::PbsError, RelayClient, HEADER_START_TIME_UNIX_MS}, + pbs::{HEADER_START_TIME_UNIX_MS, RelayClient, error::PbsError}, utils::{get_user_agent_with_version, read_chunked_body_with_max, utcnow_ms}, }; use eyre::bail; use futures::future::{join_all, select_ok}; use reqwest::header::USER_AGENT; -use tracing::{debug, error, Instrument}; +use tracing::{Instrument, debug, error}; use url::Url; use crate::{ diff --git a/crates/pbs/src/mev_boost/status.rs b/crates/pbs/src/mev_boost/status.rs index b1a82e57..c4a8cfed 100644 --- a/crates/pbs/src/mev_boost/status.rs +++ b/crates/pbs/src/mev_boost/status.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; use axum::http::HeaderMap; use cb_common::{ - pbs::{error::PbsError, RelayClient}, + pbs::{RelayClient, error::PbsError}, utils::{get_user_agent_with_version, read_chunked_body_with_max}, }; use futures::future::select_ok; diff --git a/crates/pbs/src/mev_boost/submit_block.rs b/crates/pbs/src/mev_boost/submit_block.rs index ac633f2c..0336bde8 100644 --- a/crates/pbs/src/mev_boost/submit_block.rs +++ b/crates/pbs/src/mev_boost/submit_block.rs @@ -3,10 +3,10 @@ use std::time::{Duration, Instant}; use axum::http::{HeaderMap, HeaderValue}; use cb_common::{ pbs::{ + BlindedBeaconBlock, BlindedBeaconBlockElectra, BuilderApiVersion, + HEADER_START_TIME_UNIX_MS, PayloadAndBlobsElectra, RelayClient, SignedBlindedBeaconBlock, + SubmitBlindedBlockResponse, VersionedResponse, error::{PbsError, ValidationError}, - BlindedBeaconBlock, BlindedBeaconBlockElectra, BuilderApiVersion, PayloadAndBlobsElectra, - RelayClient, SignedBlindedBeaconBlock, SubmitBlindedBlockResponse, VersionedResponse, - HEADER_START_TIME_UNIX_MS, }, utils::{get_user_agent_with_version, read_chunked_body_with_max, utcnow_ms}, }; diff --git a/crates/pbs/src/routes/register_validator.rs b/crates/pbs/src/routes/register_validator.rs index acb0168a..8ccbfec8 100644 --- a/crates/pbs/src/routes/register_validator.rs +++ b/crates/pbs/src/routes/register_validator.rs @@ -1,5 +1,5 @@ use alloy::rpc::types::beacon::relay::ValidatorRegistration; -use axum::{extract::State, http::HeaderMap, response::IntoResponse, Json}; +use axum::{Json, extract::State, http::HeaderMap, response::IntoResponse}; use cb_common::utils::get_user_agent; use reqwest::StatusCode; use tracing::{error, info, trace}; diff --git a/crates/pbs/src/routes/reload.rs b/crates/pbs/src/routes/reload.rs index 5ba33329..86c09273 100644 --- a/crates/pbs/src/routes/reload.rs +++ b/crates/pbs/src/routes/reload.rs @@ -4,10 +4,10 @@ use reqwest::StatusCode; use tracing::{error, info}; use crate::{ + BuilderApi, RELOAD_ENDPOINT_TAG, error::PbsClientError, metrics::BEACON_NODE_STATUS, state::{BuilderApiState, PbsStateGuard}, - BuilderApi, RELOAD_ENDPOINT_TAG, }; pub async fn handle_reload>( diff --git a/crates/pbs/src/routes/router.rs b/crates/pbs/src/routes/router.rs index aafcd8db..e98c89c1 100644 --- a/crates/pbs/src/routes/router.rs +++ b/crates/pbs/src/routes/router.rs @@ -1,17 +1,18 @@ use axum::{ + Router, body::HttpBody, extract::{DefaultBodyLimit, MatchedPath, Request}, middleware::{self, Next}, response::Response, routing::{get, post}, - Router, }; use axum_extra::headers::{ContentType, HeaderMapExt, UserAgent}; use cb_common::pbs::{ BUILDER_V1_API_PATH, BUILDER_V2_API_PATH, GET_HEADER_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, RELOAD_PATH, SUBMIT_BLOCK_PATH, }; -use tracing::{trace, warn}; +use tower_http::trace::TraceLayer; +use tracing::{info, trace, warn}; use uuid::Uuid; use super::{ @@ -19,15 +20,15 @@ use super::{ reload::handle_reload, }; use crate::{ + MAX_SIZE_REGISTER_VALIDATOR_REQUEST, MAX_SIZE_SUBMIT_BLOCK_RESPONSE, api::BuilderApi, routes::submit_block::handle_submit_block_v2, state::{BuilderApiState, PbsStateGuard}, - MAX_SIZE_REGISTER_VALIDATOR_REQUEST, MAX_SIZE_SUBMIT_BLOCK_RESPONSE, }; pub fn create_app_router>(state: PbsStateGuard) -> Router { // DefaultBodyLimit is 2Mib by default, so we only increase it for a few routes - // thay may need more + // that may need more let v1_builder_routes = Router::new() .route(GET_HEADER_PATH, get(handle_get_header::)) @@ -51,7 +52,13 @@ pub fn create_app_router>(state: PbsStateGu let v2_builder_router = Router::new().nest(BUILDER_V2_API_PATH, v2_builder_routes); let reload_router = Router::new().route(RELOAD_PATH, post(handle_reload::)); let builder_api = - Router::new().merge(v1_builder_router).merge(v2_builder_router).merge(reload_router); + Router::new().merge(v1_builder_router).merge(v2_builder_router).merge(reload_router).layer( + TraceLayer::new_for_http().on_response( + |response: &Response, latency: std::time::Duration, _: &tracing::Span| { + info!("Responded with {} in {} ms", response.status(), latency.as_millis()); + }, + ), + ); let app = if let Some(extra_routes) = A::extra_routes() { builder_api.merge(extra_routes) diff --git a/crates/pbs/src/routes/submit_block.rs b/crates/pbs/src/routes/submit_block.rs index a3590c6c..27d2c798 100644 --- a/crates/pbs/src/routes/submit_block.rs +++ b/crates/pbs/src/routes/submit_block.rs @@ -1,4 +1,4 @@ -use axum::{extract::State, http::HeaderMap, response::IntoResponse, Json}; +use axum::{Json, extract::State, http::HeaderMap, response::IntoResponse}; use cb_common::{ pbs::{BuilderApiVersion, SignedBlindedBeaconBlock}, utils::{get_user_agent, timestamp_of_slot_start_millis, utcnow_ms}, diff --git a/crates/pbs/src/service.rs b/crates/pbs/src/service.rs index 6dc2c92d..16afb6a7 100644 --- a/crates/pbs/src/service.rs +++ b/crates/pbs/src/service.rs @@ -6,7 +6,7 @@ use cb_common::{ types::Chain, }; use cb_metrics::provider::MetricsProvider; -use eyre::{bail, Context, Result}; +use eyre::{Context, Result, bail}; use parking_lot::RwLock; use prometheus::core::Collector; use tokio::net::TcpListener; @@ -38,7 +38,7 @@ impl PbsService { // wait for the server to start tokio::time::sleep(Duration::from_millis(250)).await; let local_url = - Url::parse(&format!("http://{}{}{}", addr, BUILDER_V1_API_PATH, GET_STATUS_PATH))?; + Url::parse(&format!("http://{addr}{BUILDER_V1_API_PATH}{GET_STATUS_PATH}"))?; let status = reqwest::get(local_url).await?; if !status.status().is_success() { diff --git a/crates/signer/build.rs b/crates/signer/build.rs index 596353ea..ad850a80 100644 --- a/crates/signer/build.rs +++ b/crates/signer/build.rs @@ -1,5 +1,7 @@ fn main() -> Result<(), Box> { - std::env::set_var("OUT_DIR", "src/proto"); + unsafe { + std::env::set_var("OUT_DIR", "src/proto"); + } tonic_build::configure().build_server(false).compile_protos( &[ "proto/pb/v1/lister.proto", diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index 526abf49..b395f4c3 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, io::Write, path::PathBuf}; use alloy::{ hex, - primitives::{aliases::B32, B256}, + primitives::{B256, aliases::B32}, }; use blsful::inner_types::{Field, G2Affine, G2Projective, Group, Scalar}; use cb_common::{ @@ -13,8 +13,8 @@ use cb_common::{ signer::ProxyStore, types::{self, BlsPublicKey, BlsSignature, Chain, ModuleId, SignatureRequestInfo}, }; -use eyre::{bail, OptionExt}; -use futures::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt}; +use eyre::{OptionExt, bail}; +use futures::{FutureExt, StreamExt, future::join_all, stream::FuturesUnordered}; use rand::Rng; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tracing::{debug, error, warn}; @@ -23,9 +23,9 @@ use tree_hash::TreeHash; use crate::{ error::SignerModuleError, proto::v1::{ - account_manager_client::AccountManagerClient, lister_client::ListerClient, sign_request, - signer_client::SignerClient, Endpoint, GenerateRequest, ListAccountsRequest, ResponseState, - SignRequest, UnlockAccountRequest, + Endpoint, GenerateRequest, ListAccountsRequest, ResponseState, SignRequest, + UnlockAccountRequest, account_manager_client::AccountManagerClient, + lister_client::ListerClient, sign_request, signer_client::SignerClient, }, }; diff --git a/crates/signer/src/metrics.rs b/crates/signer/src/metrics.rs index f7711d69..4110ec72 100644 --- a/crates/signer/src/metrics.rs +++ b/crates/signer/src/metrics.rs @@ -6,7 +6,7 @@ use cb_common::commit::constants::{ REQUEST_SIGNATURE_PROXY_BLS_PATH, REQUEST_SIGNATURE_PROXY_ECDSA_PATH, }; use lazy_static::lazy_static; -use prometheus::{register_int_counter_vec_with_registry, IntCounterVec, Registry}; +use prometheus::{IntCounterVec, Registry, register_int_counter_vec_with_registry}; use crate::constants::{ GENERATE_PROXY_KEY_ENDPOINT_TAG, GET_PUBKEYS_ENDPOINT_TAG, REQUEST_SIGNATURE_BLS_ENDPOINT_TAG, diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 2d510eeb..eb284289 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -5,15 +5,15 @@ use std::{ time::{Duration, Instant}, }; -use alloy::primitives::{keccak256, Address, B256, U256}; +use alloy::primitives::{Address, B256, U256, keccak256}; use axum::{ - body::{to_bytes, Body}, + Extension, Json, + body::{Body, to_bytes}, extract::{ConnectInfo, Request, State}, http::StatusCode, middleware::{self, Next}, response::{IntoResponse, Response}, routing::{get, post}, - Extension, Json, }; use axum_extra::TypedHeader; use axum_server::tls_rustls::RustlsConfig; @@ -37,17 +37,17 @@ use cb_common::{ }; use cb_metrics::provider::MetricsProvider; use eyre::Context; -use headers::{authorization::Bearer, Authorization}; +use headers::{Authorization, authorization::Bearer}; use parking_lot::RwLock as ParkingRwLock; -use rustls::crypto::{aws_lc_rs, CryptoProvider}; +use rustls::crypto::{CryptoProvider, aws_lc_rs}; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; use uuid::Uuid; use crate::{ error::SignerModuleError, - manager::{dirk::DirkManager, local::LocalSigningManager, SigningManager}, - metrics::{uri_to_tag, SIGNER_METRICS_REGISTRY, SIGNER_STATUS}, + manager::{SigningManager, dirk::DirkManager, local::LocalSigningManager}, + metrics::{SIGNER_METRICS_REGISTRY, SIGNER_STATUS, uri_to_tag}, }; pub const REQUEST_MAX_BODY_LENGTH: usize = 1024 * 1024; // 1 MB @@ -239,7 +239,9 @@ fn check_jwt_rate_limit(state: &SigningState, client_ip: &IpAddr) -> Result<(), // Rate limit the request let remaining = state.jwt_auth_fail_timeout.saturating_sub(elapsed); - warn!("Client {client_ip} is rate limited for {remaining:?} more seconds due to JWT auth failures"); + warn!( + "Client {client_ip} is rate limited for {remaining:?} more seconds due to JWT auth failures" + ); return Err(SignerModuleError::RateLimited(remaining.as_secs_f64())); } @@ -374,12 +376,12 @@ async fn handle_request_signature_bls( let req_id = Uuid::new_v4(); debug!(event = "bls_request_signature", ?module_id, %request, ?req_id, "New request"); handle_request_signature_bls_impl( - &module_id, - &state, - &req_id, + module_id, + state, + req_id, false, - &request.pubkey, - &request.object_root, + request.pubkey, + request.object_root, request.nonce, ) .await @@ -395,12 +397,12 @@ async fn handle_request_signature_proxy_bls( let req_id = Uuid::new_v4(); debug!(event = "proxy_bls_request_signature", ?module_id, %request, ?req_id, "New request"); handle_request_signature_bls_impl( - &module_id, - &state, - &req_id, + module_id, + state, + req_id, true, - &request.proxy, - &request.object_root, + request.proxy, + request.object_root, request.nonce, ) .await @@ -408,15 +410,15 @@ async fn handle_request_signature_proxy_bls( /// Implementation for handling a BLS signature request async fn handle_request_signature_bls_impl( - module_id: &ModuleId, - state: &SigningState, - req_id: &Uuid, + module_id: ModuleId, + state: SigningState, + req_id: Uuid, is_proxy: bool, - signing_pubkey: &BlsPublicKey, - object_root: &B256, + signing_pubkey: BlsPublicKey, + object_root: B256, nonce: u64, ) -> Result { - let Some(signing_id) = state.jwts.read().get(module_id).map(|m| m.signing_id) else { + let Some(signing_id) = state.jwts.read().get(&module_id).map(|m| m.signing_id) else { error!( event = "proxy_bls_request_signature", ?module_id, @@ -433,16 +435,16 @@ async fn handle_request_signature_bls_impl( if is_proxy { local_manager .sign_proxy_bls( - signing_pubkey, - object_root, + &signing_pubkey, + &object_root, Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await } else { local_manager .sign_consensus( - signing_pubkey, - object_root, + &signing_pubkey, + &object_root, Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await @@ -453,16 +455,16 @@ async fn handle_request_signature_bls_impl( if is_proxy { dirk_manager .request_proxy_signature( - signing_pubkey, - object_root, + &signing_pubkey, + &object_root, Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await } else { dirk_manager .request_consensus_signature( - signing_pubkey, - object_root, + &signing_pubkey, + &object_root, Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await @@ -472,7 +474,7 @@ async fn handle_request_signature_bls_impl( .map(|sig| { Json(BlsSignResponse::new( signing_pubkey.clone(), - *object_root, + object_root, signing_id, nonce, chain_id, @@ -589,7 +591,7 @@ async fn handle_generate_proxy( } async fn handle_reload( - State(mut state): State, + State(state): State, Json(request): Json, ) -> Result { let req_id = Uuid::new_v4(); @@ -637,7 +639,11 @@ async fn handle_reload( } }; - state.manager = Arc::new(RwLock::new(new_manager)); + // Replace the contents of the manager RwLock + { + let mut manager_guard = state.manager.write().await; + *manager_guard = new_manager; + } Ok(StatusCode::OK) } diff --git a/docs/docs/get_started/building.md b/docs/docs/get_started/building.md index 589b325c..3a0a964a 100644 --- a/docs/docs/get_started/building.md +++ b/docs/docs/get_started/building.md @@ -2,7 +2,6 @@ Commit-Boost's components are all written in [Rust](https://www.rust-lang.org/). This guide will walk you through the setup required to build them from source. It assumes you are on a Debian or Debian-based system (e.g., Ubuntu, Linux Mint, Pop OS). For other systems, please adapt the steps for your system's package manager accordingly. - ## Building via the Docker Builder For convenience, Commit-Boost has Dockerized the build environment for Linux `x64` and `arm64` platforms. It utilizes Docker's powerful [buildx](https://docs.docker.com/reference/cli/docker/buildx/) system. All of the prerequisites, cross-compilation tooling, and configuration are handled by the builder image. If you would like to build the CLI, PBS module, or Signer binaries and Docker images from source, you are welcome to use the Docker builder process. @@ -27,17 +26,15 @@ The `version` provided will be used to house the output binaries in `./build/ /dev/null 2>&1 && \ diff --git a/provisioning/build.Dockerfile b/provisioning/build.Dockerfile index 43713cc5..361150e7 100644 --- a/provisioning/build.Dockerfile +++ b/provisioning/build.Dockerfile @@ -1,5 +1,5 @@ # This will be the main build image -FROM --platform=${BUILDPLATFORM} rust:1.83-slim-bookworm AS chef +FROM --platform=${BUILDPLATFORM} rust:1.89-slim-bookworm AS chef ARG TARGETOS TARGETARCH BUILDPLATFORM TARGET_CRATE ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse WORKDIR /app diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0193dee3..b67e7d53 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.83.0" +channel = "1.89.0" diff --git a/tests/src/mock_relay.rs b/tests/src/mock_relay.rs index 8794df96..ab20b4de 100644 --- a/tests/src/mock_relay.rs +++ b/tests/src/mock_relay.rs @@ -1,26 +1,26 @@ use std::{ net::SocketAddr, sync::{ - atomic::{AtomicU64, Ordering}, Arc, RwLock, + atomic::{AtomicU64, Ordering}, }, }; use alloy::{primitives::U256, rpc::types::beacon::relay::ValidatorRegistration}; use axum::{ + Json, Router, extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, - Json, Router, }; use cb_common::{ pbs::{ - BlindedBeaconBlock, ExecutionPayloadHeaderMessageElectra, ExecutionRequests, - GetHeaderParams, GetHeaderResponse, KzgProof, SignedBlindedBeaconBlock, - SignedExecutionPayloadHeader, SubmitBlindedBlockResponse, VersionedResponse, - BUILDER_V1_API_PATH, BUILDER_V2_API_PATH, GET_HEADER_PATH, GET_STATUS_PATH, - REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH, + BUILDER_V1_API_PATH, BUILDER_V2_API_PATH, BlindedBeaconBlock, + ExecutionPayloadHeaderMessageElectra, ExecutionRequests, GET_HEADER_PATH, GET_STATUS_PATH, + GetHeaderParams, GetHeaderResponse, KzgProof, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH, + SignedBlindedBeaconBlock, SignedExecutionPayloadHeader, SubmitBlindedBlockResponse, + VersionedResponse, }, signature::sign_builder_root, types::{BlsSecretKey, Chain}, diff --git a/tests/src/mock_validator.rs b/tests/src/mock_validator.rs index 3a2f68c0..ab593277 100644 --- a/tests/src/mock_validator.rs +++ b/tests/src/mock_validator.rs @@ -14,12 +14,16 @@ pub struct MockValidator { impl MockValidator { pub fn new(port: u16) -> eyre::Result { - let pubkey = bls_pubkey_from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae")?; + let pubkey = bls_pubkey_from_hex( + "0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae", + )?; Ok(Self { comm_boost: generate_mock_relay(port, pubkey)? }) } pub async fn do_get_header(&self, pubkey: Option) -> eyre::Result { - let default_pubkey = bls_pubkey_from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae")?; + let default_pubkey = bls_pubkey_from_hex( + "0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae", + )?; let url = self.comm_boost.get_header_url(0, &B256::ZERO, &pubkey.unwrap_or(default_pubkey))?; Ok(self.comm_boost.client.get(url).send().await?) diff --git a/tests/src/signer_service.rs b/tests/src/signer_service.rs index 10e158fa..550ac4ce 100644 --- a/tests/src/signer_service.rs +++ b/tests/src/signer_service.rs @@ -80,14 +80,17 @@ pub async fn verify_pubkeys(response: Response) -> Result<()> { let pubkey_json = response.json::().await?; assert_eq!(pubkey_json.keys.len(), 2); let expected_pubkeys = vec![ - bls_pubkey_from_hex("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4")?, - bls_pubkey_from_hex("b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9")?, + bls_pubkey_from_hex( + "883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4", + )?, + bls_pubkey_from_hex( + "b3a22e4a673ac7a153ab5b3c17a4dbef55f7e47210b20c0cbb0e66df5b36bb49ef808577610b034172e955d2312a61b9", + )?, ]; for expected in expected_pubkeys { assert!( pubkey_json.keys.iter().any(|k| k.consensus == expected), - "Expected pubkey not found: {:?}", - expected + "Expected pubkey not found: {expected}" ); info!("Server returned expected pubkey: {:?}", expected); } diff --git a/tests/src/utils.rs b/tests/src/utils.rs index 55be8aa3..c66bfed6 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -9,10 +9,9 @@ use alloy::primitives::{B256, U256}; use cb_common::{ config::{ CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, - PbsModuleConfig, RelayConfig, SignerConfig, SignerType, StartSignerConfig, - StaticModuleConfig, StaticPbsConfig, TlsMode, SIGNER_IMAGE_DEFAULT, - SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, - SIGNER_PORT_DEFAULT, + PbsModuleConfig, RelayConfig, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, + SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_PORT_DEFAULT, SignerConfig, + SignerType, StartSignerConfig, StaticModuleConfig, StaticPbsConfig, TlsMode, }, pbs::{RelayClient, RelayEntry}, signer::SignerLoader, diff --git a/tests/tests/config.rs b/tests/tests/config.rs index b5f6dba1..2cf9f92d 100644 --- a/tests/tests/config.rs +++ b/tests/tests/config.rs @@ -80,10 +80,9 @@ async fn test_validate_bad_timeout_get_header_ms() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("timeout_get_header_ms must be greater than 0")); + assert!( + result.unwrap_err().to_string().contains("timeout_get_header_ms must be greater than 0") + ); Ok(()) } @@ -95,10 +94,9 @@ async fn test_validate_bad_timeout_get_payload_ms() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("timeout_get_payload_ms must be greater than 0")); + assert!( + result.unwrap_err().to_string().contains("timeout_get_payload_ms must be greater than 0") + ); Ok(()) } @@ -109,10 +107,12 @@ async fn test_validate_bad_timeout_register_validator_ms() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("timeout_register_validator_ms must be greater than 0")); + assert!( + result + .unwrap_err() + .to_string() + .contains("timeout_register_validator_ms must be greater than 0") + ); Ok(()) } @@ -123,10 +123,9 @@ async fn test_validate_bad_late_in_slot_time_ms() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("late_in_slot_time_ms must be greater than 0")); + assert!( + result.unwrap_err().to_string().contains("late_in_slot_time_ms must be greater than 0") + ); Ok(()) } @@ -138,10 +137,12 @@ async fn test_validate_bad_timeout_header_vs_late() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("timeout_get_header_ms must be less than late_in_slot_time_ms")); + assert!( + result + .unwrap_err() + .to_string() + .contains("timeout_get_header_ms must be less than late_in_slot_time_ms") + ); Ok(()) } @@ -164,10 +165,12 @@ async fn test_validate_missing_rpc_url() -> Result<()> { let result = config.validate().await; assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("rpc_url is required if extra_validation_enabled is true")); + assert!( + result + .unwrap_err() + .to_string() + .contains("rpc_url is required if extra_validation_enabled is true") + ); Ok(()) } diff --git a/tests/tests/payloads.rs b/tests/tests/payloads.rs index c43df7ef..2a631d22 100644 --- a/tests/tests/payloads.rs +++ b/tests/tests/payloads.rs @@ -30,12 +30,11 @@ fn test_missing_registration_field(field_name: &str) -> String { let mut values: Value = serde_json::from_str(data).unwrap(); // Remove specified field from the first validator's message - if let Value::Array(arr) = &mut values { - if let Some(first_validator) = arr.get_mut(0) { - if let Some(Value::Object(msg_obj)) = first_validator.get_mut("message") { - msg_obj.remove(field_name); - } - } + if let Value::Array(arr) = &mut values && + let Some(first_validator) = arr.get_mut(0) && + let Some(Value::Object(msg_obj)) = first_validator.get_mut("message") + { + msg_obj.remove(field_name); } // This should fail since the field is required diff --git a/tests/tests/pbs_get_header.rs b/tests/tests/pbs_get_header.rs index 268c13cd..588d3e85 100644 --- a/tests/tests/pbs_get_header.rs +++ b/tests/tests/pbs_get_header.rs @@ -7,7 +7,7 @@ use cb_common::{ }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, + mock_relay::{MockRelayState, start_mock_relay_service}, mock_validator::MockValidator, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; diff --git a/tests/tests/pbs_get_status.rs b/tests/tests/pbs_get_status.rs index 9cd55f34..2406368e 100644 --- a/tests/tests/pbs_get_status.rs +++ b/tests/tests/pbs_get_status.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration}; use cb_common::{signer::random_secret, types::Chain}; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, + mock_relay::{MockRelayState, start_mock_relay_service}, mock_validator::MockValidator, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs index 434e35a4..337729ed 100644 --- a/tests/tests/pbs_mux.rs +++ b/tests/tests/pbs_mux.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use cb_common::{config::RuntimeMuxConfig, signer::random_secret, types::Chain}; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, + mock_relay::{MockRelayState, start_mock_relay_service}, mock_validator::MockValidator, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; diff --git a/tests/tests/pbs_post_blinded_blocks.rs b/tests/tests/pbs_post_blinded_blocks.rs index 3b3c663a..01fe8a81 100644 --- a/tests/tests/pbs_post_blinded_blocks.rs +++ b/tests/tests/pbs_post_blinded_blocks.rs @@ -7,8 +7,8 @@ use cb_common::{ }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, - mock_validator::{load_test_signed_blinded_block, MockValidator}, + mock_relay::{MockRelayState, start_mock_relay_service}, + mock_validator::{MockValidator, load_test_signed_blinded_block}, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; use eyre::Result; diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs index 1b4619b2..8acb9fa8 100644 --- a/tests/tests/pbs_post_validators.rs +++ b/tests/tests/pbs_post_validators.rs @@ -7,7 +7,7 @@ use cb_common::{ }; use cb_pbs::{DefaultBuilderApi, PbsService, PbsState}; use cb_tests::{ - mock_relay::{start_mock_relay_service, MockRelayState}, + mock_relay::{MockRelayState, start_mock_relay_service}, mock_validator::MockValidator, utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config}, }; diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index c18f5ea6..37561428 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -6,7 +6,7 @@ use cb_common::{ constants::{GET_PUBKEYS_PATH, REVOKE_MODULE_PATH}, request::RevokeModuleRequest, }, - config::{load_module_signing_configs, ModuleSigningConfig}, + config::{ModuleSigningConfig, load_module_signing_configs}, types::ModuleId, utils::{create_admin_jwt, create_jwt}, }; diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index c1325d17..15680587 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -6,7 +6,7 @@ use cb_common::{ constants::REQUEST_SIGNATURE_BLS_PATH, request::SignConsensusRequest, response::BlsSignResponse, }, - config::{load_module_signing_configs, ModuleSigningConfig}, + config::{ModuleSigningConfig, load_module_signing_configs}, types::{BlsPublicKey, BlsSignature, Chain, ModuleId}, utils::create_jwt, }; @@ -19,8 +19,9 @@ use reqwest::StatusCode; const MODULE_ID_1: &str = "test-module"; const MODULE_ID_2: &str = "another-module"; -const PUBKEY_1: [u8; 48] = - hex!("883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4"); +const PUBKEY_1: [u8; 48] = hex!( + "883827193f7627cd04e621e1e8d56498362a52b2a30c9a1c72036eb935c4278dee23d38a24d2f7dda62689886f0c39f4" +); const ADMIN_SECRET: &str = "test-admin-secret"; async fn create_mod_signing_configs() -> HashMap { diff --git a/tests/tests/signer_tls.rs b/tests/tests/signer_tls.rs index 787312df..4f53bb92 100644 --- a/tests/tests/signer_tls.rs +++ b/tests/tests/signer_tls.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use alloy::primitives::b256; use cb_common::{ commit::constants::GET_PUBKEYS_PATH, - config::{load_module_signing_configs, ModuleSigningConfig}, + config::{ModuleSigningConfig, load_module_signing_configs}, types::ModuleId, utils::create_jwt, }; @@ -11,7 +11,7 @@ use cb_tests::{ signer_service::{start_server, verify_pubkeys}, utils::{self, setup_test_env}, }; -use eyre::{bail, Result}; +use eyre::{Result, bail}; use reqwest::Certificate; const JWT_MODULE: &str = "test-module"; From d566deab6e46af572c6fa84c17cc0dbc29d8be6b Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 30 Sep 2025 17:09:25 -0400 Subject: [PATCH 10/30] Fix misc findings from reaudit (#376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Manuel Iñaki Bilbao --- Cargo.lock | 49 +++++++ Cargo.toml | 2 + api/signer-api.yml | 5 + config.example.toml | 3 +- crates/common/src/commit/client.rs | 36 ++--- crates/common/src/config/signer.rs | 3 +- crates/common/src/signer/store.rs | 6 +- crates/common/src/types.rs | 2 + crates/common/src/utils.rs | 71 ++++++--- crates/signer/Cargo.toml | 1 + crates/signer/src/service.rs | 192 +++++++++++++++++-------- tests/Cargo.toml | 3 + tests/tests/signer_jwt_auth.rs | 54 +++++-- tests/tests/signer_jwt_auth_cleanup.rs | 70 +++++++++ tests/tests/signer_request_sig.rs | 23 ++- tests/tests/signer_tls.rs | 2 +- 16 files changed, 397 insertions(+), 125 deletions(-) create mode 100644 tests/tests/signer_jwt_auth_cleanup.rs diff --git a/Cargo.lock b/Cargo.lock index 0dd0e763..285bbd94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1679,6 +1679,7 @@ dependencies = [ "blsful", "cb-common", "cb-metrics", + "client-ip", "eyre", "futures", "headers", @@ -1716,6 +1717,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tracing-test", "tree_hash", "url", ] @@ -1836,6 +1838,16 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "client-ip" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31211fc26899744f5b22521fdc971e5f3875991d8880537537470685a0e9552d" +dependencies = [ + "forwarded-header-value", + "http", +] + [[package]] name = "cmake" version = "0.1.54" @@ -2828,6 +2840,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.69", +] + [[package]] name = "fs-err" version = "3.1.0" @@ -3948,6 +3970,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -6193,6 +6221,27 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "tree_hash" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index dc5ee88d..b0533144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ cb-pbs = { path = "crates/pbs" } cb-signer = { path = "crates/signer" } cipher = "0.4" clap = { version = "4.5.4", features = ["derive", "env"] } +client-ip = { version = "0.1.1", features = [ "forwarded-header" ] } color-eyre = "0.6.3" const_format = "0.2.34" ctr = "0.9.2" @@ -74,6 +75,7 @@ tower-http = { version = "0.6", features = ["trace"] } tracing = "0.1.40" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } +tracing-test = { version = "0.2.5", features = ["no-env-filter"] } tree_hash = "0.9" tree_hash_derive = "0.9" typenum = "1.17.0" diff --git a/api/signer-api.yml b/api/signer-api.yml index 9e11da34..95897ecd 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -15,6 +15,7 @@ paths: The token **must include** the following claims: - `exp` (integer): Expiration timestamp + - `route` (string): The route being requested (must be `/signer/v1/get_pubkeys` for this endpoint). - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. tags: - Signer @@ -73,6 +74,7 @@ paths: The token **must include** the following claims: - `exp` (integer): Expiration timestamp - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `route` (string): The route being requested (must be `/signer/v1/request_signature/bls` for this endpoint). - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer @@ -220,6 +222,7 @@ paths: The token **must include** the following claims: - `exp` (integer): Expiration timestamp - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-bls` for this endpoint). - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer @@ -367,6 +370,7 @@ paths: The token **must include** the following claims: - `exp` (integer): Expiration timestamp - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `route` (string): The route being requested (must be `/signer/v1/request_signature/proxy-ecdsa` for this endpoint). - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer @@ -514,6 +518,7 @@ paths: The token **must include** the following claims: - `exp` (integer): Expiration timestamp - `module` (string): The ID of the module making the request, which must match a module ID in the Commit-Boost configuration file. + - `route` (string): The route being requested (must be `/signer/v1/generate_proxy_key` for this endpoint). - `payload_hash` (string): The Keccak-256 hash of the JSON-encoded request body, with optional `0x` prefix. This is required to prevent JWT replay attacks. tags: - Signer diff --git a/config.example.toml b/config.example.toml index 67085409..5b69f108 100644 --- a/config.example.toml +++ b/config.example.toml @@ -165,7 +165,8 @@ port = 20000 # Number of JWT authentication attempts a client can fail before blocking that client temporarily from Signer access # OPTIONAL, DEFAULT: 3 jwt_auth_fail_limit = 3 -# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times +# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times. +# This also defines the interval at which failed attempts are regularly checked and expired ones are cleaned up. # OPTIONAL, DEFAULT: 300 jwt_auth_fail_timeout_seconds = 300 diff --git a/crates/common/src/commit/client.rs b/crates/common/src/commit/client.rs index 1151eb6f..98d8c26d 100644 --- a/crates/common/src/commit/client.rs +++ b/crates/common/src/commit/client.rs @@ -2,10 +2,7 @@ use std::path::PathBuf; use alloy::primitives::Address; use eyre::WrapErr; -use reqwest::{ - Certificate, - header::{AUTHORIZATION, HeaderMap, HeaderValue}, -}; +use reqwest::Certificate; use serde::{Deserialize, Serialize}; use url::Url; @@ -60,30 +57,13 @@ impl SignerClient { Ok(Self { url: signer_server_url, client: builder.build()?, module_id, jwt_secret }) } - fn refresh_jwt(&mut self) -> Result<(), SignerClientError> { - let jwt = create_jwt(&self.module_id, &self.jwt_secret, None)?; - - let mut auth_value = - HeaderValue::from_str(&format!("Bearer {jwt}")).wrap_err("invalid jwt")?; - auth_value.set_sensitive(true); - - let mut headers = HeaderMap::new(); - headers.insert(AUTHORIZATION, auth_value); - - self.client = reqwest::Client::builder() - .timeout(DEFAULT_REQUEST_TIMEOUT) - .default_headers(headers) - .build()?; - - Ok(()) - } - fn create_jwt_for_payload( &mut self, + route: &str, payload: &T, ) -> Result { let payload_vec = serde_json::to_vec(payload)?; - create_jwt(&self.module_id, &self.jwt_secret, Some(&payload_vec)) + create_jwt(&self.module_id, &self.jwt_secret, route, Some(&payload_vec)) .wrap_err("failed to create JWT for payload") .map_err(SignerClientError::JWTError) } @@ -92,10 +72,12 @@ impl SignerClient { /// requested. // TODO: add more docs on how proxy keys work pub async fn get_pubkeys(&mut self) -> Result { - self.refresh_jwt()?; + let jwt = create_jwt(&self.module_id, &self.jwt_secret, GET_PUBKEYS_PATH, None) + .wrap_err("failed to create JWT for payload") + .map_err(SignerClientError::JWTError)?; let url = self.url.join(GET_PUBKEYS_PATH)?; - let res = self.client.get(url).send().await?; + let res = self.client.get(url).bearer_auth(jwt).send().await?; if !res.status().is_success() { return Err(SignerClientError::FailedRequest { @@ -117,7 +99,7 @@ impl SignerClient { Q: Serialize, T: for<'de> Deserialize<'de>, { - let jwt = self.create_jwt_for_payload(request)?; + let jwt = self.create_jwt_for_payload(route, request)?; let url = self.url.join(route)?; let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?; @@ -165,7 +147,7 @@ impl SignerClient { where T: ProxyId + for<'de> Deserialize<'de>, { - let jwt = self.create_jwt_for_payload(request)?; + let jwt = self.create_jwt_for_payload(GENERATE_PROXY_KEY_PATH, request)?; let url = self.url.join(GENERATE_PROXY_KEY_PATH)?; let res = self.client.post(url).json(&request).bearer_auth(jwt).send().await?; diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 4e040701..b4c5db16 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -88,7 +88,8 @@ pub struct SignerConfig { pub jwt_auth_fail_limit: u32, /// Duration in seconds to rate limit an endpoint after the JWT auth failure - /// limit has been reached + /// limit has been reached. This also defines the interval at which failed + /// attempts are regularly checked and expired ones are cleaned up. #[serde(default = "default_u32::")] pub jwt_auth_fail_timeout_seconds: u32, diff --git a/crates/common/src/signer/store.rs b/crates/common/src/signer/store.rs index bd4aa103..d70ea8a0 100644 --- a/crates/common/src/signer/store.rs +++ b/crates/common/src/signer/store.rs @@ -244,14 +244,14 @@ impl ProxyStore { serde_json::from_str(&file_content)?; let signer = EcdsaSigner::new_from_bytes(&key_and_delegation.secret)?; - let pubkey = signer.address(); + let address = signer.address(); let proxy_signer = EcdsaProxySigner { signer, delegation: key_and_delegation.delegation, }; - proxy_signers.ecdsa_signers.insert(pubkey, proxy_signer); - ecdsa_map.entry(module_id.clone()).or_default().push(pubkey); + proxy_signers.ecdsa_signers.insert(address, proxy_signer); + ecdsa_map.entry(module_id.clone()).or_default().push(address); } } } diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 13c6b501..9fa3b40b 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -26,6 +26,7 @@ pub struct Jwt(pub String); pub struct JwtClaims { pub exp: u64, pub module: ModuleId, + pub route: String, pub payload_hash: Option, } @@ -33,6 +34,7 @@ pub struct JwtClaims { pub struct JwtAdminClaims { pub exp: u64, pub admin: bool, + pub route: String, pub payload_hash: Option, } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 91c3b11a..bb26edb5 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -346,11 +346,17 @@ pub fn print_logo() { } /// Create a JWT for the given module id with expiration -pub fn create_jwt(module_id: &ModuleId, secret: &str, payload: Option<&[u8]>) -> eyre::Result { +pub fn create_jwt( + module_id: &ModuleId, + secret: &str, + route: &str, + payload: Option<&[u8]>, +) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtClaims { module: module_id.clone(), + route: route.to_string(), exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, payload_hash: payload.map(keccak256), }, @@ -361,11 +367,16 @@ pub fn create_jwt(module_id: &ModuleId, secret: &str, payload: Option<&[u8]>) -> } // Creates a JWT for module administration -pub fn create_admin_jwt(admin_secret: String, payload: Option<&[u8]>) -> eyre::Result { +pub fn create_admin_jwt( + admin_secret: String, + route: &str, + payload: Option<&[u8]>, +) -> eyre::Result { jsonwebtoken::encode( &jsonwebtoken::Header::default(), &JwtAdminClaims { admin: true, + route: route.to_string(), exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, payload_hash: payload.map(keccak256), }, @@ -408,7 +419,12 @@ pub fn decode_admin_jwt(jwt: Jwt) -> eyre::Result { } /// Validate a JWT with the given secret -pub fn validate_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Result<()> { +pub fn validate_jwt( + jwt: Jwt, + secret: &str, + route: &str, + payload: Option<&[u8]>, +) -> eyre::Result<()> { let mut validation = jsonwebtoken::Validation::default(); validation.leeway = 10; @@ -419,6 +435,11 @@ pub fn validate_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Res )? .claims; + // Validate the route + if claims.route != route { + eyre::bail!("Token route does not match"); + } + // Validate the payload hash if provided if let Some(payload_bytes) = payload { if let Some(expected_hash) = claims.payload_hash { @@ -436,7 +457,12 @@ pub fn validate_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Res } /// Validate an admin JWT with the given secret -pub fn validate_admin_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyre::Result<()> { +pub fn validate_admin_jwt( + jwt: Jwt, + secret: &str, + route: &str, + payload: Option<&[u8]>, +) -> eyre::Result<()> { let mut validation = jsonwebtoken::Validation::default(); validation.leeway = 10; @@ -451,6 +477,11 @@ pub fn validate_admin_jwt(jwt: Jwt, secret: &str, payload: Option<&[u8]>) -> eyr eyre::bail!("Token is not admin") } + // Validate the route + if claims.route != route { + eyre::bail!("Token route does not match"); + } + // Validate the payload hash if provided if let Some(payload_bytes) = payload { if let Some(expected_hash) = claims.payload_hash { @@ -546,24 +577,25 @@ mod test { #[test] fn test_jwt_validation_no_payload_hash() { // Check valid JWT - let jwt = create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret", None).unwrap(); + let jwt = + create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret", "/test/route", None).unwrap(); let claims = decode_jwt(jwt.clone()).unwrap(); let module_id = claims.module; let payload_hash = claims.payload_hash; assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); assert!(payload_hash.is_none()); - let response = validate_jwt(jwt, "secret", None); + let response = validate_jwt(jwt, "secret", "/test/route", None); assert!(response.is_ok()); // Check expired JWT - let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); - let response = validate_jwt(expired_jwt, "secret", None); + let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NTgyOTkxNzIsIm1vZHVsZSI6IkRBX0NPTU1JVCIsInJvdXRlIjoiL3Rlc3Qvcm91dGUiLCJwYXlsb2FkX2hhc2giOm51bGx9._OBsNC67KLkk6f6ZQ2_CDbhYUJ2OtZ9egKAmi1L-ymA".to_string()); + let response = validate_jwt(expired_jwt, "secret", "/test/route", None); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); // Check invalid signature JWT - let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); - let response = validate_jwt(invalid_jwt, "secret", None); + let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NTgyOTkxMzQsIm1vZHVsZSI6IkRBX0NPTU1JVCIsInJvdXRlIjoiL3Rlc3Qvcm91dGUiLCJwYXlsb2FkX2hhc2giOm51bGx9.58QXayg2XeX5lXhIPw-a8kl04DWBEj5wBsqsedTeClo".to_string()); + let response = validate_jwt(invalid_jwt, "secret", "/test/route", None); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } @@ -577,25 +609,30 @@ mod test { let payload_bytes = serde_json::to_vec(&payload).unwrap(); // Check valid JWT - let jwt = - create_jwt(&ModuleId("DA_COMMIT".to_string()), "secret", Some(&payload_bytes)).unwrap(); + let jwt = create_jwt( + &ModuleId("DA_COMMIT".to_string()), + "secret", + "/test/route", + Some(&payload_bytes), + ) + .unwrap(); let claims = decode_jwt(jwt.clone()).unwrap(); let module_id = claims.module; let payload_hash = claims.payload_hash; assert_eq!(module_id, ModuleId("DA_COMMIT".to_string())); assert_eq!(payload_hash, Some(keccak256(&payload_bytes))); - let response = validate_jwt(jwt, "secret", Some(&payload_bytes)); + let response = validate_jwt(jwt, "secret", "/test/route", Some(&payload_bytes)); assert!(response.is_ok()); // Check expired JWT - let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.iiq4Z2ed2hk3c3c-cn2QOQJWE5XUOc5BoaIPT-I8q-s".to_string()); - let response = validate_jwt(expired_jwt, "secret", Some(&payload_bytes)); + let expired_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NTgyOTgzNDQsIm1vZHVsZSI6IkRBX0NPTU1JVCIsInJvdXRlIjoiL3Rlc3Qvcm91dGUiLCJwYXlsb2FkX2hhc2giOiIweGFmODk2MjY0MzUzNTFmYzIwMDBkYmEwM2JiNTlhYjcyZWE0ODJiOWEwMDBmZWQzNmNkMjBlMDU0YjE2NjZmZjEifQ.PYrSxLXadKBgYZlmLam8RBSL32I1T_zAxlZpG6xnnII".to_string()); + let response = validate_jwt(expired_jwt, "secret", "/test/route", Some(&payload_bytes)); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "ExpiredSignature"); // Check invalid signature JWT - let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI5OTU5NDYsIm1vZHVsZSI6IkRBX0NPTU1JVCJ9.w9WYdDNzgDjYTvjBkk4GGzywGNBYPxnzU2uJWzPUT1s".to_string()); - let response = validate_jwt(invalid_jwt, "secret", Some(&payload_bytes)); + let invalid_jwt = Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NTgyOTkwMDAsIm1vZHVsZSI6IkRBX0NPTU1JVCIsInJvdXRlIjoiL3Rlc3Qvcm91dGUiLCJwYXlsb2FkX2hhc2giOiIweGFmODk2MjY0MzUzNTFmYzIwMDBkYmEwM2JiNTlhYjcyZWE0ODJiOWEwMDBmZWQzNmNkMjBlMDU0YjE2NjZmZjEifQ.mnC-AexkLlR9l98SJbln3DmV6r9XyHYdbjcUVcWdi_8".to_string()); + let response = validate_jwt(invalid_jwt, "secret", "/test/route", Some(&payload_bytes)); assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 7c6e63fa..1a688e1b 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -14,6 +14,7 @@ bimap.workspace = true blsful.workspace = true cb-common.workspace = true cb-metrics.workspace = true +client-ip.workspace = true eyre.workspace = true futures.workspace = true headers.workspace = true diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index eb284289..e9480db1 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -5,12 +5,12 @@ use std::{ time::{Duration, Instant}, }; -use alloy::primitives::{Address, B256, U256, keccak256}; +use alloy::primitives::{Address, B256, U256}; use axum::{ Extension, Json, body::{Body, to_bytes}, extract::{ConnectInfo, Request, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, middleware::{self, Next}, response::{IntoResponse, Response}, routing::{get, post}, @@ -36,6 +36,7 @@ use cb_common::{ utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; +use client_ip::*; use eyre::Context; use headers::{Authorization, authorization::Bearer}; use parking_lot::RwLock as ParkingRwLock; @@ -144,13 +145,49 @@ impl SigningService { .route_layer(middleware::from_fn(log_request)) .route(STATUS_PATH, get(handle_status)); - if CryptoProvider::get_default().is_none() { - aws_lc_rs::default_provider() - .install_default() - .map_err(|_| eyre::eyre!("Failed to install TLS provider"))?; - } + // Run the JWT cleaning task + let jwt_cleaning_task = tokio::spawn(async move { + let mut interval = tokio::time::interval(state.jwt_auth_fail_timeout); + loop { + interval.tick().await; + let mut failures = state.jwt_auth_failures.write(); + let before = failures.len(); + failures + .retain(|_, info| info.last_failure.elapsed() < state.jwt_auth_fail_timeout); + let after = failures.len(); + if before != after { + debug!("Cleaned up {} old JWT auth failure entries", before - after); + } + } + }); let server_result = if let Some(tls_config) = config.tls_certificates { + if CryptoProvider::get_default().is_none() { + // Install the AWS-LC provider if no default is set, usually for CI + debug!("Installing AWS-LC as default TLS provider"); + let mut attempts = 0; + loop { + match aws_lc_rs::default_provider().install_default() { + Ok(_) => { + debug!("Successfully installed AWS-LC as default TLS provider"); + break; + } + Err(e) => { + error!( + "Failed to install AWS-LC as default TLS provider: {e:?}. Retrying..." + ); + if attempts >= 3 { + error!( + "Exceeded maximum attempts to install AWS-LC as default TLS provider" + ); + break; + } + attempts += 1; + } + } + } + } + let tls_config = RustlsConfig::from_pem(tls_config.0, tls_config.1).await?; axum_server::bind_rustls(config.endpoint, tls_config) .serve( @@ -165,6 +202,10 @@ impl SigningService { ) .await }; + + // Shutdown the JWT cleaning task + jwt_cleaning_task.abort(); + server_result.wrap_err("signer service exited") } @@ -173,39 +214,81 @@ impl SigningService { } } +/// Marks a JWT authentication failure for a given client IP +fn mark_jwt_failure(state: &SigningState, client_ip: IpAddr) { + let mut failures = state.jwt_auth_failures.write(); + let failure_info = failures + .entry(client_ip) + .or_insert(JwtAuthFailureInfo { failure_count: 0, last_failure: Instant::now() }); + failure_info.failure_count += 1; + failure_info.last_failure = Instant::now(); +} + +/// Get the true client IP from the request headers or fallback to the socket +/// address +fn get_true_ip(req_headers: &HeaderMap, addr: &SocketAddr) -> eyre::Result { + let ip_extractors = [ + cf_connecting_ip, + cloudfront_viewer_address, + fly_client_ip, + rightmost_forwarded, + rightmost_x_forwarded_for, + true_client_ip, + x_real_ip, + ]; + + // Run each extractor in order and return the first valid IP found + for extractor in ip_extractors { + match extractor(req_headers) { + Ok(true_ip) => { + return Ok(true_ip); + } + Err(e) => { + match e { + Error::AbsentHeader { .. } => continue, // Missing headers are fine + _ => return Err(eyre::eyre!(e.to_string())), // Report anything else + } + } + } + } + + // Fallback to the socket IP + Ok(addr.ip()) +} + /// Authentication middleware layer async fn jwt_auth( State(state): State, + req_headers: HeaderMap, TypedHeader(auth): TypedHeader>, addr: ConnectInfo, req: Request, next: Next, ) -> Result { // Check if the request needs to be rate limited - let client_ip = addr.ip(); + let client_ip = get_true_ip(&req_headers, &addr).map_err(|e| { + error!("Failed to get client IP: {e}"); + SignerModuleError::RequestError("failed to get client IP".to_string()) + })?; check_jwt_rate_limit(&state, &client_ip)?; // Clone the request so we can read the body let (parts, body) = req.into_parts(); + let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); SignerModuleError::RequestError(e.to_string()) })?; // Process JWT authorization - match check_jwt_auth(&auth, &state, &bytes) { + match check_jwt_auth(&auth, &state, path, &bytes) { Ok(module_id) => { let mut req = Request::from_parts(parts, Body::from(bytes)); req.extensions_mut().insert(module_id); Ok(next.run(req).await) } Err(SignerModuleError::Unauthorized) => { - let mut failures = state.jwt_auth_failures.write(); - let failure_info = failures - .entry(client_ip) - .or_insert(JwtAuthFailureInfo { failure_count: 0, last_failure: Instant::now() }); - failure_info.failure_count += 1; - failure_info.last_failure = Instant::now(); + mark_jwt_failure(&state, client_ip); Err(SignerModuleError::Unauthorized) } Err(err) => Err(err), @@ -253,6 +336,7 @@ fn check_jwt_rate_limit(state: &SigningState, client_ip: &IpAddr) -> Result<(), fn check_jwt_auth( auth: &Authorization, state: &SigningState, + path: &str, body: &[u8], ) -> Result { let jwt: Jwt = auth.token().to_string().into(); @@ -270,44 +354,33 @@ fn check_jwt_auth( SignerModuleError::Unauthorized })?; - if body.is_empty() { - // Skip payload hash comparison for requests without a body - validate_jwt(jwt, &jwt_config.jwt_secret, None).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })?; - } else { - validate_jwt(jwt, &jwt_config.jwt_secret, Some(body)).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })?; - - // Make sure the request contains a hash of the payload in its claims - if !body.is_empty() { - let payload_hash = keccak256(body); - if claims.payload_hash.is_none() || claims.payload_hash != Some(payload_hash) { - error!("Unauthorized request. Invalid payload hash in JWT claims"); - return Err(SignerModuleError::Unauthorized); - } - } - } + let body_bytes = if body.is_empty() { None } else { Some(body) }; + validate_jwt(jwt, &jwt_config.jwt_secret, path, body_bytes).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + SignerModuleError::Unauthorized + })?; Ok(claims.module) } async fn admin_auth( State(state): State, + req_headers: HeaderMap, TypedHeader(auth): TypedHeader>, addr: ConnectInfo, req: Request, next: Next, ) -> Result { // Check if the request needs to be rate limited - let client_ip = addr.ip(); + let client_ip = get_true_ip(&req_headers, &addr).map_err(|e| { + error!("Failed to get client IP: {e}"); + SignerModuleError::RequestError("failed to get client IP".to_string()) + })?; check_jwt_rate_limit(&state, &client_ip)?; // Clone the request so we can read the body let (parts, body) = req.into_parts(); + let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); SignerModuleError::RequestError(e.to_string()) @@ -316,18 +389,12 @@ async fn admin_auth( let jwt: Jwt = auth.token().to_string().into(); // Validate the admin JWT - if bytes.is_empty() { - // Skip payload hash comparison for requests without a body - validate_admin_jwt(jwt, &state.admin_secret.read(), None).map_err(|e| { - error!("Unauthorized request. Invalid JWT: {e}"); - SignerModuleError::Unauthorized - })?; - } else { - validate_admin_jwt(jwt, &state.admin_secret.read(), Some(&bytes)).map_err(|e| { - error!("Unauthorized request. Invalid payload hash in JWT claims: {e}"); - SignerModuleError::Unauthorized - })?; - } + let body_bytes: Option<&[u8]> = if bytes.is_empty() { None } else { Some(&bytes) }; + validate_admin_jwt(jwt, &state.admin_secret.read(), path, body_bytes).map_err(|e| { + error!("Unauthorized request. Invalid JWT: {e}"); + mark_jwt_failure(&state, client_ip); + SignerModuleError::Unauthorized + })?; let req = Request::from_parts(parts, Body::from(bytes)); Ok(next.run(req).await) @@ -598,6 +665,7 @@ async fn handle_reload( debug!(event = "reload", ?req_id, "New request"); + // Regenerate the config let config = match StartSignerConfig::load_from_env() { Ok(config) => config, Err(err) => { @@ -606,6 +674,16 @@ async fn handle_reload( } }; + // Start a new manager with the updated config + let new_manager = match start_manager(config).await { + Ok(manager) => manager, + Err(err) => { + error!(event = "reload", ?req_id, error = ?err, "Failed to reload manager"); + return Err(SignerModuleError::Internal("failed to reload config".to_string())); + } + }; + + // Update the JWT configs if provided in the request if let Some(jwt_secrets) = request.jwt_secrets { let mut jwt_configs = state.jwts.write(); let mut new_configs = HashMap::new(); @@ -627,23 +705,11 @@ async fn handle_reload( *jwt_configs = new_configs; } + // Update the rest of the state once everything has passed if let Some(admin_secret) = request.admin_secret { *state.admin_secret.write() = admin_secret; } - - let new_manager = match start_manager(config).await { - Ok(manager) => manager, - Err(err) => { - error!(event = "reload", ?req_id, error = ?err, "Failed to reload manager"); - return Err(SignerModuleError::Internal("failed to reload config".to_string())); - } - }; - - // Replace the contents of the manager RwLock - { - let mut manager_guard = state.manager.write().await; - *manager_guard = new_manager; - } + *state.manager.write().await = new_manager; Ok(StatusCode::OK) } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 6cd2b829..5b373706 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -21,3 +21,6 @@ tracing.workspace = true tracing-subscriber.workspace = true tree_hash.workspace = true url.workspace = true + +[dev-dependencies] +tracing-test.workspace = true diff --git a/tests/tests/signer_jwt_auth.rs b/tests/tests/signer_jwt_auth.rs index 37561428..d1b65b3f 100644 --- a/tests/tests/signer_jwt_auth.rs +++ b/tests/tests/signer_jwt_auth.rs @@ -45,7 +45,7 @@ async fn test_signer_jwt_auth_success() -> Result<()> { let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, GET_PUBKEYS_PATH, None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); let response = client.get(&url).bearer_auth(&jwt).send().await?; @@ -64,7 +64,7 @@ async fn test_signer_jwt_auth_fail() -> Result<()> { let start_config = start_server(20101, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; // Run a pubkeys request - this should fail due to invalid JWT - let jwt = create_jwt(&module_id, "incorrect secret", None)?; + let jwt = create_jwt(&module_id, "incorrect secret", GET_PUBKEYS_PATH, None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); let response = client.get(&url).bearer_auth(&jwt).send().await?; @@ -86,7 +86,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, "incorrect secret", None)?; + let jwt = create_jwt(&module_id, "incorrect secret", GET_PUBKEYS_PATH, None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); for _ in 0..start_config.jwt_auth_fail_limit { @@ -95,7 +95,7 @@ async fn test_signer_jwt_rate_limit() -> Result<()> { } // Run another request - this should fail due to rate limiting now - let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret, None)?; + let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret, GET_PUBKEYS_PATH, None)?; let response = client.get(&url).bearer_auth(&jwt).send().await?; assert!(response.status() == StatusCode::TOO_MANY_REQUESTS); @@ -119,7 +119,7 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let start_config = start_server(20400, &mod_cfgs, admin_secret.clone(), false).await?; // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, JWT_SECRET, None)?; + let jwt = create_jwt(&module_id, JWT_SECRET, GET_PUBKEYS_PATH, None)?; let client = reqwest::Client::new(); // At first, test module should be allowed to request pubkeys @@ -129,7 +129,7 @@ async fn test_signer_revoked_jwt_fail() -> Result<()> { let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; let body_bytes = serde_json::to_vec(&revoke_body)?; - let admin_jwt = create_admin_jwt(admin_secret, Some(&body_bytes))?; + let admin_jwt = create_admin_jwt(admin_secret, REVOKE_MODULE_PATH, Some(&body_bytes))?; let revoke_url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); let response = @@ -155,7 +155,7 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { let body_bytes = serde_json::to_vec(&revoke_body)?; // Run as many pubkeys requests as the fail limit - let jwt = create_jwt(&module_id, JWT_SECRET, Some(&body_bytes))?; + let jwt = create_jwt(&module_id, JWT_SECRET, REVOKE_MODULE_PATH, Some(&body_bytes))?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); @@ -164,7 +164,45 @@ async fn test_signer_only_admin_can_revoke() -> Result<()> { assert!(response.status() == StatusCode::UNAUTHORIZED); // Admin should be able to revoke modules - let admin_jwt = create_admin_jwt(admin_secret, Some(&body_bytes))?; + let admin_jwt = create_admin_jwt(admin_secret, REVOKE_MODULE_PATH, Some(&body_bytes))?; + let response = client.post(&url).json(&revoke_body).bearer_auth(&admin_jwt).send().await?; + assert!(response.status() == StatusCode::OK); + + Ok(()) +} + +#[tokio::test] +async fn test_signer_admin_jwt_rate_limit() -> Result<()> { + setup_test_env(); + let admin_secret = ADMIN_SECRET.to_string(); + let module_id = ModuleId(JWT_MODULE.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20510, &mod_cfgs, admin_secret.clone(), false).await?; + + let revoke_body = RevokeModuleRequest { module_id: ModuleId(JWT_MODULE.to_string()) }; + let body_bytes = serde_json::to_vec(&revoke_body)?; + + // Run as many pubkeys requests as the fail limit + let jwt = create_jwt(&module_id, JWT_SECRET, REVOKE_MODULE_PATH, Some(&body_bytes))?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, REVOKE_MODULE_PATH); + + // Module JWT shouldn't be able to revoke modules + for _ in 0..start_config.jwt_auth_fail_limit { + let response = client.post(&url).json(&revoke_body).bearer_auth(&jwt).send().await?; + assert!(response.status() == StatusCode::UNAUTHORIZED); + } + + // Run another request - this should fail due to rate limiting now + let admin_jwt = create_admin_jwt(admin_secret, REVOKE_MODULE_PATH, Some(&body_bytes))?; + let response = client.post(&url).json(&revoke_body).bearer_auth(&admin_jwt).send().await?; + assert!(response.status() == StatusCode::TOO_MANY_REQUESTS); + + // Wait for the rate limit timeout + tokio::time::sleep(Duration::from_secs(start_config.jwt_auth_fail_timeout_seconds as u64)) + .await; + + // Now the next request should succeed let response = client.post(&url).json(&revoke_body).bearer_auth(&admin_jwt).send().await?; assert!(response.status() == StatusCode::OK); diff --git a/tests/tests/signer_jwt_auth_cleanup.rs b/tests/tests/signer_jwt_auth_cleanup.rs new file mode 100644 index 00000000..d6fde2a4 --- /dev/null +++ b/tests/tests/signer_jwt_auth_cleanup.rs @@ -0,0 +1,70 @@ +use std::{collections::HashMap, time::Duration}; + +use alloy::primitives::b256; +use cb_common::{ + commit::constants::GET_PUBKEYS_PATH, + config::{ModuleSigningConfig, load_module_signing_configs}, + types::ModuleId, + utils::create_jwt, +}; +use cb_tests::{ + signer_service::start_server, + utils::{self}, +}; +use eyre::Result; +use reqwest::StatusCode; + +const JWT_MODULE: &str = "test-module"; +const JWT_SECRET: &str = "test-jwt-secret"; +const ADMIN_SECRET: &str = "test-admin-secret"; + +async fn create_mod_signing_configs() -> HashMap { + let mut cfg = + utils::get_commit_boost_config(utils::get_pbs_static_config(utils::get_pbs_config(0))); + + let module_id = ModuleId(JWT_MODULE.to_string()); + let signing_id = b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + cfg.modules = Some(vec![utils::create_module_config(module_id.clone(), signing_id)]); + + let jwts = HashMap::from([(module_id.clone(), JWT_SECRET.to_string())]); + + load_module_signing_configs(&cfg, &jwts).unwrap() +} + +#[tokio::test] +#[tracing_test::traced_test] +async fn test_signer_jwt_fail_cleanup() -> Result<()> { + // setup_test_env() isn't used because we want to capture logs with tracing_test + let module_id = ModuleId(JWT_MODULE.to_string()); + let mod_cfgs = create_mod_signing_configs().await; + let start_config = start_server(20102, &mod_cfgs, ADMIN_SECRET.to_string(), false).await?; + let mod_cfg = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); + + // Run as many pubkeys requests as the fail limit + let jwt = create_jwt(&module_id, "incorrect secret", GET_PUBKEYS_PATH, None)?; + let client = reqwest::Client::new(); + let url = format!("http://{}{}", start_config.endpoint, GET_PUBKEYS_PATH); + for _ in 0..start_config.jwt_auth_fail_limit { + let response = client.get(&url).bearer_auth(&jwt).send().await?; + assert!(response.status() == StatusCode::UNAUTHORIZED); + } + + // Run another request - this should fail due to rate limiting now + let jwt = create_jwt(&module_id, &mod_cfg.jwt_secret, GET_PUBKEYS_PATH, None)?; + let response = client.get(&url).bearer_auth(&jwt).send().await?; + assert!(response.status() == StatusCode::TOO_MANY_REQUESTS); + + // Wait until the cleanup task should have run properly, takes a while for the + // timing to work out + tokio::time::sleep(Duration::from_secs( + (start_config.jwt_auth_fail_timeout_seconds * 3) as u64, + )) + .await; + + // Make sure the cleanup message was logged - it's all internal state so without + // refactoring or exposing it, this is the easiest way to check if it triggered + assert!(logs_contain("Cleaned up 1 old JWT auth failure entries")); + + Ok(()) +} diff --git a/tests/tests/signer_request_sig.rs b/tests/tests/signer_request_sig.rs index 15680587..78efbf9e 100644 --- a/tests/tests/signer_request_sig.rs +++ b/tests/tests/signer_request_sig.rs @@ -62,7 +62,12 @@ async fn test_signer_sign_request_good() -> Result<()> { let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; let payload_bytes = serde_json::to_vec(&request)?; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&payload_bytes))?; + let jwt = create_jwt( + &module_id, + &jwt_config.jwt_secret, + REQUEST_SIGNATURE_BLS_PATH, + Some(&payload_bytes), + )?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; @@ -100,7 +105,12 @@ async fn test_signer_sign_request_different_module() -> Result<()> { let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); let request = SignConsensusRequest { pubkey: pubkey.clone(), object_root, nonce }; let payload_bytes = serde_json::to_vec(&request)?; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&payload_bytes))?; + let jwt = create_jwt( + &module_id, + &jwt_config.jwt_secret, + REQUEST_SIGNATURE_BLS_PATH, + Some(&payload_bytes), + )?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; @@ -146,7 +156,12 @@ async fn test_signer_sign_request_incorrect_hash() -> Result<()> { let true_object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); let true_request = SignConsensusRequest { pubkey, object_root: true_object_root, nonce }; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, Some(&fake_payload_bytes))?; + let jwt = create_jwt( + &module_id, + &jwt_config.jwt_secret, + REQUEST_SIGNATURE_BLS_PATH, + Some(&fake_payload_bytes), + )?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&true_request).bearer_auth(&jwt).send().await?; @@ -171,7 +186,7 @@ async fn test_signer_sign_request_missing_hash() -> Result<()> { let pubkey = BlsPublicKey::deserialize(&PUBKEY_1).unwrap(); let object_root = b256!("0x0123456789012345678901234567890123456789012345678901234567890123"); let request = SignConsensusRequest { pubkey, object_root, nonce }; - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, REQUEST_SIGNATURE_BLS_PATH, None)?; let client = reqwest::Client::new(); let url = format!("http://{}{}", start_config.endpoint, REQUEST_SIGNATURE_BLS_PATH); let response = client.post(&url).json(&request).bearer_auth(&jwt).send().await?; diff --git a/tests/tests/signer_tls.rs b/tests/tests/signer_tls.rs index 4f53bb92..2df98d73 100644 --- a/tests/tests/signer_tls.rs +++ b/tests/tests/signer_tls.rs @@ -41,7 +41,7 @@ async fn test_signer_tls() -> Result<()> { let jwt_config = mod_cfgs.get(&module_id).expect("JWT config for test module not found"); // Run a pubkeys request - let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, None)?; + let jwt = create_jwt(&module_id, &jwt_config.jwt_secret, GET_PUBKEYS_PATH, None)?; let cert = match start_config.tls_certificates { Some(ref certificates) => &certificates.0, None => bail!("TLS certificates not found in start config"), From 8ddb055a56435693d6d11d9d22aa0ae9ebe3c2b5 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Tue, 30 Sep 2025 17:21:40 -0400 Subject: [PATCH 11/30] Added make to the build file --- provisioning/build.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioning/build.Dockerfile b/provisioning/build.Dockerfile index 361150e7..b28b62ff 100644 --- a/provisioning/build.Dockerfile +++ b/provisioning/build.Dockerfile @@ -59,7 +59,7 @@ RUN if [ -f ${BUILD_VAR_SCRIPT} ]; then \ echo "No cross-compilation needed"; \ fi && \ apt update && \ - apt install -y git libssl-dev:${TARGETARCH} zlib1g-dev:${TARGETARCH} pkg-config && \ + apt install -y git make libssl-dev:${TARGETARCH} zlib1g-dev:${TARGETARCH} pkg-config && \ cargo chef cook ${TARGET_FLAG} --release --recipe-path recipe.json # Get the latest Protoc since the one in the Debian repo is incredibly old From a3f8d9d76a224f49cd7c0b609086db9c45340f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Mon, 20 Oct 2025 12:19:23 -0300 Subject: [PATCH 12/30] fix(signer): Mark a JWT failure if payload conversion fail (#387) --- crates/signer/src/service.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index e9480db1..1116e764 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -277,6 +277,7 @@ async fn jwt_auth( let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); + mark_jwt_failure(&state, client_ip); SignerModuleError::RequestError(e.to_string()) })?; @@ -291,7 +292,10 @@ async fn jwt_auth( mark_jwt_failure(&state, client_ip); Err(SignerModuleError::Unauthorized) } - Err(err) => Err(err), + Err(err) => { + mark_jwt_failure(&state, client_ip); + Err(err) + } } } @@ -383,6 +387,7 @@ async fn admin_auth( let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); + mark_jwt_failure(&state, client_ip); SignerModuleError::RequestError(e.to_string()) })?; From b66f41178263de21e7890988a9438fa18d21ce32 Mon Sep 17 00:00:00 2001 From: Joe Clapis Date: Mon, 20 Oct 2025 11:45:35 -0400 Subject: [PATCH 13/30] Removed a test that was deleted in main --- tests/tests/pbs_post_validators.rs | 140 ----------------------------- 1 file changed, 140 deletions(-) diff --git a/tests/tests/pbs_post_validators.rs b/tests/tests/pbs_post_validators.rs index 8acb9fa8..2f23caee 100644 --- a/tests/tests/pbs_post_validators.rs +++ b/tests/tests/pbs_post_validators.rs @@ -61,146 +61,6 @@ async fn test_register_validators() -> Result<()> { Ok(()) } -#[tokio::test] -async fn test_register_validators_returns_422_if_request_is_malformed() -> Result<()> { - setup_test_env(); - let signer = random_secret(); - let pubkey: BlsPublicKey = signer.public_key(); - - let chain = Chain::Holesky; - let pbs_port = 4100; - - // Run a mock relay - let relays = vec![generate_mock_relay(pbs_port + 1, pubkey)?]; - let mock_state = Arc::new(MockRelayState::new(chain, signer)); - tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1)); - - // Run the PBS service - let config = to_pbs_config(chain, get_pbs_config(pbs_port), relays); - let state = PbsState::new(config); - tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state)); - - // leave some time to start servers - tokio::time::sleep(Duration::from_millis(100)).await; - - let mock_validator = MockValidator::new(pbs_port)?; - let url = mock_validator.comm_boost.register_validator_url().unwrap(); - info!("Sending register validator"); - - // Bad fee recipient - let bad_json = r#"[{ - "message": { - "fee_recipient": "0xaa", - "gas_limit": "100000", - "timestamp": "1000000", - "pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }, - "signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - }]"#; - - let res = mock_validator - .comm_boost - .client - .post(url.clone()) - .header("Content-Type", "application/json") - .body(bad_json) - .send() - .await?; - - assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - - // Bad pubkey - let bad_json = r#"[{ - "message": { - "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "gas_limit": "100000", - "timestamp": "1000000", - "pubkey": "0xbbb" - }, - "signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - }]"#; - - let res = mock_validator - .comm_boost - .client - .post(url.clone()) - .header("Content-Type", "application/json") - .body(bad_json) - .send() - .await?; - - assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - - // Bad signature - let bad_json = r#"[{ - "message": { - "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "gas_limit": "100000", - "timestamp": "1000000", - "pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }, - "signature": "0xcccc" - }]"#; - - let res = mock_validator - .comm_boost - .client - .post(url.clone()) - .header("Content-Type", "application/json") - .body(bad_json) - .send() - .await?; - - assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - - // gas limit too high - let bad_json = r#"[{ - "message": { - "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "gas_limit": "10000000000000000000000000000000000000000000000000000000", - "timestamp": "1000000", - "pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - }, - "signature": "0xcccc" - }]"#; - - let res = mock_validator - .comm_boost - .client - .post(url.clone()) - .header("Content-Type", "application/json") - .body(bad_json) - .send() - .await?; - - assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - - // timestamp too high - let bad_json = r#"[{ - "message": { - "fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "gas_limit": "1000000", - "timestamp": "10000000000000000000000000000000000000000000000000000000", - "pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - }, - "signature": "0xcccc" - }]"#; - - let res = mock_validator - .comm_boost - .client - .post(url.clone()) - .header("Content-Type", "application/json") - .body(bad_json) - .send() - .await?; - - assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); - - assert_eq!(mock_state.received_register_validator(), 0); - Ok(()) -} - #[tokio::test] async fn test_register_validators_does_not_retry_on_429() -> Result<()> { setup_test_env(); From e58d67e100241c6dafd9f0e1eb6d5438d374caa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 21 Oct 2025 17:46:17 -0300 Subject: [PATCH 14/30] refactor(signer): Use config-based header to extract IP from (#388) Co-authored-by: Joe Clapis --- Cargo.lock | 27 ----------- Cargo.toml | 1 - config.example.toml | 8 ++++ crates/common/src/config/signer.rs | 19 ++++++++ crates/signer/Cargo.toml | 1 - crates/signer/src/lib.rs | 1 + crates/signer/src/service.rs | 45 ++++-------------- crates/signer/src/utils.rs | 64 ++++++++++++++++++++++++++ docs/docs/get_started/configuration.md | 27 +++++++++++ tests/src/utils.rs | 9 ++-- 10 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 crates/signer/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 8ba60cfa..3ae39ad4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1835,7 +1835,6 @@ dependencies = [ "blsful", "cb-common", "cb-metrics", - "client-ip", "eyre", "futures", "headers", @@ -1994,16 +1993,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" -[[package]] -name = "client-ip" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31211fc26899744f5b22521fdc971e5f3875991d8880537537470685a0e9552d" -dependencies = [ - "forwarded-header-value", - "http 1.3.1", -] - [[package]] name = "cmake" version = "0.1.54" @@ -3248,16 +3237,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "forwarded-header-value" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" -dependencies = [ - "nonempty", - "thiserror 1.0.69", -] - [[package]] name = "fs-err" version = "3.1.2" @@ -4562,12 +4541,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - [[package]] name = "nu-ansi-term" version = "0.50.1" diff --git a/Cargo.toml b/Cargo.toml index 27d530d8..8ab15356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ cb-pbs = { path = "crates/pbs" } cb-signer = { path = "crates/signer" } cipher = "0.4" clap = { version = "4.5.4", features = ["derive", "env"] } -client-ip = { version = "0.1.1", features = [ "forwarded-header" ] } color-eyre = "0.6.3" const_format = "0.2.34" ctr = "0.9.2" diff --git a/config.example.toml b/config.example.toml index be7e3296..a1faefa8 100644 --- a/config.example.toml +++ b/config.example.toml @@ -185,6 +185,14 @@ jwt_auth_fail_limit = 3 # OPTIONAL, DEFAULT: 300 jwt_auth_fail_timeout_seconds = 300 +# HTTP header to use to determine the real client IP, if the Signer is behind a proxy (e.g. nginx) +# OPTIONAL. If missing, the client IP will be taken directly from the TCP connection. +# [signer.reverse_proxy] +# Unique: HTTP header name to use to determine the real client IP. Expected to appear only once in the request. Requests with multiple values of this header will be rejected. +# unique = "X-Real-IP" +# Rightmost: HTTP header name to use to determine the real client IP from a comma-separated list of IPs. Rightmost IP is the client IP. If the header appears multiple times, the last value will be used. +# rightmost = "X-Forwarded-For" + # [signer.tls_mode] # How to use TLS for the Signer's HTTP server; two modes are supported: # - type = "insecure": disable TLS, so the server runs in HTTP mode (not recommended for production). diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 4702ef22..054ec717 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -69,6 +69,16 @@ pub enum TlsMode { Certificate(PathBuf), } +/// Reverse proxy setup, used to extract real client's IP +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "snake_case")] +pub enum ReverseProxyHeaderSetup { + #[default] + None, + Unique(String), + Rightmost(String), +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "snake_case")] pub struct SignerConfig { @@ -99,6 +109,10 @@ pub struct SignerConfig { #[serde(default = "default_tls_mode")] pub tls_mode: TlsMode, + /// Reverse proxy setup to extract real client's IP + #[serde(default)] + pub reverse_proxy: ReverseProxyHeaderSetup, + /// Inner type-specific configuration #[serde(flatten)] pub inner: SignerType, @@ -194,6 +208,7 @@ pub struct StartSignerConfig { pub jwt_auth_fail_timeout_seconds: u32, pub dirk: Option, pub tls_certificates: Option<(Vec, Vec)>, + pub reverse_proxy: ReverseProxyHeaderSetup, } impl StartSignerConfig { @@ -247,6 +262,8 @@ impl StartSignerConfig { } }; + let reverse_proxy = signer_config.reverse_proxy; + match signer_config.inner { SignerType::Local { loader, store, .. } => Ok(StartSignerConfig { chain: config.chain, @@ -259,6 +276,7 @@ impl StartSignerConfig { store, dirk: None, tls_certificates, + reverse_proxy, }), SignerType::Dirk { @@ -305,6 +323,7 @@ impl StartSignerConfig { }, }), tls_certificates, + reverse_proxy, }) } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 1a688e1b..7c6e63fa 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -14,7 +14,6 @@ bimap.workspace = true blsful.workspace = true cb-common.workspace = true cb-metrics.workspace = true -client-ip.workspace = true eyre.workspace = true futures.workspace = true headers.workspace = true diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 4b5e1451..b4b9ecc4 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -4,3 +4,4 @@ pub mod manager; mod metrics; mod proto; pub mod service; +mod utils; diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 1116e764..219a4ae5 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -30,13 +30,12 @@ use cb_common::{ }, response::{BlsSignResponse, EcdsaSignResponse}, }, - config::{ModuleSigningConfig, StartSignerConfig}, + config::{ModuleSigningConfig, ReverseProxyHeaderSetup, StartSignerConfig}, constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION}, types::{BlsPublicKey, Chain, Jwt, ModuleId, SignatureRequestInfo}, utils::{decode_jwt, validate_admin_jwt, validate_jwt}, }; use cb_metrics::provider::MetricsProvider; -use client_ip::*; use eyre::Context; use headers::{Authorization, authorization::Bearer}; use parking_lot::RwLock as ParkingRwLock; @@ -49,6 +48,7 @@ use crate::{ error::SignerModuleError, manager::{SigningManager, dirk::DirkManager, local::LocalSigningManager}, metrics::{SIGNER_METRICS_REGISTRY, SIGNER_STATUS, uri_to_tag}, + utils::get_true_ip, }; pub const REQUEST_MAX_BODY_LENGTH: usize = 1024 * 1024; // 1 MB @@ -83,6 +83,9 @@ struct SigningState { // JWT auth failure settings jwt_auth_fail_limit: u32, jwt_auth_fail_timeout: Duration, + + /// Header to extract the trusted client IP from + reverse_proxy: ReverseProxyHeaderSetup, } impl SigningService { @@ -102,6 +105,7 @@ impl SigningService { jwt_auth_failures: Arc::new(ParkingRwLock::new(HashMap::new())), jwt_auth_fail_limit: config.jwt_auth_fail_limit, jwt_auth_fail_timeout: Duration::from_secs(config.jwt_auth_fail_timeout_seconds as u64), + reverse_proxy: config.reverse_proxy, }; // Get the signer counts @@ -122,6 +126,7 @@ impl SigningService { loaded_proxies, jwt_auth_fail_limit =? state.jwt_auth_fail_limit, jwt_auth_fail_timeout =? state.jwt_auth_fail_timeout, + reverse_proxy =? state.reverse_proxy, "Starting signing service" ); @@ -224,38 +229,6 @@ fn mark_jwt_failure(state: &SigningState, client_ip: IpAddr) { failure_info.last_failure = Instant::now(); } -/// Get the true client IP from the request headers or fallback to the socket -/// address -fn get_true_ip(req_headers: &HeaderMap, addr: &SocketAddr) -> eyre::Result { - let ip_extractors = [ - cf_connecting_ip, - cloudfront_viewer_address, - fly_client_ip, - rightmost_forwarded, - rightmost_x_forwarded_for, - true_client_ip, - x_real_ip, - ]; - - // Run each extractor in order and return the first valid IP found - for extractor in ip_extractors { - match extractor(req_headers) { - Ok(true_ip) => { - return Ok(true_ip); - } - Err(e) => { - match e { - Error::AbsentHeader { .. } => continue, // Missing headers are fine - _ => return Err(eyre::eyre!(e.to_string())), // Report anything else - } - } - } - } - - // Fallback to the socket IP - Ok(addr.ip()) -} - /// Authentication middleware layer async fn jwt_auth( State(state): State, @@ -266,7 +239,7 @@ async fn jwt_auth( next: Next, ) -> Result { // Check if the request needs to be rate limited - let client_ip = get_true_ip(&req_headers, &addr).map_err(|e| { + let client_ip = get_true_ip(&req_headers, &addr, &state.reverse_proxy).map_err(|e| { error!("Failed to get client IP: {e}"); SignerModuleError::RequestError("failed to get client IP".to_string()) })?; @@ -376,7 +349,7 @@ async fn admin_auth( next: Next, ) -> Result { // Check if the request needs to be rate limited - let client_ip = get_true_ip(&req_headers, &addr).map_err(|e| { + let client_ip = get_true_ip(&req_headers, &addr, &state.reverse_proxy).map_err(|e| { error!("Failed to get client IP: {e}"); SignerModuleError::RequestError("failed to get client IP".to_string()) })?; diff --git a/crates/signer/src/utils.rs b/crates/signer/src/utils.rs new file mode 100644 index 00000000..a6d313ef --- /dev/null +++ b/crates/signer/src/utils.rs @@ -0,0 +1,64 @@ +use std::net::{IpAddr, SocketAddr}; + +use axum::http::HeaderMap; +use cb_common::config::ReverseProxyHeaderSetup; + +#[derive(Debug, thiserror::Error)] +pub enum IpError { + #[error("header `{0}` is not present")] + NotPresent(String), + #[error("header value has invalid characters")] + HasInvalidCharacters, + #[error("header value is not a valid IP address")] + InvalidValue, + #[error("header `{0}` appears multiple times but expected to be unique")] + NotUnique(String), +} + +/// Get the true client IP from the request headers or fallback to the socket +/// address +pub fn get_true_ip( + headers: &HeaderMap, + addr: &SocketAddr, + reverse_proxy: &ReverseProxyHeaderSetup, +) -> Result { + match reverse_proxy { + ReverseProxyHeaderSetup::None => Ok(addr.ip()), + ReverseProxyHeaderSetup::Unique(header) => get_ip_from_unique_header(headers, header), + ReverseProxyHeaderSetup::Rightmost(header) => get_ip_from_rightmost_value(headers, header), + } +} + +fn get_ip_from_unique_header(headers: &HeaderMap, header_name: &str) -> Result { + let mut values = headers.get_all(header_name).iter(); + + let first_value = values.next().ok_or(IpError::NotPresent(header_name.to_string()))?; + let ip = first_value + .to_str() + .map_err(|_| IpError::HasInvalidCharacters)? + .parse::() + .map_err(|_| IpError::InvalidValue)?; + + if values.next().is_some() { + return Err(IpError::NotUnique(header_name.to_string())); + } + + Ok(ip) +} + +fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result { + let last_value = headers + .get_all(header_name) + .iter() + .next_back() + .ok_or(IpError::NotPresent(header_name.to_string()))? + .to_str() + .map_err(|_| IpError::HasInvalidCharacters)?; + + last_value + .rsplit_once(",") + .map(|(_, rightmost)| rightmost) + .unwrap_or(last_value) + .parse::() + .map_err(|_| IpError::InvalidValue) +} diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index acff09e7..abdb9e20 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -388,6 +388,33 @@ path = "path/to/your/cert/folder" Where `path` is the aforementioned folder. It defaults to `./certs` but can be replaced with whichever directory your certificate and private key file reside in, as long as they're readable by the Signer service (or its Docker container, if using Docker). +### 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: + +```toml +[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're two options: + +- `unique`: The name of the 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 is received that has multiple values for this header, it will be considered invalid and rejected. +- `rightmost`: The name of the HTTP header that contains a comma-separated list of IPs. The rightmost IP in the list is used. If the header appears multiple times, the last occurrence is used. This is common when using `X-Forwarded-For`. + +Examples: + +```toml +[signer.reverse_proxy] +unique = "X-Real-IP" +``` + +```toml +[signer.reverse_proxy] +rightmost = "X-Forwarded-For" +``` ## Custom module diff --git a/tests/src/utils.rs b/tests/src/utils.rs index a5b5e270..95293097 100644 --- a/tests/src/utils.rs +++ b/tests/src/utils.rs @@ -9,9 +9,10 @@ use alloy::primitives::{B256, U256}; use cb_common::{ config::{ CommitBoostConfig, LogsSettings, ModuleKind, ModuleSigningConfig, PbsConfig, - PbsModuleConfig, RelayConfig, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, - SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, SIGNER_PORT_DEFAULT, SignerConfig, - SignerType, StartSignerConfig, StaticModuleConfig, StaticPbsConfig, TlsMode, + PbsModuleConfig, RelayConfig, ReverseProxyHeaderSetup, SIGNER_IMAGE_DEFAULT, + SIGNER_JWT_AUTH_FAIL_LIMIT_DEFAULT, SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, + SIGNER_PORT_DEFAULT, SignerConfig, SignerType, StartSignerConfig, StaticModuleConfig, + StaticPbsConfig, TlsMode, }, pbs::{RelayClient, RelayEntry}, signer::SignerLoader, @@ -134,6 +135,7 @@ pub fn get_signer_config(loader: SignerLoader, tls: bool) -> SignerConfig { jwt_auth_fail_timeout_seconds: SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_DEFAULT, inner: SignerType::Local { loader, store: None }, tls_mode: if tls { TlsMode::Certificate(PathBuf::new()) } else { TlsMode::Insecure }, + reverse_proxy: ReverseProxyHeaderSetup::None, } } @@ -169,6 +171,7 @@ pub fn get_start_signer_config( jwt_auth_fail_timeout_seconds: signer_config.jwt_auth_fail_timeout_seconds, dirk: None, tls_certificates, + reverse_proxy: ReverseProxyHeaderSetup::None, }, _ => panic!("Only local signers are supported in tests"), } From 85382a5a0c1b4a973a48bd72f35dc07a79373c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Wed, 10 Dec 2025 11:48:24 -0300 Subject: [PATCH 15/30] fix(signer): introduce config to set the amount of trusted proxies in rightmost setup (#410) --- config.example.toml | 14 +++++++---- crates/common/src/config/signer.rs | 33 +++++++++++++++++++++++--- crates/signer/src/service.rs | 2 +- crates/signer/src/utils.rs | 25 ++++++++++++++----- docs/docs/get_started/configuration.md | 13 ++++++---- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/config.example.toml b/config.example.toml index a1faefa8..bf3b07ba 100644 --- a/config.example.toml +++ b/config.example.toml @@ -188,10 +188,16 @@ jwt_auth_fail_timeout_seconds = 300 # HTTP header to use to determine the real client IP, if the Signer is behind a proxy (e.g. nginx) # OPTIONAL. If missing, the client IP will be taken directly from the TCP connection. # [signer.reverse_proxy] -# Unique: HTTP header name to use to determine the real client IP. Expected to appear only once in the request. Requests with multiple values of this header will be rejected. -# unique = "X-Real-IP" -# Rightmost: HTTP header name to use to determine the real client IP from a comma-separated list of IPs. Rightmost IP is the client IP. If the header appears multiple times, the last value will be used. -# rightmost = "X-Forwarded-For" +# Type of reverse proxy configuration. Supported values: +# - unique: use a single HTTP header value as the client IP. +# - rightmost: use the rightmost IP from a comma-separated list of IPs in the HTTP header. +# type = "unique" +# Unique: HTTP header name to use to determine the real client IP. If the header appears multiple times, the request will be rejected. +# header = "X-Real-IP" +# Rightmost: HTTP header name to use to determine the real client IP from a comma-separated list of IPs. If the header appears multiple times, the last value will be used. +# header = "X-Forwarded-For" +# Rightmost: number of trusted proxies in front of the Signer, whose IPs will be skipped when extracting the client IP from the rightmost side of the list. Must be greater than 0. +# trusted_count = 1 # [signer.tls_mode] # How to use TLS for the Signer's HTTP server; two modes are supported: diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 054ec717..cfd95be4 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -1,6 +1,8 @@ use std::{ collections::HashMap, + fmt::Display, net::{Ipv4Addr, SocketAddr}, + num::NonZeroUsize, path::PathBuf, }; @@ -71,12 +73,37 @@ pub enum TlsMode { /// Reverse proxy setup, used to extract real client's IP #[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "snake_case", tag = "type")] pub enum ReverseProxyHeaderSetup { #[default] None, - Unique(String), - Rightmost(String), + Unique { + header: String, + }, + Rightmost { + header: String, + trusted_count: NonZeroUsize, + }, +} + +impl Display for ReverseProxyHeaderSetup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReverseProxyHeaderSetup::None => write!(f, "None"), + ReverseProxyHeaderSetup::Unique { header } => { + write!(f, "\"{header} (unique)\"") + } + ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => { + let suffix = match trusted_count.get() % 10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + }; + write!(f, "\"{header} ({trusted_count}{suffix} from the right)\"") + } + } + } } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 219a4ae5..134da25a 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -126,7 +126,7 @@ impl SigningService { loaded_proxies, jwt_auth_fail_limit =? state.jwt_auth_fail_limit, jwt_auth_fail_timeout =? state.jwt_auth_fail_timeout, - reverse_proxy =? state.reverse_proxy, + reverse_proxy =% state.reverse_proxy, "Starting signing service" ); diff --git a/crates/signer/src/utils.rs b/crates/signer/src/utils.rs index a6d313ef..5ee808e8 100644 --- a/crates/signer/src/utils.rs +++ b/crates/signer/src/utils.rs @@ -13,6 +13,8 @@ pub enum IpError { InvalidValue, #[error("header `{0}` appears multiple times but expected to be unique")] NotUnique(String), + #[error("header does not contain enough values: found {found}, required {required}")] + NotEnoughValues { found: usize, required: usize }, } /// Get the true client IP from the request headers or fallback to the socket @@ -24,8 +26,10 @@ pub fn get_true_ip( ) -> Result { match reverse_proxy { ReverseProxyHeaderSetup::None => Ok(addr.ip()), - ReverseProxyHeaderSetup::Unique(header) => get_ip_from_unique_header(headers, header), - ReverseProxyHeaderSetup::Rightmost(header) => get_ip_from_rightmost_value(headers, header), + ReverseProxyHeaderSetup::Unique { header } => get_ip_from_unique_header(headers, header), + ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => { + get_ip_from_rightmost_value(headers, header, trusted_count.get()) + } } } @@ -46,7 +50,11 @@ fn get_ip_from_unique_header(headers: &HeaderMap, header_name: &str) -> Result Result { +fn get_ip_from_rightmost_value( + headers: &HeaderMap, + header_name: &str, + trusted_count: usize, +) -> Result { let last_value = headers .get_all(header_name) .iter() @@ -55,10 +63,15 @@ fn get_ip_from_rightmost_value(headers: &HeaderMap, header_name: &str) -> Result .to_str() .map_err(|_| IpError::HasInvalidCharacters)?; + // Selecting the first untrusted IP from the right according to: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#selecting_an_ip_address last_value - .rsplit_once(",") - .map(|(_, rightmost)| rightmost) - .unwrap_or(last_value) + .rsplit(",") + .nth(trusted_count - 1) + .ok_or(IpError::NotEnoughValues { + found: last_value.split(",").count(), + required: trusted_count, + })? .parse::() .map_err(|_| IpError::InvalidValue) } diff --git a/docs/docs/get_started/configuration.md b/docs/docs/get_started/configuration.md index abdb9e20..c50fcf0c 100644 --- a/docs/docs/get_started/configuration.md +++ b/docs/docs/get_started/configuration.md @@ -401,21 +401,26 @@ 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're two options: -- `unique`: The name of the 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 is received that has multiple values for this header, it will be considered invalid and rejected. -- `rightmost`: The name of the HTTP header that contains a comma-separated list of IPs. The rightmost IP in the list is used. If the header appears multiple times, the last occurrence is used. This is common when using `X-Forwarded-For`. +- 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, the last occurrence is used. This is common when using `X-Forwarded-For`. Examples: ```toml [signer.reverse_proxy] -unique = "X-Real-IP" +type = "unique" +header = "X-Real-IP" ``` ```toml [signer.reverse_proxy] -rightmost = "X-Forwarded-For" +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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#trusted_proxy_count) for more info. + ## Custom module We currently provide a test module that needs to be built locally. To build the module run: From 19121f36c8cc094a47d5bfdc0542e65f776b04da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20I=C3=B1aki=20Bilbao?= Date: Tue, 23 Dec 2025 16:20:23 -0300 Subject: [PATCH 16/30] fix(signer): rightmost header fixes (#420) --- crates/signer/src/utils.rs | 187 ++++++++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 11 deletions(-) diff --git a/crates/signer/src/utils.rs b/crates/signer/src/utils.rs index 5ee808e8..bfc28f9f 100644 --- a/crates/signer/src/utils.rs +++ b/crates/signer/src/utils.rs @@ -37,16 +37,17 @@ fn get_ip_from_unique_header(headers: &HeaderMap, header_name: &str) -> Result() .map_err(|_| IpError::InvalidValue)?; - if values.next().is_some() { - return Err(IpError::NotUnique(header_name.to_string())); - } - Ok(ip) } @@ -55,23 +56,187 @@ fn get_ip_from_rightmost_value( header_name: &str, trusted_count: usize, ) -> Result { - let last_value = headers + let joined_values = headers .get_all(header_name) .iter() - .next_back() - .ok_or(IpError::NotPresent(header_name.to_string()))? - .to_str() - .map_err(|_| IpError::HasInvalidCharacters)?; + .map(|x| x.to_str().map_err(|_| IpError::HasInvalidCharacters)) + .collect::, IpError>>()? + .join(","); + + if joined_values.is_empty() { + return Err(IpError::NotPresent(header_name.to_string())) + } // Selecting the first untrusted IP from the right according to: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For#selecting_an_ip_address - last_value + joined_values .rsplit(",") .nth(trusted_count - 1) .ok_or(IpError::NotEnoughValues { - found: last_value.split(",").count(), + found: joined_values.split(",").count(), required: trusted_count, })? + .trim() .parse::() .map_err(|_| IpError::InvalidValue) } + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use super::*; + + #[test] + fn test_unique_header_pass() { + let header_name = "X-Real-IP"; + let real_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + + let mut headers = HeaderMap::new(); + headers.insert(header_name, real_ip.to_string().parse().unwrap()); + + let ip = get_ip_from_unique_header(&headers, header_name).unwrap(); + assert_eq!(ip, real_ip); + } + + #[test] + fn test_unique_header_duplicated() { + let header_name = "X-Real-IP"; + let real_ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let fake_ip = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + + let mut headers = HeaderMap::new(); + headers.insert(header_name, real_ip.to_string().parse().unwrap()); + headers.append(header_name, fake_ip.to_string().parse().unwrap()); + + let err = get_ip_from_unique_header(&headers, header_name) + .expect_err("Not unique header should fail"); + assert!(matches!(err, IpError::NotUnique(_))); + } + #[test] + fn test_unique_header_not_present() { + let header_name = "X-Real-IP"; + let headers = HeaderMap::new(); + + let err = get_ip_from_unique_header(&headers, header_name) + .expect_err("Missing header should fail"); + assert!(matches!(err, IpError::NotPresent(_))); + } + + #[test] + fn test_unique_header_invalid_value() { + let header_name = "X-Real-IP"; + let mut headers = HeaderMap::new(); + headers.insert(header_name, "invalid-ip".parse().unwrap()); + + let err = + get_ip_from_unique_header(&headers, header_name).expect_err("Invalid IP should fail"); + assert!(matches!(err, IpError::InvalidValue)); + } + + #[test] + fn test_unique_header_empty_value() { + let header_name = "X-Real-IP"; + let mut headers = HeaderMap::new(); + headers.insert(header_name, "".parse().unwrap()); + + let err = + get_ip_from_unique_header(&headers, header_name).expect_err("Invalid IP should fail"); + assert!(matches!(err, IpError::InvalidValue)); + } + + #[test] + fn test_rightmost_header_comma_separated() { + let header_name = "X-Forwarded-For"; + let ip1 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let ip2 = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + let ip3 = IpAddr::V4(Ipv4Addr::new(3, 3, 3, 3)); + + let mut headers = HeaderMap::new(); + headers.insert(header_name, format!("{},{},{}", ip1, ip2, ip3).parse().unwrap()); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 1).unwrap(); + assert_eq!(ip, ip3); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 2).unwrap(); + assert_eq!(ip, ip2); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 3).unwrap(); + assert_eq!(ip, ip1); + + let err = get_ip_from_rightmost_value(&headers, header_name, 4) + .expect_err("Not enough values should fail"); + assert!(matches!(err, IpError::NotEnoughValues { .. })); + } + + #[test] + fn test_rightmost_header_comma_space_separated() { + let header_name = "X-Forwarded-For"; + let ip1 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let ip2 = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + let ip3 = IpAddr::V4(Ipv4Addr::new(3, 3, 3, 3)); + + let mut headers = HeaderMap::new(); + headers.insert(header_name, format!("{}, {}, {}", ip1, ip2, ip3).parse().unwrap()); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 1).unwrap(); + assert_eq!(ip, ip3); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 2).unwrap(); + assert_eq!(ip, ip2); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 3).unwrap(); + assert_eq!(ip, ip1); + + let err = get_ip_from_rightmost_value(&headers, header_name, 4) + .expect_err("Not enough values should fail"); + assert!(matches!(err, IpError::NotEnoughValues { .. })); + } + + #[test] + fn test_rightmost_header_duplicated() { + // If the header appears multiple times, they should be joined together + // as if they were a single value. + let header_name = "X-Forwarded-For"; + let ip1 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let ip2 = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); + let ip3 = IpAddr::V4(Ipv4Addr::new(3, 3, 3, 3)); + let ip4 = IpAddr::V4(Ipv4Addr::new(4, 4, 4, 4)); + let ip5 = IpAddr::V4(Ipv4Addr::new(5, 5, 5, 5)); + + let mut headers = HeaderMap::new(); + headers.insert(header_name, format!("{},{},{}", ip1, ip2, ip3).parse().unwrap()); + headers.append(header_name, format!("{},{}", ip4, ip5).parse().unwrap()); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 1).unwrap(); + assert_eq!(ip, ip5); + + let ip = get_ip_from_rightmost_value(&headers, header_name, 5).unwrap(); + assert_eq!(ip, ip1); + + let err = get_ip_from_rightmost_value(&headers, header_name, 6) + .expect_err("Not enough values should fail"); + assert!(matches!(err, IpError::NotEnoughValues { .. })); + } + + #[test] + fn test_rightmost_header_not_present() { + let header_name = "X-Forwarded-For"; + let headers = HeaderMap::new(); + + let err = get_ip_from_rightmost_value(&headers, header_name, 1) + .expect_err("Missing header should fail"); + assert!(matches!(err, IpError::NotPresent(_))); + } + + #[test] + fn test_rightmost_header_invalid_value() { + let header_name = "X-Forwarded-For"; + let mut headers = HeaderMap::new(); + headers.insert(header_name, "invalid-ip".parse().unwrap()); + + let err = get_ip_from_rightmost_value(&headers, header_name, 1) + .expect_err("Invalid IP should fail"); + assert!(matches!(err, IpError::InvalidValue)); + } +} From 9782d227c300313f858cce543625d0ca7b1e0e9e Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Tue, 17 Mar 2026 13:59:49 -0700 Subject: [PATCH 17/30] improve tls/cert config testing --- crates/cli/src/docker_init.rs | 238 +++++++++++++++++++++++++++++ crates/common/src/config/signer.rs | 208 +++++++++++++++++++++++++ 2 files changed, 446 insertions(+) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 242a32cb..ee0267ee 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -835,6 +835,7 @@ mod tests { use cb_common::{ config::{ CommitBoostConfig, FileLogSettings, LogsSettings, MetricsConfig, StdoutLogSettings, + TlsMode, }, signer::{ProxyStore, SignerLoader}, }; @@ -1478,4 +1479,241 @@ mod tests { } Ok(()) } + + // ------------------------------------------------------------------------- + // Helpers for TLS tests + // ------------------------------------------------------------------------- + + fn local_signer_config_with_tls(certs_path: PathBuf) -> SignerConfig { + let mut config = local_signer_config(); + config.tls_mode = TlsMode::Certificate(certs_path); + config + } + + /// Returns a `ServiceCreationInfo` whose CB config has `pbs.with_signer = + /// true` and 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 + } + + // ------------------------------------------------------------------------- + // create_cert_binding + // ------------------------------------------------------------------------- + + #[test] + fn test_create_cert_binding_volume_string() { + let certs_path = Path::new("/my/certs"); + let vol = create_cert_binding(certs_path); + let expected = format!( + "/my/certs/{}:{}/{}:ro", + SIGNER_TLS_CERTIFICATE_NAME, + SIGNER_TLS_CERTIFICATES_PATH_DEFAULT, + SIGNER_TLS_CERTIFICATE_NAME + ); + assert_eq!(vol, Volumes::Simple(expected)); + } + + // ------------------------------------------------------------------------- + // add_tls_certs_volume + // ------------------------------------------------------------------------- + + #[test] + fn test_add_tls_certs_volume_happy_path() -> eyre::Result<()> { + let dir = tempfile::tempdir()?; + let certs_path = dir.path(); + 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 volumes = vec![]; + add_tls_certs_volume(&mut volumes, certs_path)?; + + assert_eq!(volumes.len(), 2); + assert!( + matches!(&volumes[0], Volumes::Simple(s) if s.contains(SIGNER_TLS_CERTIFICATE_NAME)) + ); + assert!(matches!(&volumes[1], Volumes::Simple(s) if s.contains(SIGNER_TLS_KEY_NAME))); + Ok(()) + } + + #[test] + fn test_add_tls_certs_volume_missing_cert_returns_error() -> eyre::Result<()> { + let dir = tempfile::tempdir()?; + let certs_path = dir.path(); + std::fs::write(certs_path.join(SIGNER_TLS_KEY_NAME), b"key")?; + + let result = add_tls_certs_volume(&mut vec![], certs_path); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("certificate or key not found")); + Ok(()) + } + + #[test] + fn test_add_tls_certs_volume_missing_key_returns_error() -> eyre::Result<()> { + let dir = tempfile::tempdir()?; + let certs_path = dir.path(); + std::fs::write(certs_path.join(SIGNER_TLS_CERTIFICATE_NAME), b"cert")?; + + let result = add_tls_certs_volume(&mut vec![], certs_path); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("certificate or key not found")); + Ok(()) + } + + #[test] + fn test_add_tls_certs_volume_missing_both_returns_error() -> eyre::Result<()> { + let dir = tempfile::tempdir()?; + let result = add_tls_certs_volume(&mut vec![], dir.path()); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn test_add_tls_certs_volume_creates_missing_directory() -> eyre::Result<()> { + let dir = tempfile::tempdir()?; + let certs_path = dir.path().join("new_certs_dir"); + assert!(!certs_path.exists(), "pre-condition: directory must not exist yet"); + + let result = add_tls_certs_volume(&mut vec![], &certs_path); + + // Directory created even though cert/key are absent + assert!(certs_path.exists(), "directory should have been created"); + // cert/key still missing → error + assert!(result.is_err()); + Ok(()) + } + + // ------------------------------------------------------------------------- + // create_pbs_service – TLS cert volume/env + // ------------------------------------------------------------------------- + + #[test] + fn test_create_pbs_service_with_tls_adds_cert_env_and_volume() -> eyre::Result<()> { + 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)); + Ok(()) + } + + #[test] + fn test_create_pbs_service_without_tls_no_cert_env() -> eyre::Result<()> { + let mut sc = minimal_service_config(); + 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)); + Ok(()) + } + + // ------------------------------------------------------------------------- + // create_signer_service_local – TLS cert volumes + // ------------------------------------------------------------------------- + + #[test] + fn test_create_signer_service_local_with_tls_adds_cert_and_key_volumes() -> 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 = sc.config_info.cb_config.signer.clone().unwrap(); + let loader = SignerLoader::File { key_path: "/keys/keys.json".into() }; + let service = create_signer_service_local(&mut sc, &signer_config, &loader, &None)?; + + assert!(has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + assert!(has_volume(&service, SIGNER_TLS_KEY_NAME)); + Ok(()) + } + + #[test] + fn test_create_signer_service_local_without_tls_no_cert_key_volumes() -> eyre::Result<()> { + let mut sc = minimal_service_config(); + let signer_config = local_signer_config(); + let loader = SignerLoader::File { key_path: "/keys/keys.json".into() }; + let service = create_signer_service_local(&mut sc, &signer_config, &loader, &None)?; + + // SIGNER_TLS_CERTIFICATES_PATH_ENV is always emitted by the signer service, + // but no cert.pem / key.pem volume bindings should exist in insecure mode. + assert!(!has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + assert!(!has_volume(&service, SIGNER_TLS_KEY_NAME)); + Ok(()) + } + + // ------------------------------------------------------------------------- + // create_signer_service_dirk – TLS cert volumes + // ------------------------------------------------------------------------- + + #[test] + fn test_create_signer_service_dirk_with_tls_adds_cert_and_key_volumes() -> 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, + )?; + + assert!(has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + assert!(has_volume(&service, SIGNER_TLS_KEY_NAME)); + Ok(()) + } + + #[test] + fn test_create_signer_service_dirk_without_tls_no_cert_key_volumes() -> 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, + )?; + + assert!(!has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + assert!(!has_volume(&service, SIGNER_TLS_KEY_NAME)); + Ok(()) + } + + // ------------------------------------------------------------------------- + // create_module_service – TLS cert env/volume + // ------------------------------------------------------------------------- + + #[test] + fn test_create_module_service_with_signer_tls_adds_cert_env_and_volume() -> eyre::Result<()> { + let module = commit_module(); + let mut sc = service_config_with_tls(PathBuf::from("/my/certs")); + let (_, service) = create_module_service(&module, "https://cb_signer:20000", &mut sc)?; + + assert!(has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV)); + assert!(has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + Ok(()) + } + + #[test] + fn test_create_module_service_without_signer_tls_no_cert_env() -> eyre::Result<()> { + let module = commit_module(); + let mut sc = minimal_service_config(); + let (_, service) = create_module_service(&module, "http://cb_signer:20000", &mut sc)?; + + assert!(!has_env_key(&service, SIGNER_TLS_CERTIFICATES_PATH_ENV)); + assert!(!has_volume(&service, SIGNER_TLS_CERTIFICATE_NAME)); + Ok(()) + } } diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 0744bb4b..039d00ce 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -425,11 +425,42 @@ pub fn load_module_signing_configs( #[cfg(test)] mod tests { + use std::num::NonZeroUsize; + use alloy::primitives::{Uint, b256}; use super::*; use crate::config::{LogsSettings, ModuleKind, PbsConfig, StaticModuleConfig, StaticPbsConfig}; + // Wrapper needed because TOML requires a top-level struct (can't serialize + // a bare enum). + #[derive(Serialize, Deserialize, Debug)] + struct TlsWrapper { + tls_mode: TlsMode, + } + + fn make_local_signer_config(tls_mode: TlsMode) -> SignerConfig { + SignerConfig { + host: Ipv4Addr::new(127, 0, 0, 1), + port: 20000, + docker_image: SIGNER_IMAGE_DEFAULT.to_string(), + jwt_auth_fail_limit: 3, + jwt_auth_fail_timeout_seconds: 300, + tls_mode, + reverse_proxy: ReverseProxyHeaderSetup::None, + inner: SignerType::Local { + loader: SignerLoader::File { key_path: PathBuf::from("/keys.json") }, + store: None, + }, + } + } + + async fn get_config_with_signer(tls_mode: TlsMode) -> CommitBoostConfig { + let mut cfg = get_base_config().await; + cfg.signer = Some(make_local_signer_config(tls_mode)); + cfg + } + async fn get_base_config() -> CommitBoostConfig { CommitBoostConfig { chain: Chain::Hoodi, @@ -707,4 +738,181 @@ mod tests { } Ok(()) } + + // ── TlsMode serde ──────────────────────────────────────────────────────── + + #[test] + fn test_tls_mode_insecure_roundtrip() -> Result<()> { + let original = TlsWrapper { tls_mode: TlsMode::Insecure }; + let toml_str = toml::to_string(&original)?; + let parsed: TlsWrapper = toml::from_str(&toml_str)?; + assert!(matches!(parsed.tls_mode, TlsMode::Insecure)); + Ok(()) + } + + #[test] + fn test_tls_mode_certificate_roundtrip() -> Result<()> { + let path = PathBuf::from("/certs"); + let original = TlsWrapper { tls_mode: TlsMode::Certificate(path.clone()) }; + let toml_str = toml::to_string(&original)?; + let parsed: TlsWrapper = toml::from_str(&toml_str)?; + match parsed.tls_mode { + TlsMode::Certificate(p) => assert_eq!(p, path), + TlsMode::Insecure => panic!("Expected Certificate variant"), + } + Ok(()) + } + + #[test] + fn test_tls_mode_insecure_from_toml() -> Result<()> { + let toml_str = r#" + [tls_mode] + type = "insecure" + "#; + let parsed: TlsWrapper = toml::from_str(toml_str)?; + assert!(matches!(parsed.tls_mode, TlsMode::Insecure)); + Ok(()) + } + + #[test] + fn test_tls_mode_certificate_from_toml() -> Result<()> { + let toml_str = r#" + [tls_mode] + type = "certificate" + path = "/custom/certs" + "#; + let parsed: TlsWrapper = toml::from_str(toml_str)?; + match parsed.tls_mode { + TlsMode::Certificate(p) => assert_eq!(p, PathBuf::from("/custom/certs")), + TlsMode::Insecure => panic!("Expected Certificate variant"), + } + Ok(()) + } + + // ── signer_uses_tls ─────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_signer_uses_tls_no_signer() { + let cfg = get_base_config().await; + assert!(!cfg.signer_uses_tls()); + } + + #[tokio::test] + async fn test_signer_uses_tls_insecure() { + let cfg = get_config_with_signer(TlsMode::Insecure).await; + assert!(!cfg.signer_uses_tls()); + } + + #[tokio::test] + async fn test_signer_uses_tls_certificate() { + let cfg = get_config_with_signer(TlsMode::Certificate(PathBuf::from("/certs"))).await; + assert!(cfg.signer_uses_tls()); + } + + // ── signer_certs_path ───────────────────────────────────────────────────── + + #[tokio::test] + async fn test_signer_certs_path_no_signer() { + let cfg = get_base_config().await; + assert!(cfg.signer_certs_path().is_none()); + } + + #[tokio::test] + async fn test_signer_certs_path_insecure() { + let cfg = get_config_with_signer(TlsMode::Insecure).await; + assert!(cfg.signer_certs_path().is_none()); + } + + #[tokio::test] + async fn test_signer_certs_path_certificate() { + let certs_path = PathBuf::from("/my/certs"); + let cfg = get_config_with_signer(TlsMode::Certificate(certs_path.clone())).await; + assert_eq!(cfg.signer_certs_path(), Some(&certs_path)); + } + + // ── signer_server_url ───────────────────────────────────────────────────── + + #[tokio::test] + async fn test_signer_server_url_no_signer_uses_default_port() { + let cfg = get_base_config().await; + assert_eq!(cfg.signer_server_url(12345), "http://cb_signer:12345"); + } + + #[tokio::test] + async fn test_signer_server_url_insecure_uses_http() { + let cfg = get_config_with_signer(TlsMode::Insecure).await; + assert_eq!(cfg.signer_server_url(9999), "http://cb_signer:20000"); + } + + #[tokio::test] + async fn test_signer_server_url_certificate_uses_https() { + let cfg = get_config_with_signer(TlsMode::Certificate(PathBuf::from("/certs"))).await; + assert_eq!(cfg.signer_server_url(9999), "https://cb_signer:20000"); + } + + #[tokio::test] + async fn test_signer_server_url_remote_returned_as_is() { + let remote_url = Url::parse("https://remote-signer.example.com:8080").unwrap(); + let mut cfg = get_base_config().await; + cfg.signer = Some(SignerConfig { + host: Ipv4Addr::new(127, 0, 0, 1), + port: 20000, + docker_image: SIGNER_IMAGE_DEFAULT.to_string(), + jwt_auth_fail_limit: 3, + jwt_auth_fail_timeout_seconds: 300, + tls_mode: TlsMode::Insecure, + reverse_proxy: ReverseProxyHeaderSetup::None, + inner: SignerType::Remote { url: remote_url.clone() }, + }); + assert_eq!(cfg.signer_server_url(9999), remote_url.to_string()); + } + + // ── ReverseProxyHeaderSetup Display ────────────────────────────────────── + + #[test] + fn test_reverse_proxy_display_none() { + assert_eq!(ReverseProxyHeaderSetup::None.to_string(), "None"); + } + + #[test] + fn test_reverse_proxy_display_unique() { + let rp = ReverseProxyHeaderSetup::Unique { header: "X-Forwarded-For".to_string() }; + assert_eq!(rp.to_string(), r#""X-Forwarded-For (unique)""#); + } + + #[test] + fn test_reverse_proxy_display_rightmost_1st() { + let rp = ReverseProxyHeaderSetup::Rightmost { + header: "X-Real-IP".to_string(), + trusted_count: NonZeroUsize::new(1).unwrap(), + }; + assert_eq!(rp.to_string(), r#""X-Real-IP (1st from the right)""#); + } + + #[test] + fn test_reverse_proxy_display_rightmost_2nd() { + let rp = ReverseProxyHeaderSetup::Rightmost { + header: "X-Real-IP".to_string(), + trusted_count: NonZeroUsize::new(2).unwrap(), + }; + assert_eq!(rp.to_string(), r#""X-Real-IP (2nd from the right)""#); + } + + #[test] + fn test_reverse_proxy_display_rightmost_3rd() { + let rp = ReverseProxyHeaderSetup::Rightmost { + header: "X-Real-IP".to_string(), + trusted_count: NonZeroUsize::new(3).unwrap(), + }; + assert_eq!(rp.to_string(), r#""X-Real-IP (3rd from the right)""#); + } + + #[test] + fn test_reverse_proxy_display_rightmost_nth() { + let rp = ReverseProxyHeaderSetup::Rightmost { + header: "CF-Connecting-IP".to_string(), + trusted_count: NonZeroUsize::new(5).unwrap(), + }; + assert_eq!(rp.to_string(), r#""CF-Connecting-IP (5th from the right)""#); + } } From bb28eb08d8a85bd6912978b9ff014683a521f6ba Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 20 Mar 2026 14:57:33 -0700 Subject: [PATCH 18/30] more util test coverage and remove duplicate env read from docker_init.rs --- Cargo.lock | 1 + crates/cli/src/docker_init.rs | 1 - crates/common/Cargo.toml | 3 + crates/common/src/commit/request.rs | 23 ++++ crates/common/src/config/pbs.rs | 23 ++-- crates/common/src/config/utils.rs | 190 +++++++++++++++++++++++++++- crates/common/src/utils.rs | 152 +++++++++++++++++++++- 7 files changed, 371 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e01361..11a14635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1825,6 +1825,7 @@ dependencies = [ "serde_yaml", "sha2 0.10.9", "ssz_types", + "tempfile", "thiserror 2.0.17", "tokio", "toml", diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index ee0267ee..7976ce17 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -344,7 +344,6 @@ fn create_signer_service_local( let mut envs = IndexMap::from([ get_env_val(CONFIG_ENV, CONFIG_DEFAULT), get_env_same(JWTS_ENV), - get_env_same(JWTS_ENV), get_env_same(ADMIN_JWT_ENV), get_env_val(SIGNER_TLS_CERTIFICATES_PATH_ENV, SIGNER_TLS_CERTIFICATES_PATH_DEFAULT), ]); diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index c9a57726..ae946fee 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -52,3 +52,6 @@ tree_hash.workspace = true tree_hash_derive.workspace = true unicode-normalization.workspace = true url.workspace = true + +[dev-dependencies] + tempfile.workspace = true \ No newline at end of file diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index b1439e87..a64e9a67 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -318,6 +318,29 @@ mod tests { let _: SignedProxyDelegationEcdsa = serde_json::from_str(data).unwrap(); } + #[test] + fn test_reload_request_jwt_secrets_present() { + let data = r#"{"jwt_secrets": "module_a=secret1,module_b=secret2"}"#; + let req: ReloadRequest = serde_json::from_str(data).unwrap(); + let secrets = req.jwt_secrets.expect("should have secrets"); + assert_eq!(secrets.get(&ModuleId("module_a".into())), Some(&"secret1".to_string())); + assert_eq!(secrets.get(&ModuleId("module_b".into())), Some(&"secret2".to_string())); + } + + #[test] + fn test_reload_request_jwt_secrets_absent() { + let data = r#"{}"#; + let req: ReloadRequest = serde_json::from_str(data).unwrap(); + assert!(req.jwt_secrets.is_none()); + } + + #[test] + fn test_reload_request_jwt_secrets_invalid_format() { + // Missing '=' separator — decode_string_to_map should fail + let data = r#"{"jwt_secrets": "bad_value_no_equals"}"#; + assert!(serde_json::from_str::(data).is_err()); + } + #[test] fn test_decode_response_proxy_map() { let data = r#"{ diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 80b1b073..30964a20 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -183,19 +183,16 @@ impl PbsConfig { } if let Some(rpc_url) = &self.rpc_url { - // TODO: remove this once we support chain ids for custom chains - if !matches!(chain, Chain::Custom { .. }) { - let provider = ProviderBuilder::new().connect_http(rpc_url.clone()); - let chain_id = provider.get_chain_id().await?; - let chain_id_big = U256::from(chain_id); - ensure!( - chain_id_big == chain.id(), - "Rpc url is for the wrong chain, expected: {} ({:?}) got {}", - chain.id(), - chain, - chain_id_big - ); - } + let provider = ProviderBuilder::new().connect_http(rpc_url.clone()); + let chain_id = provider.get_chain_id().await?; + let chain_id_big = U256::from(chain_id); + ensure!( + chain_id_big == chain.id(), + "Rpc url is for the wrong chain, expected: {} ({:?}) got {}", + chain.id(), + chain, + chain_id_big + ); } ensure!( diff --git a/crates/common/src/config/utils.rs b/crates/common/src/config/utils.rs index c62fda78..579825b6 100644 --- a/crates/common/src/config/utils.rs +++ b/crates/common/src/config/utils.rs @@ -98,20 +98,68 @@ pub fn decode_string_to_map(raw: &str) -> Result> { #[cfg(test)] mod tests { + use std::sync::Mutex; + use super::*; use crate::utils::TestRandomSeed; - /// TODO: This was only used by the old JWT loader, can it be removed now? + // Serializes all tests that read/write environment variables. + // std::env::set_var is unsafe (Rust 1.81+) because mutating `environ` + // while another thread reads it is UB at the OS level. Holding this + // lock ensures our Rust threads don't race each other. + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + /// Sets or removes env vars for the duration of `f`, then restores the + /// original values. Pass `Some("val")` to set, `None` to ensure absent. + fn with_env(vars: &[(&str, Option<&str>)], f: impl FnOnce() -> R) -> R { + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let saved: Vec<(&str, Option)> = + vars.iter().map(|(k, _)| (*k, std::env::var(k).ok())).collect(); + for (k, v) in vars { + match v { + Some(val) => unsafe { std::env::set_var(k, val) }, + None => unsafe { std::env::remove_var(k) }, + } + } + let result = f(); + for (k, old) in &saved { + match old { + Some(v) => unsafe { std::env::set_var(k, v) }, + None => unsafe { std::env::remove_var(k) }, + } + } + result + } + + // Minimal TOML-deserializable type used by load_from_file / load_file_from_env + // tests. + #[derive(serde::Deserialize, Debug, PartialEq)] + struct TestConfig { + value: String, + } + + // ── decode_string_to_map ───────────────────────────────────────────────── + #[test] - fn test_decode_string_to_map() { - let raw = " KEY=VALUE , KEY2=value2 "; + fn test_decode_string_to_map_single_pair() { + let map = decode_string_to_map("ONLY=ONE").unwrap(); + assert_eq!(map.len(), 1); + assert_eq!(map.get(&ModuleId("ONLY".into())), Some(&"ONE".to_string())); + } - let map = decode_string_to_map(raw).unwrap(); + #[test] + fn test_decode_string_to_map_empty_string() { + // An empty string yields one token with no `=`, which is invalid. + assert!(decode_string_to_map("").is_err()); + } - assert_eq!(map.get(&ModuleId("KEY".into())), Some(&"VALUE".to_string())); - assert_eq!(map.get(&ModuleId("KEY2".into())), Some(&"value2".to_string())); + #[test] + fn test_decode_string_to_map_malformed_no_equals() { + assert!(decode_string_to_map("KEYONLY").is_err()); } + // ── remove_duplicate_keys ──────────────────────────────────────────────── + #[test] fn test_remove_duplicate_keys() { let key1 = BlsPublicKey::test_random(); @@ -123,4 +171,134 @@ mod tests { assert!(unique_keys.contains(&key1)); assert!(unique_keys.contains(&key2)); } + + // ── load_env_var ───────────────────────────────────────────────────────── + + #[test] + fn test_load_env_var_present() { + with_env(&[("CB_TEST_LOAD_ENV_VAR", Some("hello"))], || { + assert_eq!(load_env_var("CB_TEST_LOAD_ENV_VAR").unwrap(), "hello"); + }); + } + + #[test] + fn test_load_env_var_absent() { + with_env(&[("CB_TEST_LOAD_ENV_VAR_ABSENT", None)], || { + let err = load_env_var("CB_TEST_LOAD_ENV_VAR_ABSENT").unwrap_err(); + assert!(err.to_string().contains("CB_TEST_LOAD_ENV_VAR_ABSENT")); + }); + } + + // ── load_optional_env_var ──────────────────────────────────────────────── + + #[test] + fn test_load_optional_env_var_present() { + with_env(&[("CB_TEST_OPT_VAR", Some("world"))], || { + assert_eq!(load_optional_env_var("CB_TEST_OPT_VAR"), Some("world".to_string())); + }); + } + + #[test] + fn test_load_optional_env_var_absent() { + with_env(&[("CB_TEST_OPT_VAR_ABSENT", None)], || { + assert_eq!(load_optional_env_var("CB_TEST_OPT_VAR_ABSENT"), None); + }); + } + + // ── load_from_file ─────────────────────────────────────────────────────── + + #[test] + fn test_load_from_file_valid() { + use std::io::Write as _; + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"value = \"hello\"").unwrap(); + let path = file.path().to_path_buf(); + + let (config, returned_path): (TestConfig, _) = load_from_file(&path).unwrap(); + assert_eq!(config.value, "hello"); + assert_eq!(returned_path, path); + } + + #[test] + fn test_load_from_file_missing() { + let result: eyre::Result<(TestConfig, _)> = + load_from_file("/nonexistent/cb_test_path/file.toml"); + assert!(result.is_err()); + } + + #[test] + fn test_load_from_file_invalid_toml() { + use std::io::Write as _; + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"not valid toml !!!{{").unwrap(); + + let result: eyre::Result<(TestConfig, _)> = load_from_file(file.path()); + assert!(result.is_err()); + } + + // ── load_file_from_env ─────────────────────────────────────────────────── + + #[test] + fn test_load_file_from_env_ok() { + use std::io::Write as _; + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(b"value = \"from_env\"").unwrap(); + let path = file.path().to_str().unwrap().to_owned(); + + with_env(&[("CB_TEST_FILE_ENV", Some(&path))], || { + let (config, _): (TestConfig, _) = load_file_from_env("CB_TEST_FILE_ENV").unwrap(); + assert_eq!(config.value, "from_env"); + }); + } + + #[test] + fn test_load_file_from_env_var_not_set() { + with_env(&[("CB_TEST_FILE_ENV_ABSENT", None)], || { + let result: eyre::Result<(TestConfig, _)> = + load_file_from_env("CB_TEST_FILE_ENV_ABSENT"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("CB_TEST_FILE_ENV_ABSENT")); + }); + } + + // ── load_jwt_secrets ───────────────────────────────────────────────────── + + #[test] + fn test_load_jwt_secrets_ok() { + with_env( + &[ + (ADMIN_JWT_ENV, Some("admin_secret")), + (JWTS_ENV, Some("MODULE1=secret1,MODULE2=secret2")), + ], + || { + let (admin_jwt, secrets) = load_jwt_secrets().unwrap(); + assert_eq!(admin_jwt, "admin_secret"); + assert_eq!(secrets.get(&ModuleId("MODULE1".into())), Some(&"secret1".to_string())); + assert_eq!(secrets.get(&ModuleId("MODULE2".into())), Some(&"secret2".to_string())); + }, + ); + } + + #[test] + fn test_load_jwt_secrets_missing_admin_jwt() { + with_env(&[(ADMIN_JWT_ENV, None), (JWTS_ENV, Some("MODULE1=secret1"))], || { + let err = load_jwt_secrets().unwrap_err(); + assert!(err.to_string().contains(ADMIN_JWT_ENV)); + }); + } + + #[test] + fn test_load_jwt_secrets_missing_jwts() { + with_env(&[(ADMIN_JWT_ENV, Some("admin_secret")), (JWTS_ENV, None)], || { + let err = load_jwt_secrets().unwrap_err(); + assert!(err.to_string().contains(JWTS_ENV)); + }); + } + + #[test] + fn test_load_jwt_secrets_malformed_jwts() { + with_env(&[(ADMIN_JWT_ENV, Some("admin_secret")), (JWTS_ENV, Some("MALFORMED"))], || { + assert!(load_jwt_secrets().is_err()); + }); + } } diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 6f3d4dc4..78fe5bbe 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -572,8 +572,14 @@ pub fn bls_pubkey_from_hex_unchecked(hex: &str) -> BlsPublicKey { mod test { use alloy::primitives::keccak256; - use super::{create_jwt, decode_jwt, validate_jwt}; - use crate::types::{Jwt, ModuleId}; + use super::{ + create_admin_jwt, create_jwt, decode_admin_jwt, decode_jwt, random_jwt_secret, + validate_admin_jwt, validate_jwt, + }; + use crate::{ + constants::SIGNER_JWT_EXPIRATION, + types::{Jwt, JwtAdminClaims, ModuleId}, + }; #[test] fn test_jwt_validation_no_payload_hash() { @@ -637,4 +643,146 @@ mod test { assert!(response.is_err()); assert_eq!(response.unwrap_err().to_string(), "InvalidSignature"); } + + // ── validate_jwt: route and secret errors ──────────────────────────────── + + #[test] + fn test_validate_jwt_wrong_route() { + let jwt = create_jwt(&ModuleId("MOD".into()), "secret", "/correct/route", None).unwrap(); + let err = validate_jwt(jwt, "secret", "/wrong/route", None).unwrap_err(); + assert!(err.to_string().contains("Token route does not match")); + } + + #[test] + fn test_validate_jwt_wrong_secret() { + let jwt = create_jwt(&ModuleId("MOD".into()), "correct_secret", "/route", None).unwrap(); + let err = validate_jwt(jwt, "wrong_secret", "/route", None).unwrap_err(); + assert_eq!(err.to_string(), "InvalidSignature"); + } + + // ── validate_jwt: payload hash mismatch branches ───────────────────────── + + #[test] + fn test_validate_jwt_payload_hash_mismatch() { + let payload_a = b"payload_a"; + let payload_b = b"payload_b"; + let jwt = create_jwt(&ModuleId("MOD".into()), "secret", "/route", Some(payload_a)).unwrap(); + let err = validate_jwt(jwt, "secret", "/route", Some(payload_b)).unwrap_err(); + assert!(err.to_string().contains("Payload hash does not match")); + } + + #[test] + fn test_validate_jwt_hash_present_but_no_payload_provided() { + let payload = b"some payload"; + let jwt = create_jwt(&ModuleId("MOD".into()), "secret", "/route", Some(payload)).unwrap(); + let err = validate_jwt(jwt, "secret", "/route", None).unwrap_err(); + assert!( + err.to_string().contains("JWT contains a payload hash but no payload was provided") + ); + } + + #[test] + fn test_validate_jwt_no_hash_but_payload_provided() { + let jwt = create_jwt(&ModuleId("MOD".into()), "secret", "/route", None).unwrap(); + let err = validate_jwt(jwt, "secret", "/route", Some(b"unexpected")).unwrap_err(); + assert!(err.to_string().contains("JWT does not contain a payload hash")); + } + + // ── admin JWT roundtrip ────────────────────────────────────────────────── + + #[test] + fn test_admin_jwt_roundtrip_no_payload() { + let jwt = create_admin_jwt("admin_secret".into(), "/admin/route", None).unwrap(); + let claims = decode_admin_jwt(jwt.clone()).unwrap(); + assert!(claims.admin); + assert_eq!(claims.route, "/admin/route"); + assert!(claims.payload_hash.is_none()); + validate_admin_jwt(jwt, "admin_secret", "/admin/route", None).unwrap(); + } + + #[test] + fn test_admin_jwt_roundtrip_with_payload() { + let payload = b"admin payload"; + let jwt = create_admin_jwt("admin_secret".into(), "/admin/route", Some(payload)).unwrap(); + let claims = decode_admin_jwt(jwt.clone()).unwrap(); + assert!(claims.admin); + assert_eq!(claims.payload_hash, Some(keccak256(payload))); + validate_admin_jwt(jwt, "admin_secret", "/admin/route", Some(payload)).unwrap(); + } + + // ── validate_admin_jwt: route, secret, admin flag errors ───────────────── + + #[test] + fn test_validate_admin_jwt_wrong_route() { + let jwt = create_admin_jwt("admin_secret".into(), "/correct/route", None).unwrap(); + let err = validate_admin_jwt(jwt, "admin_secret", "/wrong/route", None).unwrap_err(); + assert!(err.to_string().contains("Token route does not match")); + } + + #[test] + fn test_validate_admin_jwt_wrong_secret() { + let jwt = create_admin_jwt("correct_secret".into(), "/route", None).unwrap(); + let err = validate_admin_jwt(jwt, "wrong_secret", "/route", None).unwrap_err(); + assert_eq!(err.to_string(), "InvalidSignature"); + } + + #[test] + fn test_validate_admin_jwt_admin_false() { + // Craft a JWT whose claims have admin: false — something create_admin_jwt + // never produces — to exercise the explicit admin flag guard. + let claims = JwtAdminClaims { + admin: false, + route: "/route".into(), + exp: jsonwebtoken::get_current_timestamp() + SIGNER_JWT_EXPIRATION, + payload_hash: None, + }; + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claims, + &jsonwebtoken::EncodingKey::from_secret(b"secret"), + ) + .unwrap(); + let jwt = Jwt::from(token); + let err = validate_admin_jwt(jwt, "secret", "/route", None).unwrap_err(); + assert!(err.to_string().contains("Token is not admin")); + } + + // ── validate_admin_jwt: payload hash mismatch branches ─────────────────── + + #[test] + fn test_validate_admin_jwt_payload_hash_mismatch() { + let payload_a = b"admin_payload_a"; + let payload_b = b"admin_payload_b"; + let jwt = create_admin_jwt("secret".into(), "/route", Some(payload_a)).unwrap(); + let err = validate_admin_jwt(jwt, "secret", "/route", Some(payload_b)).unwrap_err(); + assert!(err.to_string().contains("Payload hash does not match")); + } + + #[test] + fn test_validate_admin_jwt_hash_present_but_no_payload_provided() { + let payload = b"admin payload"; + let jwt = create_admin_jwt("secret".into(), "/route", Some(payload)).unwrap(); + let err = validate_admin_jwt(jwt, "secret", "/route", None).unwrap_err(); + assert!( + err.to_string().contains("JWT contains a payload hash but no payload was provided") + ); + } + + #[test] + fn test_validate_admin_jwt_no_hash_but_payload_provided() { + let jwt = create_admin_jwt("secret".into(), "/route", None).unwrap(); + let err = validate_admin_jwt(jwt, "secret", "/route", Some(b"unexpected")).unwrap_err(); + assert!(err.to_string().contains("JWT does not contain a payload hash")); + } + + // ── random_jwt_secret ──────────────────────────────────────────────────── + + #[test] + fn test_random_jwt_secret() { + let secret = random_jwt_secret(); + assert_eq!(secret.len(), 32); + assert!(secret.chars().all(|c| c.is_ascii_alphanumeric())); + // Two calls should produce distinct values with overwhelming probability. + assert_ne!(secret, random_jwt_secret()); + } } From 511d07dba7a8c9bf104f50c3fb4b4ccbda809557 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 22 Mar 2026 10:50:08 -0700 Subject: [PATCH 19/30] Bump lh from v8.0.0-rc.0 to stable v8.0.0, and bump rust from 1.89 to 1.91 as required. Add audit.toml to ignore irrevelent audit error in CI. --- .cargo/audit.toml | 27 + Cargo.lock | 2353 +++++++++++++++-------------- Cargo.toml | 10 +- crates/common/Cargo.toml | 1 + crates/common/src/config/mux.rs | 2 +- docs/docs/get_started/building.md | 2 +- docs/docs/get_started/overview.md | 2 +- justfile | 3 +- provisioning/build.Dockerfile | 2 +- rust-toolchain.toml | 2 +- 10 files changed, 1243 insertions(+), 1161 deletions(-) create mode 100644 .cargo/audit.toml diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000..38e462f5 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,27 @@ +# RUSTSEC-2026-0049: CRL revocation checking bug in rustls-webpki 0.101.7. +# +# Background: CRL (Certificate Revocation List) checking is an optional TLS +# feature where a client fetches a list of revoked certificates from URLs +# embedded in the cert itself, to confirm it hasn't been invalidated since +# issuance. This is distinct from normal certificate validation. +# +# The bug: when a cert lists multiple CRL distribution point URLs, only the +# first URL is checked; the rest are silently ignored. This matters only when +# CRL checking is enabled AND the UnknownStatusPolicy is set to Allow (meaning +# "if I can't determine revocation status, accept the cert anyway"). With that +# combination, a revoked certificate from a compromised CA could be accepted. +# +# Why this does not affect Commit-Boost: the vulnerable code path is never +# reached because no code in this codebase enables CRL checking at all. +# TLS is used in four places: (1) relay communication via reqwest with +# rustls-tls uses default CA validation with no CRL configured; (2) the signer +# server presents a TLS certificate but does not check client revocation; +# (3) the signer client pins a single self-signed certificate via +# add_root_certificate — CRL is irrelevant for self-signed certs; (4) the Dirk +# remote signer uses mTLS with a custom CA but again no CRL. In all cases the +# buggy CRL code in rustls-webpki is never invoked. +# +# Blocked on sigp/lighthouse upgrading past v8.0.1 without a compilation +# regression (SseEventSource missing cfg guard in eth2 error.rs). +[advisories] +ignore = ["RUSTSEC-2026-0049"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 11a14635..b8b001f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", "cipher 0.3.0", - "cpufeatures", + "cpufeatures 0.2.17", "ctr 0.8.0", "opaque-debug", ] @@ -38,7 +38,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -70,9 +70,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6cfe35f100bc496007c9a00f90b88bdf565f1421d4c707c9f07e0717e2aaad" +checksum = "4973038846323e4e69a433916522195dce2947770076c03078fc21c80ea0f1c4" dependencies = [ "alloy-consensus", "alloy-contract", @@ -96,28 +96,29 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.14" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf01dd83a1ca5e4807d0ca0223c9615e211ce5db0a9fd1443c2778cacf89b546" +checksum = "9247f0a399ef71aeb68f497b2b8fb348014f742b50d3b83b1e00dfe1b7d64b3d" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "num_enum", "strum", ] [[package]] name = "alloy-consensus" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59094911f05dbff1cf5b29046a00ef26452eccc8d47136d50a47c0cf22f00c85" +checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728" dependencies = [ "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-serde", "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -128,18 +129,18 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-consensus-any" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903cb8f728107ca27c816546f15be38c688df3c381d7bd1a4a9f215effc1ddb4" +checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-serde", "serde", @@ -147,16 +148,16 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03df5cb3b428ac96b386ad64c11d5c6e87a5505682cf1fbd6f8f773e9eda04f6" +checksum = "ca63b7125a981415898ffe2a2a696c83696c9c6bdb1671c8a912946bbd8e49e7" dependencies = [ "alloy-consensus", "alloy-dyn-abi", "alloy-json-abi", "alloy-network", "alloy-network-primitives", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-provider", "alloy-pubsub", "alloy-rpc-types-eth", @@ -165,36 +166,36 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-core" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575053cea24ea8cb7e775e39d5c53c33b19cfd0ca1cf6c0fd653f3d8c682095f" +checksum = "23e8604b0c092fabc80d075ede181c9b9e596249c70b99253082d7e689836529" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-sol-types", ] [[package]] name = "alloy-dyn-abi" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6c2905bafc2df7ccd32ca3af13f0b0d82f2e2ff9dfbeb12196c0d978d5c0deb" +checksum = "cc2db5c583aaef0255aa63a4fe827f826090142528bba48d1bf4119b62780cad" dependencies = [ "alloy-json-abi", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-sol-type-parser", "alloy-sol-types", "itoa", "serde", "serde_json", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -203,50 +204,66 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "crc", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", + "borsh", "k256", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +dependencies = [ + "alloy-primitives 1.5.7", + "alloy-rlp", + "borsh", + "serde", ] [[package]] name = "alloy-eips" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7f1c9a1ccc7f3e03c36976455751a6166a4f0d2d2c530c3f87dfe7d0cdc836" +checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8" dependencies = [ "alloy-eip2124", "alloy-eip2930", "alloy-eip7702", - "alloy-primitives 1.4.0", + "alloy-eip7928", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", "derive_more", "either", @@ -255,30 +272,31 @@ dependencies = [ "serde", "serde_with", "sha2 0.10.9", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-genesis" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1421f6c9d15e5b86afbfe5865ca84dea3b9f77173a0963c1a2ee4e626320ada9" +checksum = "7c9cf3b99f46615fbf7dc1add0c96553abb7bf88fc9ec70dfbe7ad0b47ba7fe8" dependencies = [ "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-serde", "alloy-trie", + "borsh", "serde", "serde_with", ] [[package]] name = "alloy-json-abi" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2acb6637a9c0e1cdf8971e0ced8f3fa34c04c5e9dccf6bb184f6a64fe0e37d8" +checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-sol-type-parser", "serde", "serde_json", @@ -286,31 +304,31 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f763621707fa09cece30b73ecc607eb43fd7a72451fe3b46f645b905086926" +checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-sol-types", - "http 1.3.1", + "http 1.4.0", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] [[package]] name = "alloy-network" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f59a869fa4b4c3a7f08b1c8cb79aec61c29febe6e24a24fe0fcfded8a9b5703" +checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762" dependencies = [ "alloy-consensus", "alloy-consensus-any", "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-any", "alloy-rpc-types-eth", "alloy-serde", @@ -322,27 +340,27 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-network-primitives" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e9374c667c95c41177602ebe6f6a2edd455193844f011d973d374b65501b38" +checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-serde", "serde", ] [[package]] name = "alloy-primitives" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" +checksum = "777d58b30eb9a4db0e5f59bc30e8c2caef877fee7dc8734cf242a51a60f22e05" dependencies = [ "alloy-rlp", "bytes", @@ -351,7 +369,7 @@ dependencies = [ "derive_more", "foldhash 0.1.5", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.13.0", "itoa", "k256", "keccak-asm", @@ -367,9 +385,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b77f7d5e60ad8ae6bd2200b8097919712a07a6db622a4b201e7ead6166f02e5" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" dependencies = [ "alloy-rlp", "bytes", @@ -377,27 +395,27 @@ dependencies = [ "const-hex", "derive_more", "foldhash 0.2.0", - "getrandom 0.3.3", - "hashbrown 0.16.0", - "indexmap 2.11.4", + "getrandom 0.4.2", + "hashbrown 0.16.1", + "indexmap 2.13.0", "itoa", "k256", "keccak-asm", "paste", "proptest", "rand 0.9.2", + "rapidhash", "ruint", "rustc-hash", "serde", "sha3", - "tiny-keccak", ] [[package]] name = "alloy-provider" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77818b7348bd5486491a5297579dbfe5f706a81f8e1f5976393025f1e22a7c7d" +checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870" dependencies = [ "alloy-chains", "alloy-consensus", @@ -405,7 +423,7 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-anvil", @@ -429,10 +447,10 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -441,12 +459,12 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "249b45103a66c9ad60ad8176b076106d03a2399a37f0ee7b0e03692e6b354cb9" +checksum = "e8bd82953194dec221aa4cbbbb0b1e2df46066fe9d0333ac25b43a311e122d13" dependencies = [ "alloy-json-rpc", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-transport", "auto_impl", "bimap", @@ -456,16 +474,16 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tracing", "wasmtimer", ] [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -474,23 +492,23 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "alloy-rpc-client" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2430d5623e428dd012c6c2156ae40b7fe638d6fca255e3244e0fba51fa698e93" +checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f" dependencies = [ "alloy-json-rpc", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-pubsub", "alloy-transport", "alloy-transport-http", @@ -498,12 +516,12 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "wasmtimer", @@ -511,11 +529,11 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e131624d08a25cfc40557041e7dc42e1182fa1153e7592d120f769a1edce56" +checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-anvil", "alloy-rpc-types-beacon", "alloy-rpc-types-debug", @@ -529,11 +547,11 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65e3266095e6d8e8028aab5f439c6b8736c5147314f7e606c61597e014cb8a0" +checksum = "e0a3100b76987c1b1dc81f3abe592b7edc29e92b1242067a69d65e0030b35cf9" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -541,9 +559,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07429a1099cd17227abcddb91b5e38c960aaeb02a6967467f5bb561fbe716ac6" +checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -552,30 +570,31 @@ dependencies = [ [[package]] name = "alloy-rpc-types-beacon" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e0e876b20eb9debf316d3e875536f389070635250f22b5a678cf4632a3e0cf" +checksum = "4a22e13215866f5dfd5d3278f4c41f1fad9410dc68ce39022f58593c873c26f8" dependencies = [ "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-engine", + "derive_more", "ethereum_ssz", "ethereum_ssz_derive", "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", "tree_hash", "tree_hash_derive 0.10.0", ] [[package]] name = "alloy-rpc-types-debug" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeff305b7d10cc1c888456d023e7bb8a5ea82e9e42b951e37619b88cc1a1486d" +checksum = "e1b21e1ad18ff1b31ff1030e046462ab8168cf8894e6778cd805c8bdfe2bd649" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "derive_more", "serde", "serde_with", @@ -583,13 +602,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222ecadcea6aac65e75e32b6735635ee98517aa63b111849ee01ae988a71d685" +checksum = "e4ac61f03f1edabccde1c687b5b25fff28f183afee64eaa2e767def3929e4457" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-serde", "derive_more", @@ -602,15 +621,15 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db46b0901ee16bbb68d986003c66dcb74a12f9d9b3c44f8e85d51974f2458f0f" +checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9" dependencies = [ "alloy-consensus", "alloy-consensus-any", "alloy-eips", "alloy-network-primitives", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "alloy-serde", "alloy-sol-types", @@ -618,30 +637,30 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-rpc-types-trace" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f10620724bd45f80c79668a8cdbacb6974f860686998abce28f6196ae79444" +checksum = "1ad79f1e27e161943b5a4f99fe5534ef0849876214be411e0032c12f38e94daa" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-eth", "alloy-serde", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-rpc-types-txpool" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864f41befa90102d4e02327679699a7e9510930e2924c529e31476086609fa89" +checksum = "d459f902a2313737bc66d18ed094c25d2aeb268b74d98c26bbbda2aa44182ab0" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rpc-types-eth", "alloy-serde", "serde", @@ -649,84 +668,84 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5413814be7a22fbc81e0f04a2401fcc3eb25e56fd53b04683e8acecc6e1fe01b" +checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "serde", "serde_json", ] [[package]] name = "alloy-signer" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53410a18a61916e2c073a6519499514e027b01e77eeaf96acd1df7cf96ef6bb2" +checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "async-trait", "auto_impl", "either", "elliptic-curve", "k256", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-signer-local" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6006c4cbfa5d08cadec1fcabea6cb56dc585a30a9fce40bcf81e307d6a71c8e" +checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-signer", "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-sol-macro" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c84c3637bee9b5c4a4d2b93360ee16553d299c3b932712353caf1cea76d0e6" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a882aa4e1790063362434b9b40d358942b188477ac1c44cfb8a52816ffc0cc17" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.11.4", + "indexmap 2.13.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "sha3", + "syn 2.0.117", "syn-solidity", - "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5772107f9bb265d8d8c86e0733937bb20d0857ea5425b1b6ddf51a9804042" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" dependencies = [ "alloy-json-abi", "const-hex", @@ -736,40 +755,39 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 2.0.117", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e188b939aa4793edfaaa099cb1be4e620036a775b4bdf24fdc56f1cd6fd45890" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" dependencies = [ "serde", - "winnow", + "winnow 0.7.15", ] [[package]] name = "alloy-sol-types" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c8a9a909872097caffc05df134e5ef2253a1cdb56d3a9cf0052a042ac763f9" +checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" dependencies = [ "alloy-json-abi", - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-sol-macro", "serde", ] [[package]] name = "alloy-transport" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94ee404368a3d9910dfe61b203e888c6b0e151a50e147f95da8baff9f9c7763" +checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53" dependencies = [ "alloy-json-rpc", - "alloy-primitives 1.4.0", "auto_impl", "base64 0.22.1", "derive_more", @@ -778,9 +796,9 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "wasmtimer", @@ -788,24 +806,25 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8a6338d594f6c6481292215ee8f2fd7b986c80aba23f3f44e761a8658de78" +checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" dependencies = [ "alloy-json-rpc", "alloy-transport", - "reqwest 0.12.23", + "itertools 0.14.0", + "reqwest 0.12.28", "serde_json", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", ] [[package]] name = "alloy-transport-ipc" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17a37a8ca18006fa0a58c7489645619ff58cfa073f2b29c4e052c9bd114b123a" +checksum = "c2ef85688e5ac2da72afc804e0a1f153a1f309f05a864b1998bbbed7804dbaab" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -823,15 +842,14 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "679b0122b7bca9d4dc5eb2c0549677a3c53153f6e232f23f4b3ba5575f74ebde" +checksum = "b9f00445db69d63298e2b00a0ea1d859f00e6424a3144ffc5eba9c31da995e16" dependencies = [ "alloy-pubsub", "alloy-transport", "futures", - "http 1.3.1", - "rustls 0.23.32", + "http 1.4.0", "serde_json", "tokio", "tokio-tungstenite", @@ -841,31 +859,30 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.1" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", - "arrayvec", "derive_more", "nybbles", "serde", "smallvec", + "thiserror 2.0.18", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.0.37" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64c09ec565a90ed8390d82aa08cd3b22e492321b96cb4a3d4f58414683c9e2f" +checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" dependencies = [ - "alloy-primitives 1.4.0", "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -885,9 +902,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -900,44 +917,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -950,9 +967,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] [[package]] name = "archery" @@ -1048,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1086,7 +1106,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1163,15 +1183,12 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" dependencies = [ "anstyle", "bstr", @@ -1201,7 +1218,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1212,7 +1229,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1240,7 +1257,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1251,9 +1268,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.14.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -1261,16 +1278,14 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.2" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ - "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libloading", ] [[package]] @@ -1283,7 +1298,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "itoa", @@ -1295,26 +1310,26 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 1.0.2", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "axum-macros", "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "itoa", "matchit 0.8.4", @@ -1328,7 +1343,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -1343,7 +1358,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1356,13 +1371,13 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1379,12 +1394,12 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum 0.8.6", - "axum-core 0.5.5", + "axum 0.8.8", + "axum-core 0.5.6", "bytes", "futures-util", "headers", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1404,24 +1419,24 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "axum-server" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" dependencies = [ "arc-swap", "bytes", "fs-err", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", @@ -1441,7 +1456,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -1480,9 +1495,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bimap" @@ -1493,26 +1508,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.106", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1530,15 +1525,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", @@ -1552,9 +1547,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -1589,9 +1584,9 @@ dependencies = [ [[package]] name = "bls" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "arbitrary", "blst", "ethereum_hashing", @@ -1678,6 +1673,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1700,9 +1719,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-slice-cast" @@ -1718,18 +1737,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] [[package]] name = "c-kzg" -version = "2.1.5" +version = "2.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" dependencies = [ "blst", "cc", @@ -1751,7 +1770,7 @@ name = "cb-bench-micro" version = "0.9.3" dependencies = [ "alloy", - "axum 0.8.6", + "axum 0.8.8", "cb-common", "cb-pbs", "cb-tests", @@ -1769,7 +1788,7 @@ dependencies = [ "comfy-table", "histogram", "rand 0.9.2", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -1784,7 +1803,7 @@ dependencies = [ "clap", "docker-compose-types", "eyre", - "indexmap 2.11.4", + "indexmap 2.13.0", "serde_yaml", "tempfile", "toml", @@ -1797,7 +1816,7 @@ dependencies = [ "aes 0.8.4", "alloy", "async-trait", - "axum 0.8.6", + "axum 0.8.8", "base64 0.22.1", "bimap", "bytes", @@ -1819,14 +1838,15 @@ dependencies = [ "pbkdf2 0.12.2", "rand 0.9.2", "rayon", - "reqwest 0.12.23", + "reqwest 0.12.28", + "reqwest-eventsource", "serde", "serde_json", "serde_yaml", "sha2 0.10.9", "ssz_types", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "toml", "tonic", @@ -1844,11 +1864,11 @@ dependencies = [ name = "cb-metrics" version = "0.9.3" dependencies = [ - "axum 0.8.6", + "axum 0.8.8", "cb-common", "eyre", "prometheus", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1859,7 +1879,7 @@ version = "0.9.3" dependencies = [ "alloy", "async-trait", - "axum 0.8.6", + "axum 0.8.8", "axum-extra", "cb-common", "cb-metrics", @@ -1869,7 +1889,7 @@ dependencies = [ "notify", "parking_lot", "prometheus", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -1877,7 +1897,7 @@ dependencies = [ "tracing", "tree_hash", "url", - "uuid 1.18.1", + "uuid 1.22.0", ] [[package]] @@ -1885,7 +1905,7 @@ name = "cb-signer" version = "0.9.3" dependencies = [ "alloy", - "axum 0.8.6", + "axum 0.8.8", "axum-extra", "axum-server", "bimap", @@ -1901,14 +1921,14 @@ dependencies = [ "prometheus", "prost", "rand 0.9.2", - "rustls 0.23.32", - "thiserror 2.0.17", + "rustls 0.23.37", + "thiserror 2.0.18", "tokio", "tonic", "tonic-build", "tracing", "tree_hash", - "uuid 1.18.1", + "uuid 1.22.0", ] [[package]] @@ -1916,14 +1936,14 @@ name = "cb-tests" version = "0.9.3" dependencies = [ "alloy", - "axum 0.8.6", + "axum 0.8.8", "cb-common", "cb-pbs", "cb-signer", "eyre", "jsonwebtoken", "rcgen", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "serde_json", "tempfile", @@ -1939,9 +1959,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.39" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1949,20 +1969,11 @@ dependencies = [ "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1970,16 +1981,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -2028,22 +2050,11 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" -version = "4.5.48" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -2051,39 +2062,39 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -2117,15 +2128,15 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "comfy-table" -version = "7.2.1" +version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ "crossterm", "unicode-segmentation", @@ -2157,7 +2168,7 @@ dependencies = [ [[package]] name = "compare_fields" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "itertools 0.10.5", ] @@ -2165,7 +2176,7 @@ dependencies = [ [[package]] name = "compare_fields_derive" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "quote", "syn 1.0.109", @@ -2173,12 +2184,12 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.16.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6407bff74dea37e0fa3dc1c1c974e5d46405f0c987bf9997a0762adce71eda6" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -2197,9 +2208,9 @@ checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -2218,7 +2229,7 @@ dependencies = [ [[package]] name = "context_deserialize" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "context_deserialize_derive", "milhouse", @@ -2229,12 +2240,21 @@ dependencies = [ [[package]] name = "context_deserialize_derive" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "quote", "syn 1.0.109", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2245,6 +2265,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2269,11 +2299,20 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -2360,7 +2399,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "crossterm_winapi", "document-features", "parking_lot", @@ -2397,9 +2436,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "typenum", @@ -2440,7 +2479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -2457,7 +2496,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2476,16 +2515,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - [[package]] name = "darling" version = "0.20.11" @@ -2507,17 +2536,13 @@ dependencies = [ ] [[package]] -name = "darling_core" -version = "0.13.4" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -2530,8 +2555,8 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", - "syn 2.0.106", + "strsim", + "syn 2.0.117", ] [[package]] @@ -2545,19 +2570,21 @@ dependencies = [ "proc-macro2", "quote", "serde", - "strsim 0.11.1", - "syn 2.0.106", + "strsim", + "syn 2.0.117", ] [[package]] -name = "darling_macro" -version = "0.13.4" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "darling_core 0.13.4", + "ident_case", + "proc-macro2", "quote", - "syn 1.0.109", + "strsim", + "syn 2.0.117", ] [[package]] @@ -2568,7 +2595,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2579,7 +2606,18 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", ] [[package]] @@ -2598,15 +2636,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -2614,12 +2652,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2634,9 +2672,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -2661,7 +2699,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2682,7 +2720,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2692,27 +2730,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version 0.4.1", + "syn 2.0.117", "unicode-xid", ] @@ -2751,7 +2791,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2761,7 +2801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f5e899a3da7a90647ef302f7e3050b00ed7f3f02c7b32683a04f3fbd9052541" dependencies = [ "derive_builder", - "indexmap 2.11.4", + "indexmap 2.13.0", "serde", "serde_yaml", ] @@ -2778,15 +2818,15 @@ dependencies = [ [[package]] name = "doctest-file" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" +checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359" [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -2852,7 +2892,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2874,6 +2914,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "eip_3076" +version = "0.1.0" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" +dependencies = [ + "ethereum_serde_utils 0.8.0", + "serde", + "types", +] + [[package]] name = "either" version = "1.15.0" @@ -3020,22 +3070,22 @@ dependencies = [ [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3051,15 +3101,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] name = "eth2" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "derivative", + "eip_3076", "either", "enr", "eth2_keystore", @@ -3079,7 +3130,6 @@ dependencies = [ "sensitive_url", "serde", "serde_json", - "slashing_protection", "ssz_types", "test_random_derive", "types", @@ -3089,7 +3139,7 @@ dependencies = [ [[package]] name = "eth2_interop_keypairs" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "bls", "ethereum_hashing", @@ -3102,7 +3152,7 @@ dependencies = [ [[package]] name = "eth2_key_derivation" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "bls", "num-bigint-dig", @@ -3114,7 +3164,7 @@ dependencies = [ [[package]] name = "eth2_keystore" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "aes 0.7.5", "bls", @@ -3139,7 +3189,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "ring", "sha2 0.10.9", ] @@ -3150,7 +3200,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70cbccfccf81d67bff0ab36e591fa536c8a935b078a7b0e58c1d00d418332fc9" dependencies = [ - "alloy-primitives 0.8.25", + "alloy-primitives 0.8.26", "hex", "serde", "serde_derive", @@ -3163,7 +3213,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "hex", "serde", "serde_derive", @@ -3176,7 +3226,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dcddb2554d19cde19b099fadddde576929d7a4d0c1cd3512d1fd95cf174375c" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "ethereum_serde_utils 0.8.0", "itertools 0.13.0", "serde", @@ -3194,7 +3244,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3275,30 +3325,11 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version 0.4.1", -] - -[[package]] -name = "filesystem" -version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" -dependencies = [ - "winapi", - "windows-acl", -] - [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed-hash" @@ -3315,9 +3346,9 @@ dependencies = [ [[package]] name = "fixed_bytes" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "safe_arith", ] @@ -3380,9 +3411,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.2" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", "tokio", @@ -3411,9 +3442,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -3426,9 +3457,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -3436,15 +3467,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -3453,32 +3484,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -3488,9 +3519,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -3500,7 +3531,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -3523,40 +3553,55 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.2.0" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ + "rustversion", "typenum", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -3594,7 +3639,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3603,17 +3648,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", - "indexmap 2.11.4", + "http 1.4.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3653,20 +3698,21 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", "serde", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -3687,7 +3733,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http 1.4.0", "httpdate", "mime", "sha1", @@ -3699,7 +3745,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3722,20 +3768,20 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] [[package]] name = "histogram" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95aebe0dec9a429e3207e5e34d97f2a7d1064d5ee6d8ed13ce0a26456de000ae" +checksum = "099d45a031296a7a40e01137b56c0c552f2a545568ef6058e47d674046def0db" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -3779,12 +3825,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -3806,7 +3851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -3817,7 +3862,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -3860,16 +3905,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", @@ -3901,15 +3946,15 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.7.0", + "http 1.4.0", + "hyper 1.8.1", "hyper-util", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -3918,7 +3963,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "pin-project-lite", "tokio", @@ -3946,7 +3991,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -3956,24 +4001,23 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", - "hyper 1.7.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", - "system-configuration 0.6.1", + "socket2 0.6.3", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", @@ -3982,9 +4026,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -4006,9 +4050,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -4019,9 +4063,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -4032,11 +4076,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -4047,42 +4090,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -4090,6 +4129,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4134,7 +4179,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4156,23 +4201,23 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "inotify" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -4198,48 +4243,37 @@ dependencies = [ [[package]] name = "int_to_bytes" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "bytes", ] [[package]] name = "interprocess" -version = "2.2.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +checksum = "6be5e5c847dbdb44564bd85294740d031f4f8aeb3464e5375ef7141f7538db69" dependencies = [ "doctest-file", "futures-core", "libc", "recvmsg", "tokio", - "widestring 1.2.0", + "widestring", "windows-sys 0.52.0", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -4253,14 +4287,14 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -4291,9 +4325,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -4301,15 +4335,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -4345,18 +4379,18 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] name = "keccak-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -4385,7 +4419,7 @@ dependencies = [ [[package]] name = "kzg" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "arbitrary", "c-kzg", @@ -4413,39 +4447,34 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.176" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "libloading" -version = "0.8.8" +name = "libc" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.4", -] +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libp2p-identity" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "bs58", "hkdf", "multihash", - "quick-protobuf", "sha2 0.10.9", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -4462,45 +4491,44 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.13.0" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -4517,7 +4545,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4528,13 +4556,13 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "match-lookup" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -4566,25 +4594,16 @@ checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "merkle_proof" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "ethereum_hashing", "fixed_bytes", "safe_arith", @@ -4604,25 +4623,25 @@ dependencies = [ [[package]] name = "metastruct" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74f54f231f9a18d77393ecc5cc7ab96709b2a61ee326c2b2b291009b0cc5a07" +checksum = "969a1be9bd80794bdf93b23ab552c2ec6f3e83b33164824553fd996cdad513b8" dependencies = [ "metastruct_macro", ] [[package]] name = "metastruct_macro" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7225f3a4dfbec47a0c6a730a874185fda840d365d7bbd6ba199dd81796d5" +checksum = "de9164f767d73a507c19205868c84da411dc7795f4bdabf497d3dd93cfef9930" dependencies = [ - "darling 0.13.4", - "itertools 0.10.5", + "darling 0.23.0", + "itertools 0.14.0", "proc-macro2", "quote", "smallvec", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -4631,7 +4650,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bdb104e38d3a8c5ffb7e9d2c43c522e6bcc34070edbadba565e722f0dee56c7" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "educe", "ethereum_hashing", "ethereum_ssz", @@ -4670,14 +4689,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -4729,9 +4748,9 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -4766,7 +4785,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -4780,17 +4799,20 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4805,11 +4827,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -4823,9 +4844,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -4869,9 +4890,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -4879,20 +4900,20 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "nybbles" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" dependencies = [ "alloy-rlp", "cfg-if", @@ -4913,15 +4934,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oorandom" @@ -4937,11 +4958,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -4958,29 +4979,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.3+3.5.4" +version = "300.5.5+3.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -4991,9 +5012,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "pairing" @@ -5029,14 +5050,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -5044,15 +5065,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -5082,12 +5103,12 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -5098,12 +5119,11 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", - "thiserror 2.0.17", "ucd-trie", ] @@ -5114,7 +5134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.13.0", ] [[package]] @@ -5129,29 +5149,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -5205,9 +5225,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -5260,7 +5280,7 @@ dependencies = [ [[package]] name = "pretty_reqwest_error" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "reqwest 0.11.27", "sensitive_url", @@ -5273,7 +5293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5289,11 +5309,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.25.5+spec-1.1.0", ] [[package]] @@ -5315,14 +5335,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -5339,19 +5359,18 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "proptest" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.11.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -5388,7 +5407,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.106", + "syn 2.0.117", "tempfile", ] @@ -5402,7 +5421,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5417,7 +5436,7 @@ dependencies = [ [[package]] name = "proto_array" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", @@ -5454,15 +5473,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-protobuf" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" -dependencies = [ - "byteorder", -] - [[package]] name = "quinn" version = "0.11.9" @@ -5475,9 +5485,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.32", - "socket2 0.6.0", - "thiserror 2.0.17", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -5485,20 +5495,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -5513,16 +5523,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -5534,25 +5544,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "r2d2" -version = "0.8.10" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "r2d2_sqlite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f5d0337e99cd5cacd91ffc326c6cc9d8078def459df560c4f9bf9ba4a51034" -dependencies = [ - "r2d2", - "rusqlite", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -5579,10 +5574,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", "serde", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5600,7 +5606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -5609,19 +5615,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "serde", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -5637,7 +5649,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", ] [[package]] @@ -5681,11 +5702,11 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", ] [[package]] @@ -5705,14 +5726,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -5722,9 +5743,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -5733,9 +5754,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -5785,20 +5806,20 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", @@ -5809,7 +5830,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -5819,7 +5840,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls 0.26.4", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -5827,7 +5848,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] @@ -5864,7 +5885,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -5891,9 +5912,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.0" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -5956,9 +5977,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -5992,15 +6013,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -6017,16 +6038,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.6", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -6051,9 +6072,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -6071,9 +6092,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -6089,9 +6110,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -6101,14 +6122,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safe_arith" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b147bb6111014916d3ef9d4c85173124a8e12193a67f6176d67244afd558d6c1" [[package]] name = "salsa20" @@ -6130,20 +6152,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.1", -] - -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "parking_lot", + "windows-sys 0.61.2", ] [[package]] @@ -6160,9 +6173,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -6236,12 +6249,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -6249,9 +6262,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -6290,7 +6303,7 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "sensitive_url" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "serde", "url", @@ -6332,20 +6345,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -6367,7 +6380,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6393,19 +6406,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.2.1", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -6413,14 +6425,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6429,7 +6441,7 @@ version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "itoa", "ryu", "serde", @@ -6453,7 +6465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -6465,7 +6477,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -6477,7 +6489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -6493,9 +6505,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" dependencies = [ "cc", "cfg-if", @@ -6518,10 +6530,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6537,27 +6550,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slashing_protection" -version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" -dependencies = [ - "arbitrary", - "ethereum_serde_utils 0.8.0", - "filesystem", - "r2d2", - "r2d2_sqlite", - "rusqlite", - "serde", - "serde_json", - "tempfile", - "tracing", - "types", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -6581,12 +6576,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6623,9 +6618,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -6638,24 +6633,18 @@ name = "status_api" version = "0.9.3" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.8", "color-eyre", "commit-boost", "eyre", "lazy_static", "prometheus", - "reqwest 0.12.23", + "reqwest 0.12.28", "serde", "tokio", "tracing", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -6680,7 +6669,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6691,24 +6680,24 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "superstruct" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" +checksum = "bae4a9ccd7882533c1f210e400763ec6ee64c390fc12248c238276281863719e" dependencies = [ - "darling 0.13.4", - "itertools 0.10.5", + "darling 0.23.0", + "itertools 0.14.0", "proc-macro2", "quote", "smallvec", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] name = "swap_or_not_shuffle" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "ethereum_hashing", "fixed_bytes", ] @@ -6726,9 +6715,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -6737,14 +6726,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.4.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2375c17f6067adc651d8c2c51658019cef32edfff4a982adaf1d7fd1c039f08b" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6770,7 +6759,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6780,18 +6769,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -6823,15 +6812,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -6843,7 +6832,7 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test_random_derive" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ "quote", "syn 1.0.109", @@ -6860,11 +6849,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -6875,18 +6864,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6929,30 +6918,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -6969,9 +6958,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -6989,9 +6978,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -7004,33 +6993,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.3", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -7059,15 +7045,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls 0.23.37", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -7083,7 +7069,7 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -7093,9 +7079,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -7127,9 +7113,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] @@ -7140,33 +7126,33 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.13.0", "serde", "serde_spanned", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.25.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.13.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -7186,11 +7172,11 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.13", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-timeout", "hyper-util", "percent-encoding", @@ -7218,7 +7204,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -7243,9 +7229,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -7259,18 +7245,18 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -7290,9 +7276,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -7302,32 +7288,32 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -7366,9 +7352,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -7387,9 +7373,9 @@ dependencies = [ [[package]] name = "tracing-test" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" dependencies = [ "tracing-core", "tracing-subscriber", @@ -7398,12 +7384,12 @@ dependencies = [ [[package]] name = "tracing-test-macro" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -7412,7 +7398,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee44f4cef85f88b4dea21c0b1f58320bdf35715cf56d840969487cff00613321" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "ethereum_hashing", "ethereum_ssz", "smallvec", @@ -7428,7 +7414,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -7440,14 +7426,14 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" dependencies = [ "serde", "stable_deref_trait", @@ -7467,29 +7453,29 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.9.2", - "rustls 0.23.32", + "rustls 0.23.37", "rustls-pki-types", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "types" version = "0.2.1" -source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0-rc.0#e5b4983d6baf85770fe4539a565d8a2dd462bc53" +source = "git+https://github.com/sigp/lighthouse?tag=v8.0.0#e3ee7febce64c1b5a85c3ab0be0619571ee92d58" dependencies = [ - "alloy-primitives 1.4.0", + "alloy-primitives 1.5.7", "alloy-rlp", "bls", "compare_fields", @@ -7567,15 +7553,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -7588,9 +7574,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -7618,14 +7604,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -7652,20 +7639,20 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "serde", ] [[package]] name = "uuid" -version = "1.18.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "rand 0.9.2", - "serde", + "rand 0.10.0", + "serde_core", "wasm-bindgen", ] @@ -7701,7 +7688,7 @@ checksum = "8fabeca519a296f0b39428cfe496b600c0179c9498687986449d61fa40e60806" dependencies = [ "crypto-bigint", "elliptic-curve", - "generic-array 1.2.0", + "generic-array 1.3.5", "rand_core 0.6.4", "serde", "sha3", @@ -7744,28 +7731,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wasip2", + "wit-bindgen", ] [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -7774,27 +7761,14 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -7803,9 +7777,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7813,26 +7787,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -7846,6 +7842,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -7862,9 +7870,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -7892,29 +7900,23 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - -[[package]] -name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -7938,7 +7940,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -7947,110 +7949,74 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-acl" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177b1723986bcb4c606058e77f6e8614b51c7f9ad2face6f6fd63dd5c8b3cec3" -dependencies = [ - "field-offset", - "libc", - "widestring 0.4.3", - "winapi", -] - [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -8071,31 +8037,22 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -8131,19 +8088,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -8160,9 +8117,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -8178,9 +8135,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -8196,9 +8153,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -8208,9 +8165,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -8226,9 +8183,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -8244,9 +8201,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -8262,9 +8219,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -8280,15 +8237,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" -version = "0.7.13" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] @@ -8305,15 +8271,97 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "ws_stream_wasm" @@ -8328,7 +8376,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -8354,11 +8402,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -8366,34 +8413,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -8413,7 +8460,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] @@ -8429,20 +8476,20 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -8451,9 +8498,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -8462,11 +8509,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 21540bfd..8e5e2dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2024" -rust-version = "1.89" +rust-version = "1.91" version = "0.9.3" [workspace.dependencies] @@ -52,9 +52,9 @@ headers = "0.4.0" indexmap = "2.2.6" jsonwebtoken = { version = "9.3.1", default-features = false } lazy_static = "1.5.0" -lh_eth2 = { package = "eth2", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } -lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } -lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0-rc.0" } +lh_eth2 = { package = "eth2", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0" } +lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0" } +lh_types = { package = "types", git = "https://github.com/sigp/lighthouse", tag = "v8.0.0" } notify = "8.2.0" parking_lot = "0.12.3" pbkdf2 = "0.12.2" @@ -91,4 +91,4 @@ url = { version = "2.5.0", features = ["serde"] } uuid = { version = "1.8.0", features = ["fast-rng", "serde", "v4"] } [patch.crates-io] -blstrs_plus = { git = "https://github.com/Commit-Boost/blstrs" } +blstrs_plus = { git = "https://github.com/Commit-Boost/blstrs" } \ No newline at end of file diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ae946fee..86783233 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -52,6 +52,7 @@ tree_hash.workspace = true tree_hash_derive.workspace = true unicode-normalization.workspace = true url.workspace = true +reqwest-eventsource = "=0.5.0" [dev-dependencies] tempfile.workspace = true \ No newline at end of file diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index d67f8487..afd3075c 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -330,7 +330,7 @@ where offset += limit; - if offset % 1000 == 0 { + if offset.is_multiple_of(1000) { debug!("fetched {offset} keys"); } } diff --git a/docs/docs/get_started/building.md b/docs/docs/get_started/building.md index 81968dbc..dd860be2 100644 --- a/docs/docs/get_started/building.md +++ b/docs/docs/get_started/building.md @@ -34,7 +34,7 @@ If you don't want to use the Docker builder, you can compile the Commit-Boost ar Requirements: -- Rust 1.89+ +- Rust 1.91+ - GCC (or another C compiler of your choice) - OpenSSL development libraries - Protobuf Compiler (`protoc`) diff --git a/docs/docs/get_started/overview.md b/docs/docs/get_started/overview.md index fb4f8a13..b5719567 100644 --- a/docs/docs/get_started/overview.md +++ b/docs/docs/get_started/overview.md @@ -28,7 +28,7 @@ The services are also published at [each release](https://github.com/orgs/Commit Requirements: -- Rust 1.89 +- Rust 1.91 :::note Run `rustup update` to update Rust and Cargo to the latest version diff --git a/justfile b/justfile index de70acfa..b4bd1b14 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -toolchain := "nightly-2025-06-26" +toolchain := "nightly-2026-01-01" fmt: rustup toolchain install {{toolchain}} > /dev/null 2>&1 && \ @@ -17,6 +17,7 @@ checklist: just fmt just clippy just test + cargo audit # =================================== # === Build Commands for Services === diff --git a/provisioning/build.Dockerfile b/provisioning/build.Dockerfile index b28b62ff..21b42eb0 100644 --- a/provisioning/build.Dockerfile +++ b/provisioning/build.Dockerfile @@ -1,5 +1,5 @@ # This will be the main build image -FROM --platform=${BUILDPLATFORM} rust:1.89-slim-bookworm AS chef +FROM --platform=${BUILDPLATFORM} rust:1.91-slim-bookworm AS chef ARG TARGETOS TARGETARCH BUILDPLATFORM TARGET_CRATE ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse WORKDIR /app diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b67e7d53..d72668b0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.89.0" +channel = "1.91.0" From 4afbf358a18002f392ddc457bd83b5391c604b18 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Sun, 22 Mar 2026 15:02:34 -0700 Subject: [PATCH 20/30] bump rust toolchain to nightly-2026-01-01 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a9b42ae..381d4e12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2025-06-26 + toolchain: nightly-2026-01-01 components: clippy, rustfmt - name: Install protoc From 717390e95cb64e1236cdccf61ba64d4623ef5ce8 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Tue, 24 Mar 2026 11:28:34 -0700 Subject: [PATCH 21/30] address review comments --- crates/common/src/config/mod.rs | 3 - crates/common/src/config/signer.rs | 12 +-- crates/common/src/utils.rs | 2 - crates/signer/src/manager/dirk.rs | 6 +- crates/signer/src/manager/local.rs | 20 ++-- crates/signer/src/service.rs | 142 ++++++++++++++--------------- 6 files changed, 81 insertions(+), 104 deletions(-) diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 9eda2968..e0958342 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -137,14 +137,12 @@ impl CommitBoostConfig { }) } - /// Helper to return if signer uses TLS pub fn signer_uses_tls(&self) -> bool { self.signer .as_ref() .is_some_and(|signer_config| matches!(signer_config.tls_mode, TlsMode::Certificate(_))) } - /// Helper to return signer's server URL pub fn signer_server_url(&self, default_port: u16) -> String { if let Some(SignerConfig { inner: SignerType::Remote { url }, .. }) = &self.signer { url.to_string() @@ -155,7 +153,6 @@ impl CommitBoostConfig { } } - /// Helper to return the path to the signer's TLS certificates if any pub fn signer_certs_path(&self) -> Option<&PathBuf> { self.signer .as_ref() diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 039d00ce..4e28b089 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -46,12 +46,10 @@ pub struct ModuleSigningConfig { impl ModuleSigningConfig { pub fn validate(&self) -> Result<()> { - // Ensure the JWT secret is not empty if self.jwt_secret.is_empty() { bail!("JWT secret cannot be empty"); } - // Ensure the signing ID is a valid B256 if self.signing_id.is_zero() { bail!("Signing ID cannot be zero"); } @@ -249,7 +247,6 @@ impl StartSignerConfig { let (admin_secret, jwt_secrets) = load_jwt_secrets()?; - // Load the module signing configs let mod_signing_configs = load_module_signing_configs(&config, &jwt_secrets) .wrap_err("Failed to load module signing configs")?; @@ -380,22 +377,18 @@ pub fn load_module_signing_configs( let mut seen_jwt_secrets = HashMap::new(); let mut seen_signing_ids = HashMap::new(); for module in modules { - // Validate the module ID ensure!(!module.id.is_empty(), "Module ID cannot be empty"); - // Make sure it hasn't been used yet ensure!( !mod_signing_configs.contains_key(&module.id), "Duplicate module config detected: ID {} is already used", module.id ); - // Make sure the JWT secret is present let jwt_secret = match jwt_secrets.get(&module.id) { Some(secret) => secret.clone(), None => bail!("JWT secret for module {} is missing", module.id), }; - // Create the module signing config and validate it let module_signing_config = ModuleSigningConfig { module_name: module.id.clone(), jwt_secret, @@ -405,7 +398,6 @@ pub fn load_module_signing_configs( .validate() .wrap_err(format!("Invalid signing config for module {}", module.id))?; - // Check for duplicates in JWT secrets and signing IDs if let Some(existing_module) = seen_jwt_secrets.insert(module_signing_config.jwt_secret.clone(), &module.id) { @@ -441,7 +433,7 @@ mod tests { fn make_local_signer_config(tls_mode: TlsMode) -> SignerConfig { SignerConfig { - host: Ipv4Addr::new(127, 0, 0, 1), + host: Ipv4Addr::LOCALHOST, port: 20000, docker_image: SIGNER_IMAGE_DEFAULT.to_string(), jwt_auth_fail_limit: 3, @@ -468,7 +460,7 @@ mod tests { pbs: StaticPbsConfig { docker_image: String::from("cb-fake-repo/fake-cb:latest"), pbs_config: PbsConfig { - host: Ipv4Addr::new(127, 0, 0, 1), + host: Ipv4Addr::LOCALHOST, port: 0, relay_check: false, wait_all_registrations: false, diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index 78fe5bbe..e504e477 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -419,7 +419,6 @@ pub fn decode_admin_jwt(jwt: Jwt) -> eyre::Result { Ok(claims) } -/// Validate a JWT with the given secret pub fn validate_jwt( jwt: Jwt, secret: &str, @@ -457,7 +456,6 @@ pub fn validate_jwt( Ok(()) } -/// Validate an admin JWT with the given secret pub fn validate_admin_jwt( jwt: Jwt, secret: &str, diff --git a/crates/signer/src/manager/dirk.rs b/crates/signer/src/manager/dirk.rs index a30debfb..45dcc733 100644 --- a/crates/signer/src/manager/dirk.rs +++ b/crates/signer/src/manager/dirk.rs @@ -372,9 +372,9 @@ impl DirkManager { pub async fn generate_proxy_key( &mut self, module: &ModuleId, - consensus: BlsPublicKey, + consensus: &BlsPublicKey, ) -> Result, SignerModuleError> { - let proxy_account = match self.consensus_accounts.get(&consensus) { + let proxy_account = match self.consensus_accounts.get(consensus) { Some(Account::Simple(account)) => { self.generate_simple_proxy_account(account, module).await? } @@ -393,7 +393,7 @@ impl DirkManager { proxy: proxy_account.inner.public_key().clone(), }; let delegation_signature = - self.request_consensus_signature(&consensus, &message.tree_hash_root(), None).await?; + self.request_consensus_signature(consensus, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegation { message, signature: delegation_signature }; diff --git a/crates/signer/src/manager/local.rs b/crates/signer/src/manager/local.rs index c6590a21..fc2eabae 100644 --- a/crates/signer/src/manager/local.rs +++ b/crates/signer/src/manager/local.rs @@ -94,13 +94,13 @@ impl LocalSigningManager { pub async fn create_proxy_bls( &mut self, module_id: ModuleId, - delegator: BlsPublicKey, + delegator: &BlsPublicKey, ) -> Result { let signer = BlsSigner::new_random(); let proxy_pubkey = signer.pubkey(); let message = ProxyDelegationBls { delegator: delegator.clone(), proxy: proxy_pubkey }; - let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; + let signature = self.sign_consensus(delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationBls { signature, message }; let proxy_signer = BlsProxySigner { signer, delegation: delegation.clone() }; @@ -113,13 +113,13 @@ impl LocalSigningManager { pub async fn create_proxy_ecdsa( &mut self, module_id: ModuleId, - delegator: BlsPublicKey, + delegator: &BlsPublicKey, ) -> Result { let signer = EcdsaSigner::new_random(); let proxy_address = signer.address(); let message = ProxyDelegationEcdsa { delegator: delegator.clone(), proxy: proxy_address }; - let signature = self.sign_consensus(&delegator, &message.tree_hash_root(), None).await?; + let signature = self.sign_consensus(delegator, &message.tree_hash_root(), None).await?; let delegation = SignedProxyDelegationEcdsa { signature, message }; let proxy_signer = EcdsaProxySigner { signer, delegation: delegation.clone() }; @@ -351,7 +351,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let signed_delegation = - signing_manager.create_proxy_bls(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_bls(MODULE_ID.clone(), &consensus_pk).await.unwrap(); let validation_result = signed_delegation.validate(CHAIN); @@ -372,7 +372,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let mut signed_delegation = - signing_manager.create_proxy_bls(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_bls(MODULE_ID.clone(), &consensus_pk).await.unwrap(); signed_delegation.signature = BlsSignature::test_random(); @@ -386,7 +386,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let signed_delegation = - signing_manager.create_proxy_bls(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_bls(MODULE_ID.clone(), &consensus_pk).await.unwrap(); let proxy_pk = signed_delegation.message.proxy; let data_root = B256::random(); @@ -433,7 +433,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let signed_delegation = - signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), &consensus_pk).await.unwrap(); let validation_result = signed_delegation.validate(CHAIN); @@ -454,7 +454,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let mut signed_delegation = - signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), &consensus_pk).await.unwrap(); signed_delegation.signature = BlsSignature::test_random(); @@ -468,7 +468,7 @@ mod tests { let (mut signing_manager, consensus_pk) = init_signing_manager(); let signed_delegation = - signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), consensus_pk).await.unwrap(); + signing_manager.create_proxy_ecdsa(MODULE_ID.clone(), &consensus_pk).await.unwrap(); let proxy_pk = signed_delegation.message.proxy; let data_root = B256::random(); diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 134da25a..fdf86879 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -42,7 +42,6 @@ use parking_lot::RwLock as ParkingRwLock; use rustls::crypto::{CryptoProvider, aws_lc_rs}; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; -use uuid::Uuid; use crate::{ error::SignerModuleError, @@ -396,9 +395,7 @@ async fn handle_get_pubkeys( Extension(module_id): Extension, State(state): State, ) -> Result { - let req_id = Uuid::new_v4(); - - debug!(event = "get_pubkeys", ?req_id, "New request"); + debug!(event = "get_pubkeys", ?module_id, "New request"); let keys = state .manager @@ -418,12 +415,10 @@ async fn handle_request_signature_bls( State(state): State, Json(request): Json, ) -> Result { - let req_id = Uuid::new_v4(); - debug!(event = "bls_request_signature", ?module_id, %request, ?req_id, "New request"); + debug!(event = "bls_request_signature", ?module_id, %request, "New request"); handle_request_signature_bls_impl( module_id, state, - req_id, false, request.pubkey, request.object_root, @@ -439,12 +434,10 @@ async fn handle_request_signature_proxy_bls( State(state): State, Json(request): Json>, ) -> Result { - let req_id = Uuid::new_v4(); - debug!(event = "proxy_bls_request_signature", ?module_id, %request, ?req_id, "New request"); + debug!(event = "proxy_bls_request_signature", ?module_id, %request, "New request"); handle_request_signature_bls_impl( module_id, state, - req_id, true, request.proxy, request.object_root, @@ -457,7 +450,6 @@ async fn handle_request_signature_proxy_bls( async fn handle_request_signature_bls_impl( module_id: ModuleId, state: SigningState, - req_id: Uuid, is_proxy: bool, signing_pubkey: BlsPublicKey, object_root: B256, @@ -467,17 +459,17 @@ async fn handle_request_signature_bls_impl( error!( event = "proxy_bls_request_signature", ?module_id, - ?req_id, + %signing_pubkey, + %object_root, + nonce, "Module signing ID not found" ); return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; - let chain_id: U256; - match &*state.manager.read().await { + let (chain_id, signature) = match &*state.manager.read().await { SigningManager::Local(local_manager) => { - chain_id = local_manager.get_chain().id(); - if is_proxy { + let sig = if is_proxy { local_manager .sign_proxy_bls( &signing_pubkey, @@ -493,11 +485,11 @@ async fn handle_request_signature_bls_impl( Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await - } + }; + (local_manager.get_chain().id(), sig) } SigningManager::Dirk(dirk_manager) => { - chain_id = dirk_manager.get_chain().id(); - if is_proxy { + let sig = if is_proxy { dirk_manager .request_proxy_signature( &signing_pubkey, @@ -513,24 +505,26 @@ async fn handle_request_signature_bls_impl( Some(&SignatureRequestInfo { module_signing_id: signing_id, nonce }), ) .await - } + }; + (dirk_manager.get_chain().id(), sig) } - } - .map(|sig| { - Json(BlsSignResponse::new( - signing_pubkey.clone(), - object_root, - signing_id, - nonce, - chain_id, - sig, - )) - .into_response() - }) - .map_err(|err| { - error!(event = "request_signature", ?module_id, ?req_id, "{err}"); - err - }) + }; + + signature + .inspect_err(|err| { + error!(event = "request_signature", ?module_id, %signing_pubkey, %object_root, nonce, "{err}") + }) + .map(|sig| { + Json(BlsSignResponse::new( + signing_pubkey.clone(), + object_root, + signing_id, + nonce, + chain_id, + sig, + )) + .into_response() + }) } /// Validates an ECDSA key signature request using a proxy key and returns the @@ -540,23 +534,22 @@ async fn handle_request_signature_proxy_ecdsa( State(state): State, Json(request): Json>, ) -> Result { - let req_id = Uuid::new_v4(); let Some(signing_id) = state.jwts.read().get(&module_id).map(|m| m.signing_id) else { error!( event = "proxy_ecdsa_request_signature", ?module_id, - ?req_id, + proxy = %request.proxy, + object_root = %request.object_root, + nonce = request.nonce, "Module signing ID not found" ); return Err(SignerModuleError::RequestError("Module signing ID not found".to_string())); }; - debug!(event = "proxy_ecdsa_request_signature", ?module_id, %request, ?req_id, "New request"); + debug!(event = "proxy_ecdsa_request_signature", ?module_id, %request, "New request"); - let chain_id: U256; - match &*state.manager.read().await { + let (chain_id, signature) = match &*state.manager.read().await { SigningManager::Local(local_manager) => { - chain_id = local_manager.get_chain().id(); - local_manager + let sig = local_manager .sign_proxy_ecdsa( &request.proxy, &request.object_root, @@ -565,34 +558,35 @@ async fn handle_request_signature_proxy_ecdsa( nonce: request.nonce, }), ) - .await + .await; + (local_manager.get_chain().id(), sig) } SigningManager::Dirk(_) => { - chain_id = U256::ZERO; // Dirk does not support ECDSA proxy signing + // Dirk does not support ECDSA proxy signing error!( event = "request_signature", ?module_id, - ?req_id, + proxy = %request.proxy, + object_root = %request.object_root, + nonce = request.nonce, "ECDSA proxy sign request not supported with Dirk" ); - Err(SignerModuleError::DirkNotSupported) + (U256::ZERO, Err(SignerModuleError::DirkNotSupported)) } - } - .map(|sig| { - Json(EcdsaSignResponse::new( - request.proxy, - request.object_root, - signing_id, - request.nonce, - chain_id, - sig, - )) - .into_response() - }) - .map_err(|err| { - error!(event = "request_signature", ?module_id, ?req_id, "{err}"); - err - }) + }; + signature + .inspect_err(|err| error!(event = "request_signature", ?module_id, proxy = %request.proxy, object_root = %request.object_root, nonce = request.nonce, "{err}")) + .map(|sig| { + Json(EcdsaSignResponse::new( + request.proxy, + request.object_root, + signing_id, + request.nonce, + chain_id, + sig, + )) + .into_response() + }) } async fn handle_generate_proxy( @@ -600,25 +594,23 @@ async fn handle_generate_proxy( State(state): State, Json(request): Json, ) -> Result { - let req_id = Uuid::new_v4(); - - debug!(event = "generate_proxy", ?module_id, scheme=?request.scheme, pubkey=%request.consensus_pubkey, ?req_id, "New request"); + debug!(event = "generate_proxy", ?module_id, scheme=?request.scheme, pubkey=%request.consensus_pubkey, "New request"); let mut manager = state.manager.write().await; let res = match &mut *manager { SigningManager::Local(local_manager) => match request.scheme { EncryptionScheme::Bls => local_manager - .create_proxy_bls(module_id.clone(), request.consensus_pubkey) + .create_proxy_bls(module_id.clone(), &request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => local_manager - .create_proxy_ecdsa(module_id.clone(), request.consensus_pubkey) + .create_proxy_ecdsa(module_id.clone(), &request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), }, SigningManager::Dirk(dirk_manager) => match request.scheme { EncryptionScheme::Bls => dirk_manager - .generate_proxy_key(&module_id, request.consensus_pubkey) + .generate_proxy_key(&module_id, &request.consensus_pubkey) .await .map(|proxy_delegation| Json(proxy_delegation).into_response()), EncryptionScheme::Ecdsa => { @@ -629,7 +621,7 @@ async fn handle_generate_proxy( }; if let Err(err) = &res { - error!(event = "generate_proxy", module_id=?module_id, ?req_id, "{err}"); + error!(event = "generate_proxy", ?module_id, scheme=?request.scheme, pubkey=%request.consensus_pubkey, "{err}"); } res @@ -639,15 +631,13 @@ async fn handle_reload( State(state): State, Json(request): Json, ) -> Result { - let req_id = Uuid::new_v4(); - - debug!(event = "reload", ?req_id, "New request"); + debug!(event = "reload", "New request"); // Regenerate the config let config = match StartSignerConfig::load_from_env() { Ok(config) => config, Err(err) => { - error!(event = "reload", ?req_id, error = ?err, "Failed to reload config"); + error!(event = "reload", error = ?err, "Failed to reload config"); return Err(SignerModuleError::Internal("failed to reload config".to_string())); } }; @@ -656,7 +646,7 @@ async fn handle_reload( let new_manager = match start_manager(config).await { Ok(manager) => manager, Err(err) => { - error!(event = "reload", ?req_id, error = ?err, "Failed to reload manager"); + error!(event = "reload", error = ?err, "Failed to reload manager"); return Err(SignerModuleError::Internal("failed to reload config".to_string())); } }; @@ -676,7 +666,7 @@ async fn handle_reload( let error_message = format!( "Module {module_id} signing ID not found in commit-boost config, cannot reload" ); - error!(event = "reload", ?req_id, module_id = %module_id, error = %error_message); + error!(event = "reload", module_id = %module_id, error = %error_message); return Err(SignerModuleError::RequestError(error_message)); } } From 04ae8b724dbb251862b05c7767ef177b3f7c398f Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 12:10:05 -0700 Subject: [PATCH 22/30] address Dirk issues in docker_init.rs: - add missing ADMIN_JWT_ENV and SIGNER_TLS_CERTIFICATES_PATH_ENV - support https healthchecks --- crates/cli/src/docker_init.rs | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 7976ce17..f2c5e2e4 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -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), @@ -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 { @@ -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()), @@ -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 { + 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)), @@ -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(); @@ -1690,6 +1721,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 // ------------------------------------------------------------------------- From 6ec37820207a28789c0bef343b764614b9b8d20e Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 12:31:49 -0700 Subject: [PATCH 23/30] cleaner error message if TLS CryptoProvider fails --- crates/signer/src/service.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index fdf86879..b056c96c 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -177,15 +177,14 @@ impl SigningService { break; } Err(e) => { + if attempts >= 3 { + return Err(eyre::eyre!( + "Exceeded maximum attempts to install AWS-LC as default TLS provider: {e:?}" + )); + } error!( "Failed to install AWS-LC as default TLS provider: {e:?}. Retrying..." ); - if attempts >= 3 { - error!( - "Exceeded maximum attempts to install AWS-LC as default TLS provider" - ); - break; - } attempts += 1; } } From 58b4b2254837c8a89034151d3290e8740f71ec91 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 13:03:26 -0700 Subject: [PATCH 24/30] users get error message on missing [signer] section instead of toml deserialization error message --- crates/common/src/config/module.rs | 4 ++-- crates/common/src/config/pbs.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 22884551..aec45289 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -83,7 +83,7 @@ pub fn load_commit_module_config() -> Result { chain: Chain, modules: Vec>, - signer: SignerConfig, + signer: Option, } // load module config including the extra data (if any) @@ -106,7 +106,7 @@ pub fn load_commit_module_config() -> Result None, TlsMode::Certificate(path) => Some( load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index 30964a20..907fbecf 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -347,7 +347,7 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC chain: Chain, relays: Vec, pbs: CustomPbsConfig, - signer: SignerConfig, + signer: Option, muxes: Option, } @@ -404,7 +404,11 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC // if custom pbs requires a signer client, load jwt let module_jwt = Jwt(load_env_var(MODULE_JWT_ENV)?); let signer_server_url = load_env_var(SIGNER_URL_ENV)?.parse()?; - let certs_path = match cb_config.signer.tls_mode { + let certs_path = match cb_config + .signer + .ok_or_else(|| eyre::eyre!("with_signer = true but no [signer] section in config"))? + .tls_mode + { TlsMode::Insecure => None, TlsMode::Certificate(path) => Some( load_env_var(SIGNER_TLS_CERTIFICATES_PATH_ENV) From 001b31231fb670c1c8f6193d7d1c18a5cfc522e4 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 15:06:20 -0700 Subject: [PATCH 25/30] support partial jwt reloads --- crates/signer/src/service.rs | 180 ++++++++++++++++++++++++++++++++--- 1 file changed, 168 insertions(+), 12 deletions(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index b056c96c..b301f8dc 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -632,7 +632,6 @@ async fn handle_reload( ) -> Result { debug!(event = "reload", "New request"); - // Regenerate the config let config = match StartSignerConfig::load_from_env() { Ok(config) => config, Err(err) => { @@ -641,7 +640,6 @@ async fn handle_reload( } }; - // Start a new manager with the updated config let new_manager = match start_manager(config).await { Ok(manager) => manager, Err(err) => { @@ -650,17 +648,24 @@ async fn handle_reload( } }; - // Update the JWT configs if provided in the request + apply_reload(state, request, new_manager).await +} + +/// Applies a reload request to the signing state. Separated from +/// `handle_reload` so the business logic can be tested without requiring a +/// live environment (config file, env vars, keystore on disk). +async fn apply_reload( + state: SigningState, + request: ReloadRequest, + new_manager: SigningManager, +) -> Result { + // Update the JWT configs if provided in the request. Only the provided + // modules are updated; omitted modules keep their existing secrets. if let Some(jwt_secrets) = request.jwt_secrets { let mut jwt_configs = state.jwts.write(); - let mut new_configs = HashMap::new(); for (module_id, jwt_secret) in jwt_secrets { - if let Some(signing_id) = jwt_configs.get(&module_id).map(|cfg| cfg.signing_id) { - new_configs.insert(module_id.clone(), ModuleSigningConfig { - module_name: module_id, - jwt_secret, - signing_id, - }); + if let Some(cfg) = jwt_configs.get_mut(&module_id) { + cfg.jwt_secret = jwt_secret; } else { let error_message = format!( "Module {module_id} signing ID not found in commit-boost config, cannot reload" @@ -669,10 +674,8 @@ async fn handle_reload( return Err(SignerModuleError::RequestError(error_message)); } } - *jwt_configs = new_configs; } - // Update the rest of the state once everything has passed if let Some(admin_secret) = request.admin_secret { *state.admin_secret.write() = admin_secret; } @@ -722,3 +725,156 @@ async fn start_manager(config: StartSignerConfig) -> eyre::Result ModuleSigningConfig { + ModuleSigningConfig { + module_name: ModuleId(module_name.to_string()), + jwt_secret: secret.to_string(), + signing_id, + } + } + + fn make_state(jwts: HashMap) -> SigningState { + SigningState { + manager: Arc::new(RwLock::new(SigningManager::Local( + LocalSigningManager::new(Chain::Holesky, None).unwrap(), + ))), + jwts: Arc::new(ParkingRwLock::new(jwts)), + admin_secret: Arc::new(ParkingRwLock::new("admin".to_string())), + jwt_auth_failures: Arc::new(ParkingRwLock::new(HashMap::new())), + jwt_auth_fail_limit: 3, + jwt_auth_fail_timeout: Duration::from_secs(60), + reverse_proxy: ReverseProxyHeaderSetup::None, + } + } + + fn empty_manager() -> SigningManager { + SigningManager::Local(LocalSigningManager::new(Chain::Holesky, None).unwrap()) + } + + /// Partial reload must update only the provided modules and leave omitted + /// modules with their existing secrets. + #[tokio::test] + async fn test_partial_reload_preserves_omitted_modules() { + let module_a = ModuleId("module-a".to_string()); + let module_b = ModuleId("module-b".to_string()); + let signing_id_a = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let signing_id_b = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + let state = make_state(HashMap::from([ + (module_a.clone(), make_signing_config("module-a", "secret-a", signing_id_a)), + (module_b.clone(), make_signing_config("module-b", "secret-b", signing_id_b)), + ])); + + let request = ReloadRequest { + jwt_secrets: Some(HashMap::from([(module_a.clone(), "rotated-secret-a".to_string())])), + admin_secret: None, + }; + + let result = apply_reload(state.clone(), request, empty_manager()).await; + assert!(result.is_ok(), "apply_reload should succeed"); + + let jwts = state.jwts.read(); + assert_eq!( + jwts[&module_a].jwt_secret, "rotated-secret-a", + "module_a secret should be updated" + ); + assert_eq!( + jwts[&module_b].jwt_secret, "secret-b", + "module_b secret must be preserved when omitted" + ); + } + + /// A full reload (all modules provided) should update every module. + #[tokio::test] + async fn test_full_reload_updates_all_modules() { + let module_a = ModuleId("module-a".to_string()); + let module_b = ModuleId("module-b".to_string()); + let signing_id_a = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + let signing_id_b = + b256!("0202020202020202020202020202020202020202020202020202020202020202"); + + let state = make_state(HashMap::from([ + (module_a.clone(), make_signing_config("module-a", "secret-a", signing_id_a)), + (module_b.clone(), make_signing_config("module-b", "secret-b", signing_id_b)), + ])); + + let request = ReloadRequest { + jwt_secrets: Some(HashMap::from([ + (module_a.clone(), "new-secret-a".to_string()), + (module_b.clone(), "new-secret-b".to_string()), + ])), + admin_secret: None, + }; + + apply_reload(state.clone(), request, empty_manager()).await.unwrap(); + + let jwts = state.jwts.read(); + assert_eq!(jwts[&module_a].jwt_secret, "new-secret-a"); + assert_eq!(jwts[&module_b].jwt_secret, "new-secret-b"); + } + + /// Reload with an unknown module ID in jwt_secrets should return an error + /// and leave the existing state unchanged. + #[tokio::test] + async fn test_reload_unknown_module_returns_error() { + let module_a = ModuleId("module-a".to_string()); + let signing_id_a = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + let state = make_state(HashMap::from([( + module_a.clone(), + make_signing_config("module-a", "secret-a", signing_id_a), + )])); + + let request = ReloadRequest { + jwt_secrets: Some(HashMap::from([( + ModuleId("unknown-module".to_string()), + "some-secret".to_string(), + )])), + admin_secret: None, + }; + + let result = apply_reload(state.clone(), request, empty_manager()).await; + assert!(result.is_err(), "unknown module should return an error"); + + // Existing module must be untouched + let jwts = state.jwts.read(); + assert_eq!(jwts[&module_a].jwt_secret, "secret-a"); + } + + /// Reload with no jwt_secrets should leave all module secrets unchanged. + #[tokio::test] + async fn test_reload_without_jwt_secrets_preserves_all() { + let module_a = ModuleId("module-a".to_string()); + let signing_id_a = + b256!("0101010101010101010101010101010101010101010101010101010101010101"); + + let state = make_state(HashMap::from([( + module_a.clone(), + make_signing_config("module-a", "secret-a", signing_id_a), + )])); + + let request = ReloadRequest { jwt_secrets: None, admin_secret: None }; + + apply_reload(state.clone(), request, empty_manager()).await.unwrap(); + + let jwts = state.jwts.read(); + assert_eq!(jwts[&module_a].jwt_secret, "secret-a"); + } +} From 76905ded6158c4c08ba2db441c6f54a3d3a5925e Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 15:32:54 -0700 Subject: [PATCH 26/30] require the nonce in signer-api.yml - previously was marked optional which conflicted with deserializing `SignConsensusRequest` --- api/signer-api.yml | 15 ++++++++++----- crates/common/src/commit/request.rs | 12 ++++++++++-- docs/docs/developing/prop-commit-signing.md | 4 ++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/api/signer-api.yml b/api/signer-api.yml index 95897ecd..be44f8fd 100644 --- a/api/signer-api.yml +++ b/api/signer-api.yml @@ -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. @@ -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. @@ -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. @@ -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 diff --git a/crates/common/src/commit/request.rs b/crates/common/src/commit/request.rs index a64e9a67..cd780446 100644 --- a/crates/common/src/commit/request.rs +++ b/crates/common/src/commit/request.rs @@ -84,6 +84,10 @@ impl fmt::Display for SignedProxyDelegation { pub struct SignConsensusRequest { pub pubkey: BlsPublicKey, pub object_root: B256, + /// Replay-protection nonce mixed into the signing root via + /// `PropCommitSigningInfo`. Modules that do not track nonces should + /// send `0`. Modules that do track nonces should use a monotonically + /// increasing value per key to prevent signature reuse. pub nonce: u64, } @@ -93,7 +97,7 @@ impl SignConsensusRequest { } pub fn builder(pubkey: BlsPublicKey) -> Self { - Self::new(pubkey, B256::ZERO, u64::MAX - 1) + Self::new(pubkey, B256::ZERO, 0) } pub fn with_root>(self, object_root: R) -> Self { @@ -125,6 +129,10 @@ impl Display for SignConsensusRequest { pub struct SignProxyRequest { pub proxy: T, pub object_root: B256, + /// Replay-protection nonce mixed into the signing root via + /// `PropCommitSigningInfo`. Modules that do not track nonces should + /// send `0`. Modules that do track nonces should use a monotonically + /// increasing value per key to prevent signature reuse. pub nonce: u64, } @@ -134,7 +142,7 @@ impl SignProxyRequest { } pub fn builder(proxy: T) -> Self { - Self::new(proxy, B256::ZERO, u64::MAX - 1) + Self::new(proxy, B256::ZERO, 0) } pub fn with_root>(self, object_root: R) -> Self { diff --git a/docs/docs/developing/prop-commit-signing.md b/docs/docs/developing/prop-commit-signing.md index 1e8bd249..30f70413 100644 --- a/docs/docs/developing/prop-commit-signing.md +++ b/docs/docs/developing/prop-commit-signing.md @@ -44,7 +44,7 @@ Your module has the option of using **Nonces** for each of its signature request If you want to use them within your module, your module (or whatever remote backend system it connects to) **will be responsible** for storing, comparing, validating, and otherwise using the nonces. Commit-Boost's signer service by itself **does not** store nonces or track which ones have already been used by a given module. -In terms of implementation, the nonce format conforms to the specification in [EIP-2681](https://eips.ethereum.org/EIPS/eip-2681). It is an unsigned 64-bit big-endian integer, with a minimum value of 0 and a maximum value of `2^64-2`. We recommend using `2^64-1` as a signifier indicating that your module doesn't use nonces, rather than using 0 for such a purpose. +In terms of implementation, the nonce format conforms to the specification in [EIP-2681](https://eips.ethereum.org/EIPS/eip-2681). It is an unsigned 64-bit integer, with a minimum value of 0 and a maximum value of `2^64-2`. The field is required and is always mixed into the signing root. Modules that do not use nonces for replay protection should always send `0`; modules that do should use a monotonically increasing value per key. ## Structure of a Signature @@ -63,7 +63,7 @@ where, for the sub-tree in blue: - `Signing ID` is your module's 32-byte signing ID. The signer service will load this for your module from its configuration file. -- `Nonce` is the nonce value for the signature request. While this value must be present, it can be effectively ignored by setting it to some arbitrary value if your module does not track nonces. Conforming with the tree specification, it must be added as a 256-bit unsigned little-endian integer. Most libraries will be able to do this conversion automatically if you specify the field as the language's primitive for 64-bit unsigned integers (e.g., `uint64`, `u64`, `ulong`, etc.). +- `Nonce` is the nonce value for the signature request. This field is required. Modules that do not use replay protection should always send `0`; modules that do should use a monotonically increasing value per key. Conforming with the tree specification, it must be added as a 256-bit unsigned little-endian integer. Most libraries will be able to do this conversion automatically if you specify the field as the language's primitive for 64-bit unsigned integers (e.g., `uint64`, `u64`, `ulong`, etc.). - `Chain ID` is the ID of the chain that the Signer service is currently configured to use, as indicated by the [Commit-Boost configuration file](../get_started/configuration.md). This must also be a 256-bit unsigned little-endian integer. From 8268572dd85b145ba26e43ab18cec71f30240035 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 15:43:05 -0700 Subject: [PATCH 27/30] fix suffix when displaying X-Forwaded-For --- crates/common/src/config/signer.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/common/src/config/signer.rs b/crates/common/src/config/signer.rs index 4e28b089..95110958 100644 --- a/crates/common/src/config/signer.rs +++ b/crates/common/src/config/signer.rs @@ -92,10 +92,12 @@ impl Display for ReverseProxyHeaderSetup { write!(f, "\"{header} (unique)\"") } ReverseProxyHeaderSetup::Rightmost { header, trusted_count } => { - let suffix = match trusted_count.get() % 10 { - 1 => "st", - 2 => "nd", - 3 => "rd", + let n = trusted_count.get(); + let suffix = match (n % 100, n % 10) { + (11..=13, _) => "th", + (_, 1) => "st", + (_, 2) => "nd", + (_, 3) => "rd", _ => "th", }; write!(f, "\"{header} ({trusted_count}{suffix} from the right)\"") From 7c8cce6f3c05283730db949d9ae9fce03a3fd272 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 15:54:43 -0700 Subject: [PATCH 28/30] only take the `jwt_auth_failures` writelock if strictly necessary, default to readlock --- crates/signer/src/service.rs | 52 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index b301f8dc..7edd63a8 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -273,38 +273,40 @@ async fn jwt_auth( /// Checks if the incoming request needs to be rate limited due to previous JWT /// authentication failures fn check_jwt_rate_limit(state: &SigningState, client_ip: &IpAddr) -> Result<(), SignerModuleError> { - let mut failures = state.jwt_auth_failures.write(); + let failures = state.jwt_auth_failures.read(); // Ignore clients that don't have any failures - if let Some(failure_info) = failures.get(client_ip) { - // If the last failure was more than the timeout ago, remove this entry so it's - // eligible again - let elapsed = failure_info.last_failure.elapsed(); - if elapsed > state.jwt_auth_fail_timeout { - debug!("Removing {client_ip} from JWT auth failure list"); - failures.remove(client_ip); - return Ok(()); - } + let Some(failure_info) = failures.get(client_ip) else { + debug!("Client {client_ip} has no JWT auth failures, no rate limit applied"); + return Ok(()); + }; - // If the failure threshold hasn't been met yet, don't rate limit - if failure_info.failure_count < state.jwt_auth_fail_limit { - debug!( - "Client {client_ip} has {}/{} JWT auth failures, no rate limit applied", - failure_info.failure_count, state.jwt_auth_fail_limit - ); - return Ok(()); - } + let elapsed = failure_info.last_failure.elapsed(); + + // If the last failure was more than the timeout ago, remove this entry so it's + // eligible again + if elapsed > state.jwt_auth_fail_timeout { + drop(failures); + debug!("Removing {client_ip} from JWT auth failure list"); + state.jwt_auth_failures.write().remove(client_ip); + return Ok(()); + } - // Rate limit the request - let remaining = state.jwt_auth_fail_timeout.saturating_sub(elapsed); - warn!( - "Client {client_ip} is rate limited for {remaining:?} more seconds due to JWT auth failures" + // If the failure threshold hasn't been met yet, don't rate limit + if failure_info.failure_count < state.jwt_auth_fail_limit { + debug!( + "Client {client_ip} has {}/{} JWT auth failures, no rate limit applied", + failure_info.failure_count, state.jwt_auth_fail_limit ); - return Err(SignerModuleError::RateLimited(remaining.as_secs_f64())); + return Ok(()); } - debug!("Client {client_ip} has no JWT auth failures, no rate limit applied"); - Ok(()) + // Rate limit the request + let remaining = state.jwt_auth_fail_timeout.saturating_sub(elapsed); + warn!( + "Client {client_ip} is rate limited for {remaining:?} more seconds due to JWT auth failures" + ); + Err(SignerModuleError::RateLimited(remaining.as_secs_f64())) } /// Checks if a request can successfully authenticate with the JWT secret From 4a9aff76140bbb0bbf5f669c1a4e8544b5287c7a Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 16:03:46 -0700 Subject: [PATCH 29/30] remove mark_jwt_failure() calls from failures unrelated to jwts --- crates/signer/src/service.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/signer/src/service.rs b/crates/signer/src/service.rs index 7edd63a8..81729272 100644 --- a/crates/signer/src/service.rs +++ b/crates/signer/src/service.rs @@ -248,7 +248,6 @@ async fn jwt_auth( let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); - mark_jwt_failure(&state, client_ip); SignerModuleError::RequestError(e.to_string()) })?; @@ -360,7 +359,6 @@ async fn admin_auth( let path = parts.uri.path(); let bytes = to_bytes(body, REQUEST_MAX_BODY_LENGTH).await.map_err(|e| { error!("Failed to read request body: {e}"); - mark_jwt_failure(&state, client_ip); SignerModuleError::RequestError(e.to_string()) })?; From d834242a238d376f7c6abeeff1f3cc7a3381ab37 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 25 Mar 2026 17:35:29 -0700 Subject: [PATCH 30/30] add round-trip sign->verify unit tests --- crates/common/src/signature.rs | 60 +++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/common/src/signature.rs b/crates/common/src/signature.rs index 18c10d4a..41631e33 100644 --- a/crates/common/src/signature.rs +++ b/crates/common/src/signature.rs @@ -165,10 +165,18 @@ pub fn verify_proposer_commitment_signature_ecdsa( #[cfg(test)] mod tests { - use alloy::primitives::aliases::B32; - - use super::compute_domain; - use crate::{constants::APPLICATION_BUILDER_DOMAIN, types::Chain}; + use alloy::primitives::{U256, aliases::B32}; + + use super::{compute_domain, sign_builder_message, verify_signed_message}; + use crate::{ + constants::APPLICATION_BUILDER_DOMAIN, + pbs::{ + BlindedBeaconBlockElectra, BuilderBid, BuilderBidElectra, + ExecutionPayloadHeaderElectra, ExecutionRequests, + }, + types::{BlsSecretKey, Chain}, + utils::TestRandomSeed, + }; #[test] fn test_builder_domains() { @@ -178,4 +186,48 @@ mod tests { assert_eq!(compute_domain(Chain::Sepolia, domain), Chain::Sepolia.builder_domain()); assert_eq!(compute_domain(Chain::Hoodi, domain), Chain::Hoodi.builder_domain()); } + + #[test] + fn test_builder_bid_sign_and_verify() { + let secret_key = BlsSecretKey::test_random(); + let pubkey = secret_key.public_key(); + + let message = BuilderBid::Electra(BuilderBidElectra { + header: ExecutionPayloadHeaderElectra::test_random(), + blob_kzg_commitments: Default::default(), + execution_requests: ExecutionRequests::default(), + value: U256::from(10), + pubkey: pubkey.clone().into(), + }); + + let sig = sign_builder_message(Chain::Mainnet, &secret_key, &message); + + assert!(verify_signed_message( + Chain::Mainnet, + &pubkey, + &message, + &sig, + None, + &B32::from(APPLICATION_BUILDER_DOMAIN), + )); + } + + #[test] + fn test_blinded_block_sign_and_verify() { + let secret_key = BlsSecretKey::test_random(); + let pubkey = secret_key.public_key(); + + let block = BlindedBeaconBlockElectra::test_random(); + + let sig = sign_builder_message(Chain::Mainnet, &secret_key, &block); + + assert!(verify_signed_message( + Chain::Mainnet, + &pubkey, + &block, + &sig, + None, + &B32::from(APPLICATION_BUILDER_DOMAIN), + )); + } }