diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index d345e6f5599..7eaaa3da702 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -82,7 +82,7 @@ impl BeaconChain { BeaconChainError::BlockRewardAttestationError })? } else { - self.compute_beacon_block_attestation_reward_altair_deneb(block, state) + self.compute_beacon_block_attestation_reward_altair_and_later(block, state) .map_err(|e| { error!( error = ?e, @@ -249,7 +249,7 @@ impl BeaconChain { Ok(block_reward) } - fn compute_beacon_block_attestation_reward_altair_deneb< + fn compute_beacon_block_attestation_reward_altair_and_later< Payload: AbstractExecPayload, >( &self, @@ -267,13 +267,20 @@ impl BeaconChain { let mut previous_epoch_participation = state.previous_epoch_participation()?.to_owned_list(); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); + for attestation in block.body().attestations() { let data = attestation.data(); let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); + // [Modified in Deneb:EIP7045] let participation_flag_indices = get_attestation_participation_flag_indices( state, data, + parent_slot, inclusion_delay, &self.spec, )?; 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 e8a2a92edee..f8578c54ba5 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 @@ -86,8 +86,42 @@ impl GossipVerifiedProposerPreferences { ctx.spec, )?; - // Resolve the proposer for `proposal_slot` via the proposer shuffling cache. let proposal_epoch = proposal_slot.epoch(T::EthSpec::slots_per_epoch()); + + let lookahead_epoch = proposal_epoch.saturating_sub(ctx.spec.min_seed_lookahead); + let head_block_root = ctx.canonical_head.cached_head().head_block_root(); + let fork_choice_read = ctx.canonical_head.fork_choice_read_lock(); + + let dependent_block = fork_choice_read + .get_block(&dependent_root) + .ok_or(ProposerPreferencesError::DependentRootUnknown { dependent_root })?; + + // Ensure the dependent root block is before the start of the lookahead epoch. + // Skip this check for gloas at genesis/epoch 1. + if dependent_block.slot >= lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()) + && dependent_block.slot != 0 + { + return Err(ProposerPreferencesError::DependentRootTooRecent { + dependent_root, + block_slot: dependent_block.slot, + epoch_start_slot: lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()), + }); + } + + // Look for at least one child that crosses the epoch boundary directly after `dependent_block`. + let has_qualifying_child = fork_choice_read + .get_children(&dependent_root) + .iter() + .any(|child| child.slot >= lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch())); + + // Head is exempt from this check, it may eventually have a child that crosses the epoch boundary. + if !has_qualifying_child && dependent_root != head_block_root { + return Err(ProposerPreferencesError::InvalidDependentRoot { dependent_root }); + } + + drop(fork_choice_read); + + // Resolve the proposer for `proposal_slot` via the proposer shuffling cache. let proposer = with_proposer_cache( ctx.beacon_proposer_cache, dependent_root, 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 cd05853ae91..53fd94a9271 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -47,6 +47,18 @@ pub enum ProposerPreferencesError { }, /// The signature is invalid. BadSignature, + + /// The block with root `dependent_root` is not before the start of the lookahead epoch. + DependentRootTooRecent { + dependent_root: Hash256, + block_slot: Slot, + epoch_start_slot: Slot, + }, + + /// The block with root `dependent_root` is not a possible dependent block + /// for the given epoch. + InvalidDependentRoot { dependent_root: Hash256 }, + /// Some Beacon Chain Error BeaconChainError(Arc), /// Some Beacon State error 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 9c61d84a705..7d2579aa3fe 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -7,15 +7,16 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::{Mutex, RwLock}; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; +use state_processing::AllCaches; use store::{HotColdDB, MemoryStore, StoreConfig}; use types::{ - Address, BeaconBlock, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, MinimalEthSpec, - ProposerPreferences, SignedBeaconBlock, SignedProposerPreferences, Slot, + Address, BeaconBlock, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, + SignedBeaconBlock, SignedProposerPreferences, Slot, }; use crate::{ beacon_fork_choice_store::BeaconForkChoiceStore, - beacon_proposer_cache::BeaconProposerCache, + beacon_proposer_cache::{BeaconProposerCache, ensure_state_can_determine_proposers_for_epoch}, beacon_snapshot::BeaconSnapshot, canonical_head::CanonicalHead, chain_config::FastConfirmationMode, @@ -61,11 +62,6 @@ impl TestContext { interop_genesis_state::(&keypairs, 0, Hash256::repeat_byte(0x42), None, &spec) .expect("should build genesis state"); - *state.finalized_checkpoint_mut() = Checkpoint { - epoch: Epoch::new(1), - root: Hash256::ZERO, - }; - let genesis_state_root = state .update_tree_hash_cache() .expect("should hash genesis state"); @@ -80,6 +76,9 @@ impl TestContext { let _ = store .init_anchor_info(Hash256::ZERO, Slot::new(0), Slot::new(0), false) .expect("should init anchor info"); + state + .build_all_caches(&spec) + .expect("should build state caches"); store .put_state(&genesis_state_root, &state) .expect("should persist genesis state"); @@ -147,19 +146,36 @@ impl TestContext { fn proposer_at_slot(&self, slot: Slot) -> u64 { let head = self.canonical_head.cached_head(); - let state = &head.snapshot.beacon_state; - let lookahead = state - .proposer_lookahead() - .expect("Gloas state has lookahead"); - let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize; + let mut state = head.snapshot.beacon_state.clone(); + let state_root = head.snapshot.beacon_block.message().state_root(); let epoch = slot.epoch(E::slots_per_epoch()); - let current_epoch = state.slot().epoch(E::slots_per_epoch()); - let index = if epoch == current_epoch.saturating_add(self.spec.min_seed_lookahead) { - E::slots_per_epoch() as usize + slot_in_epoch - } else { - slot_in_epoch - }; - *lookahead.get(index).expect("index in range") + state + .build_all_caches(&self.spec) + .expect("should build state caches"); + ensure_state_can_determine_proposers_for_epoch(&mut state, state_root, epoch, &self.spec) + .expect("should advance state to determine proposers"); + let proposers = state + .get_beacon_proposer_indices(epoch, &self.spec) + .expect("should compute proposer indices"); + let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize; + *proposers.get(slot_in_epoch).expect("slot within epoch") as u64 + } + + /// Insert a block into fork choice by cloning the parent's proto node with a new + /// root and slot. The block has no state in the store, so verification can only + /// proceed as far as the fork-choice-based checks. + fn add_block(&self, parent_root: Hash256, root: Hash256, slot: Slot) { + let mut fork_choice = self.canonical_head.fork_choice_write_lock(); + let mut block = fork_choice + .get_block(&parent_root) + .expect("parent block should be in fork choice"); + block.root = root; + block.parent_root = Some(parent_root); + block.slot = slot; + fork_choice + .proto_array_mut() + .process_block::(block, slot, &self.spec, Duration::ZERO) + .expect("should insert block into fork choice"); } } @@ -244,8 +260,11 @@ fn wrong_proposer_for_slot() { return; } let ctx = TestContext::new(); + // Advance to epoch 1 so the genesis head is a valid dependent root for epoch-2 + // proposals (it is strictly before the start of lookahead epoch 1). + ctx.slot_clock.set_slot(E::slots_per_epoch()); let gossip = ctx.gossip_ctx(); - let slot = Slot::new(1); + let slot = Slot::new(2 * E::slots_per_epoch()); let actual_proposer = ctx.proposer_at_slot(slot); let wrong_validator = if actual_proposer == 0 { 1 } else { 0 }; @@ -264,8 +283,11 @@ fn correct_proposer_bad_signature() { return; } let ctx = TestContext::new(); + // Clock and proposal both in epoch 2: the childless genesis head is only a + // valid dependent root via the head exemption. + ctx.slot_clock.set_slot(2 * E::slots_per_epoch()); let gossip = ctx.gossip_ctx(); - let slot = Slot::new(1); + let slot = Slot::new(2 * E::slots_per_epoch() + 1); let actual_proposer = ctx.proposer_at_slot(slot); let prefs = make_signed_preferences(slot, actual_proposer, ctx.head_block_root); @@ -291,8 +313,9 @@ fn validator_index_out_of_bounds() { return; } let ctx = TestContext::new(); + ctx.slot_clock.set_slot(E::slots_per_epoch()); let gossip = ctx.gossip_ctx(); - let slot = Slot::new(1); + let slot = Slot::new(2 * E::slots_per_epoch()); let prefs = make_signed_preferences(slot, u64::MAX, ctx.head_block_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); @@ -382,10 +405,11 @@ fn preferences_for_next_epoch_slot() { return; } let ctx = TestContext::new(); + // Clock in epoch 1, proposal in epoch 2 (the next epoch). + ctx.slot_clock.set_slot(E::slots_per_epoch()); let gossip = ctx.gossip_ctx(); - // Head is at slot 0 (epoch 0). Pick a slot in epoch 1. - let next_epoch_slot = Slot::new(E::slots_per_epoch() + 1); + let next_epoch_slot = Slot::new(2 * 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, ctx.head_block_root); @@ -397,3 +421,148 @@ fn preferences_for_next_epoch_slot() { result ); } + +/// For proposal epochs at or before the lookahead there is no block strictly before the +/// boundary; the genesis block is exempt so genesis-gloas networks can gossip preferences +/// during the first epochs. +#[test] +fn genesis_dependent_root_valid_for_early_epochs() { + 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 actual_proposer = ctx.proposer_at_slot(slot); + let prefs = make_signed_preferences(slot, actual_proposer, ctx.head_block_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!( + matches!(result, Err(ProposerPreferencesError::BadSignature)), + "expected BadSignature for genesis dependent root, got: {:?}", + result + ); +} + +/// The genesis exemption only admits the block at slot 0: any other block is still too +/// recent for early proposal epochs. +#[test] +fn non_genesis_dependent_root_too_recent_for_early_epochs() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + let gossip = ctx.gossip_ctx(); + + let recent_root = Hash256::repeat_byte(0xab); + ctx.add_block(ctx.head_block_root, recent_root, Slot::new(1)); + + let prefs = make_signed_preferences(Slot::new(2), 0, recent_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::DependentRootTooRecent { .. }) + )); +} + +#[test] +fn dependent_root_too_recent() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(E::slots_per_epoch()); + let gossip = ctx.gossip_ctx(); + + // A block after the start of lookahead epoch 1 cannot be a dependent root for + // epoch-2 proposals. + let recent_root = Hash256::repeat_byte(0xaa); + ctx.add_block( + ctx.head_block_root, + recent_root, + Slot::new(E::slots_per_epoch() + 4), + ); + + let proposal_slot = Slot::new(2 * E::slots_per_epoch() + 1); + let prefs = make_signed_preferences(proposal_slot, 0, recent_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::DependentRootTooRecent { .. }) + )); +} + +/// A known, old-enough block that has no children and is not the head is not a +/// plausible dependent root on any branch. +#[test] +fn dependent_root_childless_non_head() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(E::slots_per_epoch()); + let gossip = ctx.gossip_ctx(); + + let branch_root = Hash256::repeat_byte(0xbb); + ctx.add_block(ctx.head_block_root, branch_root, Slot::new(4)); + + let proposal_slot = Slot::new(2 * E::slots_per_epoch() + 1); + let prefs = make_signed_preferences(proposal_slot, 0, branch_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::InvalidDependentRoot { .. }) + )); +} + +/// A child that is itself before the lookahead boundary does not qualify: the block is +/// not the latest one before the boundary on that branch. +#[test] +fn dependent_root_child_before_boundary() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(E::slots_per_epoch()); + let gossip = ctx.gossip_ctx(); + + let parent_root = Hash256::repeat_byte(0xcc); + let child_root = Hash256::repeat_byte(0xdd); + ctx.add_block(ctx.head_block_root, parent_root, Slot::new(2)); + ctx.add_block(parent_root, child_root, Slot::new(4)); + + let proposal_slot = Slot::new(2 * E::slots_per_epoch() + 1); + let prefs = make_signed_preferences(proposal_slot, 0, parent_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!(matches!( + result, + Err(ProposerPreferencesError::InvalidDependentRoot { .. }) + )); +} + +/// A non-head block with a child crossing the lookahead boundary passes dependent-root +/// validation. Verification still fails later (the fabricated block has no state in the +/// store), but not with a dependent-root error. +#[test] +fn dependent_root_valid_via_boundary_crossing_child() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + let ctx = TestContext::new(); + ctx.slot_clock.set_slot(E::slots_per_epoch()); + let gossip = ctx.gossip_ctx(); + + let parent_root = Hash256::repeat_byte(0xee); + let child_root = Hash256::repeat_byte(0xef); + ctx.add_block(ctx.head_block_root, parent_root, Slot::new(4)); + ctx.add_block(parent_root, child_root, Slot::new(E::slots_per_epoch() + 4)); + + let proposal_slot = Slot::new(2 * E::slots_per_epoch() + 1); + let prefs = make_signed_preferences(proposal_slot, 0, parent_root); + let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); + assert!( + matches!(&result, Err(ProposerPreferencesError::BeaconChainError(_))), + "expected verification to pass dependent-root checks and fail at proposer resolution, got: {:?}", + result + ); +} diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index 294f160b821..8cca7a416ad 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -728,10 +728,16 @@ impl ValidatorMonitor { let data = unaggregated_attestation.data(); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); + // Get the reward indices for the unaggregated attestation or log an error match get_attestation_participation_flag_indices( state, unaggregated_attestation.data(), + parent_slot, inclusion_delay, spec, ) { 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 9701aed5f76..3480cf80653 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4275,13 +4275,15 @@ impl NetworkBeaconProcessor { | ProposerPreferencesError::BeaconChainError(_) | ProposerPreferencesError::BeaconStateError(_) | ProposerPreferencesError::UnableToReadSlot - | ProposerPreferencesError::DependentRootUnknown { .. }, + | ProposerPreferencesError::DependentRootUnknown { .. } + | ProposerPreferencesError::InvalidDependentRoot { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } Err( ProposerPreferencesError::InvalidProposalSlot { .. } - | ProposerPreferencesError::BadSignature, + | ProposerPreferencesError::BadSignature + | ProposerPreferencesError::DependentRootTooRecent { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 7b6985b4c33..60bd2f2df9d 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -81,9 +81,19 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { let att_data = att.attestation_data(); let inclusion_delay = state.slot().as_u64().checked_sub(att_data.slot.as_u64())?; - let att_participation_flags = - get_attestation_participation_flag_indices(state, &att_data, inclusion_delay, spec) - .ok()?; + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); + + let att_participation_flags = get_attestation_participation_flag_indices( + state, + &att_data, + parent_slot, + inclusion_delay, + spec, + ) + .ok()?; let fresh_validators_rewards = att .indexed diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index ea9c7b1b32d..5556baca3a5 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1595,6 +1595,18 @@ where } } + /// Returns the blocks of the direct children of the block with `block_root`. + /// + /// Returns an empty vec if the block is unknown, has no children, or is not a + /// descendant of the finalized root. + pub fn get_children(&self, block_root: &Hash256) -> Vec { + if self.is_finalized_checkpoint_or_descendant(*block_root) { + self.proto_array.get_children(block_root) + } else { + Vec::new() + } + } + /// Returns whether the execution payload for a block has been received. /// /// Returns `false` for unknown blocks and pre-Gloas nodes. diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 7b2e1c150df..8c4f07c5aa2 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -951,12 +951,34 @@ impl ProtoArrayForkChoice { pub fn get_block(&self, block_root: &Hash256) -> Option { let block = self.get_proto_node(block_root)?; + Some(self.proto_node_to_block(block)) + } + + /// Returns the blocks of the direct children of the block with `block_root`. + /// + /// Returns an empty vec if the block is unknown or has no children. + pub fn get_children(&self, block_root: &Hash256) -> Vec { + self.proto_array + .indices + .get(block_root) + .and_then(|&index| self.proto_array.children.get(index)) + .map(|children| { + children + .iter() + .filter_map(|&child_index| self.proto_array.nodes.get(child_index)) + .map(|child| self.proto_node_to_block(child)) + .collect() + }) + .unwrap_or_default() + } + + fn proto_node_to_block(&self, block: &ProtoNode) -> Block { let parent_root = block .parent() .and_then(|i| self.proto_array.nodes.get(i)) .map(|parent| parent.root()); - Some(Block { + Block { slot: block.slot(), root: block.root(), parent_root, @@ -975,7 +997,7 @@ impl ProtoArrayForkChoice { execution_payload_block_hash: block.execution_payload_block_hash().ok(), proposer_index: block.proposer_index().ok(), payload_received: block.payload_received().unwrap_or(false), - }) + } } /// Called by the proposer to decide whether to build on the full or empty diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index 8f1f000f401..ac27579d058 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -2,7 +2,7 @@ use integer_sqrt::IntegerSquareRoot; use safe_arith::SafeArith; use smallvec::SmallVec; use types::{ - AttestationData, BeaconState, BeaconStateError as Error, ChainSpec, EthSpec, + AttestationData, BeaconState, BeaconStateError as Error, ChainSpec, EthSpec, Slot, consts::altair::{ NUM_FLAG_INDICES, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, @@ -21,6 +21,7 @@ use types::{ pub fn get_attestation_participation_flag_indices( state: &BeaconState, data: &AttestationData, + parent_slot: Option, inclusion_delay: u64, spec: &ChainSpec, ) -> Result, Error> { @@ -37,6 +38,8 @@ pub fn get_attestation_participation_flag_indices( // [New in Gloas:EIP7732] let payload_matches = if state.fork_name_unchecked().gloas_enabled() { + let parent_slot = parent_slot.ok_or(Error::MissingParentSlot)?; + if state.is_attestation_same_slot(data)? { // For same-slot attestations, data.index must be 0 if data.index != 0 { @@ -45,8 +48,7 @@ pub fn get_attestation_participation_flag_indices( true } else { // For non same-slot attestations, check execution payload availability - let slot_index = data - .slot + let slot_index = parent_slot .as_usize() .safe_rem(E::slots_per_historical_root())?; let payload_index = state diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 22ec9a9ed47..2895ab756a9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -185,6 +185,7 @@ pub fn per_block_processing>( state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; + let mut parent_slot = None; // The call to the `process_execution_payload` must happen before the call to the // `process_randao` as the former depends on the `randao_mix` computed with the reveal of the // previous block. @@ -193,6 +194,7 @@ pub fn per_block_processing>( if state.fork_name_unchecked().gloas_enabled() { withdrawals::gloas::process_withdrawals::(state, spec)?; let signed_bid = block.body().signed_execution_payload_bid()?; + parent_slot = Some(state.latest_execution_payload_bid()?.slot); process_execution_payload_bid(state, signed_bid, verify_signatures, spec)?; } else { if state.fork_name_unchecked().capella_enabled() { @@ -208,7 +210,14 @@ pub fn per_block_processing>( process_randao(state, block, verify_randao, ctxt, spec)?; process_eth1_data(state, block.body().eth1_data())?; - process_operations(state, block.body(), verify_signatures, ctxt, spec)?; + process_operations( + state, + block.body(), + verify_signatures, + parent_slot, + ctxt, + spec, + )?; if let Ok(sync_aggregate) = block.body().sync_aggregate() { process_sync_aggregate( 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 b2d2d49a83b..1b8af3d09e7 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -17,6 +17,7 @@ pub fn process_operations>( state: &mut BeaconState, block_body: BeaconBlockBodyRef, verify_signatures: VerifySignatures, + parent_slot: Option, ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { @@ -40,7 +41,14 @@ pub fn process_operations>( ctxt, spec, )?; - process_attestations(state, block_body, verify_signatures, ctxt, spec)?; + process_attestations( + state, + block_body, + verify_signatures, + parent_slot, + ctxt, + spec, + )?; process_deposits(state, &block_body.deposits().to_cow_slice(), spec)?; process_exits( state, @@ -247,7 +255,7 @@ pub mod altair_deneb { let data = attestation.data(); let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + get_attestation_participation_flag_indices(state, data, None, inclusion_delay, spec)?; // Update epoch participation flags. let mut proposer_reward_numerator = 0; @@ -304,6 +312,7 @@ pub mod gloas { state: &mut BeaconState, attestations: I, verify_signatures: VerifySignatures, + parent_slot: Option, ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> @@ -311,7 +320,15 @@ pub mod gloas { I: Iterator>, { attestations.enumerate().try_for_each(|(i, attestation)| { - process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + process_attestation( + state, + attestation, + i, + verify_signatures, + parent_slot, + ctxt, + spec, + ) }) } @@ -319,8 +336,9 @@ pub mod gloas { state: &mut BeaconState, attestation: AttestationRef, att_index: usize, - ctxt: &mut ConsensusContext, verify_signatures: VerifySignatures, + parent_slot: Option, + ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { let proposer_index = ctxt.get_proposer_index(state, spec)?; @@ -339,8 +357,13 @@ pub mod gloas { // Matching roots, participation flag indices let data = attestation.data(); let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); - let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + let participation_flag_indices = get_attestation_participation_flag_indices( + state, + data, + parent_slot, + inclusion_delay, + spec, + )?; // [New in EIP-7732] let current_epoch_target = data.target.epoch == state.current_epoch(); @@ -540,6 +563,7 @@ pub fn process_attestations>( state: &mut BeaconState, block_body: BeaconBlockBodyRef, verify_signatures: VerifySignatures, + parent_slot: Option, ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { @@ -548,6 +572,7 @@ pub fn process_attestations>( state, block_body.attestations(), verify_signatures, + parent_slot, ctxt, spec, )?; diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index 63f33548fb2..4b21c108963 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -441,10 +441,15 @@ async fn invalid_attestation_no_committee_for_index() { .into_data_mut() .index += 1; let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); @@ -491,10 +496,15 @@ async fn invalid_attestation_wrong_justified_checkpoint() { .source = new_justified_checkpoint; let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); @@ -545,10 +555,15 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { } let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); @@ -585,10 +600,15 @@ async fn invalid_attestation_bad_signature() { .into_signature_mut() = AggregateSignature::empty(); let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); @@ -629,10 +649,15 @@ async fn invalid_attestation_included_too_early() { .slot = new_attesation_slot; let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); @@ -680,10 +705,15 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { .epoch += Epoch::new(1); let mut ctxt = ConsensusContext::new(state.slot()); + let parent_slot = state + .latest_execution_payload_bid() + .ok() + .map(|bid| bid.slot); let result = process_operations::process_attestations( &mut state, head_block.body(), VerifySignatures::True, + parent_slot, &mut ctxt, &spec, ); diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 66bdb1af250..887fc100022 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -25,7 +25,7 @@ pub fn translate_participation( // Translate attestation inclusion info to flag indices. let participation_flag_indices = - get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; + get_attestation_participation_flag_indices(state, data, None, inclusion_delay, spec)?; // Apply flags to all attesting validators. let committee = state.get_beacon_committee(data.slot, data.index)?; diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index b70b1c5b21e..84e3b303a16 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -237,6 +237,7 @@ pub enum BeaconStateError { InvalidIndicesCount, InvalidBuilderPendingPaymentsIndex(usize), InvalidExecutionPayloadAvailabilityIndex(usize), + MissingParentSlot, /// Merkle proofs against the `BeaconState` and `BeaconBlockBody` use progressive-container /// generalized indices from Gloas (EIP-7688) onwards, which are not implemented yet. ProgressiveMerkleProofNotSupported, diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 350d52dcf2a..c72e4a2256d 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -131,12 +131,14 @@ impl Operation for Attestation { initialize_progressive_balances_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); if state.fork_name_unchecked().gloas_enabled() { + let parent_slot = Some(state.latest_execution_payload_bid()?.slot); gloas::process_attestation( state, self.to_ref(), 0, - &mut ctxt, VerifySignatures::True, + parent_slot, + &mut ctxt, spec, ) } else if state.fork_name_unchecked().altair_enabled() {