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
27 changes: 26 additions & 1 deletion Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ ethereum_ssz_derive = "0.10"
eyre = "0.6.12"
futures = "0.3.30"
headers = "0.4.0"
headers-accept = "0.2.1"
indexmap = "2.2.6"
jsonwebtoken = { version = "9.3.1", default-features = false }
lazy_static = "1.5.0"
mediatype = "0.20.0"
lh_eth2 = { package = "eth2", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3", features = ["events"] }
lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3" }
lh_bls = { package = "bls", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3" }
Expand Down
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
2 changes: 2 additions & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ ethereum_ssz.workspace = true
ethereum_ssz_derive.workspace = true
eyre.workspace = true
futures.workspace = true
headers-accept.workspace = true
jsonwebtoken.workspace = true
lazy_static.workspace = true
lh_bls.workspace = true
lh_eth2.workspace = true
lh_eth2_keystore.workspace = true
lh_types.workspace = true
mediatype.workspace = true
notify.workspace = true
pbkdf2.workspace = true
rand.workspace = true
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: 25 additions & 0 deletions crates/common/src/pbs/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub enum PbsError {
#[error("json decode error: {err:?}, raw: {raw}")]
JsonDecode { err: serde_json::Error, raw: String },

#[error("error with request: {0}")]
GeneralRequest(String),

#[error("{0}")]
ReadResponse(#[from] ResponseReadError),

Expand Down Expand Up @@ -107,3 +110,25 @@ pub enum ValidationError {
#[error("unsupported fork")]
UnsupportedFork,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum SszValueError {
#[error("invalid payload length: required {required} but payload was {actual}")]
InvalidPayloadLength { required: usize, actual: usize },

#[error("unsupported fork")]
UnsupportedFork { name: String },
}

impl From<SszValueError> for PbsError {
fn from(err: SszValueError) -> Self {
match err {
SszValueError::InvalidPayloadLength { required, actual } => PbsError::GeneralRequest(
format!("invalid payload length: required {required} but payload was {actual}"),
),
SszValueError::UnsupportedFork { name } => {
PbsError::GeneralRequest(format!("unsupported fork: {name}"))
}
}
}
}
1 change: 1 addition & 0 deletions crates/common/src/pbs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ mod types;

pub use builder::*;
pub use constants::*;
pub use lh_types::ForkVersionDecode;
pub use relay::*;
pub use types::*;
12 changes: 11 additions & 1 deletion crates/common/src/pbs/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloy::primitives::{B256, U256, b256};
use lh_eth2::ForkVersionedResponse;
pub use lh_eth2::ForkVersionedResponse;
pub use lh_types::ForkName;
use lh_types::{BlindedPayload, ExecPayload, MainnetEthSpec};
use serde::{Deserialize, Serialize};
Expand All @@ -26,6 +26,10 @@ pub type PayloadAndBlobs = lh_eth2::types::ExecutionPayloadAndBlobs<MainnetEthSp
pub type SubmitBlindedBlockResponse = ForkVersionedResponse<PayloadAndBlobs>;

pub type ExecutionPayloadHeader = lh_types::ExecutionPayloadHeader<MainnetEthSpec>;
pub type ExecutionPayloadHeaderBellatrix =
lh_types::ExecutionPayloadHeaderBellatrix<MainnetEthSpec>;
pub type ExecutionPayloadHeaderCapella = lh_types::ExecutionPayloadHeaderCapella<MainnetEthSpec>;
pub type ExecutionPayloadHeaderDeneb = lh_types::ExecutionPayloadHeaderDeneb<MainnetEthSpec>;
pub type ExecutionPayloadHeaderElectra = lh_types::ExecutionPayloadHeaderElectra<MainnetEthSpec>;
pub type ExecutionPayloadHeaderFulu = lh_types::ExecutionPayloadHeaderFulu<MainnetEthSpec>;
pub type ExecutionPayloadHeaderRef<'a> = lh_types::ExecutionPayloadHeaderRef<'a, MainnetEthSpec>;
Expand All @@ -34,14 +38,20 @@ pub type ExecutionPayloadElectra = lh_types::ExecutionPayloadElectra<MainnetEthS
pub type ExecutionPayloadFulu = lh_types::ExecutionPayloadFulu<MainnetEthSpec>;
pub type SignedBuilderBid = lh_types::SignedBuilderBid<MainnetEthSpec>;
pub type BuilderBid = lh_types::BuilderBid<MainnetEthSpec>;
pub type BuilderBidBellatrix = lh_types::BuilderBidBellatrix<MainnetEthSpec>;
pub type BuilderBidCapella = lh_types::BuilderBidCapella<MainnetEthSpec>;
pub type BuilderBidDeneb = lh_types::BuilderBidDeneb<MainnetEthSpec>;
pub type BuilderBidElectra = lh_types::BuilderBidElectra<MainnetEthSpec>;
pub type BuilderBidFulu = lh_types::BuilderBidFulu<MainnetEthSpec>;

/// Response object of GET
/// `/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}`
pub type GetHeaderResponse = ForkVersionedResponse<SignedBuilderBid>;

pub type KzgCommitments = lh_types::KzgCommitments<MainnetEthSpec>;

pub type Uint256 = lh_types::Uint256;

/// Response params of GET
/// `/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}`
#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down
Loading