Gloas set AttestationData.index (#9100)

For gloas `attestation.data.index` should be set to 1 if we are attesting to a block whose slot is not the attestation duty slot and slot payload_status is `FULL`


  


Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
Eitan Seri-Levi
2026-04-26 15:40:22 +02:00
committed by GitHub
parent 6323cd3827
commit 276c4d5ff3
6 changed files with 209 additions and 23 deletions

View File

@@ -1956,6 +1956,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let beacon_block_root; let beacon_block_root;
let beacon_state_root; let beacon_state_root;
let target; let target;
let is_same_slot_attestation;
let current_epoch_attesting_info: Option<(Checkpoint, usize)>; let current_epoch_attesting_info: Option<(Checkpoint, usize)>;
let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS); let head_timer = metrics::start_timer(&metrics::ATTESTATION_PRODUCTION_HEAD_SCRAPE_SECONDS);
let head_span = debug_span!("attestation_production_head_scrape").entered(); let head_span = debug_span!("attestation_production_head_scrape").entered();
@@ -1996,11 +1997,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// When attesting to the head slot or later, always use the head of the chain. // When attesting to the head slot or later, always use the head of the chain.
beacon_block_root = head.beacon_block_root; beacon_block_root = head.beacon_block_root;
beacon_state_root = head.beacon_state_root(); beacon_state_root = head.beacon_state_root();
is_same_slot_attestation = request_slot == head.beacon_block.slot();
} else { } else {
// Permit attesting to slots *prior* to the current head. This is desirable when // 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. // 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_block_root = *head_state.get_block_root(request_slot)?;
beacon_state_root = *head_state.get_state_root(request_slot)?; beacon_state_root = *head_state.get_state_root(request_slot)?;
// 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()); let target_slot = request_epoch.start_slot(T::EthSpec::slots_per_epoch());
@@ -2090,6 +2100,21 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
) )
}; };
// 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::<T::EthSpec>(request_slot)
.gloas_enabled()
&& !is_same_slot_attestation
{
self.canonical_head
.block_has_canonical_payload(&beacon_block_root, &self.spec)?
} else {
false
};
Ok(Attestation::<T::EthSpec>::empty_for_signing( Ok(Attestation::<T::EthSpec>::empty_for_signing(
request_index, request_index,
committee_len, committee_len,
@@ -2097,6 +2122,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
beacon_block_root, beacon_block_root,
justified_checkpoint, justified_checkpoint,
target, target,
payload_present,
&self.spec, &self.spec,
)?) )?)
} }

View File

@@ -165,6 +165,12 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
/// - There is a cache `item` present. /// - There is a cache `item` present.
/// - If `request_slot` is in the same epoch as `item.epoch`. /// - If `request_slot` is in the same epoch as `item.epoch`.
/// - If `request_index` does not exceed `item.committee_count`. /// - If `request_index` does not exceed `item.committee_count`.
///
/// Post gloas an additional condition must be met:
/// - `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")] #[instrument(skip_all, fields(%request_slot, %request_index), level = "debug")]
pub fn try_attest( pub fn try_attest(
&self, &self,
@@ -197,6 +203,12 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
item.committee_lengths item.committee_lengths
.get_committee_length::<E>(request_slot, request_index, spec)?; .get_committee_length::<E>(request_slot, request_index, spec)?;
let is_same_slot_attestation = request_slot == item.block.slot();
if spec.fork_name_at_slot::<E>(request_slot).gloas_enabled() && !is_same_slot_attestation {
return Ok(None);
}
let payload_present = false;
let attestation = Attestation::empty_for_signing( let attestation = Attestation::empty_for_signing(
request_index, request_index,
committee_len, committee_len,
@@ -204,6 +216,7 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
item.beacon_block_root, item.beacon_block_root,
item.source, item.source,
item.target, item.target,
payload_present,
spec, spec,
) )
.map_err(Error::AttestationError)?; .map_err(Error::AttestationError)?;

View File

@@ -1451,6 +1451,7 @@ where
epoch, epoch,
root: target_root, root: target_root,
}, },
false,
&self.spec, &self.spec,
)?; )?;
@@ -1560,6 +1561,7 @@ where
epoch, epoch,
root: target_root, root: target_root,
}, },
false,
&self.spec, &self.spec,
)?) )?)
} }

View File

@@ -2,7 +2,9 @@
use beacon_chain::attestation_simulator::produce_unaggregated_attestation; use beacon_chain::attestation_simulator::produce_unaggregated_attestation;
use beacon_chain::custody_context::NodeCustodyType; 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,
};
use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS;
use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics};
use bls::{AggregateSignature, Keypair}; use bls::{AggregateSignature, Keypair};
@@ -206,7 +208,15 @@ async fn produces_attestations() {
&AggregateSignature::infinity(), &AggregateSignature::infinity(),
"bad signature" "bad signature"
); );
assert_eq!(data.index, index, "bad index"); if harness
.spec
.fork_name_at_slot::<MainnetEthSpec>(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.slot, slot, "bad slot");
assert_eq!(data.beacon_block_root, block_root, "bad block root"); assert_eq!(data.beacon_block_root, block_root, "bad block root");
assert_eq!( assert_eq!(
@@ -226,27 +236,35 @@ async fn produces_attestations() {
.build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone()));
let available_block = range_sync_block.into_available_block(); let available_block = range_sync_block.into_available_block();
let early_attestation = { // For Gloas non-same-slot attestations, the early attester cache returns None.
let proto_block = chain let is_same_slot_attestation = slot == block_slot;
.canonical_head let is_gloas = harness
.fork_choice_read_lock() .spec
.get_block(&block_root) .fork_name_at_slot::<MainnetEthSpec>(slot)
.unwrap(); .gloas_enabled();
chain if !is_gloas || is_same_slot_attestation {
.early_attester_cache let early_attestation = {
.add_head_block(block_root, &available_block, proto_block, &state) let proto_block = chain
.unwrap(); .canonical_head
chain .fork_choice_read_lock()
.early_attester_cache .get_block(&block_root)
.try_attest(slot, index, &chain.spec) .unwrap();
.unwrap() chain
.unwrap() .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!( assert_eq!(
attestation, early_attestation, attestation, early_attestation,
"early attester cache inconsistent" "early attester cache inconsistent"
); );
}
} }
} }
} }
@@ -313,3 +331,120 @@ async fn early_attester_cache_old_request() {
.unwrap(); .unwrap();
assert_eq!(attested_block.slot(), attest_slot); 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() {
if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) {
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.default_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() {
if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) {
return;
}
let harness = BeaconChainHarness::builder(MainnetEthSpec)
.default_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)"
);
}

View File

@@ -102,6 +102,7 @@ impl<E: EthSpec> Hash for Attestation<E> {
impl<E: EthSpec> Attestation<E> { impl<E: EthSpec> Attestation<E> {
/// Produces an attestation with empty signature. /// Produces an attestation with empty signature.
#[allow(clippy::too_many_arguments)]
pub fn empty_for_signing( pub fn empty_for_signing(
committee_index: u64, committee_index: u64,
committee_length: usize, committee_length: usize,
@@ -109,6 +110,7 @@ impl<E: EthSpec> Attestation<E> {
beacon_block_root: Hash256, beacon_block_root: Hash256,
source: Checkpoint, source: Checkpoint,
target: Checkpoint, target: Checkpoint,
payload_present: bool,
spec: &ChainSpec, spec: &ChainSpec,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
if spec.fork_name_at_slot::<E>(slot).electra_enabled() { if spec.fork_name_at_slot::<E>(slot).electra_enabled() {
@@ -116,12 +118,19 @@ impl<E: EthSpec> Attestation<E> {
committee_bits committee_bits
.set(committee_index as usize, true) .set(committee_index as usize, true)
.map_err(|_| Error::InvalidCommitteeIndex)?; .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::<E>(slot).gloas_enabled() && payload_present {
1u64
} else {
0u64
};
Ok(Attestation::Electra(AttestationElectra { Ok(Attestation::Electra(AttestationElectra {
aggregation_bits: BitList::with_capacity(committee_length) aggregation_bits: BitList::with_capacity(committee_length)
.map_err(|_| Error::InvalidCommitteeLength)?, .map_err(|_| Error::InvalidCommitteeLength)?,
data: AttestationData { data: AttestationData {
slot, slot,
index: 0u64, index,
beacon_block_root, beacon_block_root,
source, source,
target, target,

View File

@@ -546,6 +546,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
attestation_data.beacon_block_root, attestation_data.beacon_block_root,
attestation_data.source, attestation_data.source,
attestation_data.target, attestation_data.target,
attestation_data.index != 0,
&self.chain_spec, &self.chain_spec,
) { ) {
Ok(attestation) => attestation, Ok(attestation) => attestation,