Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions beacon_node/beacon_chain/src/beacon_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1956,6 +1956,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let beacon_block_root;
let beacon_state_root;
let target;
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();
Expand Down Expand Up @@ -1996,11 +1997,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// 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)?;

// 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());
Expand Down Expand Up @@ -2090,13 +2100,29 @@ 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(
request_index,
committee_len,
request_slot,
beacon_block_root,
justified_checkpoint,
target,
payload_present,
&self.spec,
)?)
}
Expand Down
13 changes: 13 additions & 0 deletions beacon_node/beacon_chain/src/early_attester_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
/// - 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:
/// - `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,
Expand Down Expand Up @@ -197,13 +203,20 @@ impl<E: EthSpec> EarlyAttesterCache<E> {
item.committee_lengths
.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(
request_index,
committee_len,
request_slot,
item.beacon_block_root,
item.source,
item.target,
payload_present,
spec,
)
.map_err(Error::AttestationError)?;
Expand Down
2 changes: 2 additions & 0 deletions beacon_node/beacon_chain/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,7 @@ where
epoch,
root: target_root,
},
false,
&self.spec,
)?;

Expand Down Expand Up @@ -1560,6 +1561,7 @@ where
epoch,
root: target_root,
},
false,
&self.spec,
)?)
}
Expand Down
181 changes: 158 additions & 23 deletions beacon_node/beacon_chain/tests/attestation_production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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,
};
use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS;
use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics};
use bls::{AggregateSignature, Keypair};
Expand Down Expand Up @@ -206,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::<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.beacon_block_root, block_root, "bad block root");
assert_eq!(
Expand All @@ -226,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::<MainnetEthSpec>(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"
);
}
}
}
}
Expand Down Expand Up @@ -313,3 +331,120 @@ 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() {
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)"
);
}
11 changes: 10 additions & 1 deletion consensus/types/src/attestation/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,26 +102,35 @@ impl<E: EthSpec> Hash for Attestation<E> {

impl<E: EthSpec> Attestation<E> {
/// Produces an attestation with empty signature.
#[allow(clippy::too_many_arguments)]
pub fn empty_for_signing(
committee_index: u64,
committee_length: usize,
slot: Slot,
beacon_block_root: Hash256,
source: Checkpoint,
target: Checkpoint,
payload_present: bool,
spec: &ChainSpec,
) -> Result<Self, Error> {
if spec.fork_name_at_slot::<E>(slot).electra_enabled() {
let mut committee_bits: BitVector<E::MaxCommitteesPerSlot> = BitVector::default();
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::<E>(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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ impl<S: ValidatorStore + 'static, T: SlotClock + 'static> AttestationService<S,
attestation_data.beacon_block_root,
attestation_data.source,
attestation_data.target,
attestation_data.index != 0,
&self.chain_spec,
) {
Ok(attestation) => attestation,
Expand Down
Loading