Skip to content
Merged
26 changes: 25 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
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
2 changes: 1 addition & 1 deletion crates/common/src/config/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::de::DeserializeOwned;
use crate::{
config::{ADMIN_JWT_ENV, JWTS_ENV, MUXER_HTTP_MAX_LENGTH},
types::{BlsPublicKey, ModuleId},
utils::read_chunked_body_with_max,
wire::read_chunked_body_with_max,
};

pub fn load_env_var(env: &str) -> Result<String> {
Expand Down
2 changes: 2 additions & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ pub mod interop;
pub mod pbs;
pub mod signature;
pub mod signer;
pub mod ssz;
pub mod types;
pub mod utils;
pub mod wire;

pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
15 changes: 14 additions & 1 deletion crates/common/src/pbs/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use alloy::primitives::{B256, U256};
use lh_types::ForkName;
use thiserror::Error;

use crate::{types::BlsPublicKeyBytes, utils::ResponseReadError};
use crate::{types::BlsPublicKeyBytes, wire::ResponseReadError};

#[derive(Debug, Error)]
pub enum PbsError {
Expand All @@ -28,6 +29,9 @@ pub enum PbsError {

#[error("tokio join error: {0}")]
TokioJoinError(#[from] tokio::task::JoinError),

#[error("SSZ error: {0}")]
SszError(#[from] SszValueError),
}

impl PbsError {
Expand Down Expand Up @@ -107,3 +111,12 @@ 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: {name}")]
UnsupportedFork { name: ForkName },
}
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::*;
10 changes: 9 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,7 +38,11 @@ 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>;
Comment thread
JasonVranek marked this conversation as resolved.
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}`
Expand Down
139 changes: 139 additions & 0 deletions crates/common/src/ssz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use alloy::primitives::U256;
use lh_bls::Signature;
use lh_types::ForkName;
use ssz::{BYTES_PER_LENGTH_OFFSET, Decode, Encode};

use crate::pbs::{
BuilderBidFulu, ExecutionPayloadHeaderFulu, ExecutionRequests, KzgCommitments,
error::SszValueError,
};

/// Test that SSZ encoding and decoding round-trips, returning the decoded
/// struct.
pub fn test_encode_decode_ssz<T: Encode + Decode>(d: &[u8]) -> T {
Comment thread
JasonVranek marked this conversation as resolved.
Outdated
let decoded = T::from_ssz_bytes(d).expect("deserialize");
let encoded = T::as_ssz_bytes(&decoded);
assert_eq!(encoded, d);
decoded
}

// Get the offset of the message in a SignedBuilderBid SSZ structure
fn get_ssz_value_offset_for_fork(fork: ForkName) -> Result<usize, SszValueError> {
match fork {
ForkName::Fulu => {
// Message goes header -> blob_kzg_commitments -> execution_requests -> value ->
// pubkey
Ok(get_message_offset::<BuilderBidFulu>() +
<ExecutionPayloadHeaderFulu as ssz::Decode>::ssz_fixed_len() +
<KzgCommitments as ssz::Decode>::ssz_fixed_len() +
<ExecutionRequests as ssz::Decode>::ssz_fixed_len())
}

_ => Err(SszValueError::UnsupportedFork { name: fork }),
}
}

/// Extracts the bid value from SSZ-encoded SignedBuilderBid response bytes.
pub fn get_bid_value_from_signed_builder_bid_ssz(
response_bytes: &[u8],
fork: ForkName,
) -> Result<U256, SszValueError> {
let value_offset = get_ssz_value_offset_for_fork(fork)?;

// Sanity check the response length so we don't panic trying to slice it
let end_offset = value_offset + 32; // U256 is 32 bytes
if response_bytes.len() < end_offset {
return Err(SszValueError::InvalidPayloadLength {
required: end_offset,
actual: response_bytes.len(),
});
}

// Extract the value bytes and convert to U256
let value_bytes = &response_bytes[value_offset..end_offset];
let value = U256::from_le_slice(value_bytes);
Ok(value)
}

// Get the offset where the `message` field starts in some SignedBuilderBid SSZ
// data. Requires that SignedBuilderBid always has the following structure:
// message -> signature
// where `message` is a BuilderBid type determined by the fork choice, and
// `signature` is a fixed-length Signature type.
fn get_message_offset<BuilderBidType>() -> usize
where
BuilderBidType: ssz::Encode,
{
// Since `message` is the first field, its offset is always 0
let mut offset = 0;

// If it's variable length, then it will be represented by a pointer to
// the actual data, so we need to get the location of where that data starts
if !BuilderBidType::is_ssz_fixed_len() {
offset += BYTES_PER_LENGTH_OFFSET + <Signature as ssz::Decode>::ssz_fixed_len();
}

offset
}

#[cfg(test)]
mod test {
use alloy::primitives::U256;
use lh_types::ForkName;
use ssz::Encode;

use super::get_bid_value_from_signed_builder_bid_ssz;
use crate::{
pbs::{
BuilderBid, BuilderBidFulu, ExecutionPayloadHeaderFulu, ExecutionRequests,
SignedBuilderBid, error::SszValueError,
},
types::{BlsPublicKeyBytes, BlsSignature},
utils::TestRandomSeed,
};

#[test]
fn test_ssz_value_extraction_unsupported_fork() {
let dummy_bytes = vec![0u8; 1000];
let err =
get_bid_value_from_signed_builder_bid_ssz(&dummy_bytes, ForkName::Altair).unwrap_err();
assert!(matches!(err, SszValueError::UnsupportedFork { .. }));
}

#[test]
fn test_ssz_value_extraction_truncated_payload() {
// A payload that is far too short for any supported fork's value offset
let tiny_bytes = vec![0u8; 4];
let err =
get_bid_value_from_signed_builder_bid_ssz(&tiny_bytes, ForkName::Fulu).unwrap_err();
assert!(matches!(err, SszValueError::InvalidPayloadLength { .. }));
}

/// Per-fork positive tests: construct a `SignedBuilderBid` with a known
/// value for each supported fork, SSZ-encode it, and verify
/// `get_bid_value_from_signed_builder_bid_ssz` round-trips correctly.
#[test]
fn test_ssz_value_extraction_with_known_bid() {
// Distinctive value — large enough that endianness bugs produce a
// different number and zero-matches are impossible.
let known_value = U256::from(0x0102_0304_0506_0708_u64);
let pubkey = BlsPublicKeyBytes::test_random();
let sig = BlsSignature::test_random();

// ── Fulu ─────────────────────────────────────────────────────────────
{
let message = BuilderBid::Fulu(BuilderBidFulu {
header: ExecutionPayloadHeaderFulu::test_random(),
blob_kzg_commitments: Default::default(),
execution_requests: ExecutionRequests::default(),
value: known_value,
pubkey,
});
let bid = SignedBuilderBid { message, signature: sig };
let got =
get_bid_value_from_signed_builder_bid_ssz(&bid.as_ssz_bytes(), ForkName::Fulu)
.expect("Fulu extraction failed");
assert_eq!(got, known_value, "Fulu: value mismatch");
}
}
}
Loading
Loading