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
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 {
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