From d12bb4d712602fe2df0d3101863f4e33895fdb88 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 23 Feb 2026 12:06:15 -0800 Subject: [PATCH] Trying something out --- beacon_node/beacon_chain/src/beacon_chain.rs | 68 +-- .../beacon_chain/src/beacon_proposer_cache.rs | 79 ++- .../gossip_verified_envelope.rs | 79 ++- .../src/payload_envelope_verification/mod.rs | 19 +- .../payload_envelope_verification/tests.rs | 524 ++++++++++++++++++ 5 files changed, 680 insertions(+), 89 deletions(-) create mode 100644 beacon_node/beacon_chain/src/payload_envelope_verification/tests.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a491e8559b..4894bdaee9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4,9 +4,7 @@ use crate::attestation_verification::{ batch_verify_unaggregated_attestations, }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; -use crate::beacon_proposer_cache::{ - BeaconProposerCache, EpochBlockProposers, ensure_state_can_determine_proposers_for_epoch, -}; +use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::POS_PANDA_BANNER; @@ -6548,62 +6546,14 @@ impl BeaconChain { accessor: impl Fn(&EpochBlockProposers) -> Result, state_provider: impl FnOnce() -> Result<(Hash256, BeaconState), E>, ) -> Result { - let cache_entry = self - .beacon_proposer_cache - .lock() - .get_or_insert_key(proposal_epoch, shuffling_decision_block); - - // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. - // This prevents duplication of work across multiple threads. - // - // If it is already initialised, then `get_or_try_init` will return immediately without - // executing the initialisation code at all. - let epoch_block_proposers = cache_entry.get_or_try_init(|| { - // Fetch the state on-demand if the required epoch was missing from the cache. - // If the caller wants to not compute the state they must return an error here and then - // catch it at the call site. - let (state_root, mut state) = state_provider()?; - - // Ensure the state can compute proposer duties for `epoch`. - ensure_state_can_determine_proposers_for_epoch( - &mut state, - state_root, - proposal_epoch, - &self.spec, - )?; - - // Sanity check the state. - let latest_block_root = state.get_latest_block_root(state_root); - let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( - proposal_epoch, - latest_block_root, - &self.spec, - )?; - if state_decision_block_root != shuffling_decision_block { - return Err(Error::ProposerCacheIncorrectState { - state_decision_block_root, - requested_decision_block_root: shuffling_decision_block, - } - .into()); - } - - let proposers = state.get_beacon_proposer_indices(proposal_epoch, &self.spec)?; - - // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have - // advanced the state completely into the new epoch. - let fork = self.spec.fork_at_epoch(proposal_epoch); - - debug!( - ?shuffling_decision_block, - epoch = %proposal_epoch, - "Priming proposer shuffling cache" - ); - - Ok::<_, E>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) - })?; - - // Run the accessor function on the computed epoch proposers. - accessor(epoch_block_proposers).map_err(Into::into) + crate::beacon_proposer_cache::with_proposer_cache( + &self.beacon_proposer_cache, + &self.spec, + shuffling_decision_block, + proposal_epoch, + accessor, + state_provider, + ) } /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index 912f7f3bad..141a79b202 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -12,12 +12,13 @@ use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; use fork_choice::ExecutionStatus; use lru::LruCache; use once_cell::sync::OnceCell; +use parking_lot::Mutex; use safe_arith::SafeArith; use smallvec::SmallVec; use state_processing::state_advance::partial_state_advance; use std::num::NonZeroUsize; use std::sync::Arc; -use tracing::instrument; +use tracing::{debug, instrument}; use typenum::Unsigned; use types::new_non_zero_usize; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, Hash256, Slot}; @@ -164,6 +165,82 @@ impl BeaconProposerCache { } } +/// Access the proposer cache, computing and caching the proposers if necessary. +/// +/// This is a free function that operates on references to the cache and spec, decoupled from +/// `BeaconChain`. The `accessor` is called with the cached `EpochBlockProposers` for the given +/// `(proposal_epoch, shuffling_decision_block)` key. If the cache entry is missing, the +/// `state_provider` closure is called to produce a state which is then used to compute and +/// cache the proposers. +pub fn with_proposer_cache( + beacon_proposer_cache: &Mutex, + spec: &ChainSpec, + shuffling_decision_block: Hash256, + proposal_epoch: Epoch, + accessor: impl Fn(&EpochBlockProposers) -> Result, + state_provider: impl FnOnce() -> Result<(Hash256, BeaconState), Err>, +) -> Result +where + Spec: EthSpec, + Err: From + From, +{ + let cache_entry = beacon_proposer_cache + .lock() + .get_or_insert_key(proposal_epoch, shuffling_decision_block); + + // If the cache entry is not initialised, run the code to initialise it inside a OnceCell. + // This prevents duplication of work across multiple threads. + // + // If it is already initialised, then `get_or_try_init` will return immediately without + // executing the initialisation code at all. + let epoch_block_proposers = cache_entry.get_or_try_init(|| { + // Fetch the state on-demand if the required epoch was missing from the cache. + // If the caller wants to not compute the state they must return an error here and then + // catch it at the call site. + let (state_root, mut state) = state_provider()?; + + // Ensure the state can compute proposer duties for `epoch`. + ensure_state_can_determine_proposers_for_epoch( + &mut state, + state_root, + proposal_epoch, + spec, + )?; + + // Sanity check the state. + let latest_block_root = state.get_latest_block_root(state_root); + let state_decision_block_root = state.proposer_shuffling_decision_root_at_epoch( + proposal_epoch, + latest_block_root, + spec, + )?; + if state_decision_block_root != shuffling_decision_block { + return Err(BeaconChainError::ProposerCacheIncorrectState { + state_decision_block_root, + requested_decision_block_root: shuffling_decision_block, + } + .into()); + } + + let proposers = state.get_beacon_proposer_indices(proposal_epoch, spec)?; + + // Use fork_at_epoch rather than the state's fork, because post-Fulu we may not have + // advanced the state completely into the new epoch. + let fork = spec.fork_at_epoch(proposal_epoch); + + debug!( + ?shuffling_decision_block, + epoch = %proposal_epoch, + "Priming proposer shuffling cache" + ); + + Ok::<_, Err>(EpochBlockProposers::new(proposal_epoch, fork, proposers)) + })?; + + // Run the accessor function on the computed epoch proposers. + accessor(epoch_block_proposers).map_err(Into::into) +} + /// Compute the proposer duties using the head state without cache. /// /// Return: diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs index 504a1d2c70..492b265fd0 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/gossip_verified_envelope.rs @@ -1,27 +1,43 @@ use std::sync::Arc; use educe::Educe; +use parking_lot::{Mutex, RwLock}; use slot_clock::SlotClock; use state_processing::{ VerifySignatures, envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}, }; +use store::DatabaseBlock; use tracing::{Span, debug}; use types::{ - EthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + ChainSpec, EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, NotifyExecutionLayer, PayloadVerificationOutcome, + beacon_proposer_cache::{self, BeaconProposerCache}, + canonical_head::CanonicalHead, payload_envelope_verification::{ EnvelopeError, EnvelopeImportData, EnvelopeProcessingSnapshot, ExecutionPendingEnvelope, IntoExecutionPendingEnvelope, MaybeAvailableEnvelope, load_snapshot, payload_notifier::PayloadNotifier, }, + validator_pubkey_cache::ValidatorPubkeyCache, }; +/// Bundles only the dependencies needed for gossip verification of execution payload envelopes, +/// decoupling `GossipVerifiedEnvelope::new` from the full `BeaconChain`. +pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { + pub canonical_head: &'a CanonicalHead, + pub store: &'a BeaconStore, + pub spec: &'a ChainSpec, + pub beacon_proposer_cache: &'a Mutex, + pub validator_pubkey_cache: &'a RwLock>, + pub genesis_validators_root: Hash256, +} + /// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Educe)] @@ -35,7 +51,7 @@ pub struct GossipVerifiedEnvelope { impl GossipVerifiedEnvelope { pub fn new( signed_envelope: Arc>, - chain: &BeaconChain, + ctx: &GossipVerificationContext<'_, T>, ) -> Result { let envelope = &signed_envelope.message; let payload = &envelope.payload; @@ -48,7 +64,7 @@ impl GossipVerifiedEnvelope { // 2. Blocks we've seen that are invalid (REJECT). // // Presently these two cases are conflated. - let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock(); + let fork_choice_read_lock = ctx.canonical_head.fork_choice_read_lock(); let Some(proto_block) = fork_choice_read_lock.get_block(&beacon_block_root) else { return Err(EnvelopeError::BlockRootUnknown { block_root: beacon_block_root, @@ -64,12 +80,14 @@ impl GossipVerifiedEnvelope { // TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope` // for this block root from this builder - envelope status table check - let block = chain - .get_full_block(&beacon_block_root)? - .ok_or_else(|| { - EnvelopeError::from(BeaconChainError::MissingBeaconBlock(beacon_block_root)) - }) - .map(Arc::new)?; + let block = match ctx.store.try_get_full_block(&beacon_block_root)? { + Some(DatabaseBlock::Full(block)) => Arc::new(block), + Some(DatabaseBlock::Blinded(_)) | None => { + return Err(EnvelopeError::from(BeaconChainError::MissingBeaconBlock( + beacon_block_root, + ))); + } + }; let execution_bid = &block .message() .body() @@ -118,13 +136,15 @@ impl GossipVerifiedEnvelope { let block_slot = envelope.slot; let block_epoch = block_slot.epoch(T::EthSpec::slots_per_epoch()); let proposer_shuffling_decision_block = - proto_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec); + proto_block.proposer_shuffling_root_for_child_block(block_epoch, ctx.spec); let (signature_is_valid, opt_snapshot) = if builder_index == BUILDER_INDEX_SELF_BUILD { // Fast path: self-build envelopes can be verified without loading the state. let envelope_ref = signed_envelope.as_ref(); let mut opt_snapshot = None; - let proposer = chain.with_proposer_cache::<_, EnvelopeError>( + let proposer = beacon_proposer_cache::with_proposer_cache( + ctx.beacon_proposer_cache, + ctx.spec, proposer_shuffling_decision_block, block_epoch, |proposers| proposers.get_slot::(block_slot), @@ -133,14 +153,14 @@ impl GossipVerifiedEnvelope { %beacon_block_root, "Proposer shuffling cache miss for envelope verification" ); - let snapshot = load_snapshot(envelope_ref, chain)?; + let snapshot = load_snapshot(envelope_ref, ctx.canonical_head, ctx.store)?; opt_snapshot = Some(Box::new(snapshot.clone())); - Ok((snapshot.state_root, snapshot.pre_state)) + Ok::<_, EnvelopeError>((snapshot.state_root, snapshot.pre_state)) }, )?; let fork = proposer.fork; - let pubkey_cache = chain.validator_pubkey_cache.read(); + let pubkey_cache = ctx.validator_pubkey_cache.read(); let pubkey = pubkey_cache .get(block.message().proposer_index() as usize) .ok_or_else(|| EnvelopeError::UnknownValidator { @@ -149,16 +169,16 @@ impl GossipVerifiedEnvelope { let is_valid = signed_envelope.verify_signature( pubkey, &fork, - chain.genesis_validators_root, - &chain.spec, + ctx.genesis_validators_root, + ctx.spec, ); (is_valid, opt_snapshot) } else { // TODO(gloas) if we implement a builder pubkey cache, we'll need to use it here. // External builder: must load the state to get the builder pubkey. - let snapshot = load_snapshot(signed_envelope.as_ref(), chain)?; + let snapshot = load_snapshot(signed_envelope.as_ref(), ctx.canonical_head, ctx.store)?; let is_valid = - signed_envelope.verify_signature_with_state(&snapshot.pre_state, &chain.spec)?; + signed_envelope.verify_signature_with_state(&snapshot.pre_state, ctx.spec)?; (is_valid, Some(Box::new(snapshot))) }; @@ -228,7 +248,11 @@ impl IntoExecutionPendingEnvelope for GossipVerifiedEnve let snapshot = if let Some(snapshot) = self.snapshot { *snapshot } else { - load_snapshot(signed_envelope.as_ref(), chain)? + load_snapshot( + signed_envelope.as_ref(), + &chain.canonical_head, + &chain.store, + )? }; let mut state = snapshot.pre_state; @@ -263,6 +287,18 @@ impl IntoExecutionPendingEnvelope for GossipVerifiedEnve } impl BeaconChain { + /// Build a `GossipVerificationContext` from this `BeaconChain`. + pub fn gossip_verification_context(&self) -> GossipVerificationContext<'_, T> { + GossipVerificationContext { + canonical_head: &self.canonical_head, + store: &self.store, + spec: &self.spec, + beacon_proposer_cache: &self.beacon_proposer_cache, + validator_pubkey_cache: &self.validator_pubkey_cache, + genesis_validators_root: self.genesis_validators_root, + } + } + /// Returns `Ok(GossipVerifiedEnvelope)` if the supplied `envelope` should be forwarded onto the /// gossip network. The envelope is not imported into the chain, it is just partially verified. /// @@ -287,7 +323,8 @@ impl BeaconChain { let slot = envelope.slot(); let beacon_block_root = envelope.message.beacon_block_root; - match GossipVerifiedEnvelope::new(envelope, &chain) { + let ctx = chain.gossip_verification_context(); + match GossipVerifiedEnvelope::new(envelope, &ctx) { Ok(verified) => { debug!( %slot, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 80e62f93b7..38fdd9f425 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -39,9 +39,9 @@ use types::{ }; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError, - NotifyExecutionLayer, PayloadVerificationOutcome, - block_verification::PayloadVerificationHandle, + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, + ExecutionPayloadError, NotifyExecutionLayer, PayloadVerificationOutcome, + block_verification::PayloadVerificationHandle, canonical_head::CanonicalHead, payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope, }; @@ -49,6 +49,9 @@ pub mod gossip_verified_envelope; pub mod import; mod payload_notifier; +#[cfg(test)] +mod tests; + pub trait IntoExecutionPendingEnvelope: Sized { fn into_execution_pending_envelope( self, @@ -289,7 +292,8 @@ impl From for EnvelopeError { #[instrument(skip_all, level = "debug", fields(beacon_block_root = %envelope.beacon_block_root()))] pub(crate) fn load_snapshot( envelope: &SignedExecutionPayloadEnvelope, - chain: &BeaconChain, + canonical_head: &CanonicalHead, + store: &BeaconStore, ) -> Result, EnvelopeError> { // Reject any envelope if its block is not known to fork choice. // @@ -302,7 +306,7 @@ pub(crate) fn load_snapshot( // choice, so we will not reject any child of the finalized block (this is relevant during // genesis). - let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock(); + let fork_choice_read_lock = canonical_head.fork_choice_read_lock(); let beacon_block_root = envelope.beacon_block_root(); let Some(proto_beacon_block) = fork_choice_read_lock.get_block(&beacon_block_root) else { return Err(EnvelopeError::BlockRootUnknown { @@ -317,8 +321,7 @@ pub(crate) fn load_snapshot( // We can use `get_hot_state` here rather than `get_advanced_hot_state` because the envelope // must be from the same slot as its block (so no advance is required). let cache_state = true; - let state = chain - .store + let state = store .get_hot_state(&block_state_root, cache_state) .map_err(EnvelopeError::from)? .ok_or_else(|| { @@ -342,7 +345,7 @@ impl IntoExecutionPendingEnvelope chain: &Arc>, notify_execution_layer: NotifyExecutionLayer, ) -> Result, EnvelopeError> { - GossipVerifiedEnvelope::new(self, chain)? + GossipVerifiedEnvelope::new(self, &chain.gossip_verification_context())? .into_execution_pending_envelope(chain, notify_execution_layer) } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/tests.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/tests.rs new file mode 100644 index 0000000000..c362bc6180 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/tests.rs @@ -0,0 +1,524 @@ +use std::sync::Arc; +use std::time::Duration; + +use bls::{FixedBytesExtended, Keypair, Signature}; +use fork_choice::ForkChoice; +use parking_lot::{Mutex, RwLock}; +use ssz_types::VariableList; +use store::{HotColdDB, KeyValueStore, MemoryStore, StoreConfig}; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; +use types::test_utils::generate_deterministic_keypairs; +use types::*; + +use crate::BeaconStore; +use crate::beacon_fork_choice_store::BeaconForkChoiceStore; +use crate::beacon_proposer_cache::BeaconProposerCache; +use crate::builder::Witness; +use crate::canonical_head::CanonicalHead; +use crate::payload_envelope_verification::EnvelopeError; +use crate::payload_envelope_verification::gossip_verified_envelope::{ + GossipVerificationContext, GossipVerifiedEnvelope, +}; +use crate::validator_pubkey_cache::ValidatorPubkeyCache; + +type TestEthSpec = MinimalEthSpec; +type TestTypes = Witness< + slot_clock::TestingSlotClock, + TestEthSpec, + MemoryStore, + MemoryStore, +>; + +/// Test context that holds the minimal state needed for gossip verification. +struct TestContext { + store: BeaconStore, + canonical_head: CanonicalHead, + beacon_proposer_cache: Mutex, + validator_pubkey_cache: RwLock>, + spec: Arc, + keypairs: Vec, + genesis_state: BeaconState, + genesis_block_root: Hash256, + genesis_validators_root: Hash256, +} + +impl TestContext { + fn new(validator_count: usize) -> Self { + let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::minimal())); + let keypairs = generate_deterministic_keypairs(validator_count); + + let mut genesis_state = genesis::interop_genesis_state::( + &keypairs, + 0, // genesis_time + Hash256::from_slice(&[0x42; 32]), + None, // no execution payload header + &spec, + ) + .expect("should create genesis state"); + + let genesis_validators_root = genesis_state.genesis_validators_root(); + + let store: BeaconStore = Arc::new( + HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone()) + .expect("should create ephemeral store"), + ); + + // Initialize store metadata. + let genesis_block = BeaconBlock::::empty(&spec); + let genesis_block_root = genesis_block.canonical_root(); + let signed_genesis_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty()); + + // Build caches and compute state root before storing. + genesis_state + .build_caches(&spec) + .expect("should build caches"); + + // Initialize store metadata ops (must be done before put_state). + let ops = vec![ + store + .init_anchor_info( + signed_genesis_block.parent_root(), + signed_genesis_block.slot(), + Slot::new(0), + false, + ) + .expect("should init anchor info"), + store + .init_blob_info(signed_genesis_block.slot()) + .expect("should init blob info"), + store + .init_data_column_info(signed_genesis_block.slot()) + .expect("should init data column info"), + ]; + store + .hot_db + .do_atomically(ops) + .expect("should store metadata"); + + // Store the genesis block and state. + store + .put_block(&genesis_block_root, signed_genesis_block.clone()) + .expect("should store genesis block"); + let state_root = genesis_state + .update_tree_hash_cache() + .expect("should compute state root"); + store + .put_state(&state_root, &genesis_state) + .expect("should store genesis state"); + + // Create BeaconSnapshot and fork choice. + let snapshot = crate::BeaconSnapshot { + beacon_block: Arc::new(signed_genesis_block), + beacon_block_root: genesis_block_root, + beacon_state: genesis_state.clone(), + }; + + let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), snapshot.clone()) + .expect("should create fork choice store"); + + let fork_choice = ForkChoice::from_anchor( + fc_store, + genesis_block_root, + &snapshot.beacon_block, + &snapshot.beacon_state, + None, + &spec, + ) + .expect("should create fork choice from anchor"); + + let canonical_head = CanonicalHead::new(fork_choice, Arc::new(snapshot)); + + let validator_pubkey_cache = ValidatorPubkeyCache::new(&genesis_state, store.clone()) + .expect("should create validator pubkey cache"); + + TestContext { + store, + canonical_head, + beacon_proposer_cache: Mutex::new(BeaconProposerCache::default()), + validator_pubkey_cache: RwLock::new(validator_pubkey_cache), + spec, + keypairs, + genesis_state, + genesis_block_root, + genesis_validators_root, + } + } + + fn gossip_verification_context(&self) -> GossipVerificationContext<'_, TestTypes> { + GossipVerificationContext { + canonical_head: &self.canonical_head, + store: &self.store, + spec: &self.spec, + beacon_proposer_cache: &self.beacon_proposer_cache, + validator_pubkey_cache: &self.validator_pubkey_cache, + genesis_validators_root: self.genesis_validators_root, + } + } + + /// Build a gloas block at `slot` with the given proposer, store it, add it to fork choice, + /// and return the signed block, block root, and post-state. + fn build_and_import_block( + &self, + slot: Slot, + proposer_index: usize, + execution_bid: ExecutionPayloadBid, + ) -> ( + Arc>, + Hash256, + BeaconState, + ) { + let mut state = self.genesis_state.clone(); + + // Advance the state to the target slot. + if slot > state.slot() { + state_processing::state_advance::complete_state_advance( + &mut state, None, slot, &self.spec, + ) + .expect("should advance state"); + } + + state.build_caches(&self.spec).expect("should build caches"); + + // Compute the state root so we can embed it in the block. + let state_root = state + .update_tree_hash_cache() + .expect("should compute state root"); + + let signed_bid = SignedExecutionPayloadBid { + message: execution_bid, + signature: Signature::infinity().expect("should create infinity signature"), + }; + + // Create the block body with the actual state root. + let block = BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index: proposer_index as u64, + parent_root: self.genesis_block_root, + state_root, + body: BeaconBlockBodyGloas { + randao_reveal: Signature::empty(), + eth1_data: state.eth1_data().clone(), + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + bls_to_execution_changes: VariableList::empty(), + signed_execution_payload_bid: signed_bid, + payload_attestations: VariableList::empty(), + _phantom: std::marker::PhantomData, + }, + }); + + let block_root = block.canonical_root(); + let proposer_sk = &self.keypairs[proposer_index].sk; + let fork = self + .spec + .fork_at_epoch(slot.epoch(TestEthSpec::slots_per_epoch())); + let signed_block = block.sign(proposer_sk, &fork, self.genesis_validators_root, &self.spec); + + // Store block and state. + self.store + .put_block(&block_root, signed_block.clone()) + .expect("should store block"); + self.store + .put_state(&state_root, &state) + .expect("should store state"); + + // Add block to fork choice. + let mut fork_choice = self.canonical_head.fork_choice_write_lock(); + fork_choice + .on_block( + slot, + signed_block.message(), + block_root, + Duration::from_secs(0), + &state, + crate::PayloadVerificationStatus::Verified, + &self.spec, + ) + .expect("should add block to fork choice"); + drop(fork_choice); + + (Arc::new(signed_block), block_root, state) + } + + /// Build a signed execution payload envelope for the given block. + fn build_signed_envelope( + &self, + block_root: Hash256, + slot: Slot, + builder_index: u64, + block_hash: ExecutionBlockHash, + signing_key: &bls::SecretKey, + ) -> Arc> { + let mut payload = ExecutionPayloadGloas::default(); + payload.block_hash = block_hash; + + let envelope = ExecutionPayloadEnvelope { + payload, + execution_requests: ExecutionRequests::default(), + builder_index, + beacon_block_root: block_root, + slot, + state_root: Hash256::zero(), + }; + + let fork = self + .spec + .fork_at_epoch(slot.epoch(TestEthSpec::slots_per_epoch())); + let domain = self.spec.get_domain( + slot.epoch(TestEthSpec::slots_per_epoch()), + Domain::BeaconBuilder, + &fork, + self.genesis_validators_root, + ); + let message = envelope.signing_root(domain); + let signature = signing_key.sign(message); + + Arc::new(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } + + /// Helper: build a block and matching self-build envelope. + fn build_block_and_envelope( + &self, + ) -> ( + Arc>, + Hash256, + Arc>, + ) { + let slot = Slot::new(1); + let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])); + + // Get proposer for slot 1. + let mut state = self.genesis_state.clone(); + state_processing::state_advance::complete_state_advance(&mut state, None, slot, &self.spec) + .expect("should advance state"); + state.build_caches(&self.spec).expect("should build caches"); + let proposer_index = state + .get_beacon_proposer_index(slot, &self.spec) + .expect("should get proposer index"); + + let bid = ExecutionPayloadBid { + builder_index: BUILDER_INDEX_SELF_BUILD, + block_hash, + slot, + ..Default::default() + }; + + let (signed_block, block_root, _post_state) = + self.build_and_import_block(slot, proposer_index, bid); + + let proposer_sk = &self.keypairs[proposer_index].sk; + let envelope = self.build_signed_envelope( + block_root, + slot, + BUILDER_INDEX_SELF_BUILD, + block_hash, + proposer_sk, + ); + + (signed_block, block_root, envelope) + } +} + +#[test] +fn test_valid_self_build_envelope() { + let ctx = TestContext::new(32); + let (_block, _block_root, envelope) = ctx.build_block_and_envelope(); + let gossip_ctx = ctx.gossip_verification_context(); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + result.is_ok(), + "valid self-build envelope should pass verification, got: {:?}", + result.err() + ); +} + +#[test] +fn test_unknown_block_root() { + let ctx = TestContext::new(32); + let gossip_ctx = ctx.gossip_verification_context(); + + // Build an envelope referencing a block root not in fork choice. + let unknown_root = Hash256::from_slice(&[0xff; 32]); + let envelope = ctx.build_signed_envelope( + unknown_root, + Slot::new(1), + BUILDER_INDEX_SELF_BUILD, + ExecutionBlockHash::from_root(Hash256::zero()), + &ctx.keypairs[0].sk, + ); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + matches!(result, Err(EnvelopeError::BlockRootUnknown { .. })), + "should reject envelope with unknown block root, got: {:?}", + result + ); +} + +#[test] +fn test_slot_mismatch() { + let ctx = TestContext::new(32); + let (_block, block_root, _good_envelope) = ctx.build_block_and_envelope(); + let gossip_ctx = ctx.gossip_verification_context(); + + // Build an envelope with a different slot than the block. + let wrong_slot = Slot::new(2); + let envelope = ctx.build_signed_envelope( + block_root, + wrong_slot, + BUILDER_INDEX_SELF_BUILD, + ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])), + &ctx.keypairs[0].sk, + ); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + matches!(result, Err(EnvelopeError::SlotMismatch { .. })), + "should reject envelope with slot mismatch, got: {:?}", + result + ); +} + +#[test] +fn test_builder_index_mismatch() { + let ctx = TestContext::new(32); + let gossip_ctx = ctx.gossip_verification_context(); + + let slot = Slot::new(1); + let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])); + + // Get proposer for slot 1. + let mut state = ctx.genesis_state.clone(); + state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec) + .expect("should advance state"); + state.build_caches(&ctx.spec).expect("should build caches"); + let proposer_index = state + .get_beacon_proposer_index(slot, &ctx.spec) + .expect("should get proposer index"); + + // Block has builder_index = BUILDER_INDEX_SELF_BUILD + let bid = ExecutionPayloadBid { + builder_index: BUILDER_INDEX_SELF_BUILD, + block_hash, + slot, + ..Default::default() + }; + let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid); + + // Envelope has a different builder_index. + let wrong_builder_index = 999; + let envelope = ctx.build_signed_envelope( + block_root, + slot, + wrong_builder_index, + block_hash, + &ctx.keypairs[proposer_index].sk, + ); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + matches!(result, Err(EnvelopeError::BuilderIndexMismatch { .. })), + "should reject envelope with builder index mismatch, got: {:?}", + result + ); +} + +#[test] +fn test_block_hash_mismatch() { + let ctx = TestContext::new(32); + let gossip_ctx = ctx.gossip_verification_context(); + + let slot = Slot::new(1); + let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])); + let wrong_block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xbb; 32])); + + // Get proposer for slot 1. + let mut state = ctx.genesis_state.clone(); + state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec) + .expect("should advance state"); + state.build_caches(&ctx.spec).expect("should build caches"); + let proposer_index = state + .get_beacon_proposer_index(slot, &ctx.spec) + .expect("should get proposer index"); + + // Block has block_hash = 0xaa + let bid = ExecutionPayloadBid { + builder_index: BUILDER_INDEX_SELF_BUILD, + block_hash, + slot, + ..Default::default() + }; + let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid); + + // Envelope has a different block_hash. + let envelope = ctx.build_signed_envelope( + block_root, + slot, + BUILDER_INDEX_SELF_BUILD, + wrong_block_hash, + &ctx.keypairs[proposer_index].sk, + ); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + matches!(result, Err(EnvelopeError::BlockHashMismatch { .. })), + "should reject envelope with block hash mismatch, got: {:?}", + result + ); +} + +#[test] +fn test_bad_signature() { + let ctx = TestContext::new(32); + let gossip_ctx = ctx.gossip_verification_context(); + + let slot = Slot::new(1); + let block_hash = ExecutionBlockHash::from_root(Hash256::from_slice(&[0xaa; 32])); + + // Get proposer for slot 1. + let mut state = ctx.genesis_state.clone(); + state_processing::state_advance::complete_state_advance(&mut state, None, slot, &ctx.spec) + .expect("should advance state"); + state.build_caches(&ctx.spec).expect("should build caches"); + let proposer_index = state + .get_beacon_proposer_index(slot, &ctx.spec) + .expect("should get proposer index"); + + let bid = ExecutionPayloadBid { + builder_index: BUILDER_INDEX_SELF_BUILD, + block_hash, + slot, + ..Default::default() + }; + let (_block, block_root, _post_state) = ctx.build_and_import_block(slot, proposer_index, bid); + + // Sign the envelope with the wrong key (some other validator's key). + let wrong_key_index = if proposer_index == 0 { 1 } else { 0 }; + let envelope = ctx.build_signed_envelope( + block_root, + slot, + BUILDER_INDEX_SELF_BUILD, + block_hash, + &ctx.keypairs[wrong_key_index].sk, + ); + + let result = GossipVerifiedEnvelope::new(envelope, &gossip_ctx); + assert!( + matches!(result, Err(EnvelopeError::BadSignature)), + "should reject envelope with bad signature, got: {:?}", + result + ); +} + +// NOTE: `test_prior_to_finalization` is omitted here because advancing finalization requires +// attestation-based justification which needs the full `BeaconChainHarness`. The +// `PriorToFinalization` code path is tested in the integration tests.