Gloas attestation verification (#8705)

https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#attestation-subnets

Implements attestation verification logic for Gloas and adds a few gloas related tests. Note that a few of these tests rely on gloas test harness block production which hasn't been built out yet. So for now those tests are ignored.


  


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

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
This commit is contained in:
Eitan Seri-Levi
2026-02-13 13:39:56 -08:00
committed by GitHub
parent 26db016425
commit 68ad9758a3
3 changed files with 336 additions and 10 deletions

View File

@@ -368,6 +368,13 @@ impl GossipTester {
self.harness.chain.epoch().unwrap()
}
pub fn is_gloas(&self) -> bool {
self.harness
.spec
.fork_name_at_slot::<E>(self.valid_attestation.data.slot)
.gloas_enabled()
}
pub fn earliest_valid_attestation_slot(&self) -> Slot {
let offset = if self
.harness
@@ -522,6 +529,44 @@ impl GossipTester {
self
}
/// Like `inspect_aggregate_err`, but only runs the check if gloas is enabled.
/// If gloas is not enabled, this is a no-op that returns self.
pub fn inspect_aggregate_err_if_gloas<G, I>(
self,
desc: &str,
get_attn: G,
inspect_err: I,
) -> Self
where
G: Fn(&Self, &mut SignedAggregateAndProof<E>),
I: Fn(&Self, AttnError),
{
if self.is_gloas() {
self.inspect_aggregate_err(desc, get_attn, inspect_err)
} else {
self
}
}
/// Like `inspect_unaggregate_err`, but only runs the check if gloas is enabled.
/// If gloas is not enabled, this is a no-op that returns self.
pub fn inspect_unaggregate_err_if_gloas<G, I>(
self,
desc: &str,
get_attn: G,
inspect_err: I,
) -> Self
where
G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec),
I: Fn(&Self, AttnError),
{
if self.is_gloas() {
self.inspect_unaggregate_err(desc, get_attn, inspect_err)
} else {
self
}
}
}
/// Tests verification of `SignedAggregateAndProof` from the gossip network.
#[tokio::test]
@@ -854,6 +899,27 @@ async fn aggregated_gossip_verification() {
))
},
)
/*
* [New in Gloas]: attestation.data.index must be < 2
*/
.inspect_aggregate_err_if_gloas(
"gloas: aggregate with index >= 2",
|_, a| match a.to_mut() {
SignedAggregateAndProofRefMut::Base(_) => {
panic!("Expected Electra attestation variant");
}
SignedAggregateAndProofRefMut::Electra(att) => {
att.message.aggregate.data.index = 2;
}
},
|_, err| {
assert!(
matches!(err, AttnError::CommitteeIndexInvalid),
"expected CommitteeIndexInvalid, got {:?}",
err
)
},
)
// NOTE: from here on, the tests are stateful, and rely on the valid attestation having
// been seen.
.import_valid_aggregate()
@@ -1071,6 +1137,22 @@ async fn unaggregated_gossip_verification() {
))
},
)
/*
* [New in Gloas]: attestation.data.index must be < 2
*/
.inspect_unaggregate_err_if_gloas(
"gloas: attestation with index >= 2",
|_, a, _, _| {
a.data.index = 2;
},
|_, err| {
assert!(
matches!(err, AttnError::CommitteeIndexInvalid),
"expected CommitteeIndexInvalid, got {:?}",
err
)
},
)
// NOTE: from here on, the tests are stateful, and rely on the valid attestation having
// been seen.
.import_valid_unaggregate()
@@ -1700,3 +1782,180 @@ async fn aggregated_attestation_verification_use_head_state_fork() {
);
}
}
/// [New in Gloas]: Tests that unaggregated attestations with `data.index == 1` are rejected
/// when `head_block.slot == attestation.data.slot`.
///
/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature.
// TODO(EIP-7732): Enable this test once gloas block production works in test harness.
// `state.latest_execution_payload_header()` not available in Gloas.
#[ignore]
#[tokio::test]
async fn gloas_unaggregated_attestation_same_slot_index_must_be_zero() {
let harness = get_harness(VALIDATOR_COUNT);
// Skip this test if not running with gloas fork
if !harness
.spec
.fork_name_at_epoch(Epoch::new(0))
.gloas_enabled()
{
return;
}
// Extend the chain out a few epochs so we have some chain depth to play with.
harness
.extend_chain(
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Produce a block in the current slot (this creates the same-slot scenario)
harness
.extend_chain(
1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(vec![]),
)
.await;
let current_slot = harness.chain.slot().expect("should get slot");
let head = harness.chain.head_snapshot();
// Verify head block is in the current slot
assert_eq!(
head.beacon_block.slot(),
current_slot,
"head block should be in current slot for same-slot test"
);
// Produce an attestation for the current slot
let (mut attestation, _attester_sk, subnet_id) =
get_valid_unaggregated_attestation(&harness.chain);
// Verify we have a same-slot scenario
let attested_block_slot = harness
.chain
.canonical_head
.fork_choice_read_lock()
.get_block(&attestation.data.beacon_block_root)
.expect("block should exist")
.slot;
assert_eq!(
attested_block_slot, attestation.data.slot,
"attested block slot should equal attestation slot for same-slot test"
);
// index == 1 should be rejected when head_block.slot == attestation.data.slot
attestation.data.index = 1;
let result = harness
.chain
.verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id));
assert!(
matches!(result, Err(AttnError::CommitteeIndexNonZero(_))),
"gloas: attestation with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}",
result.err()
);
}
/// [New in Gloas]: Tests that aggregated attestations with `data.index == 1` are rejected
/// when `head_block.slot == attestation.data.slot`.
///
/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature.
// TODO(EIP-7732): Enable this test once gloas block production works in test harness.
// `state.latest_execution_payload_header()` not available in Gloas.
#[ignore]
#[tokio::test]
async fn gloas_aggregated_attestation_same_slot_index_must_be_zero() {
let harness = get_harness(VALIDATOR_COUNT);
// Skip this test if not running with gloas fork
if !harness
.spec
.fork_name_at_epoch(Epoch::new(0))
.gloas_enabled()
{
return;
}
// Extend the chain out a few epochs so we have some chain depth to play with.
harness
.extend_chain(
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
// Produce a block in the current slot (this creates the same-slot scenario)
harness
.extend_chain(
1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::SomeValidators(vec![]),
)
.await;
let current_slot = harness.chain.slot().expect("should get slot");
let head = harness.chain.head_snapshot();
// Verify head block is in the current slot
assert_eq!(
head.beacon_block.slot(),
current_slot,
"head block should be in current slot for same-slot test"
);
// Produce an attestation for the current slot
let (valid_attestation, _attester_sk, _subnet_id) =
get_valid_unaggregated_attestation(&harness.chain);
// Verify we have a same-slot scenario
let attested_block_slot = harness
.chain
.canonical_head
.fork_choice_read_lock()
.get_block(&valid_attestation.data.beacon_block_root)
.expect("block should exist")
.slot;
assert_eq!(
attested_block_slot, valid_attestation.data.slot,
"attested block slot should equal attestation slot for same-slot test"
);
// Convert to aggregate
let committee = head
.beacon_state
.get_beacon_committee(current_slot, valid_attestation.committee_index)
.expect("should get committee");
let fork_name = harness
.spec
.fork_name_at_slot::<E>(valid_attestation.data.slot);
let aggregate_attestation =
single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name)
.unwrap();
let (mut valid_aggregate, _, _) =
get_valid_aggregated_attestation(&harness.chain, aggregate_attestation);
// index == 1 should be rejected when head_block.slot == attestation.data.slot
match valid_aggregate.to_mut() {
SignedAggregateAndProofRefMut::Base(att) => {
att.message.aggregate.data.index = 1;
}
SignedAggregateAndProofRefMut::Electra(att) => {
att.message.aggregate.data.index = 1;
}
}
let result = harness
.chain
.verify_aggregated_attestation_for_gossip(&valid_aggregate);
assert!(
matches!(result, Err(AttnError::CommitteeIndexNonZero(_))),
"gloas: aggregate with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}",
result.err()
);
}