From fb6d6f6117e604add55d3318b96ead1c7d584f35 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 6 Aug 2026 00:18:39 -0700 Subject: [PATCH 1/7] Skip EF tests below the top of a PR stack --- .github/workflows/test-suite.yml | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index be0d0d57085..481583f474d 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -52,6 +52,30 @@ jobs: done echo "skip_ci=$SKIP_CI" >> $GITHUB_OUTPUT + check-stack-position: + runs-on: ubuntu-latest + name: Check stacked pull request position + outputs: + skip_ef_tests: ${{ steps.set-output.outputs.SKIP_EF_TESTS }} + steps: + - name: check stack position + id: set-output + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + SKIP_EF_TESTS="false" + if [ "${{ github.event_name }}" = "pull_request" ]; then + STACK=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" --jq '.stack // empty' || true) + if [ -n "$STACK" ]; then + POSITION=$(echo "$STACK" | jq -r '.position') + SIZE=$(echo "$STACK" | jq -r '.size') + if [ "$POSITION" -lt "$SIZE" ]; then + SKIP_EF_TESTS="true" + fi + fi + fi + echo "SKIP_EF_TESTS=$SKIP_EF_TESTS" >> $GITHUB_OUTPUT + lockbud: name: lockbud runs-on: ubuntu-latest @@ -278,8 +302,10 @@ jobs: run: make run-state-transition-tests ef-tests-ubuntu: name: ef-tests-ubuntu - needs: [check-labels] - if: needs.check-labels.outputs.skip_ci != 'true' + needs: [check-labels, check-stack-position] + # Skip EF tests below the top of a stack. Stack CI tests each layer's + # combined prefix, so the top layer covers the full stack. + if: needs.check-labels.outputs.skip_ci != 'true' && needs.check-stack-position.outputs.skip_ef_tests != 'true' runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -497,6 +523,7 @@ jobs: runs-on: ubuntu-latest needs: [ 'check-labels', + 'check-stack-position', 'target-branch-check', 'forbidden-files-check', 'release-tests-ubuntu', From 7a078da1d2802ee0176be666983a3106f6d2ee5c Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 3 Aug 2026 04:50:58 -0700 Subject: [PATCH 2/7] Add dependent root check to proposer preferences validation --- .../gossip_verified_proposer_preferences.rs | 45 +++- .../proposer_preferences_verification/mod.rs | 12 + .../tests.rs | 223 ++++++++++++++++-- .../gossip_methods.rs | 6 +- 4 files changed, 258 insertions(+), 28 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 e8a2a92edee..fcfa52685e7 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,51 @@ 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::DependentRootToRecent { + dependent_root, + block_slot: dependent_block.slot, + epoch_start_slot: lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()), + }); + } + + let proto_array = fork_choice_read.proto_array().core_proto_array(); + + // Look for at least one child that crosses the epoch boundary directly after `dependent_block`. + let has_qualifying_child = proto_array + .indices + .get(&dependent_root) + .and_then(|&index| proto_array.children.get(index)) + .is_some_and(|children| { + children.iter().any(|&child_index| { + proto_array.nodes.get(child_index).is_some_and(|node| { + node.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..ed329b2f10c 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. + DependentRootToRecent { + 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..2a989ab67dc 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,152 @@ 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::DependentRootToRecent { .. }) + )); +} + +#[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::DependentRootToRecent { .. }) + )); +} + +/// 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::DependentRootToRecent { .. } + | ProposerPreferencesError::InvalidDependentRoot { .. }) + ), + "expected dependent root to be accepted, got: {:?}", + result + ); +} 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..638911219e4 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::DependentRootToRecent { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( From e148ea4c1076f27a31d8a84dfed8aab4dfb242b1 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Thu, 6 Aug 2026 15:24:47 -0700 Subject: [PATCH 3/7] Revert "Skip EF tests below the top of a PR stack" This reverts commit 8071796fa1601a89b8072b09f77035b89bc03ff7. --- .github/workflows/test-suite.yml | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 481583f474d..be0d0d57085 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -52,30 +52,6 @@ jobs: done echo "skip_ci=$SKIP_CI" >> $GITHUB_OUTPUT - check-stack-position: - runs-on: ubuntu-latest - name: Check stacked pull request position - outputs: - skip_ef_tests: ${{ steps.set-output.outputs.SKIP_EF_TESTS }} - steps: - - name: check stack position - id: set-output - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - SKIP_EF_TESTS="false" - if [ "${{ github.event_name }}" = "pull_request" ]; then - STACK=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" --jq '.stack // empty' || true) - if [ -n "$STACK" ]; then - POSITION=$(echo "$STACK" | jq -r '.position') - SIZE=$(echo "$STACK" | jq -r '.size') - if [ "$POSITION" -lt "$SIZE" ]; then - SKIP_EF_TESTS="true" - fi - fi - fi - echo "SKIP_EF_TESTS=$SKIP_EF_TESTS" >> $GITHUB_OUTPUT - lockbud: name: lockbud runs-on: ubuntu-latest @@ -302,10 +278,8 @@ jobs: run: make run-state-transition-tests ef-tests-ubuntu: name: ef-tests-ubuntu - needs: [check-labels, check-stack-position] - # Skip EF tests below the top of a stack. Stack CI tests each layer's - # combined prefix, so the top layer covers the full stack. - if: needs.check-labels.outputs.skip_ci != 'true' && needs.check-stack-position.outputs.skip_ef_tests != 'true' + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' runs-on: ${{ github.repository == 'sigp/lighthouse' && 'warp-ubuntu-latest-x64-8x;snapshot.key=lighthouse-ubuntu-latest-v1' || 'ubuntu-latest' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -523,7 +497,6 @@ jobs: runs-on: ubuntu-latest needs: [ 'check-labels', - 'check-stack-position', 'target-branch-check', 'forbidden-files-check', 'release-tests-ubuntu', From 6a17b1222d9f91aa3f4d7b33b8829198ba028a22 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 10 Aug 2026 00:43:41 -0700 Subject: [PATCH 4/7] Fix DependentRootTooRecent typo --- .../gossip_verified_proposer_preferences.rs | 2 +- .../src/proposer_preferences_verification/mod.rs | 2 +- .../src/proposer_preferences_verification/tests.rs | 6 +++--- .../network/src/network_beacon_processor/gossip_methods.rs | 2 +- 4 files changed, 6 insertions(+), 6 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 fcfa52685e7..7bff01fdf1b 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 @@ -101,7 +101,7 @@ impl GossipVerifiedProposerPreferences { if dependent_block.slot >= lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()) && dependent_block.slot != 0 { - return Err(ProposerPreferencesError::DependentRootToRecent { + return Err(ProposerPreferencesError::DependentRootTooRecent { dependent_root, block_slot: dependent_block.slot, epoch_start_slot: lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()), 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 ed329b2f10c..53fd94a9271 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -49,7 +49,7 @@ pub enum ProposerPreferencesError { BadSignature, /// The block with root `dependent_root` is not before the start of the lookahead epoch. - DependentRootToRecent { + DependentRootTooRecent { dependent_root: Hash256, block_slot: Slot, epoch_start_slot: Slot, 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 2a989ab67dc..c9e2b809054 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -461,7 +461,7 @@ fn non_genesis_dependent_root_too_recent_for_early_epochs() { let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, - Err(ProposerPreferencesError::DependentRootToRecent { .. }) + Err(ProposerPreferencesError::DependentRootTooRecent { .. }) )); } @@ -488,7 +488,7 @@ fn dependent_root_too_recent() { let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!(matches!( result, - Err(ProposerPreferencesError::DependentRootToRecent { .. }) + Err(ProposerPreferencesError::DependentRootTooRecent { .. }) )); } @@ -563,7 +563,7 @@ fn dependent_root_valid_via_boundary_crossing_child() { assert!( !matches!( &result, - Err(ProposerPreferencesError::DependentRootToRecent { .. } + Err(ProposerPreferencesError::DependentRootTooRecent { .. } | ProposerPreferencesError::InvalidDependentRoot { .. }) ), "expected dependent root to be accepted, got: {:?}", 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 638911219e4..3480cf80653 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4283,7 +4283,7 @@ impl NetworkBeaconProcessor { Err( ProposerPreferencesError::InvalidProposalSlot { .. } | ProposerPreferencesError::BadSignature - | ProposerPreferencesError::DependentRootToRecent { .. }, + | ProposerPreferencesError::DependentRootTooRecent { .. }, ) => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); self.gossip_penalize_peer( From 3103633b85976c0cd73f0a4f5b6b68e57867402a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 10 Aug 2026 00:52:36 -0700 Subject: [PATCH 5/7] Assert exact error variant in boundary-crossing dependent root test --- .../src/proposer_preferences_verification/tests.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 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 c9e2b809054..7d2579aa3fe 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -561,12 +561,8 @@ fn dependent_root_valid_via_boundary_crossing_child() { let prefs = make_signed_preferences(proposal_slot, 0, parent_root); let result = GossipVerifiedProposerPreferences::new(prefs, &gossip); assert!( - !matches!( - &result, - Err(ProposerPreferencesError::DependentRootTooRecent { .. } - | ProposerPreferencesError::InvalidDependentRoot { .. }) - ), - "expected dependent root to be accepted, got: {:?}", + matches!(&result, Err(ProposerPreferencesError::BeaconChainError(_))), + "expected verification to pass dependent-root checks and fail at proposer resolution, got: {:?}", result ); } From da61d0b5d7d425198596dd2a924522e86e995775 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 10 Aug 2026 01:07:08 -0700 Subject: [PATCH 6/7] Add get_children to fork choice for dependent root validation --- .../gossip_verified_proposer_preferences.rs | 17 +++--------- consensus/fork_choice/src/fork_choice.rs | 12 +++++++++ .../src/proto_array_fork_choice.rs | 26 +++++++++++++++++-- 3 files changed, 40 insertions(+), 15 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 7bff01fdf1b..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 @@ -108,20 +108,11 @@ impl GossipVerifiedProposerPreferences { }); } - let proto_array = fork_choice_read.proto_array().core_proto_array(); - // Look for at least one child that crosses the epoch boundary directly after `dependent_block`. - let has_qualifying_child = proto_array - .indices - .get(&dependent_root) - .and_then(|&index| proto_array.children.get(index)) - .is_some_and(|children| { - children.iter().any(|&child_index| { - proto_array.nodes.get(child_index).is_some_and(|node| { - node.slot() >= lookahead_epoch.start_slot(T::EthSpec::slots_per_epoch()) - }) - }) - }); + 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 { 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 From d44c186cba26dd00b437ff8318ca8901d04ffaba Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 4 Aug 2026 00:37:20 -0700 Subject: [PATCH 7/7] Fix payload availability lookup using the parent block's slot --- .../beacon_chain/src/beacon_block_reward.rs | 11 +++++- .../beacon_chain/src/validator_monitor.rs | 6 +++ beacon_node/operation_pool/src/attestation.rs | 16 ++++++-- .../common/get_attestation_participation.rs | 8 ++-- .../src/per_block_processing.rs | 11 +++++- .../process_operations.rs | 37 ++++++++++++++++--- .../src/per_block_processing/tests.rs | 30 +++++++++++++++ .../state_processing/src/upgrade/altair.rs | 2 +- consensus/types/src/state/beacon_state.rs | 1 + testing/ef_tests/src/cases/operations.rs | 4 +- 10 files changed, 109 insertions(+), 17 deletions(-) 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/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/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/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() {