Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions benches/microbench/src/get_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,21 @@ use std::{path::PathBuf, sync::Arc, time::Duration};

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

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

let relay_clients: Vec<_> = (0..MAX_RELAYS)
.map(|i| {
let port = BASE_PORT + 1 + i as u16;
tokio::spawn(start_mock_relay_service(mock_state.clone(), port));
// Allocate all listeners upfront so each port is reserved until the
// server takes ownership — avoids TOCTOU bind races.
let listeners: Vec<_> = {
let mut v = Vec::with_capacity(MAX_RELAYS);
for _ in 0..MAX_RELAYS {
v.push(get_free_listener().await);
}
v
};
let ports: Vec<u16> = listeners.iter().map(|l| l.local_addr().unwrap().port()).collect();

let relay_clients: Vec<_> = listeners
.into_iter()
.enumerate()
.map(|(i, listener)| {
let port = ports[i];
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), listener));
generate_mock_relay(port, pubkey.clone()).expect("relay client")
})
.collect();
Expand All @@ -103,8 +121,7 @@ fn bench_get_header(c: &mut Criterion) {
let states: Vec<PbsState> = RELAY_COUNTS
.iter()
.map(|&n| {
let config =
to_pbs_config(CHAIN, get_pbs_static_config(0), relay_clients[..n].to_vec());
let config = to_pbs_config(CHAIN, get_pbs_config(0), relay_clients[..n].to_vec());
PbsState::new(config, PathBuf::new())
})
.collect();
Expand Down Expand Up @@ -138,6 +155,10 @@ fn bench_get_header(c: &mut Criterion) {
black_box(params.clone()),
black_box(headers.clone()),
black_box(state.clone()),
black_box(AcceptedEncodings {
primary: EncodingType::Json,
fallback: Some(EncodingType::Ssz),
}),
))
.expect("get_header failed")
})
Expand Down
13 changes: 10 additions & 3 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@ min_bid_eth = 0.0
# to force local building and miniminzing the risk of missed slots. See also the timing games section below
# OPTIONAL, DEFAULT: 2000
late_in_slot_time_ms = 2000
# Whether to enable extra validation of get_header responses, if this is enabled `rpc_url` must also be set
# OPTIONAL, DEFAULT: false
extra_validation_enabled = false
# The level of validation to perform on get_header responses. Less is faster but not as safe. Supported values:
# - "none": no validation, just accept the bid provided by the relay as-is and pass it back without decoding or checking it
# - "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)
# - "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)
# OPTIONAL, DEFAULT: standard
header_validation_mode = "standard"
# The level of validation to perform on submit_block responses. Less is faster but not as safe. Supported values:
# - "none": no validation, just accept the full unblinded block provided by the relay as-is and pass it back without decoding or checking it
# - "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)
block_validation_mode = "standard"
# Execution Layer RPC url to use for extra validation
# OPTIONAL
# rpc_url = "https://ethereum-holesky-rpc.publicnode.com"
Expand Down
49 changes: 45 additions & 4 deletions crates/common/src/config/pbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ use crate::{
},
};

/// Header validation modes for get_header responses
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HeaderValidationMode {
// Bypass all validation and minimize decoding, which is faster but requires complete trust in
// the relays
None,

// Validate the header itself, ensuring that it's for a correct block on the correct chain and
// fork. This is the default mode.
Standard,

// Standard header validation, plus validation that the parent block is correct as well
Extra,
}

/// Block validation modes for submit_block responses
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BlockValidationMode {
// Bypass all validation, which is faster but requires complete trust in the relays
None,

// Validate the block matches the header previously received from get_header and that it's for
// the correct chain and fork. This is the default mode.
Standard,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RelayConfig {
Expand Down Expand Up @@ -122,8 +150,11 @@ pub struct PbsConfig {
#[serde(default = "default_u64::<LATE_IN_SLOT_TIME_MS>")]
pub late_in_slot_time_ms: u64,
/// Enable extra validation of get_header responses
#[serde(default = "default_bool::<false>")]
pub extra_validation_enabled: bool,
#[serde(default = "default_header_validation_mode")]
pub header_validation_mode: HeaderValidationMode,
/// Enable extra validation of submit_block requests
#[serde(default = "default_block_validation_mode")]
pub block_validation_mode: BlockValidationMode,
/// Execution Layer RPC url to use for extra validation
pub rpc_url: Option<Url>,
/// URL for the user's own SSV node API endpoint
Expand Down Expand Up @@ -175,10 +206,10 @@ impl PbsConfig {
format!("min bid is too high: {} ETH", format_ether(self.min_bid_wei))
);

if self.extra_validation_enabled {
if self.header_validation_mode == HeaderValidationMode::Extra {
ensure!(
self.rpc_url.is_some(),
"rpc_url is required if extra_validation_enabled is true"
"rpc_url is required if header_validation_mode is set to extra"
);
}

Expand Down Expand Up @@ -442,6 +473,16 @@ pub async fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleC
))
}

/// Default value for header validation mode
fn default_header_validation_mode() -> HeaderValidationMode {
HeaderValidationMode::Standard
}

/// Default value for block validation mode
fn default_block_validation_mode() -> BlockValidationMode {
BlockValidationMode::Standard
}

/// Default URL for the user's SSV node API endpoint (/v1/validators).
fn default_ssv_node_api_url() -> Url {
Url::parse("http://localhost:16000/v1/").expect("default URL is valid")
Expand Down
7 changes: 4 additions & 3 deletions crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,8 @@ mod tests {

use super::*;
use crate::config::{
COMMIT_BOOST_IMAGE_DEFAULT, LogsSettings, ModuleKind, PbsConfig, StaticModuleConfig,
StaticPbsConfig,
BlockValidationMode, COMMIT_BOOST_IMAGE_DEFAULT, HeaderValidationMode, LogsSettings,
ModuleKind, PbsConfig, StaticModuleConfig, StaticPbsConfig,
};

// Wrapper needed because TOML requires a top-level struct (can't serialize
Expand Down Expand Up @@ -476,7 +476,8 @@ mod tests {
skip_sigverify: false,
min_bid_wei: Uint::<256, 4>::from(0),
late_in_slot_time_ms: 0,
extra_validation_enabled: false,
header_validation_mode: HeaderValidationMode::Standard,
block_validation_mode: BlockValidationMode::Standard,
rpc_url: None,
http_timeout_seconds: 30,
register_validator_retry_limit: 3,
Expand Down
25 changes: 17 additions & 8 deletions crates/pbs/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use std::sync::Arc;

use async_trait::async_trait;
use axum::{Router, http::HeaderMap};
use cb_common::pbs::{
BuilderApiVersion, GetHeaderParams, GetHeaderResponse, SignedBlindedBeaconBlock,
SubmitBlindedBlockResponse,
use cb_common::{
pbs::{BuilderApiVersion, GetHeaderParams, SignedBlindedBeaconBlock},
utils::AcceptedEncodings,
};

use crate::{
mev_boost,
CompoundGetHeaderResponse, CompoundSubmitBlockResponse, mev_boost,
state::{BuilderApiState, PbsState, PbsStateGuard},
};

Expand All @@ -24,8 +24,9 @@ pub trait BuilderApi<S: BuilderApiState>: 'static {
params: GetHeaderParams,
req_headers: HeaderMap,
state: PbsState<S>,
) -> eyre::Result<Option<GetHeaderResponse>> {
mev_boost::get_header(params, req_headers, state).await
accepted_types: AcceptedEncodings,
) -> eyre::Result<Option<CompoundGetHeaderResponse>> {
mev_boost::get_header(params, req_headers, state, accepted_types).await
}

/// https://ethereum.github.io/builder-specs/#/Builder/status
Expand All @@ -40,8 +41,16 @@ pub trait BuilderApi<S: BuilderApiState>: 'static {
req_headers: HeaderMap,
state: PbsState<S>,
api_version: BuilderApiVersion,
) -> eyre::Result<Option<SubmitBlindedBlockResponse>> {
mev_boost::submit_block(signed_blinded_block, req_headers, state, api_version).await
accepted_types: AcceptedEncodings,
) -> eyre::Result<CompoundSubmitBlockResponse> {
mev_boost::submit_block(
signed_blinded_block,
req_headers,
state,
api_version,
accepted_types,
)
.await
}

/// https://ethereum.github.io/builder-specs/#/Builder/registerValidator
Expand Down
Loading