diff --git a/CLAUDE.md b/CLAUDE.md index 1a0d85009..848b5ecf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -669,6 +669,12 @@ existing once per chain. validator: helpers that build the active-index `Vec` and then read each index back were the largest cost left in the import profile (`docs/beacon_stf.md`, "Registry and balances"). +- **`beacon::builder` holds builder-specs containers** (`ValidatorRegistrationV1`, + `BuilderBid`, `BlindedBeaconBlock`), not consensus ones: the MEV-Boost relay + flow's wire types. `BlindedBeaconBlock`'s root equals the full block's, which + is what lets a validator sign a block whose payload it never sees. Electra and + fulu shapes only. Tests decode builder-specs' own examples, vendored under + `crates/common/types/tests/fixtures/builder_specs/`. ## Beacon Chain STF (`crates/blockchain/state_transition/src/beacon/`) @@ -872,6 +878,15 @@ transitions are in `ethlambda-types`, per the section above. Nothing above that is neither parseable nor listed, so a new fork forces a decision. - A fixture case with no `post` state asserts the input must be **rejected**. That rule lives in `check_transition`; do not add a runner that ignores it. +- **`assemble_blinded_block` does not use a blinded STF.** It runs `process_block` + on a stand-in payload built from the builder's header, then patches the two + post-state fields that depend on the payload itself + (`latest_execution_payload_header`, `latest_block_header.body_root`). It + also checks the header's `withdrawals_root` itself: the stand-in carries the + expected withdrawals, so the STF's own withdrawals check passes trivially. Fulu + only, since it is the last fork with blinded blocks. Read the function's doc + before touching fulu's `process_block`; + `blinded_and_full_blocks_reach_the_same_state_root` fails on any drift. ## Configuration Files diff --git a/crates/blockchain/state_transition/src/beacon/block_production.rs b/crates/blockchain/state_transition/src/beacon/block_production.rs index ddcda4de4..9df78e8b5 100644 --- a/crates/blockchain/state_transition/src/beacon/block_production.rs +++ b/crates/blockchain/state_transition/src/beacon/block_production.rs @@ -7,18 +7,20 @@ //! electra's block containers unchanged. //! //! The pieces the execution client supplies (the payload, its blob commitments -//! and its request list) arrive already built; this module decides everything +//! and its request list) arrive already built; `assemble_blinded_block` takes +//! them from a builder's bid instead, fulu only. This module decides everything //! the consensus layer decides, and computes the state root by running the //! block through `process_block` on a copy of the state, the way //! `validator.md` describes (`compute_new_state_root`). use ethlambda_types::beacon::{ + builder::BlindedBeaconBlock, constants, containers::{ BeaconState, SignedBeaconBlock, altair::SyncAggregate, capella::{self, Withdrawal}, - deneb::ExecutionPayload, + deneb::{ExecutionPayload, ExecutionPayloadHeader}, electra::{ self, AggregationBits, Attestation, BeaconBlock, BeaconBlockBody, CommitteeBits, ConsolidationRequest, DepositRequest, ExecutionRequests, WithdrawalRequest, @@ -44,6 +46,7 @@ use super::helpers::accessors::{ use super::helpers::electra::{ get_attesting_indices, get_indexed_attestation, is_valid_indexed_attestation, }; +use super::helpers::fulu::fulu_state; use super::helpers::misc::compute_signing_root; use super::helpers::predicates::is_slashable_validator; use super::lean_boundary::lean_state_unreachable; @@ -514,7 +517,21 @@ pub fn assemble_block( config: &Config, options: stf::block::BlockProcessingOptions, ) -> Result { - let body = BeaconBlockBody { + let body = full_body(state, inputs)?; + let mut block = unsigned_block(state, body)?; + let mut post = post_state(state, &block, config, options)?; + // Fold the block's writes into their trees first, so the root is computed + // on (and cached in) the state's own nodes rather than a throwaway copy, + // as `state_transition` does before it checks a block's state root. + post.apply_pending_mutations(); + block.state_root = post.hash_tree_root(); + Ok(block) +} + +/// The body for `inputs`, voting the state's own `eth1_data` and carrying no +/// deposits; see [`assemble_block`]. +fn full_body(state: &BeaconState, inputs: BlockInputs) -> Result { + Ok(BeaconBlockBody { randao_reveal: inputs.randao_reveal, eth1_data: state.eth1_data().clone(), graffiti: inputs.graffiti, @@ -554,8 +571,13 @@ pub fn assemble_block( .map_err(|_| Error::SpecAssert("len(blob_kzg_commitments) within bound"))?, execution_requests: inputs.execution_requests, ..BeaconBlockBody::empty() - }; - let mut block = BeaconBlock { + }) +} + +/// `body` framed as the block for the slot `state` has been advanced to, its +/// state root still zero. +fn unsigned_block(state: &BeaconState, body: BeaconBlockBody) -> Result { + Ok(BeaconBlock { slot: state.slot(), proposer_index: get_beacon_proposer_index(state)?, // `process_slots` filled the header's state root on the way out of @@ -563,8 +585,19 @@ pub fn assemble_block( parent_root: state.latest_block_header().hash_tree_root(), state_root: Root::ZERO, body, - }; + }) +} +/// A copy of `state` with `block` applied by `process_block`, under an +/// execution engine that accepts its payload. Pending mutations are left +/// unapplied, so the caller can still edit the state before computing its +/// root. +fn post_state( + state: &BeaconState, + block: &BeaconBlock, + config: &Config, + options: stf::block::BlockProcessingOptions, +) -> Result { let signed = electra::SignedBeaconBlock { message: block.clone(), signature: BlsSignature::default(), @@ -582,7 +615,7 @@ pub fn assemble_block( BeaconState::Gloas(_) => { return Err(Error::SpecAssert("block production does not support gloas")); } - BeaconState::Lean(_) => lean_state_unreachable("assemble_block"), + BeaconState::Lean(_) => lean_state_unreachable("post_state"), }; let mut post = state.clone(); stf::block::process_block( @@ -594,12 +627,115 @@ pub fn assemble_block( &ActiveBalanceCache::default(), options, )?; - // Fold the block's writes into their trees first, so the root is computed - // on (and cached in) the state's own nodes rather than a throwaway copy, - // as `state_transition` does before it checks a block's state root. + Ok(post) +} + +/// What a blinded block's body carries: [`BlockInputs`] with a builder's +/// payload header, and the bid's blob commitments and execution requests, in +/// place of a payload this node built. +#[derive(Debug, Clone)] +pub struct BlindedBlockInputs { + pub randao_reveal: BlsSignature, + pub graffiti: Bytes32, + pub attestations: Vec, + /// Slashings, exits and credential changes, as [`pack_operations`] returns + /// them. + pub operations: Operations, + pub execution_payload_header: ExecutionPayloadHeader, + pub blob_kzg_commitments: Vec, + pub execution_requests: ExecutionRequests, + /// The block's sync aggregate: [`empty_sync_aggregate`], or what + /// [`verified_sync_aggregate`] vouched for. + pub sync_aggregate: SyncAggregate, +} + +/// The unsigned blinded block for the slot `state` has been advanced to, +/// around a builder's payload header, with its state root computed. +/// +/// # A state root without the payload +/// +/// The state transition takes full payloads only, so this runs it on a body +/// whose payload is a *stand-in*, then patches the post-state fields that +/// depend on the payload itself rather than on what `process_block` checks of +/// it. Fulu's `process_block`, step by step: +/// +/// | Step | Reads of the payload | With the stand-in | +/// |---|---|---| +/// | `process_block_header` | the body's root | wrong `body_root`: patched | +/// | `process_withdrawals` | `payload.withdrawals == expected` | identical: every write uses the expected withdrawals | +/// | `process_execution_payload` | parent hash, prev_randao, timestamp; then caches the header | checks run on the header's own values; wrong cached header: patched | +/// | the execution engine | the whole payload, in the specification | a constant here, as for a local block: no one checks the builder's payload | +/// | randao, eth1, operations, sync aggregate | none | identical | +/// +/// Two things follow: +/// - The stand-in copies the three checked fields **from the header**. Copied +/// from the state, they would let any header through. +/// - The stand-in carries the expected withdrawals, so the state transition's +/// own withdrawals check passes trivially. That is why this function checks +/// the header's `withdrawals_root` against them itself. +/// +/// This is not bid validation. Beyond what the table checks, the header is +/// trusted as given: the caller verifies everything else it needs from the bid +/// (at least the response's fork version, the signature, the value, the gas +/// limit and `blob_gas_used`) before building on it. +/// +/// `options` is as in [`assemble_block`]. +/// +/// Fulu only. The table describes fulu's `process_block`, and fulu is the last +/// fork with blinded blocks (gloas has none), so it cannot change under this +/// function. `blinded_and_full_blocks_reach_the_same_state_root` is the guard. +pub fn assemble_blinded_block( + state: &BeaconState, + inputs: BlindedBlockInputs, + config: &Config, + options: stf::block::BlockProcessingOptions, +) -> Result { + if !matches!(state, BeaconState::Fulu(_)) { + return Err(Error::SpecAssert("blinded block production is fulu only")); + } + let header = inputs.execution_payload_header; + + let expected_withdrawals: capella::Withdrawals = stf::electra::get_expected_withdrawals(state)? + .0 + .try_into() + .map_err(|_| { + Error::SpecAssert("len(expected_withdrawals) <= MAX_WITHDRAWALS_PER_PAYLOAD") + })?; + verify( + header.withdrawals_root == expected_withdrawals.hash_tree_root(), + "header.withdrawals_root == hash_tree_root(get_expected_withdrawals(state))", + )?; + + let stand_in = ExecutionPayload { + parent_hash: header.parent_hash, + prev_randao: header.prev_randao, + timestamp: header.timestamp, + withdrawals: expected_withdrawals, + ..BeaconBlockBody::empty().execution_payload + }; + let body = full_body( + state, + BlockInputs { + randao_reveal: inputs.randao_reveal, + graffiti: inputs.graffiti, + attestations: inputs.attestations, + operations: inputs.operations, + execution_payload: stand_in, + blob_kzg_commitments: inputs.blob_kzg_commitments, + execution_requests: inputs.execution_requests, + sync_aggregate: inputs.sync_aggregate, + }, + )?; + let block = unsigned_block(state, body)?; + let mut post = post_state(state, &block, config, options)?; + + let mut blinded = BlindedBeaconBlock::from_parts(block, header); + let fields = fulu_state(&mut post, "assemble_blinded_block")?; + fields.latest_execution_payload_header = blinded.body.execution_payload_header.clone(); + fields.latest_block_header.body_root = blinded.body.hash_tree_root(); post.apply_pending_mutations(); - block.state_root = post.hash_tree_root(); - Ok(block) + blinded.state_root = post.hash_tree_root(); + Ok(blinded) } #[cfg(test)] @@ -612,9 +748,11 @@ mod tests { use crate::beacon::helpers::misc::compute_signing_root; use crate::beacon::helpers::test_state::{sign_for, with_signing_validators_at}; use crate::beacon::stf::block::{BlockProcessingOptions, RandaoVerification}; + use ethlambda_types::beacon::containers::bellatrix::Transaction; use ethlambda_types::beacon::containers::shared::{ AttestationData, BeaconBlockHeader, Checkpoint, SignedBeaconBlockHeader, VoluntaryExit, }; + use ethlambda_types::beacon::primitives::{BlsPubkey, ExecutionAddress}; /// A fulu state one epoch in, its lookahead and sync committee filled from /// its real registry (the builder leaves both as placeholders), advanced one @@ -1262,4 +1400,202 @@ mod tests { .all(|b| *b == 0) ); } + + /// [`payload_for`], with every field the stand-in payload does not copy + /// from the header set to something else, so a missed patch changes the + /// state root instead of passing unnoticed. + fn distinctive_payload_for(state: &BeaconState) -> ExecutionPayload { + let transaction: Transaction = vec![0xde, 0xad].try_into().unwrap(); + ExecutionPayload { + fee_recipient: ExecutionAddress::repeat_byte(0x11), + state_root: Bytes32::repeat_byte(0x22), + block_number: 42, + gas_limit: 60_000_000, + gas_used: 21_000, + block_hash: ExecutionBlockHash::repeat_byte(0x33), + transactions: vec![transaction].try_into().unwrap(), + receipts_root: Bytes32::repeat_byte(0x44), + logs_bloom: vec![0x55; preset::BYTES_PER_LOGS_BLOOM].try_into().unwrap(), + extra_data: vec![0x66, 0x77].try_into().unwrap(), + base_fee_per_gas: 7.into(), + // Any distinct non-zero value; no gas-per-blob constant exists here. + blob_gas_used: 131_072, + excess_blob_gas: 262_144, + ..payload_for(state) + } + } + + /// [`state_to_build_on`] with one validator due a partial withdrawal: ETH1 + /// credentials and a balance above what it may keep. + fn state_with_a_withdrawal_due() -> BeaconState { + let mut state = state_to_build_on(); + let validator = state.validator_mut(3).unwrap(); + validator.withdrawal_credentials = { + let mut credentials = Bytes32::repeat_byte(0x12); + credentials.0[0] = constants::ETH1_ADDRESS_WITHDRAWAL_PREFIX; + credentials + }; + validator.effective_balance = preset::MIN_ACTIVATION_BALANCE; + *state.balance_mut(3).unwrap() = preset::MIN_ACTIVATION_BALANCE + 1_000_000_000; + state.apply_pending_mutations(); + state + } + + fn full_inputs(state: &BeaconState) -> BlockInputs { + BlockInputs { + sync_aggregate: empty_sync_aggregate(), + randao_reveal: randao_reveal(state), + graffiti: Bytes32::repeat_byte(7), + attestations: Vec::new(), + operations: Operations::default(), + execution_payload: distinctive_payload_for(state), + blob_kzg_commitments: vec![KzgCommitment::default()], + execution_requests: ExecutionRequests { + deposits: vec![DepositRequest { + pubkey: BlsPubkey([0x88; 48]), + withdrawal_credentials: Bytes32::repeat_byte(0x99), + amount: preset::MIN_ACTIVATION_BALANCE, + signature: BlsSignature::default(), + index: 0, + }] + .try_into() + .unwrap(), + ..ExecutionRequests::default() + }, + } + } + + /// `inputs` as a builder would offer them: the payload's header instead + /// of the payload. + fn blinded(inputs: &BlockInputs) -> BlindedBlockInputs { + BlindedBlockInputs { + randao_reveal: inputs.randao_reveal, + graffiti: inputs.graffiti, + attestations: inputs.attestations.clone(), + operations: inputs.operations.clone(), + execution_payload_header: ExecutionPayloadHeader::from(&inputs.execution_payload), + blob_kzg_commitments: inputs.blob_kzg_commitments.clone(), + execution_requests: inputs.execution_requests.clone(), + sync_aggregate: inputs.sync_aggregate.clone(), + } + } + + /// The guard for [`assemble_blinded_block`]'s two patches: around the same + /// payload, the blinded block lands on the full block's state root and + /// has its block root, so a signature over one is a signature over the + /// other. + #[test] + fn blinded_and_full_blocks_reach_the_same_state_root() { + let state = state_with_a_withdrawal_due(); + assert!( + !stf::electra::get_expected_withdrawals(&state) + .unwrap() + .0 + .is_empty(), + "the guard needs a withdrawal due" + ); + let inputs = full_inputs(&state); + let blinded_inputs = blinded(&inputs); + let config = Config::mainnet(); + + let full = + assemble_block(&state, inputs, &config, BlockProcessingOptions::default()).unwrap(); + let blinded = assemble_blinded_block( + &state, + blinded_inputs, + &config, + BlockProcessingOptions::default(), + ) + .unwrap(); + + assert_eq!(blinded.state_root, full.state_root); + assert_eq!(blinded.hash_tree_root(), full.hash_tree_root()); + assert_eq!(blinded, BlindedBeaconBlock::from(full)); + } + + /// The specification condition a refused blinded block failed. + fn refusal(state: &BeaconState, inputs: BlindedBlockInputs) -> &'static str { + match assemble_blinded_block( + state, + inputs, + &Config::mainnet(), + BlockProcessingOptions::default(), + ) { + Err(Error::SpecAssert(what)) => what, + other => panic!("expected a SpecAssert refusal, got {other:?}"), + } + } + + fn blinded_inputs_for(state: &BeaconState) -> BlindedBlockInputs { + blinded(&full_inputs(state)) + } + + #[test] + fn a_header_with_the_wrong_withdrawals_root_is_refused() { + let state = state_to_build_on(); + let mut inputs = blinded_inputs_for(&state); + inputs.execution_payload_header.withdrawals_root = Root::repeat_byte(0xee); + assert_eq!( + refusal(&state, inputs), + "header.withdrawals_root == hash_tree_root(get_expected_withdrawals(state))" + ); + } + + #[test] + fn a_header_on_the_wrong_parent_is_refused() { + let state = state_to_build_on(); + let mut inputs = blinded_inputs_for(&state); + inputs.execution_payload_header.parent_hash = ExecutionBlockHash::repeat_byte(0xee); + assert_eq!( + refusal(&state, inputs), + "payload.parent_hash == state.latest_execution_payload_header.block_hash" + ); + } + + #[test] + fn a_header_with_the_wrong_prev_randao_is_refused() { + let state = state_to_build_on(); + let mut inputs = blinded_inputs_for(&state); + inputs.execution_payload_header.prev_randao = Bytes32::repeat_byte(0xee); + assert_eq!( + refusal(&state, inputs), + "payload.prev_randao == get_randao_mix(state, get_current_epoch(state))" + ); + } + + #[test] + fn a_header_with_the_wrong_timestamp_is_refused() { + let state = state_to_build_on(); + let mut inputs = blinded_inputs_for(&state); + inputs.execution_payload_header.timestamp += 1; + assert_eq!( + refusal(&state, inputs), + "payload.timestamp == compute_time_at_slot(state, state.slot)" + ); + } + + #[test] + fn a_bid_over_the_blob_limit_is_refused() { + let state = state_to_build_on(); + let limit = Config::mainnet().max_blobs_per_block(get_current_epoch(&state)); + let mut inputs = blinded_inputs_for(&state); + inputs.blob_kzg_commitments = vec![KzgCommitment::default(); limit as usize + 1]; + assert_eq!( + refusal(&state, inputs), + "len(body.blob_kzg_commitments) <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block" + ); + } + + #[test] + fn a_state_before_fulu_is_refused() { + // Only the fork gate runs: it comes before anything reads the inputs, + // so building them from a fulu state does not matter, and no electra + // state transition is exercised. + let inputs = blinded_inputs_for(&state_to_build_on()); + let electra = with_signing_validators_at(ForkName::Electra, 64); + assert_eq!( + refusal(&electra, inputs), + "blinded block production is fulu only" + ); + } } diff --git a/crates/blockchain/state_transition/src/beacon/helpers/fulu.rs b/crates/blockchain/state_transition/src/beacon/helpers/fulu.rs index 3ecbd7cf3..1a5bee5bf 100644 --- a/crates/blockchain/state_transition/src/beacon/helpers/fulu.rs +++ b/crates/blockchain/state_transition/src/beacon/helpers/fulu.rs @@ -48,10 +48,9 @@ //! function mutates state at an epoch boundary, and every other fork's //! equivalent epoch-processing step lives in `crate::beacon::stf`, not in a //! `helpers::` module (see [`super::altair`]'s module docs, which make -//! the same point about `process_attestation`). [`fulu_state`] is kept -//! alongside [`fulu_state_ref`] for whenever that step is written, the same -//! way altair keeps a mutable state projection ready for its own -//! not-yet-written processing steps. +//! the same point about `process_attestation`). It is +//! `crate::beacon::stf::epoch::fulu::process_proposer_lookahead`, which writes +//! the window through [`proposer_lookahead_mut`] rather than [`fulu_state`]. use crate::beacon::constants; use crate::beacon::containers::{BeaconState, fulu}; @@ -222,13 +221,13 @@ pub fn initialize_proposer_lookahead(state: &BeaconState) -> Result( state: &'a mut BeaconState, function: &'static str, diff --git a/crates/common/types/src/beacon/builder.rs b/crates/common/types/src/beacon/builder.rs new file mode 100644 index 000000000..dba64d0f8 --- /dev/null +++ b/crates/common/types/src/beacon/builder.rs @@ -0,0 +1,374 @@ +//! The builder API's own containers (`ethereum/builder-specs`), which the +//! MEV-Boost relay flow exchanges between a validator client, this node and a +//! relay. +//! +//! Not consensus containers: nothing here enters a state. Registrations and +//! bids are signed over their own roots, and [`BlindedBeaconBlock`]'s root must +//! also equal the full block's, which it does because a payload header's root +//! is the root of the payload it summarizes. That equality is what lets a +//! validator sign a blinded block for a full one it never sees. +//! +//! Electra and fulu shapes only, which are identical, as the consensus +//! containers they extend are. + +use libssz_derive::{HashTreeRoot, SszDecode, SszEncode}; +use libssz_types::SszList; + +use crate::beacon::containers::altair::SyncAggregate; +use crate::beacon::containers::capella::SignedBLSToExecutionChange; +use crate::beacon::containers::deneb::{ExecutionPayloadHeader, KzgCommitments}; +use crate::beacon::containers::electra::{ + self, Attestations, AttesterSlashings, ExecutionRequests, +}; +use crate::beacon::containers::shared::{Deposit, Eth1Data, ProposerSlashing, SignedVoluntaryExit}; +use crate::beacon::preset; +use crate::beacon::primitives::{ + BlsPubkey, BlsSignature, Bytes32, ExecutionAddress, Root, Slot, Uint256, ValidatorIndex, +}; + +// --------------------------------------------------------------------------- +// Registrations +// --------------------------------------------------------------------------- + +/// A validator's building preferences, as it sends them to builders through +/// its beacon node (`registerValidator`), builder-specs `bellatrix/builder.md`. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct ValidatorRegistrationV1 { + pub fee_recipient: ExecutionAddress, + #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] + pub gas_limit: u64, + /// Seconds since the Unix epoch. Builders sequence a validator's + /// registrations by this and reject one older than the latest they hold. + #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] + pub timestamp: u64, + pub pubkey: BlsPubkey, +} + +/// A [`ValidatorRegistrationV1`] signed by its validator under +/// [`DOMAIN_APPLICATION_BUILDER`](crate::beacon::constants::DOMAIN_APPLICATION_BUILDER). +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct SignedValidatorRegistrationV1 { + pub message: ValidatorRegistrationV1, + pub signature: BlsSignature, +} + +/// The `registerValidator` request body's SSZ form, in both the Beacon API +/// and the builder API. +pub type SignedValidatorRegistrations = + SszList; + +// --------------------------------------------------------------------------- +// Bids +// --------------------------------------------------------------------------- + +/// A relay's offer of an execution payload, builder-specs `electra/builder.md` +/// (fulu leaves it unchanged): the payload's header, its blob commitments and +/// execution requests, and what it pays. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct BuilderBid { + pub header: ExecutionPayloadHeader, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub blob_kzg_commitments: KzgCommitments, + pub execution_requests: ExecutionRequests, + /// What the proposer is paid, in Wei. + pub value: Uint256, + /// The key the bid is signed with, under + /// [`DOMAIN_APPLICATION_BUILDER`](crate::beacon::constants::DOMAIN_APPLICATION_BUILDER). + pub pubkey: BlsPubkey, +} + +/// A [`BuilderBid`] signed by its `pubkey` under +/// [`DOMAIN_APPLICATION_BUILDER`](crate::beacon::constants::DOMAIN_APPLICATION_BUILDER). +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct SignedBuilderBid { + pub message: BuilderBid, + pub signature: BlsSignature, +} + +// --------------------------------------------------------------------------- +// Blinded blocks +// --------------------------------------------------------------------------- + +/// [`electra::BeaconBlockBody`] with the payload's header in place of the +/// payload, builder-specs `electra/builder.md`. Otherwise the same container +/// field for field, which is why its root equals the full body's. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct BlindedBeaconBlockBody { + pub randao_reveal: BlsSignature, + pub eth1_data: Eth1Data, + pub graffiti: Bytes32, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub proposer_slashings: SszList, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub attester_slashings: AttesterSlashings, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub attestations: Attestations, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub deposits: SszList, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub voluntary_exits: SszList, + pub sync_aggregate: SyncAggregate, + pub execution_payload_header: ExecutionPayloadHeader, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub bls_to_execution_changes: + SszList, + #[serde(with = "crate::beacon::serde_helpers::seq")] + pub blob_kzg_commitments: KzgCommitments, + pub execution_requests: ExecutionRequests, +} + +impl BlindedBeaconBlockBody { + /// `body` with its payload swapped for `execution_payload_header`. + /// + /// The header is taken as given, so the result's root equals `body`'s only + /// when the header is `body.execution_payload`'s own. Blinded block + /// production relies on the header being kept as given: it builds the body + /// around a stand-in payload and blinds it with the builder's header. + pub fn from_parts( + body: electra::BeaconBlockBody, + execution_payload_header: ExecutionPayloadHeader, + ) -> Self { + Self { + randao_reveal: body.randao_reveal, + eth1_data: body.eth1_data, + graffiti: body.graffiti, + proposer_slashings: body.proposer_slashings, + attester_slashings: body.attester_slashings, + attestations: body.attestations, + deposits: body.deposits, + voluntary_exits: body.voluntary_exits, + sync_aggregate: body.sync_aggregate, + execution_payload_header, + bls_to_execution_changes: body.bls_to_execution_changes, + blob_kzg_commitments: body.blob_kzg_commitments, + execution_requests: body.execution_requests, + } + } +} + +/// Blinds a full body: its payload replaced by that payload's own header. +impl From for BlindedBeaconBlockBody { + fn from(body: electra::BeaconBlockBody) -> Self { + let header = ExecutionPayloadHeader::from(&body.execution_payload); + Self::from_parts(body, header) + } +} + +/// [`electra::BeaconBlock`] around a [`BlindedBeaconBlockBody`]. What a +/// validator signs when a builder supplies the payload. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct BlindedBeaconBlock { + #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] + pub slot: Slot, + #[serde(with = "crate::beacon::serde_helpers::quoted_or_bare")] + pub proposer_index: ValidatorIndex, + pub parent_root: Root, + pub state_root: Root, + pub body: BlindedBeaconBlockBody, +} + +impl BlindedBeaconBlock { + /// `block` with its body's payload swapped for `execution_payload_header`; + /// see [`BlindedBeaconBlockBody::from_parts`]. The block's `state_root` is + /// copied as is, so a caller passing a header other than the payload's own + /// must recompute it, as blinded block production does. + pub fn from_parts( + block: electra::BeaconBlock, + execution_payload_header: ExecutionPayloadHeader, + ) -> Self { + Self { + slot: block.slot, + proposer_index: block.proposer_index, + parent_root: block.parent_root, + state_root: block.state_root, + body: BlindedBeaconBlockBody::from_parts(block.body, execution_payload_header), + } + } +} + +/// Blinds a full block: its payload replaced by that payload's own header, +/// keeping the block's root. +impl From for BlindedBeaconBlock { + fn from(block: electra::BeaconBlock) -> Self { + let header = ExecutionPayloadHeader::from(&block.body.execution_payload); + Self::from_parts(block, header) + } +} + +/// A [`BlindedBeaconBlock`] signed by its proposer under the full block's +/// domain, `DOMAIN_BEACON_PROPOSER`, since the two share a root. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] +pub struct SignedBlindedBeaconBlock { + pub message: BlindedBeaconBlock, + pub signature: BlsSignature, +} + +#[cfg(test)] +mod tests { + use libssz::{SszDecode as _, SszEncode as _}; + + use super::*; + use crate::beacon::containers::deneb::ExecutionPayload; + use crate::beacon::primitives::HashTreeRoot as _; + + /// A vendored builder-specs example file (see the fixtures' README). + macro_rules! example { + ($path:literal) => { + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/builder_specs/", + $path + )) + }; + } + + /// The object an example `.json` holds under its `value` key. + fn example_value(json: &[u8]) -> serde_json::Value { + let mut document: serde_json::Value = serde_json::from_slice(json).unwrap(); + document["value"].take() + } + + #[test] + fn the_registration_example_decodes_from_both_encodings() { + let ssz = example!("bellatrix/signed_validator_registrations.ssz"); + let from_ssz = SignedValidatorRegistrations::from_ssz_bytes(ssz).unwrap(); + let value = example_value(example!("bellatrix/signed_validator_registrations.json")); + let from_json: Vec = + serde_json::from_value(value.clone()).unwrap(); + + assert_eq!(from_ssz.to_vec(), from_json); + // What this node would send a relay. + assert_eq!(serde_json::to_value(from_ssz.to_vec()).unwrap(), value); + assert_eq!(from_ssz.to_ssz(), ssz.to_vec()); + assert_eq!(from_json[0].message.gas_limit, 100); + assert_eq!(from_json[0].message.timestamp, 100); + } + + #[test] + fn the_bid_example_decodes_from_both_encodings() { + let ssz = example!("fulu/signed_builder_bid.ssz"); + let from_ssz = SignedBuilderBid::from_ssz_bytes(ssz).unwrap(); + let value = example_value(example!("fulu/signed_builder_bid.json")); + assert_eq!(value["version"], "fulu"); + let from_json: SignedBuilderBid = serde_json::from_value(value["data"].clone()).unwrap(); + + assert_eq!(from_ssz, from_json); + assert_eq!(serde_json::to_value(&from_ssz).unwrap(), value["data"]); + assert_eq!(from_ssz.to_ssz(), ssz.to_vec()); + assert_eq!(from_json.message.value, Uint256::from(1u64)); + assert_eq!(from_json.message.blob_kzg_commitments.len(), 1); + } + + /// A block with every list populated. Empty lists with equal limits share + /// a root, so a body with empty ones would not notice two of its fields + /// swapping places. + fn full_block() -> electra::BeaconBlock { + crate::beacon::containers::json_tests::electra_block().message + } + + #[test] + fn a_blinded_block_has_the_full_blocks_root() { + let block = full_block(); + let blinded = BlindedBeaconBlock::from(block.clone()); + assert_eq!(blinded.body.hash_tree_root(), block.body.hash_tree_root()); + assert_eq!(blinded.hash_tree_root(), block.hash_tree_root()); + assert_eq!(blinded.state_root, block.state_root); + } + + #[test] + fn from_parts_keeps_the_header_it_is_given() { + let block = full_block(); + let other = ExecutionPayloadHeader::from(&ExecutionPayload { + block_number: 99, + ..block.body.execution_payload.clone() + }); + let blinded = BlindedBeaconBlock::from_parts(block.clone(), other.clone()); + assert_eq!(blinded.body.execution_payload_header, other); + assert_ne!(blinded.hash_tree_root(), block.hash_tree_root()); + } + + // The example's sync aggregate is mainnet-sized (`SYNC_COMMITTEE_SIZE` + // bits), which the minimal preset's container does not decode. + #[cfg(not(feature = "preset-minimal"))] + #[test] + fn the_blinded_block_example_decodes_from_both_encodings() { + let ssz = example!("fulu/signed_blinded_beacon_block.ssz"); + let from_ssz = SignedBlindedBeaconBlock::from_ssz_bytes(ssz).unwrap(); + let value = example_value(example!("fulu/signed_blinded_beacon_block.json")); + let from_json: SignedBlindedBeaconBlock = serde_json::from_value(value.clone()).unwrap(); + + assert_eq!(from_ssz, from_json); + assert_eq!(serde_json::to_value(&from_ssz).unwrap(), value); + assert_eq!(from_ssz.to_ssz(), ssz.to_vec()); + } +} diff --git a/crates/common/types/src/beacon/constants.rs b/crates/common/types/src/beacon/constants.rs index 171ddece9..2b285c1b3 100644 --- a/crates/common/types/src/beacon/constants.rs +++ b/crates/common/types/src/beacon/constants.rs @@ -127,9 +127,19 @@ pub const DOMAIN_AGGREGATE_AND_PROOF: DomainType = [0x06, 0x00, 0x00, 0x00]; /// application-specific domains outside the specification. Every /// specification-defined `DomainType` masks to zero against this; a /// `DomainType` that does not is reserved for other uses (for example -/// re-purposing beacon chain signatures on an application layer) and this -/// crate never produces or expects one. +/// re-purposing beacon chain signatures on an application layer). [`DOMAIN_APPLICATION_BUILDER`] +/// is one. pub const DOMAIN_APPLICATION_MASK: DomainType = [0x00, 0x00, 0x00, 0x01]; +/// Domain for the builder API's own messages (`ValidatorRegistrationV1`, +/// `BuilderBid`), builder-specs `bellatrix/builder.md`. Always computed +/// against the chain's genesis fork version and a zero validators root, so +/// one signature stays valid across every fork of its chain. +/// +/// Not [`DOMAIN_BEACON_BUILDER`], gloas's in-protocol builder domain: that one +/// goes through `get_domain`, which mixes in the current fork and the chain's +/// validators root, so signing a builder API message with it produces a +/// signature no relay accepts. +pub const DOMAIN_APPLICATION_BUILDER: DomainType = [0x00, 0x00, 0x00, 0x01]; /// Domain for a `SyncCommitteeMessage` (altair). pub const DOMAIN_SYNC_COMMITTEE: DomainType = [0x07, 0x00, 0x00, 0x00]; /// Domain for a sync committee aggregator's selection proof (altair). @@ -142,6 +152,8 @@ pub const DOMAIN_CONTRIBUTION_AND_PROOF: DomainType = [0x09, 0x00, 0x00, 0x00]; /// [`ETH1_ADDRESS_WITHDRAWAL_PREFIX`]. pub const DOMAIN_BLS_TO_EXECUTION_CHANGE: DomainType = [0x0a, 0x00, 0x00, 0x00]; /// Domain for a `SignedExecutionPayloadBid` (gloas). +/// +/// Not the builder API's [`DOMAIN_APPLICATION_BUILDER`]. pub const DOMAIN_BEACON_BUILDER: DomainType = [0x0b, 0x00, 0x00, 0x00]; /// Domain for a `PayloadAttestationMessage` (gloas). pub const DOMAIN_PTC_ATTESTER: DomainType = [0x0c, 0x00, 0x00, 0x00]; @@ -461,6 +473,18 @@ mod tests { } } + #[test] + fn the_builder_domain_is_an_application_domain() { + // builder-specs' `DomainType('0x00000001')`: the reserved bit set, so + // no specification domain can collide with it. + let masked: Vec = DOMAIN_APPLICATION_BUILDER + .iter() + .zip(DOMAIN_APPLICATION_MASK) + .map(|(domain, mask)| domain & mask) + .collect(); + assert_eq!(masked, [0u8, 0, 0, 1]); + } + #[test] fn custody_settings_match_both_shipped_configs() { // Identical in configs/mainnet.yaml and configs/minimal.yaml, which is diff --git a/crates/common/types/src/beacon/containers/deneb.rs b/crates/common/types/src/beacon/containers/deneb.rs index 195dbeb20..75af4ef47 100644 --- a/crates/common/types/src/beacon/containers/deneb.rs +++ b/crates/common/types/src/beacon/containers/deneb.rs @@ -46,6 +46,7 @@ use super::shared::{ StateRoots, Validators, }; use crate::beacon::preset; +use crate::beacon::primitives::HashTreeRoot as _; use crate::beacon::primitives::{ BlobIndex, BlsSignature, Bytes32, ExecutionAddress, ExecutionBlockHash, KzgCommitment, KzgProof, Root, Slot, Uint256, ValidatorIndex, VersionedHash, WithdrawalIndex, @@ -134,7 +135,17 @@ pub struct ExecutionPayload { /// [`ExecutionPayload`] with the bulky fields replaced by their roots, which /// is what the state retains once a payload is no longer the newest one. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, SszEncode, SszDecode, HashTreeRoot)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + SszEncode, + SszDecode, + HashTreeRoot, +)] pub struct ExecutionPayloadHeader { pub parent_hash: ExecutionBlockHash, pub fee_recipient: ExecutionAddress, @@ -163,6 +174,34 @@ pub struct ExecutionPayloadHeader { pub excess_blob_gas: u64, } +/// The header the state caches for `payload`, and what a blinded block carries +/// in its place: every field copied, the two lists replaced by their roots. +/// Built the same way `process_execution_payload` builds the cached header, so +/// `hash_tree_root(header) == hash_tree_root(payload)`. +impl From<&ExecutionPayload> for ExecutionPayloadHeader { + fn from(payload: &ExecutionPayload) -> Self { + Self { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom.clone(), + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.clone(), + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions_root: payload.transactions.hash_tree_root(), + withdrawals_root: payload.withdrawals.hash_tree_root(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + /// What `process_execution_payload` hands `execution_engine.verify_and_notify_new_payload` /// to validate a proposed payload, deneb `beacon-chain.md`. /// @@ -457,8 +496,56 @@ pub struct BlobSidecar { mod tests { use libssz::{SszDecode as _, SszEncode as _}; + use super::super::bellatrix::Transaction; + use super::super::capella::Withdrawal; use super::*; + /// A payload whose every field differs from the empty one, with a + /// transaction and a withdrawal, so both list roots are non-trivial. + fn sample_payload() -> ExecutionPayload { + let transaction: Transaction = vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(); + let withdrawal = Withdrawal { + index: 1, + validator_index: 2, + address: ExecutionAddress::repeat_byte(3), + amount: 4, + }; + ExecutionPayload { + parent_hash: ExecutionBlockHash::repeat_byte(1), + fee_recipient: ExecutionAddress::repeat_byte(2), + state_root: Bytes32::repeat_byte(3), + receipts_root: Bytes32::repeat_byte(4), + logs_bloom: LogsBloom::try_from(vec![5u8; preset::BYTES_PER_LOGS_BLOOM]).unwrap(), + prev_randao: Bytes32::repeat_byte(6), + block_number: 7, + gas_limit: 8, + gas_used: 9, + timestamp: 10, + extra_data: vec![11u8; 4].try_into().unwrap(), + base_fee_per_gas: Uint256::from(12u64), + block_hash: ExecutionBlockHash::repeat_byte(13), + transactions: vec![transaction].try_into().unwrap(), + withdrawals: vec![withdrawal].try_into().unwrap(), + blob_gas_used: 14, + excess_blob_gas: 15, + } + } + + #[test] + fn a_payloads_header_has_the_payloads_root() { + let payload = sample_payload(); + let header = ExecutionPayloadHeader::from(&payload); + assert_eq!(header.hash_tree_root(), payload.hash_tree_root()); + assert_eq!( + header.transactions_root, + payload.transactions.hash_tree_root() + ); + assert_eq!( + header.withdrawals_root, + payload.withdrawals.hash_tree_root() + ); + } + #[test] fn blob_identifier_round_trips_through_ssz() { let identifier = BlobIdentifier { diff --git a/crates/common/types/src/beacon/containers/json_tests.rs b/crates/common/types/src/beacon/containers/json_tests.rs index 5506897b8..73f9d0a6a 100644 --- a/crates/common/types/src/beacon/containers/json_tests.rs +++ b/crates/common/types/src/beacon/containers/json_tests.rs @@ -188,7 +188,7 @@ fn electra_requests() -> electra::ExecutionRequests { } } -fn electra_block() -> electra::SignedBeaconBlock { +pub(crate) fn electra_block() -> electra::SignedBeaconBlock { let mut body = electra::BeaconBlockBody::empty(); body.randao_reveal = signature(1); body.graffiti = [9; 32].into(); @@ -373,6 +373,13 @@ fn a_fulu_block_round_trips_through_json() { round_trip(&electra_block()); } +#[test] +fn an_execution_payload_header_round_trips_through_json() { + round_trip(&super::deneb::ExecutionPayloadHeader::from( + &electra_payload(), + )); +} + #[test] fn a_gloas_block_round_trips_through_json() { round_trip(&gloas_block()); diff --git a/crates/common/types/src/beacon/containers/mod.rs b/crates/common/types/src/beacon/containers/mod.rs index 6c0856a45..145eed1a0 100644 --- a/crates/common/types/src/beacon/containers/mod.rs +++ b/crates/common/types/src/beacon/containers/mod.rs @@ -1921,7 +1921,7 @@ impl DataColumnSidecar { } #[cfg(test)] -mod json_tests; +pub(crate) mod json_tests; #[cfg(test)] mod tests { diff --git a/crates/common/types/src/beacon/mod.rs b/crates/common/types/src/beacon/mod.rs index 4b33915db..2dbab74ea 100644 --- a/crates/common/types/src/beacon/mod.rs +++ b/crates/common/types/src/beacon/mod.rs @@ -6,6 +6,7 @@ //! transition lives //! under this module so both sets can coexist. +pub mod builder; pub mod committees; pub mod config; pub mod constants; diff --git a/crates/common/types/tests/fixtures/builder_specs/README.md b/crates/common/types/tests/fixtures/builder_specs/README.md new file mode 100644 index 000000000..b55e49e28 --- /dev/null +++ b/crates/common/types/tests/fixtures/builder_specs/README.md @@ -0,0 +1,18 @@ +# builder-specs examples + +Copied unchanged from `ethereum/builder-specs` at +`fce64468a81a87892d7cb9b4858272bb3d010e50` (2026-10-20), `examples//`. +CC0 1.0, like the rest of that repository. + +These are the OpenAPI examples the spec's endpoints cite. Each `.json` holds +its object under `value`, OpenAPI's example wrapper, which is never sent on +the wire. Each `.ssz` is the same object's SSZ encoding. + +| Example | What it is | +|---|---| +| `bellatrix/signed_validator_registrations` | `registerValidator` request body: a list of registrations | +| `fulu/signed_builder_bid` | `getHeader` response; the JSON keeps the real `{version, data}` envelope, the SSZ is the bare `SignedBuilderBid` | +| `fulu/signed_blinded_beacon_block` | `submitBlindedBlockV2` request body | + +The registration example lives under `bellatrix/` upstream because +`ValidatorRegistrationV1` is not fork-versioned. diff --git a/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.json b/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.json new file mode 100644 index 000000000..542fe4cdd --- /dev/null +++ b/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.json @@ -0,0 +1,13 @@ +{ + "value": [ + { + "message": { + "fee_recipient": "0x000102030405060708090a0b0c0d0e0f10111213", + "gas_limit": "100", + "timestamp": "100", + "pubkey": "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f" + }, + "signature": "0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + } + ] +} diff --git a/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.ssz b/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.ssz new file mode 100644 index 000000000..590315a36 Binary files /dev/null and b/crates/common/types/tests/fixtures/builder_specs/bellatrix/signed_validator_registrations.ssz differ diff --git a/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.json b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.json new file mode 100644 index 000000000..4c23a022b --- /dev/null +++ b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.json @@ -0,0 +1,222 @@ +{ + "value": { + "message": { + "slot": "1", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "body": { + "randao_reveal": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "eth1_data": { + "deposit_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "deposit_count": "1", + "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "graffiti": "0xdeadbeefc0ffeedeadbeefc0ffeedeadbeefc0ffeedeadbeefc0ffeedeadbeef", + "proposer_slashings": [ + { + "signed_header_1": { + "message": { + "slot": "1", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + }, + "signed_header_2": { + "message": { + "slot": "1", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + } + ], + "attester_slashings": [ + { + "attestation_1": { + "attesting_indices": [ + "1" + ], + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "data": { + "slot": "1", + "index": "1", + "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "source": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "target": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + } + } + }, + "attestation_2": { + "attesting_indices": [ + "1" + ], + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "data": { + "slot": "1", + "index": "1", + "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "source": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "target": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + } + } + } + } + ], + "attestations": [ + { + "aggregation_bits": "0xffffffffffffffffffffffffffffffff", + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "data": { + "slot": "1", + "index": "1", + "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "source": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "target": { + "epoch": "1", + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + } + }, + "committee_bits": "0x0000000000000001" + } + ], + "deposits": [ + { + "proof": [ + "0xeeffb6c21a01d3abf09cd6c56e5d48f5ea0fc3bb0de906e3beea3e73776329cb", + "0x601c3b24a99d023224d50811bed19449890febb719a31d09ac414c4632f3c0ba", + "0xbb5e485e0a366e16510de33731d71204ad2fe0f7c600861fc2ac4685212c34e3", + "0x0006964745296a3e6ebf3954a1541e73205f1eefaddfc48ca9dc856bf159bca2", + "0x2c6020f1f9712b89f59550aec05b7c23cb1b113762399c0ca5b8fdd2fa85ce57", + "0x1c15634783e1d9d2cb969da66fd72cafca5026191d911b83211318d183c5ea59", + "0xdfbdf99a1fde57899df1545be1f91bc8a8a9f46c4bac619e28e92aff276de41f", + "0xfe9b0f0c05fde6bd26ce63d394058844ad4451f70b6d2547f49c5c2a5c7891a1", + "0x165f84ee467d18dbafdb07275dc42fb988ab696b0a7ad94c52f4d7a27144b994", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xecdbe5e5056b968aa726a08f1aa33f5d41540eed42f59ace020431cf38a5144e", + "0xc4498c5eb1feeb0b225a3f332bdf523dbc013a5b336a851fce1c055b4019a457", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x8a9b66ad79116c9fc6eed14bde76e8f486669e59b0b5bb0c60a6b3caea38b83d", + "0x267c5455e4806b5d0ad5573552d0162e0983595bac25dacd9078174a2766643a", + "0x27e0c6357985de4d6026d6da14f31e8bfe14524056fec69dc06d6f8a239344af", + "0xf8455aebc24849bea870fbcef1235e2d27c8fd27db24e26d30d0173f3b207874", + "0xaba01bf7fe57be4373f47ff8ea6adc4348fab087b69b2518ce630820f95f4150", + "0xd47152335d9460f2b6fb7aba05ced32a52e9f46659ccd3daa2059661d75a6308", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xf7ed070000000000000000000000000000000000000000000000000000000000" + ], + "data": { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "amount": "1", + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + } + ], + "voluntary_exits": [ + { + "message": { + "epoch": "1", + "validator_index": "1" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + ], + "sync_aggregate": { + "sync_committee_bits": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffffffffffffffffffffffbffffffffffffffffffffbffffffffffffff", + "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + }, + "execution_payload_header": { + "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "block_number": "1", + "gas_limit": "1", + "gas_used": "1", + "timestamp": "1", + "extra_data": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "base_fee_per_gas": "1", + "blob_gas_used": "1", + "excess_blob_gas": "1", + "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "transactions_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "withdrawals_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "bls_to_execution_changes": [ + { + "message": { + "validator_index": "1", + "from_bls_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "to_execution_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + ], + "blob_kzg_commitments": [ + "0xa94170080872584e54a1cf092d845703b13907f2e6b3b1c0ad573b910530499e3bcd48c6378846b80d2bfa58c81cf3d5" + ], + "execution_requests": { + "deposits": [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "amount": "1", + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "index": "1" + } + ], + "withdrawals": [ + { + "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "validator_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "amount": "1" + } + ], + "consolidations": [ + { + "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "source_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "target_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + } + ] + } + } + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + } + \ No newline at end of file diff --git a/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.ssz b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.ssz new file mode 100644 index 000000000..e85969a4b Binary files /dev/null and b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_blinded_beacon_block.ssz differ diff --git a/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.json b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.json new file mode 100644 index 000000000..382762729 --- /dev/null +++ b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.json @@ -0,0 +1,60 @@ +{ + "value": { + "version": "fulu", + "data": { + "message": { + "header": { + "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "block_number": "1", + "gas_limit": "1", + "gas_used": "1", + "timestamp": "1", + "extra_data": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "base_fee_per_gas": "1", + "blob_gas_used": "1", + "excess_blob_gas": "1", + "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "transactions_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "withdrawals_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "blob_kzg_commitments": [ + "0xa94170080872584e54a1cf092d845703b13907f2e6b3b1c0ad573b910530499e3bcd48c6378846b80d2bfa58c81cf3d5" + ], + "execution_requests": { + "deposits": [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "amount": "1", + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + "index": "1" + } + ], + "withdrawals": [ + { + "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "validator_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "amount": "1" + } + ], + "consolidations": [ + { + "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "source_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "target_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + } + ] + }, + "value": "1", + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + } + } + \ No newline at end of file diff --git a/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.ssz b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.ssz new file mode 100644 index 000000000..cd853a077 Binary files /dev/null and b/crates/common/types/tests/fixtures/builder_specs/fulu/signed_builder_bid.ssz differ