Skip to content

Commit 8eed043

Browse files
committed
Add configurable validation bypassing for get_header and submit_block
Introduce HeaderValidationMode and BlockValidationMode to allow operators to trade validation thoroughness for latency: - None: skip decoding and crypto checks, forward raw relay responses - Standard: full validation (default, existing behavior) - Extra: additional parent block checks via RPC (get_header only) Includes CompoundGetHeaderResponse and CompoundSubmitBlockResponse types to handle both full and light processing paths. Updates config, docs, and comprehensive tests for all validation modes.
1 parent d468365 commit 8eed043

21 files changed

Lines changed: 1393 additions & 328 deletions

File tree

benches/microbench/src/get_header.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,21 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
4040

4141
use alloy::primitives::B256;
4242
use axum::http::HeaderMap;
43-
use cb_common::{pbs::GetHeaderParams, signer::random_secret, types::Chain};
43+
use cb_common::{
44+
pbs::GetHeaderParams,
45+
signer::random_secret,
46+
types::Chain,
47+
utils::{AcceptedEncodings, EncodingType},
48+
};
4449
use cb_pbs::{PbsState, get_header};
4550
use cb_tests::{
46-
mock_relay::{MockRelayState, start_mock_relay_service},
47-
utils::{generate_mock_relay, get_pbs_static_config, to_pbs_config},
51+
mock_relay::{MockRelayState, start_mock_relay_service_with_listener},
52+
utils::{generate_mock_relay, get_free_listener, get_pbs_config, to_pbs_config},
4853
};
4954
use criterion::{Criterion, black_box, criterion_group, criterion_main};
5055

51-
// Ports 19201–19205 are reserved for the microbenchmark mock relays.
52-
const BASE_PORT: u16 = 19200;
56+
// Mock relay ports are allocated dynamically via get_free_listener() so that
57+
// parallel test/bench runs don't collide on hardcoded ports.
5358
const CHAIN: Chain = Chain::Hoodi;
5459
const MAX_RELAYS: usize = 5;
5560
const RELAY_COUNTS: [usize; 3] = [1, 3, MAX_RELAYS];
@@ -83,10 +88,23 @@ fn bench_get_header(c: &mut Criterion) {
8388
let pubkey = signer.public_key();
8489
let mock_state = Arc::new(MockRelayState::new(CHAIN, signer));
8590

86-
let relay_clients: Vec<_> = (0..MAX_RELAYS)
87-
.map(|i| {
88-
let port = BASE_PORT + 1 + i as u16;
89-
tokio::spawn(start_mock_relay_service(mock_state.clone(), port));
91+
// Allocate all listeners upfront so each port is reserved until the
92+
// server takes ownership — avoids TOCTOU bind races.
93+
let listeners: Vec<_> = {
94+
let mut v = Vec::with_capacity(MAX_RELAYS);
95+
for _ in 0..MAX_RELAYS {
96+
v.push(get_free_listener().await);
97+
}
98+
v
99+
};
100+
let ports: Vec<u16> = listeners.iter().map(|l| l.local_addr().unwrap().port()).collect();
101+
102+
let relay_clients: Vec<_> = listeners
103+
.into_iter()
104+
.enumerate()
105+
.map(|(i, listener)| {
106+
let port = ports[i];
107+
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), listener));
90108
generate_mock_relay(port, pubkey.clone()).expect("relay client")
91109
})
92110
.collect();
@@ -103,8 +121,7 @@ fn bench_get_header(c: &mut Criterion) {
103121
let states: Vec<PbsState> = RELAY_COUNTS
104122
.iter()
105123
.map(|&n| {
106-
let config =
107-
to_pbs_config(CHAIN, get_pbs_static_config(0), relay_clients[..n].to_vec());
124+
let config = to_pbs_config(CHAIN, get_pbs_config(0), relay_clients[..n].to_vec());
108125
PbsState::new(config, PathBuf::new())
109126
})
110127
.collect();
@@ -138,6 +155,10 @@ fn bench_get_header(c: &mut Criterion) {
138155
black_box(params.clone()),
139156
black_box(headers.clone()),
140157
black_box(state.clone()),
158+
black_box(AcceptedEncodings {
159+
primary: EncodingType::Json,
160+
fallback: Some(EncodingType::Ssz),
161+
}),
141162
))
142163
.expect("get_header failed")
143164
})

config.example.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@ min_bid_eth = 0.0
4949
# to force local building and miniminzing the risk of missed slots. See also the timing games section below
5050
# OPTIONAL, DEFAULT: 2000
5151
late_in_slot_time_ms = 2000
52-
# Whether to enable extra validation of get_header responses, if this is enabled `rpc_url` must also be set
53-
# OPTIONAL, DEFAULT: false
54-
extra_validation_enabled = false
52+
# The level of validation to perform on get_header responses. Less is faster but not as safe. Supported values:
53+
# - "none": no validation, just accept the bid provided by the relay as-is and pass it back without decoding or checking it
54+
# - "standard": perform standard validation of the header provided by the relay, which checks the bid's signature and several hashes to make sure it's legal (default)
55+
# - "extra": perform extra validation on top of standard validation, which includes checking the bid against the execution layer via the `rpc_url` (requires `rpc_url` to be set)
56+
# OPTIONAL, DEFAULT: standard
57+
header_validation_mode = "standard"
58+
# The level of validation to perform on submit_block responses. Less is faster but not as safe. Supported values:
59+
# - "none": no validation, just accept the full unblinded block provided by the relay as-is and pass it back without decoding or checking it
60+
# - "standard": perform standard validation of the unblinded block provided by the relay, which verifies things like the included KZG commitments and the block hash (default)
61+
block_validation_mode = "standard"
5562
# Execution Layer RPC url to use for extra validation
5663
# OPTIONAL
5764
# rpc_url = "https://ethereum-holesky-rpc.publicnode.com"

crates/common/src/config/pbs.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ use crate::{
3838
},
3939
};
4040

41+
/// Header validation modes for get_header responses
42+
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
43+
#[serde(rename_all = "snake_case")]
44+
pub enum HeaderValidationMode {
45+
// Bypass all validation and minimize decoding, which is faster but requires complete trust in
46+
// the relays
47+
None,
48+
49+
// Validate the header itself, ensuring that it's for a correct block on the correct chain and
50+
// fork. This is the default mode.
51+
Standard,
52+
53+
// Standard header validation, plus validation that the parent block is correct as well
54+
Extra,
55+
}
56+
57+
/// Block validation modes for submit_block responses
58+
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
59+
#[serde(rename_all = "snake_case")]
60+
pub enum BlockValidationMode {
61+
// Bypass all validation, which is faster but requires complete trust in the relays
62+
None,
63+
64+
// Validate the block matches the header previously received from get_header and that it's for
65+
// the correct chain and fork. This is the default mode.
66+
Standard,
67+
}
68+
4169
#[derive(Debug, Clone, Deserialize, Serialize)]
4270
#[serde(deny_unknown_fields)]
4371
pub struct RelayConfig {
@@ -122,8 +150,11 @@ pub struct PbsConfig {
122150
#[serde(default = "default_u64::<LATE_IN_SLOT_TIME_MS>")]
123151
pub late_in_slot_time_ms: u64,
124152
/// Enable extra validation of get_header responses
125-
#[serde(default = "default_bool::<false>")]
126-
pub extra_validation_enabled: bool,
153+
#[serde(default = "default_header_validation_mode")]
154+
pub header_validation_mode: HeaderValidationMode,
155+
/// Enable extra validation of submit_block requests
156+
#[serde(default = "default_block_validation_mode")]
157+
pub block_validation_mode: BlockValidationMode,
127158
/// Execution Layer RPC url to use for extra validation
128159
pub rpc_url: Option<Url>,
129160
/// URL for the user's own SSV node API endpoint
@@ -175,10 +206,10 @@ impl PbsConfig {
175206
format!("min bid is too high: {} ETH", format_ether(self.min_bid_wei))
176207
);
177208

178-
if self.extra_validation_enabled {
209+
if self.header_validation_mode == HeaderValidationMode::Extra {
179210
ensure!(
180211
self.rpc_url.is_some(),
181-
"rpc_url is required if extra_validation_enabled is true"
212+
"rpc_url is required if header_validation_mode is set to extra"
182213
);
183214
}
184215

@@ -442,6 +473,16 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleC
442473
))
443474
}
444475

476+
/// Default value for header validation mode
477+
fn default_header_validation_mode() -> HeaderValidationMode {
478+
HeaderValidationMode::Standard
479+
}
480+
481+
/// Default value for block validation mode
482+
fn default_block_validation_mode() -> BlockValidationMode {
483+
BlockValidationMode::Standard
484+
}
485+
445486
/// Default URL for the user's SSV node API endpoint (/v1/validators).
446487
fn default_ssv_node_api_url() -> Url {
447488
Url::parse("http://localhost:16000/v1/").expect("default URL is valid")

crates/common/src/config/signer.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,8 @@ mod tests {
426426

427427
use super::*;
428428
use crate::config::{
429-
COMMIT_BOOST_IMAGE_DEFAULT, LogsSettings, ModuleKind, PbsConfig, StaticModuleConfig,
430-
StaticPbsConfig,
429+
BlockValidationMode, COMMIT_BOOST_IMAGE_DEFAULT, HeaderValidationMode, LogsSettings,
430+
ModuleKind, PbsConfig, StaticModuleConfig, StaticPbsConfig,
431431
};
432432

433433
// Wrapper needed because TOML requires a top-level struct (can't serialize
@@ -476,7 +476,8 @@ mod tests {
476476
skip_sigverify: false,
477477
min_bid_wei: Uint::<256, 4>::from(0),
478478
late_in_slot_time_ms: 0,
479-
extra_validation_enabled: false,
479+
header_validation_mode: HeaderValidationMode::Standard,
480+
block_validation_mode: BlockValidationMode::Standard,
480481
rpc_url: None,
481482
http_timeout_seconds: 30,
482483
register_validator_retry_limit: 3,

crates/pbs/src/api.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use std::sync::Arc;
22

33
use async_trait::async_trait;
44
use axum::{Router, http::HeaderMap};
5-
use cb_common::pbs::{
6-
BuilderApiVersion, GetHeaderParams, GetHeaderResponse, SignedBlindedBeaconBlock,
7-
SubmitBlindedBlockResponse,
5+
use cb_common::{
6+
pbs::{BuilderApiVersion, GetHeaderParams, SignedBlindedBeaconBlock},
7+
utils::AcceptedEncodings,
88
};
99

1010
use crate::{
11-
mev_boost,
11+
CompoundGetHeaderResponse, CompoundSubmitBlockResponse, mev_boost,
1212
state::{BuilderApiState, PbsState, PbsStateGuard},
1313
};
1414

@@ -24,8 +24,9 @@ pub trait BuilderApi<S: BuilderApiState>: 'static {
2424
params: GetHeaderParams,
2525
req_headers: HeaderMap,
2626
state: PbsState<S>,
27-
) -> eyre::Result<Option<GetHeaderResponse>> {
28-
mev_boost::get_header(params, req_headers, state).await
27+
accepted_types: AcceptedEncodings,
28+
) -> eyre::Result<Option<CompoundGetHeaderResponse>> {
29+
mev_boost::get_header(params, req_headers, state, accepted_types).await
2930
}
3031

3132
/// https://ethereum.github.io/builder-specs/#/Builder/status
@@ -40,8 +41,16 @@ pub trait BuilderApi<S: BuilderApiState>: 'static {
4041
req_headers: HeaderMap,
4142
state: PbsState<S>,
4243
api_version: BuilderApiVersion,
43-
) -> eyre::Result<Option<SubmitBlindedBlockResponse>> {
44-
mev_boost::submit_block(signed_blinded_block, req_headers, state, api_version).await
44+
accepted_types: AcceptedEncodings,
45+
) -> eyre::Result<CompoundSubmitBlockResponse> {
46+
mev_boost::submit_block(
47+
signed_blinded_block,
48+
req_headers,
49+
state,
50+
api_version,
51+
accepted_types,
52+
)
53+
.await
4554
}
4655

4756
/// https://ethereum.github.io/builder-specs/#/Builder/registerValidator

0 commit comments

Comments
 (0)