Single attestation "Full" implementation (#7444)

#6970


  This allows for us to receive `SingleAttestation` over gossip and process it without converting. There is still a conversion to `Attestation` as a final step in the attestation verification process, but by then the `SingleAttestation` is fully verified.

I've also fully removed the `submitPoolAttestationsV1` endpoint as its been deprecated

I've also pre-emptively deprecated supporting `Attestation` in `submitPoolAttestationsV2` endpoint. See here for more info: https://github.com/ethereum/beacon-APIs/pull/531

I tried to the minimize the diff here by only making the "required" changes. There are some unnecessary complexities with the way we manage the different attestation verification wrapper types. We could probably consolidate this to one wrapper type and refactor this even further. We could leave that to a separate PR if we feel like cleaning things up in the future.

Note that I've also updated the test harness to always submit `SingleAttestation` regardless of fork variant. I don't see a problem in that approach and it allows us to delete more code :)
This commit is contained in:
Eitan Seri-Levi
2025-06-17 12:01:26 +03:00
committed by GitHub
parent 3d2d65bf8d
commit 6786b9d12a
24 changed files with 777 additions and 981 deletions

View File

@@ -149,10 +149,41 @@ async fn attestations_across_fork_with_skip_slots() {
.flat_map(|(atts, _)| atts.iter().map(|(att, _)| att.clone()))
.collect::<Vec<_>>();
let unaggregated_attestations = unaggregated_attestations
.into_iter()
.map(|attn| {
let aggregation_bits = attn.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = fork_state
.get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
attn.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap()
})
.collect::<Vec<_>>();
assert!(!unaggregated_attestations.is_empty());
let fork_name = harness.spec.fork_name_at_slot::<E>(fork_slot);
client
.post_beacon_pool_attestations_v1(&unaggregated_attestations)
.post_beacon_pool_attestations_v2::<E>(unaggregated_attestations, fork_name)
.await
.unwrap();

View File

@@ -5,7 +5,6 @@ use beacon_chain::{
ChainConfig,
};
use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage;
use either::Either;
use eth2::types::ProduceBlockV3Response;
use eth2::types::{DepositContractData, StateId};
use execution_layer::{ForkchoiceState, PayloadAttributes};
@@ -539,7 +538,7 @@ pub async fn proposer_boost_re_org_test(
slot_a,
num_parent_votes,
);
harness.process_attestations(block_a_parent_votes);
harness.process_attestations(block_a_parent_votes, &state_a);
// Attest to block A during slot B.
for _ in 0..parent_distance {
@@ -553,7 +552,7 @@ pub async fn proposer_boost_re_org_test(
slot_b,
num_empty_votes,
);
harness.process_attestations(block_a_empty_votes);
harness.process_attestations(block_a_empty_votes, &state_a);
let remaining_attesters = all_validators
.iter()
@@ -586,7 +585,7 @@ pub async fn proposer_boost_re_org_test(
slot_b,
num_head_votes,
);
harness.process_attestations(block_b_head_votes);
harness.process_attestations(block_b_head_votes, &state_b);
let payload_lookahead = harness.chain.config.prepare_payload_lookahead;
let fork_choice_lookahead = Duration::from_millis(500);
@@ -818,10 +817,10 @@ pub async fn fork_choice_before_proposal() {
block_root_c,
slot_c,
);
harness.process_attestations(attestations_c);
harness.process_attestations(attestations_c, &state_c);
// Apply the attestations to B, but don't re-run fork choice.
harness.process_attestations(attestations_b);
harness.process_attestations(attestations_b, &state_b);
// Due to proposer boost, the head should be C during slot C.
assert_eq!(
@@ -894,7 +893,7 @@ async fn queue_attestations_from_http() {
let fork_name = tester.harness.spec.fork_name_at_slot::<E>(attestation_slot);
// Make attestations to the block and POST them to the beacon node on a background thread.
let attestation_future = if fork_name.electra_enabled() {
let attestation_future = {
let single_attestations = harness
.make_single_attestations(
&all_validators,
@@ -907,30 +906,9 @@ async fn queue_attestations_from_http() {
.flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att))
.collect::<Vec<_>>();
let attestations = Either::Right(single_attestations);
tokio::spawn(async move {
client
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.await
.expect("attestations should be processed successfully")
})
} else {
let attestations = harness
.make_unaggregated_attestations(
&all_validators,
&post_state,
block.0.state_root(),
block_root.into(),
attestation_slot,
)
.into_iter()
.flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att))
.collect::<Vec<_>>();
tokio::spawn(async move {
client
.post_beacon_pool_attestations_v1(&attestations)
.post_beacon_pool_attestations_v2::<E>(single_attestations, fork_name)
.await
.expect("attestations should be processed successfully")
})

View File

@@ -3,7 +3,6 @@ use beacon_chain::{
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType},
BeaconChain, ChainConfig, StateSkipConfig, WhenSlotSkipped,
};
use either::Either;
use eth2::{
mixin::{RequestAccept, ResponseForkName, ResponseOptional},
reqwest::RequestBuilder,
@@ -1907,18 +1906,46 @@ impl ApiTester {
}
pub async fn test_post_beacon_pool_attestations_valid(mut self) -> Self {
self.client
.post_beacon_pool_attestations_v1(self.attestations.as_slice())
.await
.unwrap();
let fork_name = self
.attestations
.first()
.map(|att| self.chain.spec.fork_name_at_slot::<E>(att.data().slot))
.unwrap();
let attestations = Either::Left(self.attestations.clone());
let state = &self.chain.head_snapshot().beacon_state;
let attestations = self
.attestations
.clone()
.into_iter()
.map(|attn| {
let aggregation_bits = attn.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = state
.get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
attn.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap()
})
.collect::<Vec<_>>();
self.client
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
@@ -1943,9 +1970,8 @@ impl ApiTester {
.map(|att| self.chain.spec.fork_name_at_slot::<E>(att.data.slot))
.unwrap();
let attestations = Either::Right(self.single_attestations.clone());
self.client
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.post_beacon_pool_attestations_v2::<E>(self.single_attestations.clone(), fork_name)
.await
.unwrap();
assert!(
@@ -1958,18 +1984,87 @@ impl ApiTester {
pub async fn test_post_beacon_pool_attestations_invalid_v1(mut self) -> Self {
let mut attestations = Vec::new();
let state = &self.chain.head_snapshot().beacon_state;
for attestation in &self.attestations {
let mut invalid_attestation = attestation.clone();
invalid_attestation.data_mut().slot += 1;
// Convert valid attestation into valid `SingleAttestation`
let aggregation_bits = attestation.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = state
.get_beacon_committee(
attestation.data().slot,
attestation.committee_index().unwrap(),
)
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
let attestation = attestation
.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap();
// Convert invalid attestation to invalid `SingleAttestation`
let aggregation_bits = invalid_attestation.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = state
.get_beacon_committee(
invalid_attestation.data().slot,
invalid_attestation.committee_index().unwrap(),
)
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
let invalid_attestation = invalid_attestation
.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap();
// add both to ensure we only fail on invalid attestations
attestations.push(attestation.clone());
attestations.push(invalid_attestation);
}
let fork_name = self
.attestations
.first()
.map(|att| self.chain.spec.fork_name_at_slot::<E>(att.data().slot))
.unwrap();
let err = self
.client
.post_beacon_pool_attestations_v1(attestations.as_slice())
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.await
.unwrap_err();
@@ -2011,7 +2106,6 @@ impl ApiTester {
.first()
.map(|att| self.chain.spec.fork_name_at_slot::<E>(att.data().slot))
.unwrap();
let attestations = Either::Right(attestations);
let err_v2 = self
.client
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
@@ -4177,9 +4271,47 @@ impl ApiTester {
assert_eq!(result, expected);
let attestations = self
.attestations
.clone()
.into_iter()
.map(|attn| {
let aggregation_bits = attn.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = head_state
.get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
attn.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap()
})
.collect::<Vec<_>>();
let fork_name = self
.chain
.spec
.fork_name_at_slot::<E>(attestations.first().unwrap().data.slot);
// Attest to the current slot
self.client
.post_beacon_pool_attestations_v1(self.attestations.as_slice())
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.await
.unwrap();
@@ -5916,9 +6048,47 @@ impl ApiTester {
assert_eq!(result, expected);
let attestations = self
.attestations
.clone()
.into_iter()
.map(|attn| {
let aggregation_bits = attn.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = head_state
.get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
attn.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap()
})
.collect::<Vec<_>>();
let fork_name = self
.chain
.spec
.fork_name_at_slot::<E>(attestations.first().unwrap().data.slot);
// Attest to the current slot
self.client
.post_beacon_pool_attestations_v1(self.attestations.as_slice())
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.await
.unwrap();
@@ -5973,8 +6143,47 @@ impl ApiTester {
let expected_attestation_len = self.attestations.len();
let state = self.harness.get_current_state();
let attestations = self
.attestations
.clone()
.into_iter()
.map(|attn| {
let aggregation_bits = attn.get_aggregation_bits();
if aggregation_bits.len() != 1 {
panic!("Must be an unaggregated attestation")
}
let aggregation_bit = *aggregation_bits.first().unwrap();
let committee = state
.get_beacon_committee(attn.data().slot, attn.committee_index().unwrap())
.unwrap();
let attester_index = committee
.committee
.iter()
.enumerate()
.find_map(|(i, &index)| {
if aggregation_bit as usize == i {
return Some(index);
}
None
})
.unwrap();
attn.to_single_attestation_with_attester_index(attester_index as u64)
.unwrap()
})
.collect::<Vec<_>>();
let fork_name = self
.chain
.spec
.fork_name_at_slot::<E>(attestations.first().unwrap().data.slot);
self.client
.post_beacon_pool_attestations_v1(self.attestations.as_slice())
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.await
.unwrap();
@@ -6247,9 +6456,9 @@ impl ApiTester {
.chain
.spec
.fork_name_at_slot::<E>(self.chain.slot().unwrap());
let attestations = Either::Right(self.single_attestations.clone());
self.client
.post_beacon_pool_attestations_v2::<E>(attestations, fork_name)
.post_beacon_pool_attestations_v2::<E>(self.single_attestations.clone(), fork_name)
.await
.unwrap();