From 474d0cc36f1432932df97d2f6d655e5718ba983a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 01:53:47 -0700 Subject: [PATCH 01/14] Set gloas attestation data index to 0 or 1 depending on payload --- beacon_node/beacon_chain/src/beacon_chain.rs | 25 ++++++++++++++++++- .../beacon_chain/src/early_attester_cache.rs | 1 + beacon_node/beacon_chain/src/test_utils.rs | 2 ++ consensus/fork_choice/src/fork_choice.rs | 5 ++++ .../types/src/attestation/attestation.rs | 10 +++++++- .../src/attestation_service.rs | 1 + 6 files changed, 42 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e226c707a4e..6d316e32714 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1941,6 +1941,7 @@ impl BeaconChain { let beacon_block_root; let beacon_state_root; let target; + let is_attesting_to_head_slot; let current_epoch_attesting_info: Option<(Checkpoint, usize)>; let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS); let head_span = debug_span!("attestation_production_head_scrape").entered(); @@ -1977,7 +1978,8 @@ impl BeaconChain { }); } - if request_slot >= head_state.slot() { + is_attesting_to_head_slot = request_slot >= head_state.slot(); + if is_attesting_to_head_slot { // When attesting to the head slot or later, always use the head of the chain. beacon_block_root = head.beacon_block_root; beacon_state_root = head.beacon_state_root(); @@ -2080,6 +2082,26 @@ impl BeaconChain { ) }; + // TODO(gloas): add integration test: verify that + // `produce_unaggregated_attestation` sets index=1 when payload received for a prior + // slot, index=0 for same-slot, and index=0 when payload not received. + // + // For gloas the attestation data index indicates payload presence: + // `payload_present=false` for same-slot attestations or when payload not received. + // `payload_present=true` when attesting to a prior slot whose payload has been received. + let payload_present = if self + .spec + .fork_name_at_slot::(request_slot) + .gloas_enabled() + && !is_attesting_to_head_slot + { + self.canonical_head + .fork_choice_read_lock() + .is_payload_received(&beacon_block_root) + } else { + false + }; + Ok(Attestation::::empty_for_signing( request_index, committee_len, @@ -2087,6 +2109,7 @@ impl BeaconChain { beacon_block_root, justified_checkpoint, target, + payload_present, &self.spec, )?) } diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 752e4d1a967..5b55c8e2dab 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -204,6 +204,7 @@ impl EarlyAttesterCache { item.beacon_block_root, item.source, item.target, + false, spec, ) .map_err(Error::AttestationError)?; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 13dcf221086..baff1424bab 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1400,6 +1400,7 @@ where epoch, root: target_root, }, + false, &self.spec, )?; @@ -1509,6 +1510,7 @@ where epoch, root: target_root, }, + false, &self.spec, )?) } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 92fd4c1faf3..6c1a79f820b 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1484,6 +1484,11 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Returns `true` if the block's execution payload envelope has been received. + pub fn is_payload_received(&self, block_root: &Hash256) -> bool { + self.proto_array.is_payload_received(block_root) + } + /// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. pub fn get_block(&self, block_root: &Hash256) -> Option { if self.is_finalized_checkpoint_or_descendant(*block_root) { diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 693b5889f53..5131be44248 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -109,6 +109,7 @@ impl Attestation { beacon_block_root: Hash256, source: Checkpoint, target: Checkpoint, + payload_present: bool, spec: &ChainSpec, ) -> Result { if spec.fork_name_at_slot::(slot).electra_enabled() { @@ -116,12 +117,19 @@ impl Attestation { committee_bits .set(committee_index as usize, true) .map_err(|_| Error::InvalidCommitteeIndex)?; + // Gloas attestation data index now indicates payload presence. + // Pre-gloas index is always 0. + let index = if spec.fork_name_at_slot::(slot).gloas_enabled() && payload_present { + 1u64 + } else { + 0u64 + }; Ok(Attestation::Electra(AttestationElectra { aggregation_bits: BitList::with_capacity(committee_length) .map_err(|_| Error::InvalidCommitteeLength)?, data: AttestationData { slot, - index: 0u64, + index, beacon_block_root, source, target, diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index fe808efd88e..de55422f490 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -546,6 +546,7 @@ impl AttestationService attestation, From 2c8df63f00de50970b7c63fefde56cf71bf80165 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 02:01:50 -0700 Subject: [PATCH 02/14] allow too many args --- consensus/types/src/attestation/attestation.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 5131be44248..187003ec8d5 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -101,7 +101,9 @@ impl Hash for Attestation { } impl Attestation { + /// Produces an attestation with empty signature. + #[allow(clippy::too_many_arguments)] pub fn empty_for_signing( committee_index: u64, committee_length: usize, From 2b1f0435213e7bf89f4a131e6fd04077aedd12e4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 6 Apr 2026 02:15:02 -0700 Subject: [PATCH 03/14] FMT --- consensus/types/src/attestation/attestation.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/consensus/types/src/attestation/attestation.rs b/consensus/types/src/attestation/attestation.rs index 187003ec8d5..28059efee6e 100644 --- a/consensus/types/src/attestation/attestation.rs +++ b/consensus/types/src/attestation/attestation.rs @@ -101,7 +101,6 @@ impl Hash for Attestation { } impl Attestation { - /// Produces an attestation with empty signature. #[allow(clippy::too_many_arguments)] pub fn empty_for_signing( From b36219d83d7a628be224d01673f1b903abf2281e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:08:55 +0900 Subject: [PATCH 04/14] Add canonicity check and tests --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 +- .../tests/attestation_production.rs | 115 +++++++++++++++++- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 96ed5206bdd..232da163881 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2092,10 +2092,6 @@ impl BeaconChain { ) }; - // TODO(gloas): add integration test: verify that - // `produce_unaggregated_attestation` sets index=1 when payload received for a prior - // slot, index=0 for same-slot, and index=0 when payload not received. - // // For gloas the attestation data index indicates payload presence: // `payload_present=false` for same-slot attestations or when payload not received. // `payload_present=true` when attesting to a prior slot whose payload has been received. @@ -2106,8 +2102,7 @@ impl BeaconChain { && !is_attesting_to_head_slot { self.canonical_head - .fork_choice_read_lock() - .is_payload_received(&beacon_block_root) + .block_has_canonical_payload(&beacon_block_root, &self.spec)? } else { false }; diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index a3ab959d122..c021f6d6c2d 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,7 +8,7 @@ use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, ChainSpec, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; @@ -313,3 +313,116 @@ async fn early_attester_cache_old_request() { .unwrap(); assert_eq!(attested_block.slot(), attest_slot); } + +/// Verify that `produce_unaggregated_attestation` sets `data.index = 1` (payload_present) +/// when a gloas validator attests to a prior slot whose block+envelope have been received. +/// +/// Setup: build a chain at gloas genesis, produce a block with envelope at slot N, +/// then advance the clock to slot N+1 without producing a block (skipped slot). +/// Attesting at slot N+1 should target the block at slot N with payload_present = true. +#[tokio::test] +async fn gloas_attestation_index_payload_present() { + let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + // Build a few blocks so the chain is established (slots 1..=3). + harness.advance_slot(); + harness + .extend_chain( + 3, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let head = chain.head_snapshot(); + assert_eq!(head.beacon_block.slot(), Slot::new(3)); + + // Advance clock to slot 4 without producing a block (skipped slot). + harness.advance_slot(); + let attest_slot = chain.slot().unwrap(); + assert_eq!(attest_slot, Slot::new(4)); + + // Attest at slot 4 — this should target the block at slot 3 whose payload was received. + let attestation = chain + .produce_unaggregated_attestation(attest_slot, 0) + .expect("should produce attestation"); + + assert_eq!(attestation.data().slot, attest_slot); + assert_eq!( + attestation.data().index, + 1, + "gloas attestation to prior slot with payload should have index=1 (payload_present)" + ); +} + +/// Verify that `produce_unaggregated_attestation` sets `data.index = 0` (payload NOT present) +/// when a gloas validator attests to a prior slot whose block was imported but whose +/// payload envelope was never received. +/// +/// Setup: build a chain at gloas genesis through slot 2, then at slot 3 import only the +/// beacon block (no envelope), advance to slot 4 (skipped), and attest. +#[tokio::test] +async fn gloas_attestation_index_payload_absent() { + let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + // Build slots 1..=2 normally (with envelopes). + harness.advance_slot(); + harness + .extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(2)); + + // Slot 3: produce and import the beacon block but do NOT process the envelope. + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _envelope, _new_state) = + harness.make_block_with_envelope(state, Slot::new(3)).await; + + let block_root = block_contents.0.canonical_root(); + harness + .process_block(Slot::new(3), block_root, block_contents) + .await + .expect("block should import without envelope"); + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3)); + + // Advance clock to slot 4 without producing a block (skipped slot). + harness.advance_slot(); + let attest_slot = chain.slot().unwrap(); + assert_eq!(attest_slot, Slot::new(4)); + + // Attest at slot 4 — targets slot 3 whose payload was NOT received. + let attestation = chain + .produce_unaggregated_attestation(attest_slot, 0) + .expect("should produce attestation"); + + assert_eq!(attestation.data().slot, attest_slot); + assert_eq!( + attestation.data().index, + 0, + "gloas attestation to prior slot without payload should have index=0 (payload_absent)" + ); +} From 1229abf5cf2bd76a27b2b49c5298c0742b27c32f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:22:24 +0900 Subject: [PATCH 05/14] Fix test --- .../tests/attestation_production.rs | 18 ++++++++++++------ consensus/fork_choice/src/fork_choice.rs | 5 ----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index c021f6d6c2d..185a7b4c903 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -2,13 +2,15 @@ use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::custody_context::NodeCustodyType; -use beacon_chain::test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy}; +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, test_spec, +}; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, ChainSpec, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; @@ -322,10 +324,12 @@ async fn early_attester_cache_old_request() { /// Attesting at slot N+1 should target the block at slot N with payload_present = true. #[tokio::test] async fn gloas_attestation_index_payload_present() { - let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() @@ -372,10 +376,12 @@ async fn gloas_attestation_index_payload_present() { /// beacon block (no envelope), advance to slot 4 (skipped), and attest. #[tokio::test] async fn gloas_attestation_index_payload_absent() { - let spec = Arc::new(ForkName::Gloas.make_genesis_spec(ChainSpec::mainnet())); + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) + .default_spec() .keypairs(KEYPAIRS[..].to_vec()) .fresh_ephemeral_store() .mock_execution_layer() diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 781777cab76..f9d779fd24f 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1498,11 +1498,6 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } - /// Returns `true` if the block's execution payload envelope has been received. - pub fn is_payload_received(&self, block_root: &Hash256) -> bool { - self.proto_array.is_payload_received(block_root) - } - /// Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root. pub fn get_block(&self, block_root: &Hash256) -> Option { if self.is_finalized_checkpoint_or_descendant(*block_root) { From 7e16aadde521bf0fec26ee9c8215916cdd2fdbfa Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 14:32:49 +0900 Subject: [PATCH 06/14] Lint --- beacon_node/beacon_chain/tests/attestation_production.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 185a7b4c903..7c584deff3e 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -3,14 +3,14 @@ use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, fork_name_from_env, }; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; -use types::{Attestation, EthSpec, ForkName, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; pub const VALIDATOR_COUNT: usize = 32; From dacb8aeffeee89f07ad3758968def056aeadec86 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:30:34 +0900 Subject: [PATCH 07/14] Small fix --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 232da163881..d44a8d7ecc4 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1956,7 +1956,7 @@ impl BeaconChain { let beacon_block_root; let beacon_state_root; let target; - let is_attesting_to_head_slot; + let is_same_slot_attestation; let current_epoch_attesting_info: Option<(Checkpoint, usize)>; let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS); let head_span = debug_span!("attestation_production_head_scrape").entered(); @@ -1993,16 +1993,20 @@ impl BeaconChain { }); } - is_attesting_to_head_slot = request_slot >= head_state.slot(); + let is_attesting_to_head_slot = request_slot >= head_state.slot(); + if is_attesting_to_head_slot { // When attesting to the head slot or later, always use the head of the chain. beacon_block_root = head.beacon_block_root; beacon_state_root = head.beacon_state_root(); + is_same_slot_attestation = request_slot == head.beacon_block.slot(); } else { // Permit attesting to slots *prior* to the current head. This is desirable when // the VC and BN are out-of-sync due to time issues or overloading. beacon_block_root = *head_state.get_block_root(request_slot)?; beacon_state_root = *head_state.get_state_root(request_slot)?; + // TODO(gloas) if request_slot is a skipped slot, this is not a same slot attestation. + is_same_slot_attestation = true; }; let target_slot = request_epoch.start_slot(T::EthSpec::slots_per_epoch()); @@ -2099,7 +2103,7 @@ impl BeaconChain { .spec .fork_name_at_slot::(request_slot) .gloas_enabled() - && !is_attesting_to_head_slot + && !is_same_slot_attestation { self.canonical_head .block_has_canonical_payload(&beacon_block_root, &self.spec)? From 2e561a2a9f9f0f933617b075c45d8d0265f87e3a Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:44:33 +0900 Subject: [PATCH 08/14] Handle historic attestations correctly --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d44a8d7ecc4..390cbf5cb80 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2005,8 +2005,14 @@ impl BeaconChain { // the VC and BN are out-of-sync due to time issues or overloading. beacon_block_root = *head_state.get_block_root(request_slot)?; beacon_state_root = *head_state.get_state_root(request_slot)?; - // TODO(gloas) if request_slot is a skipped slot, this is not a same slot attestation. - is_same_slot_attestation = true; + + // Fetch the previous block root. If the previous block root equals + // the block root being attested to, the `request_slot` is a skipped slot + // and this is not a same slot attestation. + let prior_slot_root = head_state + .get_block_root(request_slot.saturating_sub(1u64)) + .ok(); + is_same_slot_attestation = prior_slot_root != Some(&beacon_block_root); }; let target_slot = request_epoch.start_slot(T::EthSpec::slots_per_epoch()); From 5beddd17cef654150f265c9cc149b2d37ceb1cf6 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 15:52:11 +0900 Subject: [PATCH 09/14] Fix tests --- .../beacon_chain/tests/attestation_production.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 7c584deff3e..224d40290fa 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -208,7 +208,15 @@ async fn produces_attestations() { &AggregateSignature::infinity(), "bad signature" ); - assert_eq!(data.index, index, "bad index"); + if harness + .spec + .fork_name_at_slot::(data.slot) + .gloas_enabled() + { + assert!(data.index <= 1, "invalid index"); + } else { + assert_eq!(data.index, index, "bad index"); + } assert_eq!(data.slot, slot, "bad slot"); assert_eq!(data.beacon_block_root, block_root, "bad block root"); assert_eq!( From a7fc388a9af40df1beeab5166bad8e5dc3fc6f78 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:15:54 +0900 Subject: [PATCH 10/14] Fix early attester cache --- beacon_node/beacon_chain/src/early_attester_cache.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 5b55c8e2dab..a433c4fc75f 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -197,6 +197,15 @@ impl EarlyAttesterCache { item.committee_lengths .get_committee_length::(request_slot, request_index, spec)?; + let is_same_slot_attestation = request_slot == item.block.slot(); + let payload_present = if spec.fork_name_at_slot::(request_slot).gloas_enabled() + && !is_same_slot_attestation + { + item.proto_block.payload_status == PayloadStatus::Full + } else { + false + }; + let attestation = Attestation::empty_for_signing( request_index, committee_len, @@ -204,7 +213,7 @@ impl EarlyAttesterCache { item.beacon_block_root, item.source, item.target, - false, + payload_present, spec, ) .map_err(Error::AttestationError)?; From 9ef3799c3610befc50239fb367bbdd2e9a99dae8 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:34:17 +0900 Subject: [PATCH 11/14] Add same slot attestation logic to early attester cache --- .../beacon_chain/src/early_attester_cache.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index a433c4fc75f..8b8c8edc9bb 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -165,6 +165,9 @@ impl EarlyAttesterCache { /// - There is a cache `item` present. /// - If `request_slot` is in the same epoch as `item.epoch`. /// - If `request_index` does not exceed `item.committee_count`. + /// + /// Post gloas an additional condition must be met: + /// - If `request_slot` is the same slot as `item.block.slot` (i.e. a same slot attestation) #[instrument(skip_all, fields(%request_slot, %request_index), level = "debug")] pub fn try_attest( &self, @@ -198,13 +201,10 @@ impl EarlyAttesterCache { .get_committee_length::(request_slot, request_index, spec)?; let is_same_slot_attestation = request_slot == item.block.slot(); - let payload_present = if spec.fork_name_at_slot::(request_slot).gloas_enabled() - && !is_same_slot_attestation - { - item.proto_block.payload_status == PayloadStatus::Full - } else { - false - }; + if spec.fork_name_at_slot::(request_slot).gloas_enabled() && !is_same_slot_attestation { + return Ok(None); + } + let payload_present = false; let attestation = Attestation::empty_for_signing( request_index, From 3f8621fd521efa0f6de517f44e25aa73a2c18474 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sat, 25 Apr 2026 16:48:56 +0900 Subject: [PATCH 12/14] Disable early attester cache test for non same slot attestaitons psot gloas --- .../tests/attestation_production.rs | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 224d40290fa..1b87fc041a2 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -236,27 +236,35 @@ async fn produces_attestations() { .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); let available_block = range_sync_block.into_available_block(); - let early_attestation = { - let proto_block = chain - .canonical_head - .fork_choice_read_lock() - .get_block(&block_root) - .unwrap(); - chain - .early_attester_cache - .add_head_block(block_root, &available_block, proto_block, &state) - .unwrap(); - chain - .early_attester_cache - .try_attest(slot, index, &chain.spec) - .unwrap() - .unwrap() - }; - - assert_eq!( - attestation, early_attestation, - "early attester cache inconsistent" - ); + // For Gloas non-same-slot attestations, the early attester cache returns None. + let is_same_slot_attestation = slot == block_slot; + let is_gloas = harness + .spec + .fork_name_at_slot::(slot) + .gloas_enabled(); + if !is_gloas || is_same_slot_attestation { + let early_attestation = { + let proto_block = chain + .canonical_head + .fork_choice_read_lock() + .get_block(&block_root) + .unwrap(); + chain + .early_attester_cache + .add_head_block(block_root, &available_block, proto_block, &state) + .unwrap(); + chain + .early_attester_cache + .try_attest(slot, index, &chain.spec) + .unwrap() + .unwrap() + }; + + assert_eq!( + attestation, early_attestation, + "early attester cache inconsistent" + ); + } } } } From 22788d1092858d4c74d87e9bf0fd10a9dae4ae62 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:02:28 +0200 Subject: [PATCH 13/14] reduce diff --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b6b47563c20..b556e6d849a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1993,9 +1993,7 @@ impl BeaconChain { }); } - let is_attesting_to_head_slot = request_slot >= head_state.slot(); - - if is_attesting_to_head_slot { + if request_slot >= head_state.slot() { // When attesting to the head slot or later, always use the head of the chain. beacon_block_root = head.beacon_block_root; beacon_state_root = head.beacon_state_root(); From 2aa727262c40008770f59e40fcfce64a5dfdd4b0 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:02:33 +0200 Subject: [PATCH 14/14] document edge case --- beacon_node/beacon_chain/src/early_attester_cache.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 8b8c8edc9bb..e3a83f9374a 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -167,7 +167,10 @@ impl EarlyAttesterCache { /// - If `request_index` does not exceed `item.committee_count`. /// /// Post gloas an additional condition must be met: - /// - If `request_slot` is the same slot as `item.block.slot` (i.e. a same slot attestation) + /// - `request_slot` is the same slot as `item.block.slot` (i.e. a same slot attestation). + /// + /// Non-same-slot Gloas attestations need `data.index` set from the canonical payload + /// status, which the cache doesn't track. Returning `None` falls through to fork choice. #[instrument(skip_all, fields(%request_slot, %request_index), level = "debug")] pub fn try_attest( &self,