diff --git a/Cargo.lock b/Cargo.lock index d42bcd8fc17..28a8f29158d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8426,9 +8426,11 @@ dependencies = [ "int_to_bytes", "integer-sqrt", "itertools 0.14.0", + "lru 0.12.5", "merkle_proof", "metrics", "milhouse", + "parking_lot", "rand 0.9.2", "rayon", "safe_arith", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index db8f55a18aa..1d34c945670 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -120,7 +120,9 @@ use slasher::Slasher; use slot_clock::SlotClock; use ssz::Encode; use state_processing::{ - BlockSignatureStrategy, ConsensusContext, SigVerifiedOp, VerifyBlockRoot, VerifyOperation, + BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, SigVerifiedOp, + VerifyBlockRoot, VerifyOperation, + builder_deposits_cache::OnboardBuildersCache, common::get_attesting_indices_from_state, epoch_cache::initialize_epoch_cache, per_block_processing, @@ -510,6 +512,9 @@ pub struct BeaconChain { pub pending_payload_cache: Arc>, /// The KZG trusted setup used by this chain. pub kzg: Arc, + /// Pre-validates builder deposit signatures. + /// Only required when gloas is enabled. + pub builder_onboarding_cache: Option>, /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. pub rng: Arc>>, } @@ -1513,7 +1518,14 @@ impl BeaconChain { while state.slot() < slot { // Note: supplying some `state_root` when it is known would be a cheap and easy // optimization. - match per_slot_processing(&mut state, skip_state_root, &self.spec) { + match per_slot_processing( + &mut state, + skip_state_root, + GloasVerificationContext::from_cache( + self.builder_onboarding_cache.as_deref(), + ), + &self.spec, + ) { Ok(_) => (), Err(e) => { warn!( @@ -2116,6 +2128,7 @@ impl BeaconChain { &mut state, Some(advanced_state_root), request_epoch.start_slot(T::EthSpec::slots_per_epoch()), + self.builder_onboarding_cache.as_deref(), &self.spec, ) .map_err(Error::StateAdvanceError)?; @@ -4544,6 +4557,24 @@ impl BeaconChain { current_slot, ); + // If gloas is enabled, we get the deposits from the payload instead of + // the state. + if !state.fork_name_unchecked().gloas_enabled() + && let Some(builder_onboarding_cache) = &self.builder_onboarding_cache + { + let cache = builder_onboarding_cache.clone(); + let spec = self.spec.clone(); + let executor = self.task_executor.clone(); + // Using rayon pool here since `add_new_pending_deposits` uses rayon threads to + // perform the signature verification in batches. + // We have until the fork transition for the cache to be used, so we use the low priority pool. + executor.spawn_blocking_with_rayon( + move || cache.add_new_pending_deposits::(&state, &spec), + RayonPoolType::LowPriority, + "pre_verify_deposits", + ); + } + Ok(block_root) } @@ -5178,6 +5209,7 @@ impl BeaconChain { &mut advanced_state, Some(unadvanced_state_root), proposal_slot, + self.builder_onboarding_cache.as_deref(), &self.spec, )?; @@ -5196,6 +5228,7 @@ impl BeaconChain { apply_parent_execution_payload( &mut advanced_state, &envelope.message.execution_requests, + self.builder_onboarding_cache.as_deref(), &self.spec, ) .map_err(Error::PrepareProposerFailed)?; @@ -6199,6 +6232,7 @@ impl BeaconChain { signature_strategy, VerifyBlockRoot::True, &mut ctxt, + self.builder_onboarding_cache.as_deref(), &self.spec, )?; drop(process_timer); @@ -7005,6 +7039,7 @@ impl BeaconChain { proposal_epoch, accessor, state_provider, + self.builder_onboarding_cache.as_deref(), &self.spec, ) } @@ -7162,7 +7197,13 @@ impl BeaconChain { if state.current_epoch() + 1 < shuffling_epoch { // Advance the state into the required slot, using the "partial" method since the // state roots are not relevant for the shuffling. - partial_state_advance(&mut state, Some(state_root), target_slot, &self.spec)?; + partial_state_advance( + &mut state, + Some(state_root), + target_slot, + self.builder_onboarding_cache.as_deref(), + &self.spec, + )?; } metrics::stop_timer(state_skip_timer); diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index b258d7471f7..d8c337856db 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -15,6 +15,7 @@ use once_cell::sync::OnceCell; use parking_lot::Mutex; use safe_arith::SafeArith; use smallvec::SmallVec; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::state_advance::partial_state_advance; use std::num::NonZeroUsize; use std::sync::Arc; @@ -178,6 +179,7 @@ pub fn with_proposer_cache( proposal_epoch: Epoch, accessor: impl Fn(&EpochBlockProposers) -> Result, state_provider: impl FnOnce() -> Result<(Hash256, BeaconState), Err>, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result where @@ -204,6 +206,7 @@ where &mut state, state_root, proposal_epoch, + builder_onboarding_cache, spec, )?; @@ -275,6 +278,7 @@ pub fn compute_proposer_duties_from_head( &mut state, head_state_root, request_epoch, + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; @@ -318,6 +322,7 @@ pub fn ensure_state_can_determine_proposers_for_epoch( state: &mut BeaconState, state_root: Hash256, target_epoch: Epoch, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), BeaconChainError> { // The decision slot is the end of an epoch, so we add 1 to reach the first slot of the epoch @@ -338,7 +343,13 @@ pub fn ensure_state_can_determine_proposers_for_epoch( } else { // State's current epoch is less than the minimum epoch. // Advance the state up to the minimum epoch. - partial_state_advance(state, Some(state_root), minimum_slot, spec) - .map_err(BeaconChainError::from) + partial_state_advance( + state, + Some(state_root), + minimum_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from) } } diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 82dad6f6ad1..af218cabe1d 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -637,6 +637,7 @@ impl BeaconChain { signature_strategy, VerifyBlockRoot::True, &mut ctxt, + self.builder_onboarding_cache.as_deref(), &self.spec, )?; drop(process_timer); @@ -978,6 +979,7 @@ fn get_execution_payload_gloas( apply_parent_execution_payload( &mut withdrawals_state, &envelope.message.execution_requests, + chain.builder_onboarding_cache.as_deref(), spec, )?; Withdrawals::::from(get_expected_withdrawals(&withdrawals_state, spec)?) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 24f971f736f..83744e47345 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -79,10 +79,11 @@ use safe_arith::ArithError; use slot_clock::SlotClock; use ssz::Encode; use ssz_derive::{Decode, Encode}; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::per_block_processing::errors::IntoWithIndex; use state_processing::{ - AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, - VerifyBlockRoot, + AllCaches, BlockProcessingError, BlockSignatureStrategy, ConsensusContext, + GloasVerificationContext, SlotProcessingError, VerifyBlockRoot, block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError}, per_block_processing, per_slot_processing, state_advance::partial_state_advance, @@ -609,6 +610,7 @@ pub fn signature_verify_chain_segment( &mut parent.pre_state, parent.beacon_state_root, highest_slot, + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; @@ -1109,6 +1111,7 @@ impl SignatureVerifiedBlock { &mut parent.pre_state, parent.beacon_state_root, block.slot(), + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; @@ -1180,6 +1183,7 @@ impl SignatureVerifiedBlock { &mut parent.pre_state, parent.beacon_state_root, block.slot(), + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; @@ -1542,7 +1546,12 @@ impl ExecutionPendingBlock { state_root }; - if let Some(summary) = per_slot_processing(&mut state, Some(state_root), &chain.spec)? { + if let Some(summary) = per_slot_processing( + &mut state, + Some(state_root), + GloasVerificationContext::from_cache(chain.builder_onboarding_cache.as_deref()), + &chain.spec, + )? { // Expose Prometheus metrics. if let Err(e) = summary.observe_metrics() { error!( @@ -1611,6 +1620,7 @@ impl ExecutionPendingBlock { BlockSignatureStrategy::NoVerification, VerifyBlockRoot::True, &mut consensus_context, + chain.builder_onboarding_cache.as_deref(), &chain.spec, ) { match err { @@ -2088,6 +2098,7 @@ pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobEr state: &'a mut BeaconState, state_root_opt: Option, block_slot: Slot, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result>, Err> { let block_epoch = block_slot.epoch(E::slots_per_epoch()); @@ -2107,8 +2118,14 @@ pub fn cheap_state_advance_to_obtain_committees<'a, E: EthSpec, Err: BlockBlobEr // Advance the state into the same epoch as the block. Use the "partial" method since state // roots are not important for proposer/attester shuffling. - partial_state_advance(&mut state, state_root_opt, target_slot, spec) - .map_err(BeaconChainError::from)?; + partial_state_advance( + &mut state, + state_root_opt, + target_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 61c026e0a95..2188f9de4be 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -36,8 +36,9 @@ use rayon::prelude::*; use slasher::Slasher; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::AllCaches; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::genesis::genesis_block; -use state_processing::per_slot_processing; +use state_processing::{GloasVerificationContext, per_slot_processing}; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; @@ -445,8 +446,13 @@ where "Advancing checkpoint state to boundary" ); while weak_subj_state.slot() % slots_per_epoch != 0 { - per_slot_processing(&mut weak_subj_state, None, &self.spec) - .map_err(|e| format!("Error advancing state: {e:?}"))?; + per_slot_processing( + &mut weak_subj_state, + None, + GloasVerificationContext::FullVerification, + &self.spec, + ) + .map_err(|e| format!("Error advancing state: {e:?}"))?; } } @@ -1084,6 +1090,7 @@ where rng: Arc::new(Mutex::new(rng)), gossip_verified_payload_bid_cache: <_>::default(), gossip_verified_proposer_preferences_cache: <_>::default(), + builder_onboarding_cache: OnboardBuildersCache::new(&self.spec).map(Arc::new), }; let head = beacon_chain.head_snapshot(); @@ -1155,6 +1162,20 @@ where .process_prune_blobs(data_availability_boundary); } + // Seed the builder onboarding cache in the background from the current head state. + if let Some(onboarding_cache) = &beacon_chain.builder_onboarding_cache { + let cache = onboarding_cache.clone(); + let spec = self.spec.clone(); + let executor = beacon_chain.task_executor.clone(); + // Using rayon pool here since `seed_from_state` uses rayon threads to + // perform the signature verification in batches. + executor.spawn_blocking_with_rayon( + move || cache.seed_from_state(&head.beacon_state, &spec), + task_executor::RayonPoolType::HighPriority, + "initialize_builder_onboarding_cache", + ); + } + Ok(beacon_chain) } } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index c36c73b344d..8d9935f55c0 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -10,6 +10,7 @@ use eth2::types::{EventKind, ForkVersionedResponse}; use parking_lot::RwLock; use safe_arith::SafeArith; use slot_clock::SlotClock; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; use state_processing::state_advance::partial_state_advance; use std::borrow::Cow; @@ -18,6 +19,7 @@ use types::{ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestati pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, + pub builder_onboarding_cache: Option<&'a OnboardBuildersCache>, pub observed_payload_attesters: &'a RwLock>, pub canonical_head: &'a CanonicalHead, pub validator_pubkey_cache: &'a RwLock>, @@ -113,8 +115,14 @@ impl VerifiedPayloadAttestationMessage { .map_err(BeaconChainError::from)? < message_epoch { - partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec) - .map_err(BeaconChainError::from)?; + partial_state_advance( + &mut state, + Some(state_root), + target_slot, + ctx.builder_onboarding_cache, + ctx.spec, + ) + .map_err(BeaconChainError::from)?; } Some(state) @@ -202,6 +210,7 @@ impl BeaconChain { GossipVerificationContext { slot_clock: &self.slot_clock, spec: &self.spec, + builder_onboarding_cache: self.builder_onboarding_cache.as_deref(), observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, validator_pubkey_cache: &self.validator_pubkey_cache, diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index c45df51ac88..20f0a3b8ef2 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -115,6 +115,7 @@ impl TestContext { GossipVerificationContext { slot_clock: &self.slot_clock, spec: &self.spec, + builder_onboarding_cache: None, observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, validator_pubkey_cache: &self.validator_pubkey_cache, @@ -373,6 +374,7 @@ async fn stale_head_with_partial_advance() { &mut reference_state, Some(head.snapshot.beacon_state_root()), target_slot, + harness.chain.builder_onboarding_cache.as_deref(), &harness.spec, ) .expect("should advance reference state"); 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 a20963302b0..8b484d55314 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 @@ -3,6 +3,7 @@ use std::sync::Arc; use educe::Educe; use eth2::types::{EventKind, SseExecutionPayloadGossip}; use parking_lot::{Mutex, RwLock}; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use store::DatabaseBlock; use tracing::debug; use types::{ @@ -26,6 +27,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub canonical_head: &'a CanonicalHead, pub store: &'a BeaconStore, pub spec: &'a ChainSpec, + pub builder_onboarding_cache: Option<&'a OnboardBuildersCache>, pub beacon_proposer_cache: &'a Mutex, pub validator_pubkey_cache: &'a RwLock>, pub genesis_validators_root: Hash256, @@ -173,6 +175,7 @@ impl GossipVerifiedEnvelope { opt_snapshot = Some(Box::new(snapshot.clone())); Ok::<_, EnvelopeError>((snapshot.state_root, snapshot.pre_state)) }, + ctx.builder_onboarding_cache, ctx.spec, )?; let expected_proposer = proposer.index; @@ -247,6 +250,7 @@ impl BeaconChain { canonical_head: &self.canonical_head, store: &self.store, spec: &self.spec, + builder_onboarding_cache: self.builder_onboarding_cache.as_deref(), beacon_proposer_cache: &self.beacon_proposer_cache, validator_pubkey_cache: &self.validator_pubkey_cache, genesis_validators_root: self.genesis_validators_root, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 73ddb43273f..9ecd151e90e 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -55,6 +55,25 @@ impl BeaconChain { ); } + // Pre-verify builder deposit signatures while awaiting EL verification. + if let Some(ref cache) = self.builder_onboarding_cache { + let cache = cache.clone(); + let deposits = unverified_envelope + .signed_envelope + .message + .execution_requests + .deposits + .clone(); + let spec = self.spec.clone(); + let executor = self.task_executor.clone(); + // Using rayon pool here since `cache_deposit_requests` uses rayon threads to + // perform the signature verification in batches. + executor.spawn_blocking_with_rayon( + move || cache.cache_deposit_requests(&deposits, &spec), + task_executor::RayonPoolType::HighPriority, + "pre_verify_builder_deposits", + ); + } // TODO(gloas) insert the pre-executed envelope into some type of cache? let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index cb916cb5142..7c32324b637 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -18,7 +18,7 @@ use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, }; use slot_clock::SlotClock; -use state_processing::per_slot_processing; +use state_processing::{GloasVerificationContext, per_slot_processing}; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -286,9 +286,13 @@ fn advance_head(beacon_chain: &Arc>) -> Resu let initial_epoch = state.current_epoch(); // Advance the state a single slot. - if let Some(summary) = - per_slot_processing(&mut state, Some(head_state_root), &beacon_chain.spec) - .map_err(BeaconChainError::from)? + if let Some(summary) = per_slot_processing( + &mut state, + Some(head_state_root), + GloasVerificationContext::from_cache(beacon_chain.builder_onboarding_cache.as_deref()), + &beacon_chain.spec, + ) + .map_err(BeaconChainError::from)? { // Expose Prometheus metrics. if let Err(e) = summary.observe_metrics() { diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c2ccad7d8c5..ba1954b2489 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1425,6 +1425,7 @@ where BlockSignatureStrategy::NoVerification, VerifyBlockRoot::False, &mut ctxt, + None, &self.spec, ) .unwrap_or_else(|e| panic!("per_block_processing failed at slot {}: {e:?}", slot)); diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index da7f380e361..922c2c8e52f 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -20,7 +20,7 @@ use fixed_bytes::FixedBytesExtended; use genesis::{DEFAULT_ETH1_BLOCK_HASH, interop_genesis_state}; use int_to_bytes::int_to_bytes32; use slasher::{Config as SlasherConfig, Slasher}; -use state_processing::per_slot_processing; +use state_processing::{GloasVerificationContext, per_slot_processing}; use std::sync::{Arc, LazyLock}; use tempfile::tempdir; use tree_hash::TreeHash; @@ -1217,7 +1217,13 @@ async fn attestation_that_skips_epochs() { .expect("should find state"); while state.slot() < current_slot { - per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + &harness.spec, + ) + .expect("should process slot"); } let state_root = state.update_tree_hash_cache().unwrap(); @@ -1328,7 +1334,13 @@ async fn attestation_validator_receive_proposer_reward_and_withdrawals() { .expect("should find state"); while state.slot() < current_slot { - per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + &harness.spec, + ) + .expect("should process slot"); } let state_root = state.update_tree_hash_cache().unwrap(); @@ -1405,7 +1417,13 @@ async fn attestation_to_finalized_block() { .expect("should find state"); while state.slot() < current_slot { - per_slot_processing(&mut state, None, &harness.spec).expect("should process slot"); + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + &harness.spec, + ) + .expect("should process slot"); } let state_root = state.update_tree_hash_cache().unwrap(); diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 533ef612197..cb7fee9e834 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -20,7 +20,8 @@ use fixed_bytes::FixedBytesExtended; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ - BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, + VerifyBlockRoot, common::{attesting_indices_base, attesting_indices_electra}, per_block_processing, per_slot_processing, }; @@ -1696,7 +1697,13 @@ async fn add_base_block_to_altair_chain() { { let mut state = state; let mut ctxt = ConsensusContext::new(base_block.slot()); - per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + &harness.chain.spec, + ) + .unwrap(); assert!(matches!( per_block_processing( &mut state, @@ -1704,6 +1711,7 @@ async fn add_base_block_to_altair_chain() { BlockSignatureStrategy::NoVerification, VerifyBlockRoot::True, &mut ctxt, + None, &harness.chain.spec, ), Err(BlockProcessingError::InconsistentBlockFork( @@ -1847,7 +1855,13 @@ async fn add_altair_block_to_base_chain() { { let mut state = state; let mut ctxt = ConsensusContext::new(altair_block.slot()); - per_slot_processing(&mut state, None, &harness.chain.spec).unwrap(); + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + &harness.chain.spec, + ) + .unwrap(); assert!(matches!( per_block_processing( &mut state, @@ -1855,6 +1869,7 @@ async fn add_altair_block_to_base_chain() { BlockSignatureStrategy::NoVerification, VerifyBlockRoot::True, &mut ctxt, + None, &harness.chain.spec, ), Err(BlockProcessingError::InconsistentBlockFork( diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index de8bfb3865f..eedd3a8120c 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -237,6 +237,7 @@ async fn prepare_payload_generic( apply_parent_execution_payload( &mut unadvanced_full_state, &envelope.message.execution_requests, + None, &spec, ) .unwrap(); @@ -245,6 +246,7 @@ async fn prepare_payload_generic( apply_parent_execution_payload( &mut advanced_full_state, &envelope.message.execution_requests, + None, &spec, ) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 7e50f4e5ac7..67bc864eeca 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1687,6 +1687,7 @@ async fn proposer_lookahead_gloas_fork_epoch() { &mut head_state, head_state_root, gloas_fork_epoch, + None, spec, ) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 3958ce6c6df..9099c50d74b 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -12,7 +12,10 @@ use beacon_chain::{ use bls::Keypair; use operation_pool::PersistedOperationPool; use state_processing::EpochProcessingError; -use state_processing::{per_slot_processing, per_slot_processing::Error as SlotProcessingError}; +use state_processing::{ + GloasVerificationContext, per_slot_processing, + per_slot_processing::Error as SlotProcessingError, +}; use std::sync::LazyLock; use types::{ BeaconState, BeaconStateError, BlockImportSource, ChainSpec, Checkpoint, @@ -107,7 +110,12 @@ fn massive_skips() { // Run per_slot_processing until it returns an error. let error = loop { - match per_slot_processing(&mut state, None, spec) { + match per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + spec, + ) { Ok(_) => continue, Err(e) => break e, } diff --git a/beacon_node/http_api/src/attester_duties.rs b/beacon_node/http_api/src/attester_duties.rs index b42e474b5c4..805e5d5aba3 100644 --- a/beacon_node/http_api/src/attester_duties.rs +++ b/beacon_node/http_api/src/attester_duties.rs @@ -4,6 +4,7 @@ use crate::state_id::StateId; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{self as api_types}; use slot_clock::SlotClock; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::state_advance::partial_state_advance; use types::{AttestationDuty, BeaconState, ChainSpec, Epoch, EthSpec, Hash256, RelativeEpoch}; @@ -113,6 +114,7 @@ fn compute_historic_attester_duties( &mut state, state_root, request_epoch, + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; (state, execution_optimistic) @@ -171,6 +173,7 @@ fn ensure_state_knows_attester_duties_for_epoch( state: &mut BeaconState, state_root: Hash256, target_epoch: Epoch, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), warp::reject::Rejection> { // Protect against an inconsistent slot clock. @@ -188,9 +191,15 @@ fn ensure_state_knows_attester_duties_for_epoch( .start_slot(E::slots_per_epoch()); // A "partial" state advance is adequate since attester duties don't rely on state roots. - partial_state_advance(state, Some(state_root), target_slot, spec) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::unhandled_error)?; + partial_state_advance( + state, + Some(state_root), + target_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?; } Ok(()) diff --git a/beacon_node/http_api/src/builder_states.rs b/beacon_node/http_api/src/builder_states.rs index 73e01debcd7..cfec7167428 100644 --- a/beacon_node/http_api/src/builder_states.rs +++ b/beacon_node/http_api/src/builder_states.rs @@ -22,8 +22,13 @@ pub fn get_next_withdrawals( let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); let (state_root, _, _) = state_id.root(chain)?; if proposal_epoch != state.current_epoch() - && let Err(e) = - partial_state_advance(&mut state, Some(state_root), proposal_slot, &chain.spec) + && let Err(e) = partial_state_advance( + &mut state, + Some(state_root), + proposal_slot, + chain.builder_onboarding_cache.as_deref(), + &chain.spec, + ) { return Err(warp_utils::reject::custom_server_error(format!( "failed to advance to the epoch of the proposal slot: {:?}", diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 0b0926f955c..3a832661bab 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -251,19 +251,24 @@ fn compute_historic_proposer_duties( } }; - let (state, execution_optimistic) = if let Some((state_root, mut state, execution_optimistic)) = - state_opt - { - // If we've loaded the head state it might be from a previous epoch, ensure it's in a - // suitable epoch. - ensure_state_can_determine_proposers_for_epoch(&mut state, state_root, epoch, &chain.spec) + let (state, execution_optimistic) = + if let Some((state_root, mut state, execution_optimistic)) = state_opt { + // If we've loaded the head state it might be from a previous epoch, ensure it's in a + // suitable epoch. + ensure_state_can_determine_proposers_for_epoch( + &mut state, + state_root, + epoch, + chain.builder_onboarding_cache.as_deref(), + &chain.spec, + ) .map_err(warp_utils::reject::unhandled_error)?; - (state, execution_optimistic) - } else { - let (state, execution_optimistic, _finalized) = - StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?; - (state, execution_optimistic) - }; + (state, execution_optimistic) + } else { + let (state, execution_optimistic, _finalized) = + StateId::from_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())).state(chain)?; + (state, execution_optimistic) + }; // Ensure the state lookup was correct. if state.current_epoch() != epoch && state.current_epoch() + 1 != epoch { diff --git a/beacon_node/http_api/src/ptc_duties.rs b/beacon_node/http_api/src/ptc_duties.rs index f727b840048..6d27fa565a2 100644 --- a/beacon_node/http_api/src/ptc_duties.rs +++ b/beacon_node/http_api/src/ptc_duties.rs @@ -4,6 +4,7 @@ use crate::state_id::StateId; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::{self as api_types, PtcDuty}; use slot_clock::SlotClock; +use state_processing::builder_deposits_cache::OnboardBuildersCache; use state_processing::state_advance::partial_state_advance; use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256}; @@ -114,6 +115,7 @@ fn compute_ptc_duties_from_state( &mut state, state_root, request_epoch, + chain.builder_onboarding_cache.as_deref(), &chain.spec, )?; (state, execution_optimistic) @@ -148,6 +150,7 @@ fn ensure_state_knows_ptc_duties_for_epoch( state: &mut BeaconState, state_root: Hash256, target_epoch: Epoch, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), warp::reject::Rejection> { if state.current_epoch() > target_epoch { @@ -161,9 +164,15 @@ fn ensure_state_knows_ptc_duties_for_epoch( .saturating_sub(1_u64) .start_slot(E::slots_per_epoch()); - partial_state_advance(state, Some(state_root), target_slot, spec) - .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::unhandled_error)?; + partial_state_advance( + state, + Some(state_root), + target_slot, + builder_onboarding_cache, + spec, + ) + .map_err(BeaconChainError::from) + .map_err(warp_utils::reject::unhandled_error)?; } Ok(()) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 3da0841a4ef..26486cc1fc0 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -40,8 +40,8 @@ use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::{BitList, Decode}; use state_processing::per_block_processing::get_expected_withdrawals; -use state_processing::per_slot_processing; use state_processing::state_advance::partial_state_advance; +use state_processing::{GloasVerificationContext, per_slot_processing}; use std::convert::TryInto; use std::sync::Arc; use tokio::time::Duration; @@ -5011,7 +5011,13 @@ impl ApiTester { let mut head = self.chain.head_snapshot().as_ref().clone(); while head.beacon_state.current_epoch() < epoch { - per_slot_processing(&mut head.beacon_state, None, &self.chain.spec).unwrap(); + per_slot_processing( + &mut head.beacon_state, + None, + GloasVerificationContext::FullVerification, + &self.chain.spec, + ) + .unwrap(); } head.beacon_state .build_committee_cache(RelativeEpoch::Current, &self.chain.spec) @@ -7533,6 +7539,7 @@ impl ApiTester { &mut state, Some(state_root), proposal_slot, + self.chain.builder_onboarding_cache.as_deref(), &self.chain.spec, ); } diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 2fb40daa0de..a9df4701b32 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -5,8 +5,8 @@ use crate::metrics; use crate::{DBColumn, Error, ItemStore}; use itertools::{Itertools, process_results}; use state_processing::{ - BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing, - per_slot_processing, + BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, VerifyBlockRoot, + per_block_processing, per_slot_processing, }; use std::sync::Arc; use tracing::{debug, info}; @@ -145,8 +145,13 @@ where }; // Advance state to slot. - per_slot_processing(&mut state, prev_state_root.take(), &self.spec) - .map_err(HotColdDBError::BlockReplaySlotError)?; + per_slot_processing( + &mut state, + prev_state_root.take(), + GloasVerificationContext::FullVerification, + &self.spec, + ) + .map_err(HotColdDBError::BlockReplaySlotError)?; // Apply block. if let Some(block) = block { @@ -160,6 +165,7 @@ where BlockSignatureStrategy::NoVerification, VerifyBlockRoot::True, &mut ctxt, + None, &self.spec, ) .map_err(HotColdDBError::BlockReplayBlockError)?; diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index 72d0e17d999..c8eb985ea3d 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -29,9 +29,11 @@ fixed_bytes = { workspace = true } int_to_bytes = { workspace = true } integer-sqrt = "0.1.5" itertools = { workspace = true } +lru = {workspace = true } merkle_proof = { workspace = true } metrics = { workspace = true } milhouse = { workspace = true } +parking_lot = {workspace = true } rand = { workspace = true } rayon = { workspace = true } safe_arith = { workspace = true } diff --git a/consensus/state_processing/src/block_replayer.rs b/consensus/state_processing/src/block_replayer.rs index 56e667cdd37..2c75a060dea 100644 --- a/consensus/state_processing/src/block_replayer.rs +++ b/consensus/state_processing/src/block_replayer.rs @@ -1,7 +1,7 @@ use crate::{ - BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError, - VerifyBlockRoot, per_block_processing, per_epoch_processing::EpochProcessingSummary, - per_slot_processing, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, + SlotProcessingError, VerifyBlockRoot, per_block_processing, + per_epoch_processing::EpochProcessingSummary, per_slot_processing, }; use itertools::Itertools; use std::iter::Peekable; @@ -230,8 +230,13 @@ where pre_slot_hook(state_root, &mut self.state)?; } - let summary = per_slot_processing(&mut self.state, Some(state_root), self.spec) - .map_err(BlockReplayError::from)?; + let summary = per_slot_processing( + &mut self.state, + Some(state_root), + GloasVerificationContext::FullVerification, + self.spec, + ) + .map_err(BlockReplayError::from)?; if let Some(ref mut post_slot_hook) = self.post_slot_hook { let is_skipped_slot = self.state.slot() < block.slot(); @@ -259,6 +264,7 @@ where self.block_sig_strategy, verify_block_root, &mut ctxt, + None, self.spec, ) .map_err(BlockReplayError::from)?; @@ -276,8 +282,13 @@ where pre_slot_hook(state_root, &mut self.state)?; } - let summary = per_slot_processing(&mut self.state, Some(state_root), self.spec) - .map_err(BlockReplayError::from)?; + let summary = per_slot_processing( + &mut self.state, + Some(state_root), + GloasVerificationContext::FullVerification, + self.spec, + ) + .map_err(BlockReplayError::from)?; if let Some(ref mut post_slot_hook) = self.post_slot_hook { // No more blocks to apply (from our perspective) so we consider these slots diff --git a/consensus/state_processing/src/builder_deposits_cache.rs b/consensus/state_processing/src/builder_deposits_cache.rs new file mode 100644 index 00000000000..49fe12ae08c --- /dev/null +++ b/consensus/state_processing/src/builder_deposits_cache.rs @@ -0,0 +1,494 @@ +use crate::per_block_processing::is_valid_deposit_signature_batch; +use lru::LruCache; +use parking_lot::Mutex; +use tracing::{debug, instrument}; +use tree_hash::{Hash256, TreeHash}; +use types::{ + BeaconState, ChainSpec, DepositData, DepositRequest, EthSpec, PendingDeposit, + is_builder_withdrawal_credential, new_non_zero_usize, +}; + +use std::num::NonZeroUsize; + +/// This is a very high limit to enable worst case testing. +/// The actual lru storage ends up being quite small in the worst case. +/// +/// The internal hashmap representation would take 32 bytes for key + 1 byte for the bool. +/// With additional overhead in the LRU cache, each entry would come out to ~88 bytes. +/// +/// So worst case storage is ~25MB. +/// +/// In practice, current mainnet gas limits would not allow more than a couple hundred deposits +/// every slot. +const CACHE_SIZE: NonZeroUsize = new_non_zero_usize(262144); + +/// A simple cache that performs signature verification on `PendingDeposit` entries in the +/// beacon state for 0x03 credentials and caches the result. +/// +/// The key is the hash_tree_root of the `Deposit` and the value is the verification result. +/// In gloas, there are 2 places where we need to do bulk signature verification: +/// 1. `onboard_builders_from_pending_deposits` in `upgrade_to_gloas` happens at the fork transition. +/// If the `pending_deposits` queue at fork has many signatures to verify, then verifying them +/// in the hot path could be very expensive. +/// 2. `process_deposit_requests_post_gloas` in `process_operations` can contain upto 8192 signatures +/// to verify in the hot block verification path based on limits today. Since the deposits are +/// received a couple seconds before the actual deposits processing, we can cache those signatures +/// too to ensure the deposit processing is just a lookup operation in the worst case. +pub struct OnboardBuildersCache { + cache: Mutex>, +} + +impl OnboardBuildersCache { + /// Returns `None` if gloas is not scheduled currently. + pub fn new(spec: &ChainSpec) -> Option { + if spec.is_gloas_scheduled() { + Some(Self { + cache: Mutex::new(LruCache::new(CACHE_SIZE)), + }) + } else { + None + } + } + + /// Initializes the cache with all the `pending_deposits` from the passed state + /// that would need to be onboarded at the gloas fork. + /// + /// Further block imports that result in additional deposits should be handled by the + /// [`Self::add_new_pending_deposits`] method. + #[instrument(skip_all)] + pub fn seed_from_state(&self, state: &BeaconState, spec: &ChainSpec) { + let Some(pending_deposits) = state.pending_deposits().ok() else { + return; + }; + let pending_deposits = pending_deposits.iter().collect::>(); + if pending_deposits.is_empty() { + return; + } + + debug!( + pending_deposits_count = pending_deposits.len(), + "Seeding builder onboarding cache from head state" + ); + + self.cache_pending_deposits(pending_deposits, spec); + } + + /// Gets the new deposits added to the `pending_cache` for `state.slot. + /// Signature verifies and caches them for later use. + #[instrument(skip_all)] + pub fn add_new_pending_deposits( + &self, + current_state: &BeaconState, + spec: &ChainSpec, + ) { + let pending_deposits = pending_deposits_to_verify(current_state); + if pending_deposits.is_empty() { + return; + } + + debug!( + pending_deposits_count = pending_deposits.len(), + slot = %current_state.slot(), + "Adding new pending deposits to builder onboarding cache" + ); + + self.cache_pending_deposits(pending_deposits, spec); + } + + /// Takes a list of pending deposits, signature verifies them and caches the result. + fn cache_pending_deposits(&self, deposits: Vec<&PendingDeposit>, spec: &ChainSpec) { + let mut builder_deposits = Vec::new(); + let mut builder_deposit_keys = Vec::new(); + + { + let mut cache = self.cache.lock(); + for deposit in deposits { + if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { + continue; + } + + let deposit_data = DepositData { + amount: deposit.amount, + pubkey: deposit.pubkey, + signature: deposit.signature.clone(), + withdrawal_credentials: deposit.withdrawal_credentials, + }; + let key = deposit_data.tree_hash_root(); + if cache.get(&key).is_some() { + continue; + } + + builder_deposit_keys.push(key); + builder_deposits.push(deposit_data); + } + } + + if builder_deposits.is_empty() { + return; + } + + debug!( + builder_deposits_count = builder_deposits.len(), + "Pre-verifying builder onboarding deposit signatures" + ); + + let verified = is_valid_deposit_signature_batch(builder_deposits, spec); + let mut cache = self.cache.lock(); + for (key, value) in builder_deposit_keys.into_iter().zip(verified) { + cache.push(key, value); + } + } + + /// Pre-verifies builder deposit signatures from execution payload deposit requests + /// and caches the results for later use during `process_deposit_requests_post_gloas`. + #[instrument(skip_all)] + pub fn cache_deposit_requests(&self, deposit_requests: &[DepositRequest], spec: &ChainSpec) { + let mut builder_deposits = Vec::new(); + let mut builder_deposit_keys = Vec::new(); + + { + let mut cache = self.cache.lock(); + for request in deposit_requests { + if !is_builder_withdrawal_credential(request.withdrawal_credentials, spec) { + continue; + } + + let deposit_data = DepositData { + amount: request.amount, + pubkey: request.pubkey, + signature: request.signature.clone(), + withdrawal_credentials: request.withdrawal_credentials, + }; + let key = deposit_data.tree_hash_root(); + if cache.get(&key).is_some() { + continue; + } + + builder_deposit_keys.push(key); + builder_deposits.push(deposit_data); + } + } + + if builder_deposits.is_empty() { + return; + } + + debug!( + builder_deposits_count = builder_deposits.len(), + "Pre-verifying builder deposit signatures from payload envelope" + ); + + let verified = is_valid_deposit_signature_batch(builder_deposits, spec); + let mut cache = self.cache.lock(); + for (key, value) in builder_deposit_keys.into_iter().zip(verified) { + cache.push(key, value); + } + } + + /// Returns `Some(true)` if the deposit exists in the cache and has a valid signature. + /// Returns `Some(false)` if the deposit exists and failed signature verification. + /// Returns `None` if the deposit doesn't exist in the cache. + pub fn cached_is_valid_signature(&self, deposit: &PendingDeposit) -> Option { + let deposit_data = DepositData { + amount: deposit.amount, + pubkey: deposit.pubkey, + signature: deposit.signature.clone(), + withdrawal_credentials: deposit.withdrawal_credentials, + }; + self.get(&deposit_data) + } + + /// Looks up a `DepositData` in the cache by its tree hash root. + pub fn get(&self, deposit_data: &DepositData) -> Option { + let key = deposit_data.tree_hash_root(); + self.cache.lock().get(&key).copied() + } +} + +/// Returns a list of `pending_deposits` that were added for the same slot as the passed state. +fn pending_deposits_to_verify(state: &BeaconState) -> Vec<&PendingDeposit> { + let current_slot = state.slot(); + let Some(pending_deposits) = state.pending_deposits().ok() else { + return Vec::new(); + }; + // Get the index of the first `pending_deposit` for the current slot + // + // Need to do this roundabout way because milhouse iterators aren't double ended, so + // rev().take_while() won't work. + let mut first_current_slot_index = 0; + for index in (0..pending_deposits.len()).rev() { + if pending_deposits + .get(index) + .is_some_and(|deposit| deposit.slot != current_slot) + { + first_current_slot_index = index.saturating_add(1); + break; + } + } + + pending_deposits + .iter() + .skip(first_current_slot_index) + .collect() +} + +#[cfg(all(test, not(feature = "fake_crypto")))] +mod tests { + use super::*; + use bls::{Keypair, SignatureBytes}; + use std::sync::LazyLock; + use types::{ForkName, MainnetEthSpec, Slot}; + + static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(10)); + + fn gloas_spec() -> ChainSpec { + ForkName::Gloas.make_genesis_spec(MainnetEthSpec::default_spec()) + } + + fn non_gloas_spec() -> ChainSpec { + ForkName::Fulu.make_genesis_spec(MainnetEthSpec::default_spec()) + } + + fn builder_credentials(spec: &ChainSpec) -> Hash256 { + let mut credentials = [0u8; 32]; + credentials[0] = spec.builder_withdrawal_prefix_byte; + Hash256::from_slice(&credentials) + } + + fn non_builder_credentials() -> Hash256 { + let mut credentials = [0u8; 32]; + credentials[0] = 0x01; // ETH1 withdrawal credentials + Hash256::from_slice(&credentials) + } + + fn make_valid_builder_deposit(keypair: &Keypair, spec: &ChainSpec) -> PendingDeposit { + let withdrawal_credentials = builder_credentials(spec); + let mut deposit_data = DepositData { + pubkey: keypair.pk.compress(), + withdrawal_credentials, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + + PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature, + slot: Slot::new(0), + } + } + + fn make_invalid_builder_deposit(keypair: &Keypair, spec: &ChainSpec) -> PendingDeposit { + let withdrawal_credentials = builder_credentials(spec); + PendingDeposit { + pubkey: keypair.pk.compress(), + withdrawal_credentials, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + slot: Slot::new(0), + } + } + + fn make_non_builder_deposit(keypair: &Keypair, spec: &ChainSpec) -> PendingDeposit { + let mut deposit_data = DepositData { + pubkey: keypair.pk.compress(), + withdrawal_credentials: non_builder_credentials(), + amount: 32_000_000_000, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + + PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature, + slot: Slot::new(0), + } + } + + #[test] + fn new_returns_none_when_gloas_not_scheduled() { + let spec = non_gloas_spec(); + assert!(OnboardBuildersCache::new(&spec).is_none()); + } + + #[test] + fn new_returns_some_when_gloas_scheduled() { + let spec = gloas_spec(); + assert!(OnboardBuildersCache::new(&spec).is_some()); + } + + #[test] + fn cache_valid_builder_deposit() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let deposit = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + + cache.cache_pending_deposits(vec![&deposit], &spec); + + assert_eq!(cache.cached_is_valid_signature(&deposit), Some(true)); + } + + #[test] + fn cache_invalid_builder_deposit() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let deposit = make_invalid_builder_deposit(&KEYPAIRS[0], &spec); + + cache.cache_pending_deposits(vec![&deposit], &spec); + + assert_eq!(cache.cached_is_valid_signature(&deposit), Some(false)); + } + + #[test] + fn cache_miss_returns_none() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let deposit = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + + assert_eq!(cache.cached_is_valid_signature(&deposit), None); + } + + #[test] + fn non_builder_deposits_filtered_out() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let non_builder = make_non_builder_deposit(&KEYPAIRS[0], &spec); + + cache.cache_pending_deposits(vec![&non_builder], &spec); + + assert_eq!(cache.cached_is_valid_signature(&non_builder), None); + } + + #[test] + fn mixed_deposits_only_caches_builder() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let builder_deposit = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + let non_builder_deposit = make_non_builder_deposit(&KEYPAIRS[1], &spec); + + cache.cache_pending_deposits(vec![&non_builder_deposit, &builder_deposit], &spec); + + assert_eq!( + cache.cached_is_valid_signature(&builder_deposit), + Some(true) + ); + assert_eq!(cache.cached_is_valid_signature(&non_builder_deposit), None); + } + + #[test] + fn duplicate_deposits_not_reverified() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let deposit = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + + cache.cache_pending_deposits(vec![&deposit], &spec); + // Second call with same deposit - should be skipped (already in cache) + cache.cache_pending_deposits(vec![&deposit], &spec); + + assert_eq!(cache.cached_is_valid_signature(&deposit), Some(true)); + } + + #[test] + fn multiple_valid_and_invalid_deposits() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + + let valid_0 = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + let valid_1 = make_valid_builder_deposit(&KEYPAIRS[1], &spec); + let invalid_0 = make_invalid_builder_deposit(&KEYPAIRS[2], &spec); + let invalid_1 = make_invalid_builder_deposit(&KEYPAIRS[3], &spec); + + cache.cache_pending_deposits(vec![&valid_0, &invalid_0, &valid_1, &invalid_1], &spec); + + assert_eq!(cache.cached_is_valid_signature(&valid_0), Some(true)); + assert_eq!(cache.cached_is_valid_signature(&valid_1), Some(true)); + assert_eq!(cache.cached_is_valid_signature(&invalid_0), Some(false)); + assert_eq!(cache.cached_is_valid_signature(&invalid_1), Some(false)); + } + + #[test] + fn cache_deposit_requests_works() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let withdrawal_credentials = builder_credentials(&spec); + + let mut deposit_data = DepositData { + pubkey: KEYPAIRS[0].pk.compress(), + withdrawal_credentials, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&KEYPAIRS[0].sk, &spec); + + let request = DepositRequest { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature.clone(), + index: 0, + }; + + cache.cache_deposit_requests(&[request], &spec); + + assert_eq!(cache.get(&deposit_data), Some(true)); + } + + #[test] + fn cache_deposit_requests_filters_non_builder() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + + let mut deposit_data = DepositData { + pubkey: KEYPAIRS[0].pk.compress(), + withdrawal_credentials: non_builder_credentials(), + amount: 32_000_000_000, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&KEYPAIRS[0].sk, &spec); + + let request = DepositRequest { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature.clone(), + index: 0, + }; + + cache.cache_deposit_requests(&[request], &spec); + + assert_eq!(cache.get(&deposit_data), None); + } + + #[test] + fn get_by_deposit_data() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + let deposit = make_valid_builder_deposit(&KEYPAIRS[0], &spec); + + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + + cache.cache_pending_deposits(vec![&deposit], &spec); + + assert_eq!(cache.get(&deposit_data), Some(true)); + } + + #[test] + fn empty_deposits_list_is_noop() { + let spec = gloas_spec(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + + cache.cache_pending_deposits(vec![], &spec); + cache.cache_deposit_requests(&[], &spec); + // No panic, no entries + } +} diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index c643ad56e34..ff3f1d7d2fd 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -4,8 +4,8 @@ use super::per_block_processing::{ use crate::common::DepositDataTree; use crate::upgrade::electra::upgrade_state_to_electra; use crate::upgrade::{ - upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, upgrade_to_fulu, - upgrade_to_gloas, + GloasVerificationContext, upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, + upgrade_to_deneb, upgrade_to_fulu, upgrade_to_gloas, }; use fixed_bytes::FixedBytesExtended; use safe_arith::{ArithError, SafeArith}; @@ -162,7 +162,7 @@ pub fn initialize_beacon_state_from_eth1( .gloas_fork_epoch .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { - upgrade_to_gloas(&mut state, spec)?; + upgrade_to_gloas(&mut state, GloasVerificationContext::FullVerification, spec)?; // Remove intermediate Fulu fork from `state.fork`. state.fork_mut().previous_version = spec.gloas_fork_version; diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index e37c5265799..b0d4d1aaedf 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -18,6 +18,7 @@ mod metrics; pub mod all_caches; pub mod block_replayer; +pub mod builder_deposits_cache; pub mod common; pub mod consensus_context; pub mod envelope_processing; @@ -46,4 +47,5 @@ pub use per_epoch_processing::{ }; pub use per_slot_processing::{Error as SlotProcessingError, per_slot_processing}; pub use types::{EpochCache, EpochCacheError, EpochCacheKey}; +pub use upgrade::GloasVerificationContext; pub use verify_operation::{SigVerifiedOp, TransformPersist, VerifyOperation, VerifyOperationAt}; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f13f2a339b8..ee772960346 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,3 +1,4 @@ +use crate::builder_deposits_cache::OnboardBuildersCache; use crate::consensus_context::ConsensusContext; use errors::{ BlockOperationError, BlockProcessingError, ExecutionPayloadBidInvalid, HeaderInvalid, @@ -27,7 +28,8 @@ pub use verify_attestation::{ }; pub use verify_bls_to_execution_change::verify_bls_to_execution_change; pub use verify_deposit::{ - get_existing_validator_index, is_valid_deposit_signature, verify_deposit_merkle_proof, + get_existing_validator_index, is_valid_deposit_signature, is_valid_deposit_signature_batch, + verify_deposit_merkle_proof, }; pub use verify_exit::verify_exit; pub use withdrawals::get_expected_withdrawals; @@ -115,6 +117,7 @@ pub fn per_block_processing>( block_signature_strategy: BlockSignatureStrategy, verify_block_root: VerifyBlockRoot, ctxt: &mut ConsensusContext, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let block = signed_block.message(); @@ -131,7 +134,7 @@ pub fn per_block_processing>( // Process deferred execution requests from the parent's envelope. if fork_name.gloas_enabled() { - process_parent_execution_payload(state, block, spec)?; + process_parent_execution_payload(state, block, builder_onboarding_cache, spec)?; } // Build epoch cache if it hasn't already been built, or if it is no longer valid @@ -548,6 +551,7 @@ pub fn compute_timestamp_at_slot( pub fn process_parent_execution_payload>( state: &mut BeaconState, block: BeaconBlockRef<'_, E, Payload>, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let bid_parent_block_hash = block @@ -577,7 +581,7 @@ pub fn process_parent_execution_payload( state: &mut BeaconState, requests: &ExecutionRequests, + onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let parent_bid = state.latest_execution_payload_bid()?.clone(); @@ -596,7 +601,12 @@ pub fn apply_parent_execution_payload( let parent_epoch = parent_slot.epoch(E::slots_per_epoch()); // Process execution requests from the parent's payload - process_operations::process_deposit_requests_post_gloas(state, &requests.deposits, spec)?; + process_operations::process_deposit_requests_post_gloas( + state, + &requests.deposits, + onboarding_cache, + spec, + )?; process_operations::process_withdrawal_requests(state, &requests.withdrawals, spec)?; process_operations::process_consolidation_requests(state, &requests.consolidations, spec)?; diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index f88a325d4e9..64d5fa0c94b 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -1,5 +1,8 @@ +use std::collections::{BTreeSet, HashMap}; + use super::*; use crate::VerifySignatures; +use crate::builder_deposits_cache::OnboardBuildersCache; use crate::common::{ get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit, slash_validator, @@ -10,10 +13,12 @@ use crate::per_block_processing::builder::{ use crate::per_block_processing::errors::{BlockProcessingError, ExitInvalid, IntoWithIndex}; use crate::per_block_processing::signature_sets::{exit_signature_set, get_pubkey_from_state}; use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; -use bls::{PublicKeyBytes, SignatureBytes}; +use bls::PublicKeyBytes; +use milhouse::List; use ssz_types::FixedVector; use typenum::U33; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; +use types::{Address, Builder}; pub fn process_operations>( state: &mut BeaconState, @@ -901,28 +906,16 @@ pub fn process_deposit_requests_pre_gloas( Ok(()) } -pub fn process_deposit_requests_post_gloas( - state: &mut BeaconState, - deposit_requests: &[DepositRequest], - spec: &ChainSpec, -) -> Result<(), BlockProcessingError> { - for request in deposit_requests { - process_deposit_request_post_gloas(state, request, spec)?; - } - - Ok(()) -} - /// Check if there is a pending deposit for a new validator with the given pubkey. // TODO(gloas): cache the deposit signature validation or remove this loop entirely if possible, // it is `O(n * m)` where `n` is max 8192 and `m` is max 128M. -pub fn is_pending_validator<'a>( - pending_deposits: impl IntoIterator, +pub fn is_pending_validator( + deposits: &List, pubkey: &PublicKeyBytes, spec: &ChainSpec, ) -> bool { - pending_deposits.into_iter().any(|deposit| { - deposit.pubkey == *pubkey + for deposit in deposits.iter() { + if deposit.pubkey == *pubkey && is_valid_deposit_signature( &DepositData { pubkey: deposit.pubkey, @@ -933,108 +926,229 @@ pub fn is_pending_validator<'a>( spec, ) .is_ok() - }) + { + return true; + } + } + false +} + +#[derive(Copy, Clone)] +pub enum VerifyBuilderSignature { + /// Verification not done + Verify, + VerifiedValid, + VerifiedInvalid, } -pub fn process_deposit_request_post_gloas( +pub fn process_deposit_requests_post_gloas( state: &mut BeaconState, - deposit_request: &DepositRequest, + deposit_requests: &[DepositRequest], + onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - // [New in Gloas:EIP7732] - // Regardless of the withdrawal credentials prefix, if a builder/validator - // already exists with this pubkey, apply the deposit to their balance - // TODO(gloas): this could be more efficient in the builder case, see: - // https://github.com/sigp/lighthouse/issues/8783 - let builder_index = state - .builders()? - .iter() - .enumerate() - .find(|(_, builder)| builder.pubkey == deposit_request.pubkey) - .map(|(i, _)| i as u64); - let is_builder = builder_index.is_some(); - - let validator_index = state.get_validator_index(&deposit_request.pubkey)?; - let is_validator = validator_index.is_some(); + let mut new_builder_signature_candidates = Vec::with_capacity(deposit_requests.len()); + let slot = state.slot(); + let current_epoch = state.current_epoch(); - let has_builder_prefix = - is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); + // Contains the pubkey to index mapping for existing builders and any new builders + // added with the `deposit_requests` coming in. + // TODO(gloas): can potentially skip this if we add a builder pubkey cache later. + let state_builders = state.builders()?; + let mut builder_index_map = HashMap::with_capacity(state_builders.len()); + // A one pass cache for repeated calls to `get_index_for_new_builder`. + // state.builders() allows reusing indices. This holds the list of indices where + // potentially new builders can be inserted. + // Doing this avoids walking through the entire `state.builders` list and appending + // in the end everytime we want to insert a new builder. + // For a high number of builder deposits, this can take quite long. + let mut reusable_builder_indices = BTreeSet::new(); + + for (index, builder) in state_builders.iter().enumerate() { + builder_index_map.insert(builder.pubkey, index as BuilderIndex); + if builder.withdrawable_epoch <= current_epoch && builder.balance == 0 { + reusable_builder_indices.insert(index as BuilderIndex); + } + } - if is_builder - || (has_builder_prefix - && !is_validator - && !is_pending_validator(state.pending_deposits()?, &deposit_request.pubkey, spec)) - { - // Apply builder deposits immediately - apply_deposit_for_builder( - state, - builder_index, - deposit_request.pubkey, - deposit_request.withdrawal_credentials, - deposit_request.amount, - deposit_request.signature.clone(), - state.slot(), - spec, - )?; - return Ok(()); + // Step 1: Collect all requests that could create a new builder when evaluated against the + // current state. + for (request_index, deposit_request) in deposit_requests.iter().enumerate() { + let is_builder = builder_index_map.contains_key(&deposit_request.pubkey); + let is_validator = state + .get_validator_index(&deposit_request.pubkey)? + .is_some(); + let has_builder_prefix = + is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); + + if !is_builder && has_builder_prefix && !is_validator { + new_builder_signature_candidates.push(( + request_index, + DepositData { + pubkey: deposit_request.pubkey, + withdrawal_credentials: deposit_request.withdrawal_credentials, + amount: deposit_request.amount, + signature: deposit_request.signature.clone(), + }, + )); + } } - // Add validator deposits to the queue - let slot = state.slot(); - state.pending_deposits_mut()?.push(PendingDeposit { - pubkey: deposit_request.pubkey, - withdrawal_credentials: deposit_request.withdrawal_credentials, - amount: deposit_request.amount, - signature: deposit_request.signature.clone(), - slot, - })?; + // Step 2: Check cache for already-verified signatures, batch verify the rest. + let mut builder_signature_results_by_request = + vec![VerifyBuilderSignature::Verify; deposit_requests.len()]; + + let mut uncached_request_indices = Vec::new(); + let mut uncached_deposits = Vec::new(); + + for (request_index, deposit_data) in &new_builder_signature_candidates { + let cached_result = onboarding_cache.and_then(|c| c.get(deposit_data)); + if let Some(result) = builder_signature_results_by_request.get_mut(*request_index) { + *result = match cached_result { + Some(true) => VerifyBuilderSignature::VerifiedValid, + Some(false) => VerifyBuilderSignature::VerifiedInvalid, + None => { + uncached_request_indices.push(*request_index); + uncached_deposits.push(deposit_data.clone()); + VerifyBuilderSignature::Verify + } + }; + } + } - Ok(()) -} + let batch_results = is_valid_deposit_signature_batch(uncached_deposits, spec); -#[allow(clippy::too_many_arguments)] -pub fn apply_deposit_for_builder( - state: &mut BeaconState, - builder_index_opt: Option, - pubkey: PublicKeyBytes, - withdrawal_credentials: Hash256, - amount: u64, - signature: SignatureBytes, - slot: Slot, - spec: &ChainSpec, -) -> Result, BeaconStateError> { - match builder_index_opt { - None => { - // Verify the deposit signature (proof of possession) which is not checked by the deposit contract - let deposit_data = DepositData { - pubkey, - withdrawal_credentials, - amount, - signature, + for (&request_index, is_valid) in uncached_request_indices.iter().zip(batch_results) { + if let Some(res) = builder_signature_results_by_request.get_mut(request_index) { + *res = if is_valid { + VerifyBuilderSignature::VerifiedValid + } else { + VerifyBuilderSignature::VerifiedInvalid }; - if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - let builder_index = state.add_builder_to_registry( - pubkey, - withdrawal_credentials, - amount, - slot, + } + } + + // Step 3: Second pass over the requests that is equivalent to the spec function only + // with the signature verification cached. + for (request_index, deposit_request) in deposit_requests.iter().enumerate() { + let builder_index = builder_index_map.get(&deposit_request.pubkey).copied(); + let is_builder = builder_index.is_some(); + + let is_validator = state + .get_validator_index(&deposit_request.pubkey)? + .is_some(); + let has_builder_prefix = + is_builder_withdrawal_credential(deposit_request.withdrawal_credentials, spec); + if is_builder + || (has_builder_prefix + && !is_validator + && !is_pending_validator::( + state.pending_deposits()?, + &deposit_request.pubkey, spec, - )?; - Ok(Some(builder_index)) + )) + { + // Directly increase the balance if existing builder and move on + // to the next deposit. + // The signature validity is irrelevant in the case when + // the builder already exists in the state. + if let Some(builder_index) = builder_index { + state + .builders_mut()? + .get_mut(builder_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(builder_index))? + .balance + .safe_add_assign(deposit_request.amount)?; + // If the existing builder's balance was 0, then its no longer the + // case after the top-up. We cannot reuse its index anymore, remove + // the index from the set. + if deposit_request.amount > 0 && reusable_builder_indices.contains(&builder_index) { + reusable_builder_indices.remove(&builder_index); + } + continue; + } + + // If the builder does not exist in the state, then we need to + // verify the signature and only add builders that have a valid + // signature. + let verify_signature = builder_signature_results_by_request + .get(request_index) + .copied() + .unwrap_or(VerifyBuilderSignature::Verify); + + let is_valid = match verify_signature { + VerifyBuilderSignature::Verify => { + let deposit_data = deposit_request.into(); + is_valid_deposit_signature(&deposit_data, spec).is_ok() + } + VerifyBuilderSignature::VerifiedValid => true, + VerifyBuilderSignature::VerifiedInvalid => false, + }; + + // Signature is valid, create the builder and insert it in the correct location + if is_valid { + let version = *deposit_request + .withdrawal_credentials + .as_slice() + .first() + .ok_or(BeaconStateError::WithdrawalCredentialMissingVersion)?; + let execution_address = deposit_request + .withdrawal_credentials + .as_slice() + .get(12..) + .and_then(|bytes| Address::try_from(bytes).ok()) + .ok_or(BeaconStateError::WithdrawalCredentialMissingAddress)?; + + let builder = Builder { + pubkey: deposit_request.pubkey, + version, + execution_address, + balance: deposit_request.amount, + deposit_epoch: slot.epoch(E::slots_per_epoch()), + withdrawable_epoch: spec.far_future_epoch, + }; + // There are reusable indices that opened up because of older builders getting + // evicted, reuse the indices by inserting the new builder in the correct + // location + let new_index = if let Some(reusable_index) = reusable_builder_indices.pop_first() { + let old_pubkey = state + .builders()? + .get(reusable_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(reusable_index))? + .pubkey; + builder_index_map.remove(&old_pubkey); + *state + .builders_mut()? + .get_mut(reusable_index as usize) + .ok_or(BeaconStateError::UnknownBuilder(reusable_index))? = builder; + reusable_index + } else { + // There are no reusable indices, insert at the end. + let builders = state.builders_mut()?; + let next_index = builders.len() as BuilderIndex; + builders.push(builder)?; + next_index + }; + + // Keep the local mapping updated + builder_index_map.insert(deposit_request.pubkey, new_index); + continue; } else { - Ok(None) + // signature is invalid, cannot create a new builder. drop the deposit and continue + continue; } } - Some(builder_index) => { - state - .builders_mut()? - .get_mut(builder_index as usize) - .ok_or(BeaconStateError::UnknownBuilder(builder_index))? - .balance - .safe_add_assign(amount)?; - Ok(Some(builder_index)) - } + + state.pending_deposits_mut()?.push(PendingDeposit { + pubkey: deposit_request.pubkey, + withdrawal_credentials: deposit_request.withdrawal_credentials, + amount: deposit_request.amount, + signature: deposit_request.signature.clone(), + slot, + })?; } + + Ok(()) } // Make sure to build the pubkey cache before calling this function diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 593a2557e86..61e10916a00 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -1,10 +1,12 @@ #![cfg(all(test, not(feature = "fake_crypto"), not(debug_assertions)))] +use crate::builder_deposits_cache::OnboardBuildersCache; use crate::per_block_processing::errors::{ AttestationInvalid, AttesterSlashingInvalid, BlockOperationError, BlockProcessingError, DepositInvalid, HeaderInvalid, IndexedAttestationInvalid, IntoWithIndex, ProposerSlashingInvalid, }; +use crate::upgrade::gloas::GloasVerificationContext; use crate::{BlockReplayError, BlockReplayer, per_block_processing}; use crate::{ BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, @@ -34,12 +36,26 @@ static KEYPAIRS: LazyLock> = async fn get_harness( epoch_offset: u64, num_validators: usize, +) -> BeaconChainHarness> { + get_harness_at_fork::(epoch_offset, num_validators, ForkName::Electra).await +} + +async fn get_gloas_harness( + epoch_offset: u64, + num_validators: usize, +) -> BeaconChainHarness> { + get_harness_at_fork::(epoch_offset, num_validators, ForkName::Gloas).await +} + +async fn get_harness_at_fork( + epoch_offset: u64, + num_validators: usize, + fork_name: ForkName, ) -> BeaconChainHarness> { // Set the state and block to be in the last slot of the `epoch_offset`th epoch. let last_slot_of_epoch = (MainnetEthSpec::genesis_epoch() + epoch_offset).end_slot(E::slots_per_epoch()); - // Use Electra spec to ensure blocks are created at the same fork as the state - let spec = Arc::new(ForkName::Electra.make_genesis_spec(E::default_spec())); + let spec = Arc::new(fork_name.make_genesis_spec(E::default_spec())); let harness = BeaconChainHarness::>::builder(E::default()) .spec(spec.clone()) .keypairs(KEYPAIRS[0..num_validators].to_vec()) @@ -62,6 +78,78 @@ async fn get_harness( harness } +/// Helper to create a harness with Fulu genesis and gloas at a later epoch. +async fn get_fulu_harness_with_gloas_scheduled( + gloas_epoch: u64, + num_validators: usize, +) -> BeaconChainHarness> { + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + spec.gloas_fork_epoch = Some(Epoch::new(gloas_epoch)); + let spec = Arc::new(spec); + let last_slot_of_pre_gloas_epoch = Epoch::new(gloas_epoch).start_slot(E::slots_per_epoch()) - 1; + let harness = BeaconChainHarness::>::builder(E::default()) + .spec(spec.clone()) + .keypairs(KEYPAIRS[0..num_validators].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + let state = harness.get_current_state(); + if last_slot_of_pre_gloas_epoch > Slot::new(0) { + harness + .add_attested_blocks_at_slots( + state, + (1..=last_slot_of_pre_gloas_epoch.as_u64()) + .map(Slot::new) + .collect::>() + .as_slice(), + (0..num_validators).collect::>().as_slice(), + ) + .await; + } + harness +} + +fn builder_withdrawal_credentials(spec: &ChainSpec) -> Hash256 { + let mut credentials = [0u8; 32]; + credentials[0] = spec.builder_withdrawal_prefix_byte; + Hash256::from_slice(&credentials) +} + +fn make_deposit_request( + keypair: &Keypair, + withdrawal_credentials: Hash256, + amount: u64, + spec: &ChainSpec, + index: u64, +) -> DepositRequest { + let mut deposit_data = DepositData { + pubkey: keypair.pk.compress(), + withdrawal_credentials, + amount, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + + DepositRequest { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature, + index, + } +} + +fn find_builder_index( + state: &BeaconState, + pubkey: &PublicKeyBytes, +) -> Option { + state + .builders() + .unwrap() + .iter() + .position(|builder| builder.pubkey == *pubkey) +} + #[tokio::test] async fn valid_block_ok() { let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; @@ -80,6 +168,7 @@ async fn valid_block_ok() { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &spec, ); @@ -105,6 +194,7 @@ async fn invalid_block_header_state_slot() { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &spec, ); @@ -137,6 +227,7 @@ async fn invalid_parent_block_root() { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &spec, ); @@ -167,6 +258,7 @@ async fn invalid_block_signature() { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &spec, ); @@ -199,6 +291,7 @@ async fn invalid_randao_reveal_signature() { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &spec, ); @@ -377,6 +470,644 @@ async fn invalid_deposit_invalid_pub_key() { assert_eq!(result, Ok(())); } +#[tokio::test] +async fn deposit_signature_batch_returns_true_for_valid_and_false_for_invalid() { + let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let (mut deposits, _) = harness.make_deposits(&mut state, 10, None, None); + deposits[1].data.signature = SignatureBytes::empty(); + deposits[8].data.pubkey = PublicKeyBytes::empty(); + + let result = per_block_processing::is_valid_deposit_signature_batch( + deposits.into_iter().map(|deposit| deposit.data).collect(), + &spec, + ); + + assert_eq!(result.len(), 10); + assert!(!result[1]); + assert!(!result[8]); + assert!( + result + .iter() + .enumerate() + .all(|(index, is_valid)| matches!(index, 1 | 8) || *is_valid) + ); +} + +#[tokio::test] +async fn deposit_signature_batch_returns_true_for_all_valid_signatures() { + let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let (deposits, _) = harness.make_deposits(&mut state, 10, None, None); + + let result = per_block_processing::is_valid_deposit_signature_batch( + deposits.into_iter().map(|deposit| deposit.data).collect(), + &spec, + ); + + assert_eq!(result, vec![true; 10]); +} + +#[tokio::test] +async fn deposit_signature_batch_falls_back_to_individual_verification() { + let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let (mut deposits, _) = harness.make_deposits(&mut state, 10, None, None); + let wrong_signature = deposits[4].data.signature.clone(); + deposits[3].data.signature = wrong_signature; + + let result = per_block_processing::is_valid_deposit_signature_batch( + deposits.into_iter().map(|deposit| deposit.data).collect(), + &spec, + ); + + assert_eq!(result.len(), 10); + assert!(!result[3]); + assert!( + result + .iter() + .enumerate() + .all(|(index, is_valid)| index == 3 || *is_valid) + ); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_batches_new_builder_signature_verification() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let existing_builder_keypair = &KEYPAIRS[VALIDATOR_COUNT]; + let existing_builder_credentials = builder_withdrawal_credentials(&spec); + let existing_builder_amount = 11; + let slot = state.slot(); + state + .add_builder_to_registry( + existing_builder_keypair.pk.compress(), + existing_builder_credentials, + existing_builder_amount, + slot, + &spec, + ) + .unwrap(); + + let valid_builder_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 1], + builder_withdrawal_credentials(&spec), + 13, + &spec, + 0, + ); + + let mut invalid_builder_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 2], + builder_withdrawal_credentials(&spec), + 17, + &spec, + 1, + ); + invalid_builder_request.signature = SignatureBytes::empty(); + + let mut existing_builder_top_up = make_deposit_request( + existing_builder_keypair, + existing_builder_credentials, + 19, + &spec, + 2, + ); + existing_builder_top_up.signature = SignatureBytes::empty(); + + let mut pending_validator_request = + make_deposit_request(&KEYPAIRS[VALIDATOR_COUNT + 3], Hash256::ZERO, 23, &spec, 3); + pending_validator_request.signature = SignatureBytes::empty(); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[ + valid_builder_request.clone(), + invalid_builder_request.clone(), + existing_builder_top_up.clone(), + pending_validator_request.clone(), + ], + None, + &spec, + ) + .unwrap(); + + let valid_builder_index = find_builder_index(&state, &valid_builder_request.pubkey); + assert!(valid_builder_index.is_some()); + + let invalid_builder_index = find_builder_index(&state, &invalid_builder_request.pubkey); + assert!(invalid_builder_index.is_none()); + + let existing_builder_index = + find_builder_index(&state, &existing_builder_top_up.pubkey).unwrap(); + let existing_builder = state + .builders() + .unwrap() + .get(existing_builder_index) + .unwrap(); + assert_eq!( + existing_builder.balance, + existing_builder_amount + existing_builder_top_up.amount + ); + + let pending_deposits = state.pending_deposits().unwrap(); + assert_eq!(pending_deposits.len(), 1); + assert_eq!( + pending_deposits.get(0).unwrap().pubkey, + pending_validator_request.pubkey + ); + assert_eq!( + pending_deposits.get(0).unwrap().amount, + pending_validator_request.amount + ); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_preserves_existing_builder_path() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let pubkey = KEYPAIRS[VALIDATOR_COUNT + 4].pk.compress(); + let withdrawal_credentials = builder_withdrawal_credentials(&spec); + + let first_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 4], + withdrawal_credentials, + 29, + &spec, + 0, + ); + let mut second_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 4], + withdrawal_credentials, + 31, + &spec, + 1, + ); + second_request.signature = SignatureBytes::empty(); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[first_request.clone(), second_request.clone()], + None, + &spec, + ) + .unwrap(); + + let builder_index = find_builder_index(&state, &pubkey).unwrap(); + let builder = state.builders().unwrap().get(builder_index).unwrap(); + assert_eq!( + builder.balance, + first_request.amount + second_request.amount + ); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_preserves_pending_validator_path() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let pending_validator_request = + make_deposit_request(&KEYPAIRS[VALIDATOR_COUNT + 5], Hash256::ZERO, 37, &spec, 0); + let builder_prefixed_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 5], + builder_withdrawal_credentials(&spec), + 41, + &spec, + 1, + ); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[ + pending_validator_request.clone(), + builder_prefixed_request.clone(), + ], + None, + &spec, + ) + .unwrap(); + + assert!(find_builder_index(&state, &pending_validator_request.pubkey).is_none()); + + let pending_deposits = state.pending_deposits().unwrap(); + assert_eq!(pending_deposits.len(), 2); + assert_eq!( + pending_deposits.get(0).unwrap().pubkey, + pending_validator_request.pubkey + ); + assert_eq!( + pending_deposits.get(1).unwrap().pubkey, + builder_prefixed_request.pubkey + ); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_preserves_existing_builder_before_validator_path() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let builder_prefixed_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 6], + builder_withdrawal_credentials(&spec), + 43, + &spec, + 0, + ); + let validator_prefixed_request = + make_deposit_request(&KEYPAIRS[VALIDATOR_COUNT + 6], Hash256::ZERO, 47, &spec, 1); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[ + builder_prefixed_request.clone(), + validator_prefixed_request.clone(), + ], + None, + &spec, + ) + .unwrap(); + + let builder_index = find_builder_index(&state, &builder_prefixed_request.pubkey).unwrap(); + let builder = state.builders().unwrap().get(builder_index).unwrap(); + assert_eq!( + builder.balance, + builder_prefixed_request.amount + validator_prefixed_request.amount + ); + assert!(state.pending_deposits().unwrap().is_empty()); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_does_not_reuse_topped_up_builder_index() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let reusable_builder_keypair = &KEYPAIRS[VALIDATOR_COUNT + 7]; + let reusable_builder_credentials = builder_withdrawal_credentials(&spec); + let slot = state.slot(); + let reusable_builder_index = state + .add_builder_to_registry( + reusable_builder_keypair.pk.compress(), + reusable_builder_credentials, + 11, + slot, + &spec, + ) + .unwrap() as usize; + + let current_epoch = state.current_epoch(); + let reusable_builder = state + .builders_mut() + .unwrap() + .get_mut(reusable_builder_index) + .unwrap(); + reusable_builder.balance = 0; + reusable_builder.withdrawable_epoch = current_epoch; + + let mut existing_builder_top_up = make_deposit_request( + reusable_builder_keypair, + reusable_builder_credentials, + 29, + &spec, + 0, + ); + existing_builder_top_up.signature = SignatureBytes::empty(); + + let new_builder_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 8], + builder_withdrawal_credentials(&spec), + 31, + &spec, + 1, + ); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[existing_builder_top_up.clone(), new_builder_request.clone()], + None, + &spec, + ) + .unwrap(); + + let builders = state.builders().unwrap(); + assert_eq!(builders.len(), 2); + + let topped_up_builder = builders.get(reusable_builder_index).unwrap(); + assert_eq!(topped_up_builder.pubkey, existing_builder_top_up.pubkey); + assert_eq!(topped_up_builder.balance, existing_builder_top_up.amount); + + let new_builder_index = find_builder_index(&state, &new_builder_request.pubkey).unwrap(); + assert_ne!(new_builder_index, reusable_builder_index); +} + +#[tokio::test] +async fn process_deposit_requests_post_gloas_preserves_pre_state_pending_validator_path() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let pending_validator_request = + make_deposit_request(&KEYPAIRS[VALIDATOR_COUNT + 7], Hash256::ZERO, 53, &spec, 0); + let slot = state.slot(); + state + .pending_deposits_mut() + .unwrap() + .push(PendingDeposit { + pubkey: pending_validator_request.pubkey, + withdrawal_credentials: pending_validator_request.withdrawal_credentials, + amount: pending_validator_request.amount, + signature: pending_validator_request.signature.clone(), + slot, + }) + .unwrap(); + + let builder_prefixed_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 7], + builder_withdrawal_credentials(&spec), + 59, + &spec, + 1, + ); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + std::slice::from_ref(&builder_prefixed_request), + None, + &spec, + ) + .unwrap(); + + assert!(find_builder_index(&state, &builder_prefixed_request.pubkey).is_none()); + + let pending_deposits = state.pending_deposits().unwrap(); + assert_eq!(pending_deposits.len(), 2); + assert_eq!( + pending_deposits.get(0).unwrap().pubkey, + pending_validator_request.pubkey + ); + assert_eq!( + pending_deposits.get(1).unwrap().pubkey, + builder_prefixed_request.pubkey + ); +} + +// Tests that exercise the cached verification path used in production. + +#[tokio::test] +async fn process_deposit_requests_with_pre_seeded_cache() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let valid_builder_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 1], + builder_withdrawal_credentials(&spec), + 13, + &spec, + 0, + ); + + let mut invalid_builder_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 2], + builder_withdrawal_credentials(&spec), + 17, + &spec, + 1, + ); + invalid_builder_request.signature = SignatureBytes::empty(); + + let deposit_requests = [ + valid_builder_request.clone(), + invalid_builder_request.clone(), + ]; + + // Pre-seed the cache with these deposit requests (simulating payload envelope arrival) + let cache = OnboardBuildersCache::new(&spec).unwrap(); + cache.cache_deposit_requests(&deposit_requests, &spec); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &deposit_requests, + Some(&cache), + &spec, + ) + .unwrap(); + + // Valid builder should be onboarded via cache hit + assert!(find_builder_index(&state, &valid_builder_request.pubkey).is_some()); + // Invalid signature should be rejected via cache hit + assert!(find_builder_index(&state, &invalid_builder_request.pubkey).is_none()); +} + +#[tokio::test] +async fn process_deposit_requests_cache_partial_hit() { + let harness = get_gloas_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + let cached_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 1], + builder_withdrawal_credentials(&spec), + 13, + &spec, + 0, + ); + + let uncached_request = make_deposit_request( + &KEYPAIRS[VALIDATOR_COUNT + 2], + builder_withdrawal_credentials(&spec), + 17, + &spec, + 1, + ); + + // Only seed cache with first request + let cache = OnboardBuildersCache::new(&spec).unwrap(); + cache.cache_deposit_requests(std::slice::from_ref(&cached_request), &spec); + + process_operations::process_deposit_requests_post_gloas( + &mut state, + &[cached_request.clone(), uncached_request.clone()], + Some(&cache), + &spec, + ) + .unwrap(); + + // Both should be onboarded: one from cache, one from batch verification fallback + assert!(find_builder_index(&state, &cached_request.pubkey).is_some()); + assert!(find_builder_index(&state, &uncached_request.pubkey).is_some()); +} + +#[tokio::test] +async fn upgrade_to_gloas_with_cached_verification() { + let harness = + get_fulu_harness_with_gloas_scheduled::(EPOCH_OFFSET, VALIDATOR_COUNT) + .await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + // Add builder pending deposits to the pre-gloas state + let valid_keypair = &KEYPAIRS[VALIDATOR_COUNT + 1]; + let invalid_keypair = &KEYPAIRS[VALIDATOR_COUNT + 2]; + let builder_creds = builder_withdrawal_credentials(&spec); + + let mut valid_deposit_data = DepositData { + pubkey: valid_keypair.pk.compress(), + withdrawal_credentials: builder_creds, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + }; + valid_deposit_data.signature = valid_deposit_data.create_signature(&valid_keypair.sk, &spec); + + let invalid_deposit_data = DepositData { + pubkey: invalid_keypair.pk.compress(), + withdrawal_credentials: builder_creds, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + }; + + let slot = state.slot(); + state + .pending_deposits_mut() + .unwrap() + .push(PendingDeposit { + pubkey: valid_deposit_data.pubkey, + withdrawal_credentials: valid_deposit_data.withdrawal_credentials, + amount: valid_deposit_data.amount, + signature: valid_deposit_data.signature.clone(), + slot, + }) + .unwrap(); + state + .pending_deposits_mut() + .unwrap() + .push(PendingDeposit { + pubkey: invalid_deposit_data.pubkey, + withdrawal_credentials: invalid_deposit_data.withdrawal_credentials, + amount: invalid_deposit_data.amount, + signature: invalid_deposit_data.signature.clone(), + slot, + }) + .unwrap(); + + // Seed the cache from the state (production path) + let cache = OnboardBuildersCache::new(&spec).unwrap(); + cache.seed_from_state(&state, &spec); + + // Run upgrade_to_gloas with cached verification + crate::upgrade::upgrade_to_gloas( + &mut state, + GloasVerificationContext::CachedVerification(&cache), + &spec, + ) + .unwrap(); + + // Valid builder deposit should be onboarded + assert!(find_builder_index(&state, &valid_deposit_data.pubkey).is_some()); + // Invalid signature should be rejected (not added to builders) + assert!(find_builder_index(&state, &invalid_deposit_data.pubkey).is_none()); + // Invalid builder deposit is consumed (dropped) during onboarding, not kept in pending + let pending = state.pending_deposits().unwrap(); + assert!( + !pending + .iter() + .any(|d| d.pubkey == invalid_deposit_data.pubkey) + ); +} + +#[tokio::test] +async fn upgrade_to_gloas_cached_matches_full_verification() { + let harness = + get_fulu_harness_with_gloas_scheduled::(EPOCH_OFFSET, VALIDATOR_COUNT) + .await; + let spec = harness.spec.clone(); + let mut state = harness.get_current_state(); + + // Add multiple builder deposits: some valid, some invalid + let builder_creds = builder_withdrawal_credentials(&spec); + let slot = state.slot(); + + for i in 0..4 { + let keypair = &KEYPAIRS[VALIDATOR_COUNT + i + 1]; + let mut deposit_data = DepositData { + pubkey: keypair.pk.compress(), + withdrawal_credentials: builder_creds, + amount: 256_000_000_000, + signature: SignatureBytes::empty(), + }; + // Make even-indexed deposits valid, odd-indexed invalid + if i % 2 == 0 { + deposit_data.signature = deposit_data.create_signature(&keypair.sk, &spec); + } + state + .pending_deposits_mut() + .unwrap() + .push(PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount: deposit_data.amount, + signature: deposit_data.signature.clone(), + slot, + }) + .unwrap(); + } + + // Run with full verification to get ground truth + let mut state_full = state.clone(); + crate::upgrade::upgrade_to_gloas( + &mut state_full, + GloasVerificationContext::FullVerification, + &spec, + ) + .unwrap(); + + // Run with cached verification + let mut state_cached = state.clone(); + let cache = OnboardBuildersCache::new(&spec).unwrap(); + cache.seed_from_state(&state_cached, &spec); + crate::upgrade::upgrade_to_gloas( + &mut state_cached, + GloasVerificationContext::CachedVerification(&cache), + &spec, + ) + .unwrap(); + + // Both paths should produce identical builder registries + let builders_full = state_full.builders().unwrap(); + let builders_cached = state_cached.builders().unwrap(); + assert_eq!(builders_full.len(), builders_cached.len()); + for i in 0..builders_full.len() { + let bf = builders_full.get(i).unwrap(); + let bc = builders_cached.get(i).unwrap(); + assert_eq!(bf.pubkey, bc.pubkey); + assert_eq!(bf.balance, bc.balance); + assert_eq!(bf.execution_address, bc.execution_address); + } + + // Both paths should produce identical pending_deposits + let pending_full = state_full.pending_deposits().unwrap(); + let pending_cached = state_cached.pending_deposits().unwrap(); + assert_eq!(pending_full.len(), pending_cached.len()); + for i in 0..pending_full.len() { + assert_eq!( + pending_full.get(i).unwrap().pubkey, + pending_cached.get(i).unwrap().pubkey + ); + } +} + #[tokio::test] async fn invalid_attestation_no_committee_for_index() { let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; diff --git a/consensus/state_processing/src/per_block_processing/verify_deposit.rs b/consensus/state_processing/src/per_block_processing/verify_deposit.rs index d403bfa82b6..97f984242ed 100644 --- a/consensus/state_processing/src/per_block_processing/verify_deposit.rs +++ b/consensus/state_processing/src/per_block_processing/verify_deposit.rs @@ -1,13 +1,18 @@ use super::errors::{BlockOperationError, DepositInvalid}; use crate::per_block_processing::signature_sets::deposit_pubkey_signature_message; -use bls::PublicKeyBytes; +use bls::{PublicKey, PublicKeyBytes, Signature, SignatureSet, verify_signature_sets}; use merkle_proof::verify_merkle_proof; +use rayon::prelude::*; use safe_arith::SafeArith; +use std::borrow::Cow; +use tracing::instrument; use tree_hash::TreeHash; use types::*; type Result = std::result::Result>; +const DEPOSIT_SIGNATURE_BATCH_SIZE: usize = 8; + fn error(reason: DepositInvalid) -> BlockOperationError { BlockOperationError::invalid(reason) } @@ -66,3 +71,83 @@ pub fn verify_deposit_merkle_proof( Ok(()) } + +/// Batch verify a slice of deposit signatures +fn verify_deposit_signature_sets(entries: &[&(usize, PublicKey, Signature, Hash256)]) -> bool { + if entries.is_empty() { + return true; + } + + let signature_sets = entries + .iter() + .map(|(_, public_key, signature, message)| { + SignatureSet::single_pubkey(signature, Cow::Borrowed(public_key), *message) + }) + .collect::>(); + + verify_signature_sets(signature_sets.iter()) +} + +/// Helper for verifying a single deposit signature. +fn verify_deposit_signature(entry: &(usize, PublicKey, Signature, Hash256)) -> bool { + let (_, public_key, signature, message) = entry; + signature.verify(public_key, *message) +} + +/// Verify `Deposit.pubkey` signed `Deposit.signature` for each deposit in batches of `DEPOSIT_SIGNATURE_BATCH_SIZE`. +/// +/// Returns `true` for valid signatures and `false` for invalid signatures. +/// +/// Note: decompression failures are also considered as invalid signatures. +#[instrument(name = "is_valid_deposit_signature_batch", skip_all, level = "debug")] +pub fn is_valid_deposit_signature_batch( + deposit_data: Vec, + spec: &ChainSpec, +) -> Vec { + let decompressed = deposit_data + .par_iter() + .enumerate() + .map(|(index, deposit)| { + deposit_pubkey_signature_message(deposit, spec) + .map(|(public_key, signature, message)| (index, public_key, signature, message)) + }) + .collect::>(); + + // Initialize with false to ensure signatures that fail decompression above are also + // marked as signature verification failures + let mut results = vec![false; decompressed.len()]; + + let batch_results = decompressed + .par_chunks(DEPOSIT_SIGNATURE_BATCH_SIZE) + .map(|chunk| { + let valid_entries = chunk + .iter() + .filter_map(|entry| entry.as_ref()) + .collect::>(); + + // All signatures in this batch are valid + if verify_deposit_signature_sets(&valid_entries) { + valid_entries + .into_iter() + .map(|entry| (entry.0, true)) + .collect::>() + // There were some invalid signatures in this batch, + // verify individually to detect the invalid signatures. + } else { + valid_entries + .into_iter() + .map(|entry| (entry.0, verify_deposit_signature(entry))) + .collect::>() + } + }) + .collect::>(); + + for (index, is_valid) in batch_results.into_iter().flatten() { + debug_assert!(index < results.len()); + if let Some(res) = results.get_mut(index) { + *res = is_valid; + } + } + + results +} diff --git a/consensus/state_processing/src/per_epoch_processing/tests.rs b/consensus/state_processing/src/per_epoch_processing/tests.rs index 29716866b56..d6c37147017 100644 --- a/consensus/state_processing/src/per_epoch_processing/tests.rs +++ b/consensus/state_processing/src/per_epoch_processing/tests.rs @@ -37,7 +37,8 @@ async fn runs_without_error() { mod release_tests { use super::*; use crate::{ - EpochProcessingError, SlotProcessingError, per_slot_processing::per_slot_processing, + EpochProcessingError, GloasVerificationContext, SlotProcessingError, + per_slot_processing::per_slot_processing, }; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; use std::sync::Arc; @@ -81,8 +82,13 @@ mod release_tests { // Check the state is valid before starting this test. process_epoch(&mut altair_state.clone(), &spec) .expect("state passes intial epoch processing"); - per_slot_processing(&mut altair_state.clone(), None, &spec) - .expect("state passes intial slot processing"); + per_slot_processing( + &mut altair_state.clone(), + None, + GloasVerificationContext::FullVerification, + &spec, + ) + .expect("state passes intial slot processing"); // Modify the spec so altair never happens. spec.altair_fork_epoch = None; @@ -98,7 +104,12 @@ mod release_tests { Err(EpochProcessingError::InconsistentStateFork(expected_err)) ); assert_eq!( - per_slot_processing(&mut altair_state.clone(), None, &spec), + per_slot_processing( + &mut altair_state.clone(), + None, + GloasVerificationContext::FullVerification, + &spec + ), Err(SlotProcessingError::InconsistentStateFork(expected_err)) ); } @@ -141,8 +152,13 @@ mod release_tests { // Check the state is valid before starting this test. process_epoch(&mut base_state.clone(), &spec) .expect("state passes intial epoch processing"); - per_slot_processing(&mut base_state.clone(), None, &spec) - .expect("state passes intial slot processing"); + per_slot_processing( + &mut base_state.clone(), + None, + GloasVerificationContext::FullVerification, + &spec, + ) + .expect("state passes intial slot processing"); // Modify the spec so Altair happens at the first epoch. spec.altair_fork_epoch = Some(Epoch::new(1)); @@ -158,7 +174,12 @@ mod release_tests { Err(EpochProcessingError::InconsistentStateFork(expected_err)) ); assert_eq!( - per_slot_processing(&mut base_state.clone(), None, &spec), + per_slot_processing( + &mut base_state.clone(), + None, + GloasVerificationContext::FullVerification, + &spec + ), Err(SlotProcessingError::InconsistentStateFork(expected_err)) ); } diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index f26ea567a26..75430896269 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -1,3 +1,4 @@ +use crate::upgrade::gloas::GloasVerificationContext; use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, upgrade_to_electra, upgrade_to_fulu, upgrade_to_gloas, @@ -38,6 +39,7 @@ impl From for Error { pub fn per_slot_processing( state: &mut BeaconState, state_root: Option, + gloas_context: GloasVerificationContext<'_>, spec: &ChainSpec, ) -> Result>, Error> { // Verify that the `BeaconState` instantiation matches the fork at `state.slot()`. @@ -100,7 +102,7 @@ pub fn per_slot_processing( // Gloas. if spec.gloas_fork_epoch == Some(state.current_epoch()) { - upgrade_to_gloas(state, spec)?; + upgrade_to_gloas(state, gloas_context, spec)?; } // Additionally build all caches so that all valid states that are advanced always have diff --git a/consensus/state_processing/src/state_advance.rs b/consensus/state_processing/src/state_advance.rs index 11145621553..a72e66ce0cd 100644 --- a/consensus/state_processing/src/state_advance.rs +++ b/consensus/state_processing/src/state_advance.rs @@ -4,6 +4,8 @@ //! These functions are not in the specification, however they're defined here to reduce code //! duplication and protect against some easy-to-make mistakes when performing state advances. +use crate::builder_deposits_cache::OnboardBuildersCache; +use crate::upgrade::GloasVerificationContext; use crate::*; use fixed_bytes::FixedBytesExtended; use tracing::instrument; @@ -40,7 +42,13 @@ pub fn complete_state_advance( // future iterations. let state_root_opt = state_root_opt.take(); - per_slot_processing(state, state_root_opt, spec).map_err(Error::PerSlotProcessing)?; + per_slot_processing( + state, + state_root_opt, + GloasVerificationContext::FullVerification, + spec, + ) + .map_err(Error::PerSlotProcessing)?; } Ok(()) @@ -65,6 +73,7 @@ pub fn partial_state_advance( state: &mut BeaconState, state_root_opt: Option, target_slot: Slot, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), Error> { check_target_slot(state.slot(), target_slot)?; @@ -95,7 +104,13 @@ pub fn partial_state_advance( // with the correct state root. let state_root = initial_state_root.take().unwrap_or_else(Hash256::zero); - per_slot_processing(state, Some(state_root), spec).map_err(Error::PerSlotProcessing)?; + per_slot_processing( + state, + Some(state_root), + GloasVerificationContext::from_cache(builder_onboarding_cache), + spec, + ) + .map_err(Error::PerSlotProcessing)?; } Ok(()) diff --git a/consensus/state_processing/src/upgrade.rs b/consensus/state_processing/src/upgrade.rs index d175c3ae408..5f516e79911 100644 --- a/consensus/state_processing/src/upgrade.rs +++ b/consensus/state_processing/src/upgrade.rs @@ -12,4 +12,4 @@ pub use capella::upgrade_to_capella; pub use deneb::upgrade_to_deneb; pub use electra::upgrade_to_electra; pub use fulu::upgrade_to_fulu; -pub use gloas::upgrade_to_gloas; +pub use gloas::{GloasVerificationContext, upgrade_to_gloas}; diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index c26547e3041..9e089c633a3 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,24 +1,55 @@ -use crate::per_block_processing::process_operations::apply_deposit_for_builder; +use crate::builder_deposits_cache::OnboardBuildersCache; +use crate::per_block_processing::is_valid_deposit_signature; use crate::per_block_processing::process_operations::is_pending_validator; +use bls::PublicKeyBytes; use milhouse::{List, Vector}; use safe_arith::SafeArith; use ssz_types::BitVector; use ssz_types::FixedVector; use std::collections::HashMap; use std::mem; +use tracing::instrument; use tree_hash::TreeHash; use typenum::Unsigned; use types::{ - BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, is_builder_withdrawal_credential, + Address, BeaconState, BeaconStateError as Error, BeaconStateGloas, Builder, + BuilderPendingPayment, ChainSpec, DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequests, + Fork, is_builder_withdrawal_credential, }; +/// Controls how builder onboarding is handled during the Gloas fork `upgrade_to_gloas`. +/// +/// This is also useful for caching builder deposit signatures post gloas so that +/// potentially expensive operations doesn't slow down block processing in the block verification +/// path. +pub enum GloasVerificationContext<'a> { + /// Use the pre-computed builder signature cache. + CachedVerification(&'a OnboardBuildersCache), + /// Verify each builder deposit signature individually. + /// + /// This can be significantly slower if there are many builder deposits + /// that need to be onboarded at the fork boundary. This variant should be used + /// for tests and other non-production paths. + FullVerification, +} + +impl<'a> GloasVerificationContext<'a> { + pub fn from_cache(cache: Option<&'a OnboardBuildersCache>) -> Self { + match cache { + Some(c) => GloasVerificationContext::CachedVerification(c), + None => GloasVerificationContext::FullVerification, + } + } +} + /// Transform a `Fulu` state into a `Gloas` state. +#[instrument(skip_all)] pub fn upgrade_to_gloas( pre_state: &mut BeaconState, + context: GloasVerificationContext<'_>, spec: &ChainSpec, ) -> Result<(), Error> { - let post = upgrade_state_to_gloas(pre_state, spec)?; + let post = upgrade_state_to_gloas(pre_state, context, spec)?; *pre_state = post; @@ -27,6 +58,7 @@ pub fn upgrade_to_gloas( pub fn upgrade_state_to_gloas( pre_state: &mut BeaconState, + context: GloasVerificationContext<'_>, spec: &ChainSpec, ) -> Result, Error> { let epoch = pre_state.current_epoch(); @@ -121,8 +153,15 @@ pub fn upgrade_state_to_gloas( epoch_cache: mem::take(&mut pre.epoch_cache), }); // [New in Gloas:EIP7732] - onboard_builders_from_pending_deposits(&mut post, spec)?; initialize_ptc_window(&mut post, spec)?; + match context { + GloasVerificationContext::CachedVerification(cache) => { + onboard_builders_from_pending_deposits(&mut post, Some(cache), spec)?; + } + GloasVerificationContext::FullVerification => { + onboard_builders_from_pending_deposits(&mut post, None, spec)?; + } + } Ok(post) } @@ -132,6 +171,7 @@ pub fn upgrade_state_to_gloas( /// The window contains: /// - One epoch of empty entries (previous epoch) /// - Computed PTC for the current epoch through `1 + MIN_SEED_LOOKAHEAD` epochs +#[instrument(skip_all)] fn initialize_ptc_window( state: &mut BeaconState, spec: &ChainSpec, @@ -162,64 +202,103 @@ fn initialize_ptc_window( } /// Applies any pending deposit for builders, effectively onboarding builders at the fork. +/// +/// This function is optimised to use the builder onboarding cache which signature verifies +/// and caches all 0x03 `PendingDeposits` before the fork to ensure that the fork upgrade is fast. +#[instrument(skip_all)] fn onboard_builders_from_pending_deposits( state: &mut BeaconState, + builder_onboarding_cache: Option<&OnboardBuildersCache>, spec: &ChainSpec, ) -> Result<(), Error> { - // Clone pending deposits to avoid borrow conflicts when mutating state. let current_pending_deposits = state.pending_deposits()?.clone(); + // At fork time the builders list is empty and all deposits are new registrations. + let mut builder_pubkey_to_index: HashMap = HashMap::new(); + let mut builders_vec: Vec = Vec::with_capacity(current_pending_deposits.len()); let mut pending_deposits = List::empty(); - // TODO(gloas): introduce a global builder pubkey cache, see: - // https://github.com/sigp/lighthouse/issues/8783 - let mut builder_pubkey_to_index = state - .builders()? - .iter() - .enumerate() - .map(|(i, b)| (b.pubkey, i as u64)) - .collect::>(); - for deposit in ¤t_pending_deposits { - // Deposits for existing validators stay in the pending queue. if state.get_validator_index(&deposit.pubkey)?.is_some() { pending_deposits.push(deposit.clone())?; continue; } - if !builder_pubkey_to_index.contains_key(&deposit.pubkey) { - // Deposits without builder withdrawal credentials are for new validators. - if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { - pending_deposits.push(deposit.clone())?; - continue; - } + let builder_index = builder_pubkey_to_index.get(&deposit.pubkey).copied(); - // If there is a valid pending deposit for a new validator with this pubkey, - // keep this deposit in the pending queue to be applied to that validator later. - if is_pending_validator(&pending_deposits, &deposit.pubkey, spec) { - pending_deposits.push(deposit.clone())?; - continue; - } + if builder_index.is_none() + && (!is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) + || is_pending_validator::(&pending_deposits, &deposit.pubkey, spec)) + { + pending_deposits.push(deposit.clone())?; + continue; } - let builder_index = builder_pubkey_to_index.get(&deposit.pubkey).copied(); + // Use the builder onboarding cache to get the signature verification result. + let is_valid_signature = if let Some(cache) = builder_onboarding_cache { + cache.cached_is_valid_signature(deposit) + } else { + None + }; - if let Some(new_builder_index) = apply_deposit_for_builder( - state, - builder_index, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature.clone(), - deposit.slot, - spec, - )? { - builder_pubkey_to_index - .entry(deposit.pubkey) - .or_insert(new_builder_index); + // Note: this is a deviation from the spec. The spec simply calls `state.add_builder_to_registry`. + // `state.add_builder_to_registry` adds the deposits sequentially + // with `milhouse::List::push`. For a high number of deposits, this get significantly slower, + // since its a O(nlogn) operation. + match builder_index { + Some(idx) => { + // Top-up existing builder. + builders_vec + .get_mut(idx as usize) + .ok_or(Error::UnknownBuilder(idx))? + .balance + .safe_add_assign(deposit.amount)?; + } + None => { + // New builder registration — verify signature then add to vec. + let valid = match is_valid_signature { + Some(v) => v, + None => { + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + is_valid_deposit_signature(&deposit_data, spec).is_ok() + } + }; + + if valid { + let next_index = builders_vec.len() as u64; + builder_pubkey_to_index.insert(deposit.pubkey, next_index); + + let version = *deposit + .withdrawal_credentials + .as_slice() + .first() + .ok_or(Error::WithdrawalCredentialMissingVersion)?; + let execution_address = deposit + .withdrawal_credentials + .as_slice() + .get(12..) + .and_then(|bytes| Address::try_from(bytes).ok()) + .ok_or(Error::WithdrawalCredentialMissingAddress)?; + + builders_vec.push(Builder { + pubkey: deposit.pubkey, + version, + execution_address, + balance: deposit.amount, + deposit_epoch: deposit.slot.epoch(E::slots_per_epoch()), + withdrawable_epoch: spec.far_future_epoch, + }); + } + } } } + *state.builders_mut()? = List::new(builders_vec)?; *state.pending_deposits_mut()? = pending_deposits; Ok(()) diff --git a/consensus/types/src/deposit/deposit_data.rs b/consensus/types/src/deposit/deposit_data.rs index bd39643ebd5..f0e07c986d0 100644 --- a/consensus/types/src/deposit/deposit_data.rs +++ b/consensus/types/src/deposit/deposit_data.rs @@ -5,6 +5,7 @@ use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; use crate::{ + DepositRequest, PendingDeposit, core::{ChainSpec, Hash256, SignedRoot}, deposit::DepositMessage, fork::ForkName, @@ -47,6 +48,28 @@ impl DepositData { } } +impl From<&DepositRequest> for DepositData { + fn from(value: &DepositRequest) -> Self { + DepositData { + pubkey: value.pubkey, + withdrawal_credentials: value.withdrawal_credentials, + amount: value.amount, + signature: value.signature.clone(), + } + } +} + +impl From<&PendingDeposit> for DepositData { + fn from(value: &PendingDeposit) -> Self { + DepositData { + pubkey: value.pubkey, + withdrawal_credentials: value.withdrawal_credentials, + amount: value.amount, + signature: value.signature.clone(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/lcli/src/skip_slots.rs b/lcli/src/skip_slots.rs index 88332c1a850..3ccfdf2c68a 100644 --- a/lcli/src/skip_slots.rs +++ b/lcli/src/skip_slots.rs @@ -134,7 +134,7 @@ pub fn run( let start = Instant::now(); if partial { - partial_state_advance(&mut state, Some(state_root), target_slot, spec) + partial_state_advance(&mut state, Some(state_root), target_slot, None, spec) .map_err(|e| format!("Unable to perform partial advance: {:?}", e))?; } else { complete_state_advance(&mut state, Some(state_root), target_slot, spec) diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 69d3975d09b..afaed71b3eb 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -399,6 +399,7 @@ fn do_transition( BlockSignatureStrategy::NoVerification, VerifyBlockRoot::True, &mut ctxt, + None, spec, ) .map_err(|e| format!("State transition failed: {:?}", e))?; diff --git a/testing/ef_tests/src/cases/fork.rs b/testing/ef_tests/src/cases/fork.rs index 54efb9f9cec..b352bb01880 100644 --- a/testing/ef_tests/src/cases/fork.rs +++ b/testing/ef_tests/src/cases/fork.rs @@ -3,8 +3,8 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::upgrade::{ - upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_electra, upgrade_to_fulu, upgrade_to_gloas, + GloasVerificationContext, upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, + upgrade_to_deneb, upgrade_to_electra, upgrade_to_fulu, upgrade_to_gloas, }; use types::BeaconState; @@ -72,7 +72,12 @@ impl Case for ForkTest { ForkName::Deneb => upgrade_to_deneb(&mut result_state, spec).map(|_| result_state), ForkName::Electra => upgrade_to_electra(&mut result_state, spec).map(|_| result_state), ForkName::Fulu => upgrade_to_fulu(&mut result_state, spec).map(|_| result_state), - ForkName::Gloas => upgrade_to_gloas(&mut result_state, spec).map(|_| result_state), + ForkName::Gloas => upgrade_to_gloas( + &mut result_state, + GloasVerificationContext::FullVerification, + spec, + ) + .map(|_| result_state), }; compare_beacon_state_results_without_caches(&mut result, &mut expected) diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index f5c999920dc..7405b9e2b68 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -582,7 +582,7 @@ impl Operation for ParentExecutionPayloadBlock { spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { - process_parent_execution_payload(state, self.block.to_ref(), spec) + process_parent_execution_payload(state, self.block.to_ref(), None, spec) } } @@ -716,7 +716,7 @@ impl Operation for DepositRequest { _extra: &Operations, ) -> Result<(), BlockProcessingError> { if state.fork_name_unchecked().gloas_enabled() { - process_deposit_requests_post_gloas(state, std::slice::from_ref(self), spec) + process_deposit_requests_post_gloas(state, std::slice::from_ref(self), None, spec) } else { process_deposit_requests_pre_gloas(state, std::slice::from_ref(self), spec) } diff --git a/testing/ef_tests/src/cases/sanity_blocks.rs b/testing/ef_tests/src/cases/sanity_blocks.rs index 538783eaa90..28707bbb51c 100644 --- a/testing/ef_tests/src/cases/sanity_blocks.rs +++ b/testing/ef_tests/src/cases/sanity_blocks.rs @@ -4,8 +4,8 @@ use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::{ - BlockProcessingError, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, - per_block_processing, per_slot_processing, + BlockProcessingError, BlockSignatureStrategy, ConsensusContext, GloasVerificationContext, + VerifyBlockRoot, per_block_processing, per_slot_processing, }; use types::{BeaconState, RelativeEpoch, SignedBeaconBlock}; @@ -79,8 +79,20 @@ impl Case for SanityBlocks { .try_for_each(|signed_block| { let block = signed_block.message(); while bulk_state.slot() < block.slot() { - per_slot_processing(&mut bulk_state, None, spec).unwrap(); - per_slot_processing(&mut indiv_state, None, spec).unwrap(); + per_slot_processing( + &mut bulk_state, + None, + GloasVerificationContext::FullVerification, + spec, + ) + .unwrap(); + per_slot_processing( + &mut indiv_state, + None, + GloasVerificationContext::FullVerification, + spec, + ) + .unwrap(); } bulk_state @@ -98,6 +110,7 @@ impl Case for SanityBlocks { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, spec, )?; @@ -108,6 +121,7 @@ impl Case for SanityBlocks { BlockSignatureStrategy::VerifyBulk, VerifyBlockRoot::True, &mut ctxt, + None, spec, )?; diff --git a/testing/ef_tests/src/cases/sanity_slots.rs b/testing/ef_tests/src/cases/sanity_slots.rs index 71c782c78f4..1abe00811aa 100644 --- a/testing/ef_tests/src/cases/sanity_slots.rs +++ b/testing/ef_tests/src/cases/sanity_slots.rs @@ -3,7 +3,7 @@ use crate::bls_setting::BlsSetting; use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; -use state_processing::per_slot_processing; +use state_processing::{GloasVerificationContext, per_slot_processing}; use types::BeaconState; #[derive(Debug, Clone, Default, Deserialize)] @@ -64,7 +64,15 @@ impl Case for SanitySlots { state.build_caches(spec).unwrap(); let mut result = (0..self.slots) - .try_for_each(|_| per_slot_processing(&mut state, None, spec).map(|_| ())) + .try_for_each(|_| { + per_slot_processing( + &mut state, + None, + GloasVerificationContext::FullVerification, + spec, + ) + .map(|_| ()) + }) .map(|_| state); compare_beacon_state_results_without_caches(&mut result, &mut expected) diff --git a/testing/ef_tests/src/cases/transition.rs b/testing/ef_tests/src/cases/transition.rs index 06aa8136506..66598625d8e 100644 --- a/testing/ef_tests/src/cases/transition.rs +++ b/testing/ef_tests/src/cases/transition.rs @@ -133,6 +133,7 @@ impl Case for TransitionTest { BlockSignatureStrategy::VerifyBulk, VerifyBlockRoot::True, &mut ctxt, + None, spec, ) .map_err(|e| format!("Block processing failed: {:?}", e))?; diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index 3b0fe7d8ec2..8264fd17e43 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -71,6 +71,7 @@ impl ExitTest { BlockSignatureStrategy::VerifyIndividual, VerifyBlockRoot::True, &mut ctxt, + None, &test_spec::(), ) }