From 4760df5e6be7ebcfbe5c1432a6b06cc01e324bbd Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 13:23:59 +0300 Subject: [PATCH 1/4] use correct state --- .../gossip_verified_proposer_preferences.rs | 91 ++++++++++++------- .../proposer_preferences_verification/mod.rs | 6 +- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs index 586721d8c1..8997c4d239 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs @@ -1,27 +1,24 @@ use std::sync::Arc; use crate::{ - BeaconChain, BeaconChainTypes, CanonicalHead, + BeaconChain, BeaconChainTypes, BeaconStore, CanonicalHead, proposer_preferences_verification::{ ProposerPreferencesError, proposer_preference_cache::GossipVerifiedProposerPreferenceCache, }, }; use slot_clock::SlotClock; use state_processing::signature_sets::{get_pubkey_from_state, proposer_preferences_signature_set}; +use state_processing::state_advance::partial_state_advance; use tracing::debug; -use types::{ - BeaconState, ChainSpec, EthSpec, ProposerPreferences, SignedProposerPreferences, Slot, -}; +use types::{ChainSpec, EthSpec, ProposerPreferences, SignedProposerPreferences, Slot}; /// Verify that proposer preferences are consistent with the current chain state pub(crate) fn verify_preferences_consistency( preferences: &ProposerPreferences, current_slot: Slot, - head_state: &BeaconState, spec: &ChainSpec, ) -> Result<(), ProposerPreferencesError> { let proposal_slot = preferences.proposal_slot; - let validator_index = preferences.validator_index; let current_epoch = current_slot.epoch(E::slots_per_epoch()); let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch()); @@ -38,13 +35,6 @@ pub(crate) fn verify_preferences_consistency( }); } - if !head_state.is_valid_proposal_slot(preferences, spec)? { - return Err(ProposerPreferencesError::InvalidProposalSlot { - validator_index, - proposal_slot, - }); - } - Ok(()) } @@ -53,6 +43,7 @@ pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub gossip_verified_proposer_preferences_cache: &'a GossipVerifiedProposerPreferenceCache, pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, + pub store: &'a BeaconStore, } /// A wrapper around `SignedProposerPreferences` that has been verified for gossip propagation. @@ -74,7 +65,6 @@ impl GossipVerifiedProposerPreferences { .slot_clock .now() .ok_or(ProposerPreferencesError::UnableToReadSlot)?; - let head_state = &cached_head.snapshot.beacon_state; if ctx .gossip_verified_proposer_preferences_cache @@ -86,17 +76,61 @@ impl GossipVerifiedProposerPreferences { }); } - verify_preferences_consistency( + verify_preferences_consistency::( &signed_preferences.message, current_slot, - head_state, ctx.spec, )?; - // Verify signature + // Get the block at dependent_root from fork choice to verify canonicity and get state_root + let fork_choice = ctx.canonical_head.fork_choice_read_lock(); + let dependent_block = fork_choice + .get_block(&dependent_root) + .ok_or(ProposerPreferencesError::DependentRootUnknown { dependent_root })?; + let head_root = cached_head.head_block_root(); + if !fork_choice.is_descendant(dependent_root, head_root) { + return Err(ProposerPreferencesError::DependentRootNotCanonical { dependent_root }); + } + let dependent_state_root = dependent_block.state_root; + let dependent_block_slot = dependent_block.slot; + drop(fork_choice); + + // Fetch the state at the dependent_root block. + // Per spec, we need the checkpoint state at epoch (proposal_epoch - MIN_SEED_LOOKAHEAD). + // The dependent_root is the block root at the proposer shuffling decision slot. + let (state_root, mut dependent_state) = ctx + .store + .get_advanced_hot_state(dependent_root, dependent_block_slot, dependent_state_root) + .map_err(crate::BeaconChainError::DBError)? + .ok_or(ProposerPreferencesError::DependentRootUnknown { dependent_root })?; + + // We need to advance the state to `target_epoch` so epoch transition runs and + // `process_proposer_lookahead` populates the lookahead for the proposal epoch. + let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); + let target_epoch = proposal_epoch.saturating_sub(ctx.spec.min_seed_lookahead); + let target_slot = target_epoch.start_slot(T::EthSpec::slots_per_epoch()); + + if dependent_state.current_epoch() < target_epoch { + partial_state_advance( + &mut dependent_state, + Some(state_root), + target_slot, + ctx.spec, + ) + .map_err(crate::BeaconChainError::StateAdvanceError)?; + } + + if !dependent_state.is_valid_proposal_slot(&signed_preferences.message, ctx.spec)? { + return Err(ProposerPreferencesError::InvalidProposalSlot { + validator_index, + proposal_slot, + }); + } + + // Verify signature using the dependent state (which has the validator pubkeys) proposer_preferences_signature_set( - head_state, - |i| get_pubkey_from_state(head_state, i), + &dependent_state, + |i| get_pubkey_from_state(&dependent_state, i), &signed_preferences, ctx.spec, ) @@ -127,6 +161,7 @@ impl BeaconChain { .gossip_verified_proposer_preferences_cache, slot_clock: &self.slot_clock, spec: &self.spec, + store: &self.store, } } @@ -162,10 +197,7 @@ impl BeaconChain { #[cfg(test)] mod tests { - use types::{ - Address, BeaconState, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, - Slot, - }; + use types::{Address, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, Slot}; use super::verify_preferences_consistency; use crate::proposer_preferences_verification::ProposerPreferencesError; @@ -183,11 +215,6 @@ mod tests { } } - fn state() -> BeaconState { - let spec = spec(); - BeaconState::new(0, <_>::default(), &spec) - } - fn spec() -> ChainSpec { test_spec::() } @@ -200,7 +227,7 @@ mod tests { let current_slot = Slot::new(2 * E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); + let result = verify_preferences_consistency::(&prefs, current_slot, &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -215,7 +242,7 @@ mod tests { let current_slot = Slot::new(E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3 * E::slots_per_epoch() + 1), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); + let result = verify_preferences_consistency::(&prefs, current_slot, &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -230,7 +257,7 @@ mod tests { let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(9), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); + let result = verify_preferences_consistency::(&prefs, current_slot, &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) @@ -245,7 +272,7 @@ mod tests { let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(10), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); + let result = verify_preferences_consistency::(&prefs, current_slot, &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs index 6c79e56733..8c85f024c8 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -12,7 +12,7 @@ use std::sync::Arc; -use types::{BeaconStateError, Epoch, Slot}; +use types::{BeaconStateError, Epoch, Hash256, Slot}; use crate::BeaconChainError; @@ -38,6 +38,10 @@ pub enum ProposerPreferencesError { }, /// The slot clock cannot be read. UnableToReadSlot, + /// The block with root `dependent_root` has not been seen. + DependentRootUnknown { dependent_root: Hash256 }, + /// The block with root `dependent_root` is not canonical. + DependentRootNotCanonical { dependent_root: Hash256 }, /// A valid message from this validator for this slot has already been seen. AlreadySeen { validator_index: u64, From 5a34031b8483c121a969469df3419480b41ab386 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 14:08:31 +0300 Subject: [PATCH 2/4] fix tests --- .../gossip_verified_proposer_preferences.rs | 38 +++++++------------ .../tests.rs | 5 ++- .../gossip_methods.rs | 4 +- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs index 8997c4d239..99ea0f29ba 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs @@ -92,45 +92,35 @@ impl GossipVerifiedProposerPreferences { return Err(ProposerPreferencesError::DependentRootNotCanonical { dependent_root }); } let dependent_state_root = dependent_block.state_root; - let dependent_block_slot = dependent_block.slot; drop(fork_choice); - // Fetch the state at the dependent_root block. - // Per spec, we need the checkpoint state at epoch (proposal_epoch - MIN_SEED_LOOKAHEAD). - // The dependent_root is the block root at the proposer shuffling decision slot. - let (state_root, mut dependent_state) = ctx - .store - .get_advanced_hot_state(dependent_root, dependent_block_slot, dependent_state_root) - .map_err(crate::BeaconChainError::DBError)? - .ok_or(ProposerPreferencesError::DependentRootUnknown { dependent_root })?; - - // We need to advance the state to `target_epoch` so epoch transition runs and - // `process_proposer_lookahead` populates the lookahead for the proposal epoch. + // We need a state at `target_epoch` so we have the correct proposer lookahead. let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); let target_epoch = proposal_epoch.saturating_sub(ctx.spec.min_seed_lookahead); let target_slot = target_epoch.start_slot(T::EthSpec::slots_per_epoch()); - if dependent_state.current_epoch() < target_epoch { - partial_state_advance( - &mut dependent_state, - Some(state_root), - target_slot, - ctx.spec, - ) - .map_err(crate::BeaconChainError::StateAdvanceError)?; + let (state_root, mut state) = ctx + .store + .get_advanced_hot_state(dependent_root, target_slot, dependent_state_root) + .map_err(crate::BeaconChainError::DBError)? + .ok_or(ProposerPreferencesError::DependentRootUnknown { dependent_root })?; + + if state.current_epoch() < target_epoch { + partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec) + .map_err(crate::BeaconChainError::StateAdvanceError)?; } - if !dependent_state.is_valid_proposal_slot(&signed_preferences.message, ctx.spec)? { + if !state.is_valid_proposal_slot(&signed_preferences.message, ctx.spec)? { return Err(ProposerPreferencesError::InvalidProposalSlot { validator_index, proposal_slot, }); } - // Verify signature using the dependent state (which has the validator pubkeys) + // Verify signature proposer_preferences_signature_set( - &dependent_state, - |i| get_pubkey_from_state(&dependent_state, i), + &state, + |i| get_pubkey_from_state(&state, i), &signed_preferences, ctx.spec, ) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index 53c1c4ded3..f8fcdcc830 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -6,7 +6,7 @@ use fork_choice::ForkChoice; use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -use store::{HotColdDB, StoreConfig}; +use store::{HotColdDB, MemoryStore, StoreConfig}; use types::{ Address, BeaconBlock, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, SignedBeaconBlock, SignedProposerPreferences, Slot, @@ -36,6 +36,7 @@ struct TestContext { preferences_cache: GossipVerifiedProposerPreferenceCache, slot_clock: TestingSlotClock, spec: ChainSpec, + store: Arc>, } impl TestContext { @@ -91,6 +92,7 @@ impl TestContext { preferences_cache: GossipVerifiedProposerPreferenceCache::default(), slot_clock, spec, + store, } } @@ -100,6 +102,7 @@ impl TestContext { gossip_verified_proposer_preferences_cache: &self.preferences_cache, slot_clock: &self.slot_clock, spec: &self.spec, + store: &self.store, } } diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 3e8845f017..488c9915fe 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4113,7 +4113,9 @@ impl NetworkBeaconProcessor { | ProposerPreferencesError::ProposalSlotAlreadyPassed { .. } | ProposerPreferencesError::BeaconChainError(_) | ProposerPreferencesError::BeaconStateError(_) - | ProposerPreferencesError::UnableToReadSlot, + | ProposerPreferencesError::UnableToReadSlot + | ProposerPreferencesError::DependentRootUnknown { .. } + | ProposerPreferencesError::DependentRootNotCanonical { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } From 97c125505ac3c23cd0d3542e79ef64d16371428e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 18:03:41 +0300 Subject: [PATCH 3/4] fix tests --- .../tests.rs | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index f8fcdcc830..c04685e40e 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -37,6 +37,7 @@ struct TestContext { slot_clock: TestingSlotClock, spec: ChainSpec, store: Arc>, + head_block_root: Hash256, } impl TestContext { @@ -59,12 +60,17 @@ impl TestContext { }; let mut genesis_block = BeaconBlock::empty(&spec); - *genesis_block.state_root_mut() = state + let genesis_state_root = state .update_tree_hash_cache() .expect("should hash genesis state"); + *genesis_block.state_root_mut() = genesis_state_root; let signed_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty()); let block_root = signed_block.canonical_root(); + store + .put_state(&genesis_state_root, &state) + .expect("should persist genesis state"); + let snapshot = BeaconSnapshot::new( Arc::new(signed_block.clone()), None, @@ -93,6 +99,7 @@ impl TestContext { slot_clock, spec, store, + head_block_root: block_root, } } @@ -127,10 +134,11 @@ impl TestContext { fn make_signed_preferences( proposal_slot: Slot, validator_index: u64, + dependent_root: Hash256, ) -> Arc { Arc::new(SignedProposerPreferences { message: ProposerPreferences { - dependent_root: Hash256::ZERO, + dependent_root, proposal_slot, validator_index, fee_recipient: Address::ZERO, @@ -150,11 +158,11 @@ fn already_seen_validator() { let slot = Slot::new(1); let verified = GossipVerifiedProposerPreferences { - signed_preferences: make_signed_preferences(slot, 42), + signed_preferences: make_signed_preferences(slot, 42, Hash256::ZERO), }; ctx.preferences_cache.insert_seen_validator(&verified); - let prefs = make_signed_preferences(slot, 42); + let prefs = make_signed_preferences(slot, 42, Hash256::ZERO); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -174,7 +182,7 @@ fn invalid_epoch_too_far_ahead() { let gossip = ctx.gossip_ctx(); let far_slot = Slot::new(3 * E::slots_per_epoch()); - let prefs = make_signed_preferences(far_slot, 0); + let prefs = make_signed_preferences(far_slot, 0, Hash256::ZERO); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -190,7 +198,7 @@ fn proposal_slot_already_passed() { let ctx = TestContext::new(); let gossip = ctx.gossip_ctx(); - let prefs = make_signed_preferences(Slot::new(0), 0); + let prefs = make_signed_preferences(Slot::new(0), 0, Hash256::ZERO); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -210,7 +218,7 @@ fn wrong_proposer_for_slot() { let actual_proposer = ctx.proposer_at_slot(slot); let wrong_validator = if actual_proposer == 0 { 1 } else { 0 }; - let prefs = make_signed_preferences(slot, wrong_validator); + let prefs = make_signed_preferences(slot, wrong_validator, ctx.head_block_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -228,7 +236,7 @@ fn correct_proposer_bad_signature() { let slot = Slot::new(1); let actual_proposer = ctx.proposer_at_slot(slot); - let prefs = make_signed_preferences(slot, actual_proposer); + let prefs = make_signed_preferences(slot, actual_proposer, ctx.head_block_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -236,7 +244,7 @@ fn correct_proposer_bad_signature() { )); assert!( !ctx.preferences_cache - .get_seen_validator(&slot, Hash256::ZERO, actual_proposer) + .get_seen_validator(&slot, ctx.head_block_root, actual_proposer) ); assert!(ctx.preferences_cache.get_preferences(&slot).is_none()); } @@ -250,7 +258,7 @@ fn validator_index_out_of_bounds() { let gossip = ctx.gossip_ctx(); let slot = Slot::new(1); - let prefs = make_signed_preferences(slot, u64::MAX); + let prefs = make_signed_preferences(slot, u64::MAX, ctx.head_block_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, @@ -293,6 +301,43 @@ fn same_validator_different_dependent_root_not_deduplicated() { ); } +#[test] +fn dependent_root_unknown() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + let slot = Slot::new(1); + + let unknown_root = Hash256::repeat_byte(0xff); + let prefs = make_signed_preferences(slot, 0, unknown_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::DependentRootUnknown { .. }) + )); +} + +#[test] +fn invalid_epoch_too_old() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + // Advance the clock so that epoch 0 slots are too old. + ctx.slot_clock.set_slot(3 * E::slots_per_epoch()); + let gossip = ctx.gossip_ctx(); + + let old_slot = Slot::new(1); + let prefs = make_signed_preferences(old_slot, 0, Hash256::ZERO); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) + )); +} + // TODO(gloas) add successful proposer preferences check once we have proposer preferences signing logic #[test] @@ -307,7 +352,7 @@ fn preferences_for_next_epoch_slot() { let next_epoch_slot = Slot::new(E::slots_per_epoch() + 1); let actual_proposer = ctx.proposer_at_slot(next_epoch_slot); - let prefs = make_signed_preferences(next_epoch_slot, actual_proposer); + let prefs = make_signed_preferences(next_epoch_slot, actual_proposer, ctx.head_block_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); // Should pass consistency checks but fail on signature (empty sig). assert!( From d35c1cb363ac7c5ce22007e0a58d5fbc6ae8e759 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 19:31:48 +0300 Subject: [PATCH 4/4] fix tests --- .../src/proposer_preferences_verification/tests.rs | 14 ++++++++++++-- beacon_node/http_api/tests/tests.rs | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index c04685e40e..e547594f31 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -59,14 +59,24 @@ impl TestContext { root: Hash256::ZERO, }; - let mut genesis_block = BeaconBlock::empty(&spec); let genesis_state_root = state .update_tree_hash_cache() .expect("should hash genesis state"); + + let mut anchor_header = state.latest_block_header().clone(); + if anchor_header.state_root.is_zero() { + anchor_header.state_root = genesis_state_root; + } + let block_root = anchor_header.canonical_root(); + + // Build a signed block with the correct state root for the snapshot. + let mut genesis_block = BeaconBlock::empty(&spec); *genesis_block.state_root_mut() = genesis_state_root; let signed_block = SignedBeaconBlock::from_block(genesis_block, Signature::empty()); - let block_root = signed_block.canonical_root(); + let _ = store + .init_anchor_info(Hash256::ZERO, Slot::new(0), Slot::new(0), false) + .expect("should init anchor info"); store .put_state(&genesis_state_root, &state) .expect("should persist genesis state"); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 3da0841a4e..5783a011fd 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2925,7 +2925,7 @@ impl ApiTester { .expect("slot index should be in lookahead") as usize; let preferences = ProposerPreferences { - dependent_root: Hash256::ZERO, + dependent_root: head.beacon_block_root, proposal_slot, validator_index: validator_index as u64, fee_recipient: Address::repeat_byte(0xaa),