Skip to content

Commit 0b82ae1

Browse files
committed
- move logic from utils to ssz and wire modules.
- ssz drops support for past forks
1 parent cfd290a commit 0b82ae1

17 files changed

Lines changed: 1080 additions & 1159 deletions

File tree

crates/common/src/config/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::de::DeserializeOwned;
99
use crate::{
1010
config::{ADMIN_JWT_ENV, JWTS_ENV, MUXER_HTTP_MAX_LENGTH},
1111
types::{BlsPublicKey, ModuleId},
12-
utils::read_chunked_body_with_max,
12+
wire::read_chunked_body_with_max,
1313
};
1414

1515
pub fn load_env_var(env: &str) -> Result<String> {

crates/common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ pub mod interop;
77
pub mod pbs;
88
pub mod signature;
99
pub mod signer;
10+
pub mod ssz;
1011
pub mod types;
1112
pub mod utils;
13+
pub mod wire;
1214

1315
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);

crates/common/src/pbs/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use alloy::primitives::{B256, U256};
22
use lh_types::ForkName;
33
use thiserror::Error;
44

5-
use crate::{types::BlsPublicKeyBytes, utils::ResponseReadError};
5+
use crate::{types::BlsPublicKeyBytes, wire::ResponseReadError};
66

77
#[derive(Debug, Error)]
88
pub enum PbsError {

crates/common/src/ssz.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use alloy::primitives::U256;
2+
use lh_bls::Signature;
3+
use lh_types::ForkName;
4+
use ssz::{BYTES_PER_LENGTH_OFFSET, Decode, Encode};
5+
6+
use crate::pbs::{
7+
BuilderBidFulu, ExecutionPayloadHeaderFulu, ExecutionRequests, KzgCommitments,
8+
error::SszValueError,
9+
};
10+
11+
/// Test that SSZ encoding and decoding round-trips, returning the decoded
12+
/// struct.
13+
pub fn test_encode_decode_ssz<T: Encode + Decode>(d: &[u8]) -> T {
14+
let decoded = T::from_ssz_bytes(d).expect("deserialize");
15+
let encoded = T::as_ssz_bytes(&decoded);
16+
assert_eq!(encoded, d);
17+
decoded
18+
}
19+
20+
// Get the offset of the message in a SignedBuilderBid SSZ structure
21+
fn get_ssz_value_offset_for_fork(fork: ForkName) -> Result<usize, SszValueError> {
22+
match fork {
23+
ForkName::Fulu => {
24+
// Message goes header -> blob_kzg_commitments -> execution_requests -> value ->
25+
// pubkey
26+
Ok(get_message_offset::<BuilderBidFulu>() +
27+
<ExecutionPayloadHeaderFulu as ssz::Decode>::ssz_fixed_len() +
28+
<KzgCommitments as ssz::Decode>::ssz_fixed_len() +
29+
<ExecutionRequests as ssz::Decode>::ssz_fixed_len())
30+
}
31+
32+
_ => Err(SszValueError::UnsupportedFork { name: fork }),
33+
}
34+
}
35+
36+
/// Extracts the bid value from SSZ-encoded SignedBuilderBid response bytes.
37+
pub fn get_bid_value_from_signed_builder_bid_ssz(
38+
response_bytes: &[u8],
39+
fork: ForkName,
40+
) -> Result<U256, SszValueError> {
41+
let value_offset = get_ssz_value_offset_for_fork(fork)?;
42+
43+
// Sanity check the response length so we don't panic trying to slice it
44+
let end_offset = value_offset + 32; // U256 is 32 bytes
45+
if response_bytes.len() < end_offset {
46+
return Err(SszValueError::InvalidPayloadLength {
47+
required: end_offset,
48+
actual: response_bytes.len(),
49+
});
50+
}
51+
52+
// Extract the value bytes and convert to U256
53+
let value_bytes = &response_bytes[value_offset..end_offset];
54+
let value = U256::from_le_slice(value_bytes);
55+
Ok(value)
56+
}
57+
58+
// Get the offset where the `message` field starts in some SignedBuilderBid SSZ
59+
// data. Requires that SignedBuilderBid always has the following structure:
60+
// message -> signature
61+
// where `message` is a BuilderBid type determined by the fork choice, and
62+
// `signature` is a fixed-length Signature type.
63+
fn get_message_offset<BuilderBidType>() -> usize
64+
where
65+
BuilderBidType: ssz::Encode,
66+
{
67+
// Since `message` is the first field, its offset is always 0
68+
let mut offset = 0;
69+
70+
// If it's variable length, then it will be represented by a pointer to
71+
// the actual data, so we need to get the location of where that data starts
72+
if !BuilderBidType::is_ssz_fixed_len() {
73+
offset += BYTES_PER_LENGTH_OFFSET + <Signature as ssz::Decode>::ssz_fixed_len();
74+
}
75+
76+
offset
77+
}
78+
79+
#[cfg(test)]
80+
mod test {
81+
use alloy::primitives::U256;
82+
use lh_types::ForkName;
83+
use ssz::Encode;
84+
85+
use super::get_bid_value_from_signed_builder_bid_ssz;
86+
use crate::{
87+
pbs::{
88+
BuilderBid, BuilderBidFulu, ExecutionPayloadHeaderFulu, ExecutionRequests,
89+
SignedBuilderBid, error::SszValueError,
90+
},
91+
types::{BlsPublicKeyBytes, BlsSignature},
92+
utils::TestRandomSeed,
93+
};
94+
95+
#[test]
96+
fn test_ssz_value_extraction_unsupported_fork() {
97+
let dummy_bytes = vec![0u8; 1000];
98+
let err =
99+
get_bid_value_from_signed_builder_bid_ssz(&dummy_bytes, ForkName::Altair).unwrap_err();
100+
assert!(matches!(err, SszValueError::UnsupportedFork { .. }));
101+
}
102+
103+
#[test]
104+
fn test_ssz_value_extraction_truncated_payload() {
105+
// A payload that is far too short for any supported fork's value offset
106+
let tiny_bytes = vec![0u8; 4];
107+
let err =
108+
get_bid_value_from_signed_builder_bid_ssz(&tiny_bytes, ForkName::Fulu).unwrap_err();
109+
assert!(matches!(err, SszValueError::InvalidPayloadLength { .. }));
110+
}
111+
112+
/// Per-fork positive tests: construct a `SignedBuilderBid` with a known
113+
/// value for each supported fork, SSZ-encode it, and verify
114+
/// `get_bid_value_from_signed_builder_bid_ssz` round-trips correctly.
115+
#[test]
116+
fn test_ssz_value_extraction_with_known_bid() {
117+
// Distinctive value — large enough that endianness bugs produce a
118+
// different number and zero-matches are impossible.
119+
let known_value = U256::from(0x0102_0304_0506_0708_u64);
120+
let pubkey = BlsPublicKeyBytes::test_random();
121+
let sig = BlsSignature::test_random();
122+
123+
// ── Fulu ─────────────────────────────────────────────────────────────
124+
{
125+
let message = BuilderBid::Fulu(BuilderBidFulu {
126+
header: ExecutionPayloadHeaderFulu::test_random(),
127+
blob_kzg_commitments: Default::default(),
128+
execution_requests: ExecutionRequests::default(),
129+
value: known_value,
130+
pubkey,
131+
});
132+
let bid = SignedBuilderBid { message, signature: sig };
133+
let got =
134+
get_bid_value_from_signed_builder_bid_ssz(&bid.as_ssz_bytes(), ForkName::Fulu)
135+
.expect("Fulu extraction failed");
136+
assert_eq!(got, known_value, "Fulu: value mismatch");
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)