mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-06 10:11:44 +00:00
Optimise and refine SingleAttestation conversion (#6934)
Closes - https://github.com/sigp/lighthouse/issues/6805 - Use a new `WorkEvent::GossipAttestationToConvert` to handle the conversion from `SingleAttestation` to `Attestation` _on_ the beacon processor (prevents a Tokio thread being blocked). - Improve the error handling for single attestations. I think previously we had no ability to reprocess single attestations for unknown blocks -- we would just error. This seemed to be the case in both gossip processing and processing of `SingleAttestation`s from the HTTP API. - Move the `SingleAttestation -> Attestation` conversion function into `beacon_chain` so that it can return the `attestation_verification::Error` type, which has well-defined error handling and peer penalties. The now-unused variants of `types::Attestation::Error` have been removed.
This commit is contained in:
@@ -14,6 +14,7 @@ use beacon_chain::{
|
||||
light_client_finality_update_verification::Error as LightClientFinalityUpdateError,
|
||||
light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError,
|
||||
observed_operations::ObservationOutcome,
|
||||
single_attestation::single_attestation_to_attestation,
|
||||
sync_committee_verification::{self, Error as SyncCommitteeError},
|
||||
validator_monitor::{get_block_delay_ms, get_slot_delay_ms},
|
||||
AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError,
|
||||
@@ -32,12 +33,12 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use store::hot_cold_store::HotColdDBError;
|
||||
use tokio::sync::mpsc;
|
||||
use types::{
|
||||
beacon_block::BlockImportSource, Attestation, AttestationRef, AttesterSlashing, BlobSidecar,
|
||||
DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation,
|
||||
LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing,
|
||||
beacon_block::BlockImportSource, Attestation, AttestationData, AttestationRef,
|
||||
AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256,
|
||||
IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing,
|
||||
SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange,
|
||||
SignedContributionAndProof, SignedVoluntaryExit, Slot, SubnetId, SyncCommitteeMessage,
|
||||
SyncSubnetId,
|
||||
SignedContributionAndProof, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId,
|
||||
SyncCommitteeMessage, SyncSubnetId,
|
||||
};
|
||||
|
||||
use beacon_processor::{
|
||||
@@ -45,7 +46,7 @@ use beacon_processor::{
|
||||
QueuedAggregate, QueuedGossipBlock, QueuedLightClientUpdate, QueuedUnaggregate,
|
||||
ReprocessQueueMessage,
|
||||
},
|
||||
DuplicateCache, GossipAggregatePackage, GossipAttestationPackage,
|
||||
DuplicateCache, GossipAggregatePackage, GossipAttestationBatch,
|
||||
};
|
||||
|
||||
/// Set to `true` to introduce stricter penalties for peers who send some types of late consensus
|
||||
@@ -127,6 +128,11 @@ enum FailedAtt<E: EthSpec> {
|
||||
should_import: bool,
|
||||
seen_timestamp: Duration,
|
||||
},
|
||||
// This variant is just a dummy variant for now, as SingleAttestation reprocessing is handled
|
||||
// separately.
|
||||
SingleUnaggregate {
|
||||
attestation: Box<SingleAttestation>,
|
||||
},
|
||||
Aggregate {
|
||||
attestation: Box<SignedAggregateAndProof<E>>,
|
||||
seen_timestamp: Duration,
|
||||
@@ -135,20 +141,22 @@ enum FailedAtt<E: EthSpec> {
|
||||
|
||||
impl<E: EthSpec> FailedAtt<E> {
|
||||
pub fn beacon_block_root(&self) -> &Hash256 {
|
||||
&self.attestation().data().beacon_block_root
|
||||
&self.attestation_data().beacon_block_root
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
FailedAtt::Unaggregate { .. } => "unaggregated",
|
||||
FailedAtt::SingleUnaggregate { .. } => "unaggregated",
|
||||
FailedAtt::Aggregate { .. } => "aggregated",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attestation(&self) -> AttestationRef<E> {
|
||||
pub fn attestation_data(&self) -> &AttestationData {
|
||||
match self {
|
||||
FailedAtt::Unaggregate { attestation, .. } => attestation.to_ref(),
|
||||
FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate(),
|
||||
FailedAtt::Unaggregate { attestation, .. } => attestation.data(),
|
||||
FailedAtt::SingleUnaggregate { attestation, .. } => &attestation.data,
|
||||
FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate().data(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +237,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
|
||||
pub fn process_gossip_attestation_batch(
|
||||
self: Arc<Self>,
|
||||
packages: Vec<GossipAttestationPackage<T::EthSpec>>,
|
||||
packages: GossipAttestationBatch<T::EthSpec>,
|
||||
reprocess_tx: Option<mpsc::Sender<ReprocessQueueMessage>>,
|
||||
) {
|
||||
let attestations_and_subnets = packages
|
||||
@@ -399,6 +407,155 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an unaggregated attestation requiring conversion.
|
||||
///
|
||||
/// This function performs the conversion, and if successfull queues a new message to be
|
||||
/// processed by `process_gossip_attestation`. If unsuccessful due to block unavailability,
|
||||
/// a retry message will be pushed to the `reprocess_tx` if it is `Some`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn process_gossip_attestation_to_convert(
|
||||
self: Arc<Self>,
|
||||
message_id: MessageId,
|
||||
peer_id: PeerId,
|
||||
single_attestation: Box<SingleAttestation>,
|
||||
subnet_id: SubnetId,
|
||||
should_import: bool,
|
||||
reprocess_tx: Option<mpsc::Sender<ReprocessQueueMessage>>,
|
||||
seen_timestamp: Duration,
|
||||
) {
|
||||
let conversion_result = self.chain.with_committee_cache(
|
||||
single_attestation.data.target.root,
|
||||
single_attestation
|
||||
.data
|
||||
.slot
|
||||
.epoch(T::EthSpec::slots_per_epoch()),
|
||||
|committee_cache, _| {
|
||||
let slot = single_attestation.data.slot;
|
||||
let committee_index = single_attestation.committee_index;
|
||||
let Some(committee) = committee_cache.get_beacon_committee(slot, committee_index)
|
||||
else {
|
||||
return Ok(Err(AttnError::NoCommitteeForSlotAndIndex {
|
||||
slot,
|
||||
index: committee_index,
|
||||
}));
|
||||
};
|
||||
|
||||
Ok(single_attestation_to_attestation(
|
||||
&single_attestation,
|
||||
committee.committee,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
match conversion_result {
|
||||
Ok(Ok(attestation)) => {
|
||||
let slot = attestation.data().slot;
|
||||
if let Err(e) = self.send_unaggregated_attestation(
|
||||
message_id.clone(),
|
||||
peer_id,
|
||||
attestation,
|
||||
subnet_id,
|
||||
should_import,
|
||||
seen_timestamp,
|
||||
) {
|
||||
error!(
|
||||
&self.log,
|
||||
"Unable to queue converted SingleAttestation";
|
||||
"error" => %e,
|
||||
"slot" => slot,
|
||||
);
|
||||
self.propagate_validation_result(
|
||||
message_id,
|
||||
peer_id,
|
||||
MessageAcceptance::Ignore,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Outermost error (from `with_committee_cache`) indicating that the block is not known
|
||||
// and that this conversion should be retried.
|
||||
Err(BeaconChainError::MissingBeaconBlock(beacon_block_root)) => {
|
||||
if let Some(sender) = reprocess_tx {
|
||||
metrics::inc_counter(
|
||||
&metrics::BEACON_PROCESSOR_UNAGGREGATED_ATTESTATION_REQUEUED_TOTAL,
|
||||
);
|
||||
// We don't know the block, get the sync manager to handle the block lookup, and
|
||||
// send the attestation to be scheduled for re-processing.
|
||||
self.sync_tx
|
||||
.send(SyncMessage::UnknownBlockHashFromAttestation(
|
||||
peer_id,
|
||||
beacon_block_root,
|
||||
))
|
||||
.unwrap_or_else(|_| {
|
||||
warn!(
|
||||
self.log,
|
||||
"Failed to send to sync service";
|
||||
"msg" => "UnknownBlockHash"
|
||||
)
|
||||
});
|
||||
let processor = self.clone();
|
||||
// Do not allow this attestation to be re-processed beyond this point.
|
||||
let reprocess_msg =
|
||||
ReprocessQueueMessage::UnknownBlockUnaggregate(QueuedUnaggregate {
|
||||
beacon_block_root,
|
||||
process_fn: Box::new(move || {
|
||||
processor.process_gossip_attestation_to_convert(
|
||||
message_id,
|
||||
peer_id,
|
||||
single_attestation,
|
||||
subnet_id,
|
||||
should_import,
|
||||
None,
|
||||
seen_timestamp,
|
||||
)
|
||||
}),
|
||||
});
|
||||
if sender.try_send(reprocess_msg).is_err() {
|
||||
error!(
|
||||
self.log,
|
||||
"Failed to send attestation for re-processing";
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// We shouldn't make any further attempts to process this attestation.
|
||||
//
|
||||
// Don't downscore the peer since it's not clear if we requested this head
|
||||
// block from them or not.
|
||||
self.propagate_validation_result(
|
||||
message_id,
|
||||
peer_id,
|
||||
MessageAcceptance::Ignore,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
// We already handled reprocessing above so do not attempt it in the error handler.
|
||||
self.handle_attestation_verification_failure(
|
||||
peer_id,
|
||||
message_id,
|
||||
FailedAtt::SingleUnaggregate {
|
||||
attestation: single_attestation,
|
||||
},
|
||||
None,
|
||||
error,
|
||||
seen_timestamp,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
// We already handled reprocessing above so do not attempt it in the error handler.
|
||||
self.handle_attestation_verification_failure(
|
||||
peer_id,
|
||||
message_id,
|
||||
FailedAtt::SingleUnaggregate {
|
||||
attestation: single_attestation,
|
||||
},
|
||||
None,
|
||||
AttnError::BeaconChainError(error),
|
||||
seen_timestamp,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Process the aggregated attestation received from the gossip network and:
|
||||
///
|
||||
/// - If it passes gossip propagation criteria, tell the network thread to forward it.
|
||||
@@ -2207,9 +2364,9 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
// network.
|
||||
let seen_clock = &self.chain.slot_clock.freeze_at(seen_timestamp);
|
||||
let hindsight_verification =
|
||||
attestation_verification::verify_propagation_slot_range(
|
||||
attestation_verification::verify_propagation_slot_range::<_, T::EthSpec>(
|
||||
seen_clock,
|
||||
failed_att.attestation(),
|
||||
failed_att.attestation_data(),
|
||||
&self.chain.spec,
|
||||
);
|
||||
|
||||
@@ -2294,6 +2451,19 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
"attn_agg_not_in_committee",
|
||||
);
|
||||
}
|
||||
AttnError::AttesterNotInCommittee { .. } => {
|
||||
/*
|
||||
* `SingleAttestation` from a validator is invalid because the `attester_index` is
|
||||
* not in the claimed committee. There is no reason a non-faulty validator would
|
||||
* send this message.
|
||||
*/
|
||||
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject);
|
||||
self.gossip_penalize_peer(
|
||||
peer_id,
|
||||
PeerAction::LowToleranceError,
|
||||
"attn_single_not_in_committee",
|
||||
);
|
||||
}
|
||||
AttnError::AttestationSupersetKnown { .. } => {
|
||||
/*
|
||||
* The aggregate attestation has already been observed on the network or in
|
||||
@@ -2439,6 +2609,17 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
}),
|
||||
})
|
||||
}
|
||||
FailedAtt::SingleUnaggregate { .. } => {
|
||||
// This should never happen, as we handle the unknown head block case
|
||||
// for `SingleAttestation`s separately and should not be able to hit
|
||||
// an `UnknownHeadBlock` error.
|
||||
error!(
|
||||
self.log,
|
||||
"Dropping SingleAttestation instead of requeueing";
|
||||
"block_root" => ?beacon_block_root,
|
||||
);
|
||||
return;
|
||||
}
|
||||
FailedAtt::Unaggregate {
|
||||
attestation,
|
||||
subnet_id,
|
||||
@@ -2661,7 +2842,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
self.log,
|
||||
"Ignored attestation to finalized block";
|
||||
"block_root" => ?beacon_block_root,
|
||||
"attestation_slot" => failed_att.attestation().data().slot,
|
||||
"attestation_slot" => failed_att.attestation_data().slot,
|
||||
);
|
||||
|
||||
self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore);
|
||||
@@ -2684,9 +2865,9 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
debug!(
|
||||
self.log,
|
||||
"Dropping attestation";
|
||||
"target_root" => ?failed_att.attestation().data().target.root,
|
||||
"target_root" => ?failed_att.attestation_data().target.root,
|
||||
"beacon_block_root" => ?beacon_block_root,
|
||||
"slot" => ?failed_att.attestation().data().slot,
|
||||
"slot" => ?failed_att.attestation_data().slot,
|
||||
"type" => ?attestation_type,
|
||||
"error" => ?e,
|
||||
"peer_id" => % peer_id
|
||||
@@ -2705,7 +2886,7 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
self.log,
|
||||
"Unable to validate attestation";
|
||||
"beacon_block_root" => ?beacon_block_root,
|
||||
"slot" => ?failed_att.attestation().data().slot,
|
||||
"slot" => ?failed_att.attestation_data().slot,
|
||||
"type" => ?attestation_type,
|
||||
"peer_id" => %peer_id,
|
||||
"error" => ?e,
|
||||
@@ -3106,9 +3287,9 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
message_id: MessageId,
|
||||
peer_id: PeerId,
|
||||
) {
|
||||
let is_timely = attestation_verification::verify_propagation_slot_range(
|
||||
let is_timely = attestation_verification::verify_propagation_slot_range::<_, T::EthSpec>(
|
||||
&self.chain.slot_clock,
|
||||
attestation,
|
||||
attestation.data(),
|
||||
&self.chain.spec,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
@@ -94,46 +94,34 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
should_import: bool,
|
||||
seen_timestamp: Duration,
|
||||
) -> Result<(), Error<T::EthSpec>> {
|
||||
let result = self.chain.with_committee_cache(
|
||||
single_attestation.data.target.root,
|
||||
single_attestation
|
||||
.data
|
||||
.slot
|
||||
.epoch(T::EthSpec::slots_per_epoch()),
|
||||
|committee_cache, _| {
|
||||
let Some(committee) = committee_cache.get_beacon_committee(
|
||||
single_attestation.data.slot,
|
||||
single_attestation.committee_index,
|
||||
) else {
|
||||
warn!(
|
||||
self.log,
|
||||
"No beacon committee for slot and index";
|
||||
"slot" => single_attestation.data.slot,
|
||||
"index" => single_attestation.committee_index
|
||||
);
|
||||
return Ok(Ok(()));
|
||||
};
|
||||
let processor = self.clone();
|
||||
let process_individual = move |package: GossipAttestationPackage<SingleAttestation>| {
|
||||
let reprocess_tx = processor.reprocess_tx.clone();
|
||||
processor.process_gossip_attestation_to_convert(
|
||||
package.message_id,
|
||||
package.peer_id,
|
||||
package.attestation,
|
||||
package.subnet_id,
|
||||
package.should_import,
|
||||
Some(reprocess_tx),
|
||||
package.seen_timestamp,
|
||||
)
|
||||
};
|
||||
|
||||
let attestation = single_attestation.to_attestation(committee.committee)?;
|
||||
|
||||
Ok(self.send_unaggregated_attestation(
|
||||
message_id.clone(),
|
||||
self.try_send(BeaconWorkEvent {
|
||||
drop_during_sync: true,
|
||||
work: Work::GossipAttestationToConvert {
|
||||
attestation: Box::new(GossipAttestationPackage {
|
||||
message_id,
|
||||
peer_id,
|
||||
attestation,
|
||||
attestation: Box::new(single_attestation),
|
||||
subnet_id,
|
||||
should_import,
|
||||
seen_timestamp,
|
||||
))
|
||||
}),
|
||||
process_individual: Box::new(process_individual),
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
warn!(self.log, "Failed to send SingleAttestation"; "error" => ?e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new `Work` event for some unaggregated attestation.
|
||||
@@ -148,18 +136,19 @@ impl<T: BeaconChainTypes> NetworkBeaconProcessor<T> {
|
||||
) -> Result<(), Error<T::EthSpec>> {
|
||||
// Define a closure for processing individual attestations.
|
||||
let processor = self.clone();
|
||||
let process_individual = move |package: GossipAttestationPackage<T::EthSpec>| {
|
||||
let reprocess_tx = processor.reprocess_tx.clone();
|
||||
processor.process_gossip_attestation(
|
||||
package.message_id,
|
||||
package.peer_id,
|
||||
package.attestation,
|
||||
package.subnet_id,
|
||||
package.should_import,
|
||||
Some(reprocess_tx),
|
||||
package.seen_timestamp,
|
||||
)
|
||||
};
|
||||
let process_individual =
|
||||
move |package: GossipAttestationPackage<Attestation<T::EthSpec>>| {
|
||||
let reprocess_tx = processor.reprocess_tx.clone();
|
||||
processor.process_gossip_attestation(
|
||||
package.message_id,
|
||||
package.peer_id,
|
||||
package.attestation,
|
||||
package.subnet_id,
|
||||
package.should_import,
|
||||
Some(reprocess_tx),
|
||||
package.seen_timestamp,
|
||||
)
|
||||
};
|
||||
|
||||
// Define a closure for processing batches of attestations.
|
||||
let processor = self.clone();
|
||||
|
||||
Reference in New Issue
Block a user