Skip to content

Commit 8445ee4

Browse files
committed
Optimise and refine SingleAttestation conversion (#6934)
Squashed commit of the following: commit 3f113e5 Merge: 4334687 59afe41 Author: Michael Sproul <[email protected]> Date: Fri Feb 7 16:53:17 2025 +1100 Merge branch 'unstable' into single-attestation-take-3 commit 4334687 Author: Michael Sproul <[email protected]> Date: Fri Feb 7 13:51:13 2025 +1100 Optimise and refine SingleAttestation conversion
1 parent 1093142 commit 8445ee4

File tree

10 files changed

+379
-145
lines changed

10 files changed

+379
-145
lines changed

beacon_node/beacon_chain/src/attestation_verification.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ use std::borrow::Cow;
6060
use strum::AsRefStr;
6161
use tree_hash::TreeHash;
6262
use types::{
63-
Attestation, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec,
64-
CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof,
65-
SignedAggregateAndProof, SingleAttestation, Slot, SubnetId,
63+
Attestation, AttestationData, AttestationRef, BeaconCommittee,
64+
BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256,
65+
IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId,
6666
};
6767

6868
pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations};
@@ -115,6 +115,17 @@ pub enum Error {
115115
///
116116
/// The peer has sent an invalid message.
117117
AggregatorNotInCommittee { aggregator_index: u64 },
118+
/// The `attester_index` for a `SingleAttestation` is not a member of the committee defined
119+
/// by its `beacon_block_root`, `committee_index` and `slot`.
120+
///
121+
/// ## Peer scoring
122+
///
123+
/// The peer has sent an invalid message.
124+
AttesterNotInCommittee {
125+
attester_index: u64,
126+
committee_index: u64,
127+
slot: Slot,
128+
},
118129
/// The aggregator index refers to a validator index that we have not seen.
119130
///
120131
/// ## Peer scoring
@@ -485,7 +496,11 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> {
485496
// MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance).
486497
//
487498
// We do not queue future attestations for later processing.
488-
verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?;
499+
verify_propagation_slot_range::<_, T::EthSpec>(
500+
&chain.slot_clock,
501+
attestation.data(),
502+
&chain.spec,
503+
)?;
489504

490505
// Check the attestation's epoch matches its target.
491506
if attestation.data().slot.epoch(T::EthSpec::slots_per_epoch())
@@ -817,7 +832,11 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> {
817832
// MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance).
818833
//
819834
// We do not queue future attestations for later processing.
820-
verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?;
835+
verify_propagation_slot_range::<_, T::EthSpec>(
836+
&chain.slot_clock,
837+
attestation.data(),
838+
&chain.spec,
839+
)?;
821840

822841
// Check to ensure that the attestation is "unaggregated". I.e., it has exactly one
823842
// aggregation bit set.
@@ -1133,10 +1152,10 @@ fn verify_head_block_is_known<T: BeaconChainTypes>(
11331152
/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
11341153
pub fn verify_propagation_slot_range<S: SlotClock, E: EthSpec>(
11351154
slot_clock: &S,
1136-
attestation: AttestationRef<E>,
1155+
attestation: &AttestationData,
11371156
spec: &ChainSpec,
11381157
) -> Result<(), Error> {
1139-
let attestation_slot = attestation.data().slot;
1158+
let attestation_slot = attestation.slot;
11401159
let latest_permissible_slot = slot_clock
11411160
.now_with_future_tolerance(spec.maximum_gossip_clock_disparity())
11421161
.ok_or(BeaconChainError::UnableToReadSlot)?;

beacon_node/beacon_chain/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod pre_finalization_cache;
5454
pub mod proposer_prep_service;
5555
pub mod schema_change;
5656
pub mod shuffling_cache;
57+
pub mod single_attestation;
5758
pub mod state_advance_timer;
5859
pub mod sync_committee_rewards;
5960
pub mod sync_committee_verification;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use crate::attestation_verification::Error;
2+
use types::{Attestation, AttestationElectra, BitList, BitVector, EthSpec, SingleAttestation};
3+
4+
pub fn single_attestation_to_attestation<E: EthSpec>(
5+
single_attestation: &SingleAttestation,
6+
committee: &[usize],
7+
) -> Result<Attestation<E>, Error> {
8+
let attester_index = single_attestation.attester_index;
9+
let committee_index = single_attestation.committee_index;
10+
let slot = single_attestation.data.slot;
11+
12+
let aggregation_bit = committee
13+
.iter()
14+
.enumerate()
15+
.find_map(|(i, &validator_index)| {
16+
if attester_index as usize == validator_index {
17+
return Some(i);
18+
}
19+
None
20+
})
21+
.ok_or(Error::AttesterNotInCommittee {
22+
attester_index,
23+
committee_index,
24+
slot,
25+
})?;
26+
27+
let mut committee_bits: BitVector<E::MaxCommitteesPerSlot> = BitVector::default();
28+
committee_bits
29+
.set(committee_index as usize, true)
30+
.map_err(|e| Error::Invalid(e.into()))?;
31+
32+
let mut aggregation_bits =
33+
BitList::with_capacity(committee.len()).map_err(|e| Error::Invalid(e.into()))?;
34+
aggregation_bits
35+
.set(aggregation_bit, true)
36+
.map_err(|e| Error::Invalid(e.into()))?;
37+
38+
// TODO(electra): consider eventually allowing conversion to non-Electra attestations as well
39+
// to maintain invertability (`Attestation` -> `SingleAttestation` -> `Attestation`).
40+
Ok(Attestation::Electra(AttestationElectra {
41+
aggregation_bits,
42+
committee_bits,
43+
data: single_attestation.data.clone(),
44+
signature: single_attestation.signature.clone(),
45+
}))
46+
}

beacon_node/beacon_chain/src/test_utils.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub use crate::persisted_beacon_chain::PersistedBeaconChain;
77
pub use crate::{
88
beacon_chain::{BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY},
99
migrate::MigratorConfig,
10+
single_attestation::single_attestation_to_attestation,
1011
sync_committee_verification::Error as SyncCommitteeError,
1112
validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig},
1213
BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification,
@@ -1156,7 +1157,8 @@ where
11561157
let single_attestation =
11571158
attestation.to_single_attestation_with_attester_index(attester_index as u64)?;
11581159

1159-
let attestation: Attestation<E> = single_attestation.to_attestation(committee.committee)?;
1160+
let attestation: Attestation<E> =
1161+
single_attestation_to_attestation(&single_attestation, committee.committee).unwrap();
11601162

11611163
assert_eq!(
11621164
single_attestation.committee_index,

beacon_node/beacon_processor/src/lib.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ use task_executor::TaskExecutor;
6262
use tokio::sync::mpsc;
6363
use tokio::sync::mpsc::error::TrySendError;
6464
use types::{
65-
Attestation, BeaconState, ChainSpec, Hash256, RelativeEpoch, SignedAggregateAndProof, SubnetId,
65+
Attestation, BeaconState, ChainSpec, EthSpec, Hash256, RelativeEpoch, SignedAggregateAndProof,
66+
SingleAttestation, Slot, SubnetId,
6667
};
67-
use types::{EthSpec, Slot};
6868
use work_reprocessing_queue::{
6969
spawn_reprocess_scheduler, QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock,
7070
QueuedUnaggregate, ReadyWork,
@@ -504,10 +504,10 @@ impl<E: EthSpec> From<ReadyWork> for WorkEvent<E> {
504504

505505
/// Items required to verify a batch of unaggregated gossip attestations.
506506
#[derive(Debug)]
507-
pub struct GossipAttestationPackage<E: EthSpec> {
507+
pub struct GossipAttestationPackage<T> {
508508
pub message_id: MessageId,
509509
pub peer_id: PeerId,
510-
pub attestation: Box<Attestation<E>>,
510+
pub attestation: Box<T>,
511511
pub subnet_id: SubnetId,
512512
pub should_import: bool,
513513
pub seen_timestamp: Duration,
@@ -549,21 +549,32 @@ pub enum BlockingOrAsync {
549549
Blocking(BlockingFn),
550550
Async(AsyncFn),
551551
}
552+
pub type GossipAttestationBatch<E> = Vec<GossipAttestationPackage<Attestation<E>>>;
552553

553554
/// Indicates the type of work to be performed and therefore its priority and
554555
/// queuing specifics.
555556
pub enum Work<E: EthSpec> {
556557
GossipAttestation {
557-
attestation: Box<GossipAttestationPackage<E>>,
558-
process_individual: Box<dyn FnOnce(GossipAttestationPackage<E>) + Send + Sync>,
559-
process_batch: Box<dyn FnOnce(Vec<GossipAttestationPackage<E>>) + Send + Sync>,
558+
attestation: Box<GossipAttestationPackage<Attestation<E>>>,
559+
process_individual: Box<dyn FnOnce(GossipAttestationPackage<Attestation<E>>) + Send + Sync>,
560+
process_batch: Box<dyn FnOnce(GossipAttestationBatch<E>) + Send + Sync>,
561+
},
562+
// Attestation requiring conversion before processing.
563+
//
564+
// For now this is a `SingleAttestation`, but eventually we will switch this around so that
565+
// legacy `Attestation`s are converted and the main processing pipeline operates on
566+
// `SingleAttestation`s.
567+
GossipAttestationToConvert {
568+
attestation: Box<GossipAttestationPackage<SingleAttestation>>,
569+
process_individual:
570+
Box<dyn FnOnce(GossipAttestationPackage<SingleAttestation>) + Send + Sync>,
560571
},
561572
UnknownBlockAttestation {
562573
process_fn: BlockingFn,
563574
},
564575
GossipAttestationBatch {
565-
attestations: Vec<GossipAttestationPackage<E>>,
566-
process_batch: Box<dyn FnOnce(Vec<GossipAttestationPackage<E>>) + Send + Sync>,
576+
attestations: GossipAttestationBatch<E>,
577+
process_batch: Box<dyn FnOnce(GossipAttestationBatch<E>) + Send + Sync>,
567578
},
568579
GossipAggregate {
569580
aggregate: Box<GossipAggregatePackage<E>>,
@@ -639,6 +650,7 @@ impl<E: EthSpec> fmt::Debug for Work<E> {
639650
#[strum(serialize_all = "snake_case")]
640651
pub enum WorkType {
641652
GossipAttestation,
653+
GossipAttestationToConvert,
642654
UnknownBlockAttestation,
643655
GossipAttestationBatch,
644656
GossipAggregate,
@@ -690,6 +702,7 @@ impl<E: EthSpec> Work<E> {
690702
fn to_type(&self) -> WorkType {
691703
match self {
692704
Work::GossipAttestation { .. } => WorkType::GossipAttestation,
705+
Work::GossipAttestationToConvert { .. } => WorkType::GossipAttestationToConvert,
693706
Work::GossipAttestationBatch { .. } => WorkType::GossipAttestationBatch,
694707
Work::GossipAggregate { .. } => WorkType::GossipAggregate,
695708
Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch,
@@ -849,6 +862,7 @@ impl<E: EthSpec> BeaconProcessor<E> {
849862
let mut aggregate_queue = LifoQueue::new(queue_lengths.aggregate_queue);
850863
let mut aggregate_debounce = TimeLatch::default();
851864
let mut attestation_queue = LifoQueue::new(queue_lengths.attestation_queue);
865+
let mut attestation_to_convert_queue = LifoQueue::new(queue_lengths.attestation_queue);
852866
let mut attestation_debounce = TimeLatch::default();
853867
let mut unknown_block_aggregate_queue =
854868
LifoQueue::new(queue_lengths.unknown_block_aggregate_queue);
@@ -1180,6 +1194,9 @@ impl<E: EthSpec> BeaconProcessor<E> {
11801194
None
11811195
}
11821196
}
1197+
// Convert any gossip attestations that need to be converted.
1198+
} else if let Some(item) = attestation_to_convert_queue.pop() {
1199+
Some(item)
11831200
// Check sync committee messages after attestations as their rewards are lesser
11841201
// and they don't influence fork choice.
11851202
} else if let Some(item) = sync_contribution_queue.pop() {
@@ -1301,6 +1318,9 @@ impl<E: EthSpec> BeaconProcessor<E> {
13011318
match work {
13021319
_ if can_spawn => self.spawn_worker(work, idle_tx),
13031320
Work::GossipAttestation { .. } => attestation_queue.push(work),
1321+
Work::GossipAttestationToConvert { .. } => {
1322+
attestation_to_convert_queue.push(work)
1323+
}
13041324
// Attestation batches are formed internally within the
13051325
// `BeaconProcessor`, they are not sent from external services.
13061326
Work::GossipAttestationBatch { .. } => crit!(
@@ -1431,6 +1451,7 @@ impl<E: EthSpec> BeaconProcessor<E> {
14311451
if let Some(modified_queue_id) = modified_queue_id {
14321452
let queue_len = match modified_queue_id {
14331453
WorkType::GossipAttestation => attestation_queue.len(),
1454+
WorkType::GossipAttestationToConvert => attestation_to_convert_queue.len(),
14341455
WorkType::UnknownBlockAttestation => unknown_block_attestation_queue.len(),
14351456
WorkType::GossipAttestationBatch => 0, // No queue
14361457
WorkType::GossipAggregate => aggregate_queue.len(),
@@ -1563,6 +1584,12 @@ impl<E: EthSpec> BeaconProcessor<E> {
15631584
} => task_spawner.spawn_blocking(move || {
15641585
process_individual(*attestation);
15651586
}),
1587+
Work::GossipAttestationToConvert {
1588+
attestation,
1589+
process_individual,
1590+
} => task_spawner.spawn_blocking(move || {
1591+
process_individual(*attestation);
1592+
}),
15661593
Work::GossipAttestationBatch {
15671594
attestations,
15681595
process_batch,

beacon_node/http_api/src/publish_attestations.rs

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
//! attestations and there's no immediate cause for concern.
3737
use crate::task_spawner::{Priority, TaskSpawner};
3838
use beacon_chain::{
39-
validator_monitor::timestamp_now, AttestationError, BeaconChain, BeaconChainError,
40-
BeaconChainTypes,
39+
single_attestation::single_attestation_to_attestation, validator_monitor::timestamp_now,
40+
AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes,
4141
};
4242
use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage};
4343
use either::Either;
@@ -183,10 +183,10 @@ fn convert_to_attestation<'a, T: BeaconChainTypes>(
183183
chain: &Arc<BeaconChain<T>>,
184184
attestation: &'a Either<Attestation<T::EthSpec>, SingleAttestation>,
185185
) -> Result<Cow<'a, Attestation<T::EthSpec>>, Error> {
186-
let a = match attestation {
187-
Either::Left(a) => Cow::Borrowed(a),
188-
Either::Right(single_attestation) => chain
189-
.with_committee_cache(
186+
match attestation {
187+
Either::Left(a) => Ok(Cow::Borrowed(a)),
188+
Either::Right(single_attestation) => {
189+
let conversion_result = chain.with_committee_cache(
190190
single_attestation.data.target.root,
191191
single_attestation
192192
.data
@@ -197,24 +197,33 @@ fn convert_to_attestation<'a, T: BeaconChainTypes>(
197197
single_attestation.data.slot,
198198
single_attestation.committee_index,
199199
) else {
200-
return Err(BeaconChainError::AttestationError(
201-
types::AttestationError::NoCommitteeForSlotAndIndex {
202-
slot: single_attestation.data.slot,
203-
index: single_attestation.committee_index,
204-
},
205-
));
200+
return Ok(Err(AttestationError::NoCommitteeForSlotAndIndex {
201+
slot: single_attestation.data.slot,
202+
index: single_attestation.committee_index,
203+
}));
206204
};
207205

208-
let attestation =
209-
single_attestation.to_attestation::<T::EthSpec>(committee.committee)?;
210-
211-
Ok(Cow::Owned(attestation))
206+
Ok(single_attestation_to_attestation::<T::EthSpec>(
207+
single_attestation,
208+
committee.committee,
209+
)
210+
.map(Cow::Owned))
212211
},
213-
)
214-
.map_err(Error::FailedConversion)?,
215-
};
216-
217-
Ok(a)
212+
);
213+
match conversion_result {
214+
Ok(Ok(attestation)) => Ok(attestation),
215+
Ok(Err(e)) => Err(Error::Validation(e)),
216+
// Map the error returned by `with_committee_cache` for unknown blocks into the
217+
// `UnknownHeadBlock` error that is gracefully handled.
218+
Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => {
219+
Err(Error::Validation(AttestationError::UnknownHeadBlock {
220+
beacon_block_root,
221+
}))
222+
}
223+
Err(e) => Err(Error::FailedConversion(e)),
224+
}
225+
}
226+
}
218227
}
219228

220229
pub async fn publish_attestations<T: BeaconChainTypes>(

0 commit comments

Comments
 (0)