mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-17 11:52:42 +00:00
[Altair] Sync committee pools (#2321)
Add pools supporting sync committees: - naive sync aggregation pool - observed sync contributions pool - observed sync contributors pool - observed sync aggregators pool Add SSZ types and tests related to sync committee signatures. Co-authored-by: Michael Sproul <michael@sigmaprime.io> Co-authored-by: realbigsean <seananderson33@gmail.com>
This commit is contained in:
@@ -27,11 +27,9 @@
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
beacon_chain::{
|
||||
HEAD_LOCK_TIMEOUT, MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT,
|
||||
},
|
||||
beacon_chain::{MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT},
|
||||
metrics,
|
||||
observed_attestations::ObserveOutcome,
|
||||
observed_aggregates::ObserveOutcome,
|
||||
observed_attesters::Error as ObservedAttestersError,
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
};
|
||||
@@ -430,7 +428,7 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
|
||||
match chain
|
||||
.observed_aggregators
|
||||
.read()
|
||||
.validator_has_been_observed(attestation, aggregator_index as usize)
|
||||
.validator_has_been_observed(attestation.data.target.epoch, aggregator_index as usize)
|
||||
{
|
||||
Ok(true) => Err(Error::AggregatorAlreadyKnown(aggregator_index)),
|
||||
Ok(false) => Ok(()),
|
||||
@@ -485,7 +483,7 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
|
||||
if let ObserveOutcome::AlreadyKnown = chain
|
||||
.observed_attestations
|
||||
.write()
|
||||
.observe_attestation(attestation, Some(attestation_root))
|
||||
.observe_item(attestation, Some(attestation_root))
|
||||
.map_err(|e| Error::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(Error::AttestationAlreadyKnown(attestation_root));
|
||||
@@ -498,7 +496,7 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
|
||||
if chain
|
||||
.observed_aggregators
|
||||
.write()
|
||||
.observe_validator(&attestation, aggregator_index as usize)
|
||||
.observe_validator(attestation.data.target.epoch, aggregator_index as usize)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorAttestationKnown {
|
||||
@@ -689,7 +687,7 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
|
||||
if chain
|
||||
.observed_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(&attestation, validator_index as usize)
|
||||
.validator_has_been_observed(attestation.data.target.epoch, validator_index as usize)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorAttestationKnown {
|
||||
@@ -716,7 +714,7 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
|
||||
if chain
|
||||
.observed_attesters
|
||||
.write()
|
||||
.observe_validator(&attestation, validator_index as usize)
|
||||
.observe_validator(attestation.data.target.epoch, validator_index as usize)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorAttestationKnown {
|
||||
@@ -923,10 +921,8 @@ pub fn verify_attestation_signature<T: BeaconChainTypes>(
|
||||
.ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?;
|
||||
|
||||
let fork = chain
|
||||
.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or(BeaconChainError::CanonicalHeadLockTimeout)
|
||||
.map(|head| head.beacon_state.fork())?;
|
||||
.spec
|
||||
.fork_at_epoch(indexed_attestation.data.target.epoch);
|
||||
|
||||
let signature_set = indexed_attestation_signature_set_from_pubkeys(
|
||||
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
|
||||
@@ -1029,10 +1025,8 @@ pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>(
|
||||
}
|
||||
|
||||
let fork = chain
|
||||
.canonical_head
|
||||
.try_read_for(HEAD_LOCK_TIMEOUT)
|
||||
.ok_or(BeaconChainError::CanonicalHeadLockTimeout)
|
||||
.map(|head| head.beacon_state.fork())?;
|
||||
.spec
|
||||
.fork_at_epoch(indexed_attestation.data.target.epoch);
|
||||
|
||||
let signature_sets = vec![
|
||||
signed_aggregate_selection_proof_signature_set(
|
||||
|
||||
@@ -14,15 +14,25 @@ use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend};
|
||||
use crate::events::ServerSentEventHandler;
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::migrate::BackgroundMigrator;
|
||||
use crate::naive_aggregation_pool::{Error as NaiveAggregationError, NaiveAggregationPool};
|
||||
use crate::observed_attestations::{Error as AttestationObservationError, ObservedAttestations};
|
||||
use crate::observed_attesters::{ObservedAggregators, ObservedAttesters};
|
||||
use crate::naive_aggregation_pool::{
|
||||
AggregatedAttestationMap, Error as NaiveAggregationError, NaiveAggregationPool,
|
||||
SyncContributionAggregateMap,
|
||||
};
|
||||
use crate::observed_aggregates::{
|
||||
Error as AttestationObservationError, ObservedAggregateAttestations, ObservedSyncContributions,
|
||||
};
|
||||
use crate::observed_attesters::{
|
||||
ObservedAggregators, ObservedAttesters, ObservedSyncAggregators, ObservedSyncContributors,
|
||||
};
|
||||
use crate::observed_block_producers::ObservedBlockProducers;
|
||||
use crate::observed_operations::{ObservationOutcome, ObservedOperations};
|
||||
use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT};
|
||||
use crate::persisted_fork_choice::PersistedForkChoice;
|
||||
use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache};
|
||||
use crate::snapshot_cache::SnapshotCache;
|
||||
use crate::sync_committee_verification::{
|
||||
Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution,
|
||||
};
|
||||
use crate::timeout_rw_lock::TimeoutRwLock;
|
||||
use crate::validator_monitor::{
|
||||
get_block_delay_ms, get_slot_delay_ms, timestamp_now, ValidatorMonitor,
|
||||
@@ -39,6 +49,7 @@ use itertools::process_results;
|
||||
use itertools::Itertools;
|
||||
use operation_pool::{OperationPool, PersistedOperationPool};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use safe_arith::SafeArith;
|
||||
use slasher::Slasher;
|
||||
use slog::{crit, debug, error, info, trace, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
@@ -221,14 +232,28 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
///
|
||||
/// This pool accepts `Attestation` objects that only have one aggregation bit set and provides
|
||||
/// a method to get an aggregated `Attestation` for some `AttestationData`.
|
||||
pub naive_aggregation_pool: RwLock<NaiveAggregationPool<T::EthSpec>>,
|
||||
pub naive_aggregation_pool: RwLock<NaiveAggregationPool<AggregatedAttestationMap<T::EthSpec>>>,
|
||||
/// A pool of `SyncCommitteeContribution` dedicated to the "naive aggregation strategy" defined in the eth2
|
||||
/// specs.
|
||||
///
|
||||
/// This pool accepts `SyncCommitteeContribution` objects that only have one aggregation bit set and provides
|
||||
/// a method to get an aggregated `SyncCommitteeContribution` for some `SyncCommitteeContributionData`.
|
||||
pub naive_sync_aggregation_pool:
|
||||
RwLock<NaiveAggregationPool<SyncContributionAggregateMap<T::EthSpec>>>,
|
||||
/// Contains a store of attestations which have been observed by the beacon chain.
|
||||
pub(crate) observed_attestations: RwLock<ObservedAttestations<T::EthSpec>>,
|
||||
pub(crate) observed_attestations: RwLock<ObservedAggregateAttestations<T::EthSpec>>,
|
||||
/// Contains a store of sync contributions which have been observed by the beacon chain.
|
||||
pub(crate) observed_sync_contributions: RwLock<ObservedSyncContributions<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have been seen to attest in recent epochs.
|
||||
pub(crate) observed_attesters: RwLock<ObservedAttesters<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have been seen sending sync messages in recent epochs.
|
||||
pub(crate) observed_sync_contributors: RwLock<ObservedSyncContributors<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have been seen to create `SignedAggregateAndProofs`
|
||||
/// in recent epochs.
|
||||
pub(crate) observed_aggregators: RwLock<ObservedAggregators<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have been seen to create `SignedContributionAndProofs`
|
||||
/// in recent epochs.
|
||||
pub(crate) observed_sync_aggregators: RwLock<ObservedSyncAggregators<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have proposed blocks for each slot.
|
||||
pub(crate) observed_block_producers: RwLock<ObservedBlockProducers<T::EthSpec>>,
|
||||
/// Maintains a record of which validators have submitted voluntary exits.
|
||||
@@ -823,6 +848,80 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the sync committee at `slot + 1` from the canonical chain.
|
||||
///
|
||||
/// This is useful when dealing with sync committee messages, because messages are signed
|
||||
/// and broadcast one slot prior to the slot of the sync committee (which is relevant at
|
||||
/// sync committee period boundaries).
|
||||
pub fn sync_committee_at_next_slot(
|
||||
&self,
|
||||
slot: Slot,
|
||||
) -> Result<Arc<SyncCommittee<T::EthSpec>>, Error> {
|
||||
let epoch = slot.safe_add(1)?.epoch(T::EthSpec::slots_per_epoch());
|
||||
self.sync_committee_at_epoch(epoch)
|
||||
}
|
||||
|
||||
/// Return the sync committee at `epoch` from the canonical chain.
|
||||
pub fn sync_committee_at_epoch(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
) -> Result<Arc<SyncCommittee<T::EthSpec>>, Error> {
|
||||
// Try to read a committee from the head. This will work most of the time, but will fail
|
||||
// for faraway committees, or if there are skipped slots at the transition to Altair.
|
||||
let spec = &self.spec;
|
||||
let committee_from_head =
|
||||
self.with_head(
|
||||
|head| match head.beacon_state.get_built_sync_committee(epoch, spec) {
|
||||
Ok(committee) => Ok(Some(committee.clone())),
|
||||
Err(BeaconStateError::SyncCommitteeNotKnown { .. })
|
||||
| Err(BeaconStateError::IncorrectStateVariant) => Ok(None),
|
||||
Err(e) => Err(Error::from(e)),
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some(committee) = committee_from_head {
|
||||
Ok(committee)
|
||||
} else {
|
||||
// Slow path: load a state (or advance the head).
|
||||
let sync_committee_period = epoch.sync_committee_period(spec)?;
|
||||
let committee = self
|
||||
.state_for_sync_committee_period(sync_committee_period)?
|
||||
.get_built_sync_committee(epoch, spec)?
|
||||
.clone();
|
||||
Ok(committee)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a state suitable for determining the sync committee for the given period.
|
||||
///
|
||||
/// Specifically, the state at the start of the *previous* sync committee period.
|
||||
///
|
||||
/// This is sufficient for historical duties, and efficient in the case where the head
|
||||
/// is lagging the current period and we need duties for the next period (because we only
|
||||
/// have to transition the head to start of the current period).
|
||||
///
|
||||
/// We also need to ensure that the load slot is after the Altair fork.
|
||||
///
|
||||
/// **WARNING**: the state returned will have dummy state roots. It should only be used
|
||||
/// for its sync committees (determining duties, etc).
|
||||
pub fn state_for_sync_committee_period(
|
||||
&self,
|
||||
sync_committee_period: u64,
|
||||
) -> Result<BeaconState<T::EthSpec>, Error> {
|
||||
let altair_fork_epoch = self
|
||||
.spec
|
||||
.altair_fork_epoch
|
||||
.ok_or(Error::AltairForkDisabled)?;
|
||||
|
||||
let load_slot = std::cmp::max(
|
||||
self.spec.epochs_per_sync_committee_period * sync_committee_period.saturating_sub(1),
|
||||
altair_fork_epoch,
|
||||
)
|
||||
.start_slot(T::EthSpec::slots_per_epoch());
|
||||
|
||||
self.state_at_slot(load_slot, StateSkipConfig::WithoutStateRoots)
|
||||
}
|
||||
|
||||
/// Returns info representing the head block and state.
|
||||
///
|
||||
/// A summarized version of `Self::head` that involves less cloning.
|
||||
@@ -1270,6 +1369,36 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some `SyncCommitteeMessage` from the network and attempts to verify it, returning `Ok(_)` if
|
||||
/// it is valid to be (re)broadcast on the gossip network.
|
||||
pub fn verify_sync_committee_message_for_gossip(
|
||||
&self,
|
||||
sync_message: SyncCommitteeMessage,
|
||||
subnet_id: SyncSubnetId,
|
||||
) -> Result<VerifiedSyncCommitteeMessage, SyncCommitteeError> {
|
||||
metrics::inc_counter(&metrics::SYNC_MESSAGE_PROCESSING_REQUESTS);
|
||||
let _timer = metrics::start_timer(&metrics::SYNC_MESSAGE_GOSSIP_VERIFICATION_TIMES);
|
||||
|
||||
VerifiedSyncCommitteeMessage::verify(sync_message, subnet_id, self).map(|v| {
|
||||
metrics::inc_counter(&metrics::SYNC_MESSAGE_PROCESSING_SUCCESSES);
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some `SignedContributionAndProof` from the network and attempts to verify it,
|
||||
/// returning `Ok(_)` if it is valid to be (re)broadcast on the gossip network.
|
||||
pub fn verify_sync_contribution_for_gossip(
|
||||
&self,
|
||||
sync_contribution: SignedContributionAndProof<T::EthSpec>,
|
||||
) -> Result<VerifiedSyncContribution<T>, SyncCommitteeError> {
|
||||
metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_PROCESSING_REQUESTS);
|
||||
let _timer = metrics::start_timer(&metrics::SYNC_CONTRIBUTION_GOSSIP_VERIFICATION_TIMES);
|
||||
VerifiedSyncContribution::verify(sync_contribution, self).map(|v| {
|
||||
metrics::inc_counter(&metrics::SYNC_CONTRIBUTION_PROCESSING_SUCCESSES);
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
/// Accepts some attestation-type object and attempts to verify it in the context of fork
|
||||
/// choice. If it is valid it is applied to `self.fork_choice`.
|
||||
///
|
||||
@@ -1339,6 +1468,70 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
Ok(unaggregated_attestation)
|
||||
}
|
||||
|
||||
/// Accepts a `VerifiedSyncCommitteeMessage` and attempts to apply it to the "naive
|
||||
/// aggregation pool".
|
||||
///
|
||||
/// The naive aggregation pool is used by local validators to produce
|
||||
/// `SignedContributionAndProof`.
|
||||
///
|
||||
/// If the sync message is too old (low slot) to be included in the pool it is simply dropped
|
||||
/// and no error is returned.
|
||||
pub fn add_to_naive_sync_aggregation_pool(
|
||||
&self,
|
||||
verified_sync_committee_message: VerifiedSyncCommitteeMessage,
|
||||
) -> Result<VerifiedSyncCommitteeMessage, SyncCommitteeError> {
|
||||
let sync_message = verified_sync_committee_message.sync_message();
|
||||
let positions_by_subnet_id: &HashMap<SyncSubnetId, Vec<usize>> =
|
||||
verified_sync_committee_message.subnet_positions();
|
||||
for (subnet_id, positions) in positions_by_subnet_id.iter() {
|
||||
for position in positions {
|
||||
let _timer =
|
||||
metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_APPLY_TO_AGG_POOL);
|
||||
let contribution = SyncCommitteeContribution::from_message(
|
||||
sync_message,
|
||||
subnet_id.into(),
|
||||
*position,
|
||||
)?;
|
||||
|
||||
match self
|
||||
.naive_sync_aggregation_pool
|
||||
.write()
|
||||
.insert(&contribution)
|
||||
{
|
||||
Ok(outcome) => trace!(
|
||||
self.log,
|
||||
"Stored unaggregated sync committee message";
|
||||
"outcome" => ?outcome,
|
||||
"index" => sync_message.validator_index,
|
||||
"slot" => sync_message.slot.as_u64(),
|
||||
),
|
||||
Err(NaiveAggregationError::SlotTooLow {
|
||||
slot,
|
||||
lowest_permissible_slot,
|
||||
}) => {
|
||||
trace!(
|
||||
self.log,
|
||||
"Refused to store unaggregated sync committee message";
|
||||
"lowest_permissible_slot" => lowest_permissible_slot.as_u64(),
|
||||
"slot" => slot.as_u64(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
self.log,
|
||||
"Failed to store unaggregated sync committee message";
|
||||
"error" => ?e,
|
||||
"index" => sync_message.validator_index,
|
||||
"slot" => sync_message.slot.as_u64(),
|
||||
);
|
||||
return Err(Error::from(e).into());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(verified_sync_committee_message)
|
||||
}
|
||||
|
||||
/// Accepts a `VerifiedAggregatedAttestation` and attempts to apply it to `self.op_pool`.
|
||||
///
|
||||
/// The op pool is used by local block producers to pack blocks with operations.
|
||||
@@ -1368,6 +1561,26 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
Ok(signed_aggregate)
|
||||
}
|
||||
|
||||
/// Accepts a `VerifiedSyncContribution` and attempts to apply it to `self.op_pool`.
|
||||
///
|
||||
/// The op pool is used by local block producers to pack blocks with operations.
|
||||
pub fn add_contribution_to_block_inclusion_pool(
|
||||
&self,
|
||||
contribution: VerifiedSyncContribution<T>,
|
||||
) -> Result<(), SyncCommitteeError> {
|
||||
let _timer = metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_APPLY_TO_OP_POOL);
|
||||
|
||||
// If there's no eth1 chain then it's impossible to produce blocks and therefore
|
||||
// useless to put things in the op pool.
|
||||
if self.eth1_chain.is_some() {
|
||||
self.op_pool
|
||||
.insert_sync_contribution(contribution.contribution())
|
||||
.map_err(Error::from)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Filter an attestation from the op pool for shuffling compatibility.
|
||||
///
|
||||
/// Use the provided `filter_cache` map to memoize results.
|
||||
@@ -1818,11 +2031,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// Iterate through the attestations in the block and register them as an "observed
|
||||
// attestation". This will stop us from propagating them on the gossip network.
|
||||
for a in signed_block.message().body().attestations() {
|
||||
match self
|
||||
.observed_attestations
|
||||
.write()
|
||||
.observe_attestation(a, None)
|
||||
{
|
||||
match self.observed_attestations.write().observe_item(a, None) {
|
||||
// If the observation was successful or if the slot for the attestation was too
|
||||
// low, continue.
|
||||
//
|
||||
|
||||
@@ -251,6 +251,13 @@ where
|
||||
.get_item::<PersistedOperationPool<TEthSpec>>(&OP_POOL_DB_KEY)
|
||||
.map_err(|e| format!("DB error whilst reading persisted op pool: {:?}", e))?
|
||||
.map(PersistedOperationPool::into_operation_pool)
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Error while creating the op pool from the persisted op pool: {:?}",
|
||||
e
|
||||
)
|
||||
})?
|
||||
.unwrap_or_else(OperationPool::new),
|
||||
);
|
||||
|
||||
@@ -506,12 +513,20 @@ where
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
naive_aggregation_pool: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
naive_sync_aggregation_pool: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_attestations: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_sync_contributions: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_attesters: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_sync_contributors: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_aggregators: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_sync_aggregators: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_block_producers: <_>::default(),
|
||||
// TODO: allow for persisting and loading the pool from disk.
|
||||
observed_voluntary_exits: <_>::default(),
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError;
|
||||
use crate::eth1_chain::Error as Eth1ChainError;
|
||||
use crate::migrate::PruningError;
|
||||
use crate::naive_aggregation_pool::Error as NaiveAggregationError;
|
||||
use crate::observed_attestations::Error as ObservedAttestationsError;
|
||||
use crate::observed_aggregates::Error as ObservedAttestationsError;
|
||||
use crate::observed_attesters::Error as ObservedAttestersError;
|
||||
use crate::observed_block_producers::Error as ObservedBlockProducersError;
|
||||
use futures::channel::mpsc::TrySendError;
|
||||
@@ -14,7 +14,7 @@ use state_processing::{
|
||||
block_signature_verifier::Error as BlockSignatureVerifierError,
|
||||
per_block_processing::errors::{
|
||||
AttestationValidationError, AttesterSlashingValidationError, ExitValidationError,
|
||||
ProposerSlashingValidationError,
|
||||
ProposerSlashingValidationError, SyncCommitteeMessageValidationError,
|
||||
},
|
||||
signature_sets::Error as SignatureSetError,
|
||||
state_advance::Error as StateAdvanceError,
|
||||
@@ -60,6 +60,7 @@ pub enum BeaconChainError {
|
||||
},
|
||||
CannotAttestToFutureState,
|
||||
AttestationValidationError(AttestationValidationError),
|
||||
SyncCommitteeMessageValidationError(SyncCommitteeMessageValidationError),
|
||||
ExitValidationError(ExitValidationError),
|
||||
ProposerSlashingValidationError(ProposerSlashingValidationError),
|
||||
AttesterSlashingValidationError(AttesterSlashingValidationError),
|
||||
@@ -121,10 +122,12 @@ pub enum BeaconChainError {
|
||||
old_slot: Slot,
|
||||
new_slot: Slot,
|
||||
},
|
||||
AltairForkDisabled,
|
||||
}
|
||||
|
||||
easy_from_to!(SlotProcessingError, BeaconChainError);
|
||||
easy_from_to!(AttestationValidationError, BeaconChainError);
|
||||
easy_from_to!(SyncCommitteeMessageValidationError, BeaconChainError);
|
||||
easy_from_to!(ExitValidationError, BeaconChainError);
|
||||
easy_from_to!(ProposerSlashingValidationError, BeaconChainError);
|
||||
easy_from_to!(AttesterSlashingValidationError, BeaconChainError);
|
||||
|
||||
@@ -14,7 +14,7 @@ mod head_tracker;
|
||||
mod metrics;
|
||||
pub mod migrate;
|
||||
mod naive_aggregation_pool;
|
||||
mod observed_attestations;
|
||||
mod observed_aggregates;
|
||||
mod observed_attesters;
|
||||
mod observed_block_producers;
|
||||
pub mod observed_operations;
|
||||
@@ -24,6 +24,7 @@ pub mod schema_change;
|
||||
mod shuffling_cache;
|
||||
mod snapshot_cache;
|
||||
pub mod state_advance_timer;
|
||||
pub mod sync_committee_verification;
|
||||
pub mod test_utils;
|
||||
mod timeout_rw_lock;
|
||||
pub mod validator_monitor;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::observed_attesters::SlotSubcommitteeIndex;
|
||||
use crate::types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
|
||||
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use lazy_static::lazy_static;
|
||||
pub use lighthouse_metrics::*;
|
||||
@@ -144,10 +146,6 @@ lazy_static! {
|
||||
"beacon_attestation_processing_apply_to_agg_pool",
|
||||
"Time spent applying an attestation to the naive aggregation pool"
|
||||
);
|
||||
pub static ref ATTESTATION_PROCESSING_AGG_POOL_MAPS_WRITE_LOCK: Result<Histogram> = try_create_histogram(
|
||||
"beacon_attestation_processing_agg_pool_maps_write_lock",
|
||||
"Time spent waiting for the maps write lock when adding to the agg poll"
|
||||
);
|
||||
pub static ref ATTESTATION_PROCESSING_AGG_POOL_PRUNE: Result<Histogram> = try_create_histogram(
|
||||
"beacon_attestation_processing_agg_pool_prune",
|
||||
"Time spent for the agg pool to prune"
|
||||
@@ -329,6 +327,8 @@ lazy_static! {
|
||||
try_create_int_gauge("beacon_op_pool_proposer_slashings_total", "Count of proposer slashings in the op pool");
|
||||
pub static ref OP_POOL_NUM_VOLUNTARY_EXITS: Result<IntGauge> =
|
||||
try_create_int_gauge("beacon_op_pool_voluntary_exits_total", "Count of voluntary exits in the op pool");
|
||||
pub static ref OP_POOL_NUM_SYNC_CONTRIBUTIONS: Result<IntGauge> =
|
||||
try_create_int_gauge("beacon_op_pool_sync_contributions_total", "Count of sync contributions in the op pool");
|
||||
|
||||
/*
|
||||
* Participation Metrics
|
||||
@@ -357,6 +357,18 @@ lazy_static! {
|
||||
"beacon_attn_observation_epoch_aggregators",
|
||||
"Count of aggregators that have been seen by the beacon chain in the previous epoch"
|
||||
);
|
||||
|
||||
/*
|
||||
* Sync Committee Observation Metrics
|
||||
*/
|
||||
pub static ref SYNC_COMM_OBSERVATION_PREV_SLOT_SIGNERS: Result<IntGauge> = try_create_int_gauge(
|
||||
"beacon_sync_comm_observation_slot_signers",
|
||||
"Count of sync committee contributors that have been seen by the beacon chain in the previous slot"
|
||||
);
|
||||
pub static ref SYNC_COMM_OBSERVATION_PREV_SLOT_AGGREGATORS: Result<IntGauge> = try_create_int_gauge(
|
||||
"beacon_sync_comm_observation_slot_aggregators",
|
||||
"Count of sync committee aggregators that have been seen by the beacon chain in the previous slot"
|
||||
);
|
||||
}
|
||||
|
||||
// Third lazy-static block is used to account for macro recursion limit.
|
||||
@@ -649,6 +661,93 @@ lazy_static! {
|
||||
);
|
||||
}
|
||||
|
||||
// Fourth lazy-static block is used to account for macro recursion limit.
|
||||
lazy_static! {
|
||||
/*
|
||||
* Sync Committee Message Verification
|
||||
*/
|
||||
pub static ref SYNC_MESSAGE_PROCESSING_REQUESTS: Result<IntCounter> = try_create_int_counter(
|
||||
"beacon_sync_committee_message_processing_requests_total",
|
||||
"Count of all sync messages submitted for processing"
|
||||
);
|
||||
pub static ref SYNC_MESSAGE_PROCESSING_SUCCESSES: Result<IntCounter> = try_create_int_counter(
|
||||
"beacon_sync_committee_message_processing_successes_total",
|
||||
"Number of sync messages verified for gossip"
|
||||
);
|
||||
pub static ref SYNC_MESSAGE_GOSSIP_VERIFICATION_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_committee_message_gossip_verification_seconds",
|
||||
"Full runtime of sync contribution gossip verification"
|
||||
);
|
||||
|
||||
/*
|
||||
* Sync Committee Contribution Verification
|
||||
*/
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_REQUESTS: Result<IntCounter> = try_create_int_counter(
|
||||
"beacon_sync_contribution_processing_requests_total",
|
||||
"Count of all sync contributions submitted for processing"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_SUCCESSES: Result<IntCounter> = try_create_int_counter(
|
||||
"beacon_sync_contribution_processing_successes_total",
|
||||
"Number of sync contributions verified for gossip"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_GOSSIP_VERIFICATION_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_gossip_verification_seconds",
|
||||
"Full runtime of sync contribution gossip verification"
|
||||
);
|
||||
|
||||
/*
|
||||
* General Sync Committee Contribution Processing
|
||||
*/
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_APPLY_TO_AGG_POOL: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_apply_to_agg_pool",
|
||||
"Time spent applying a sync contribution to the naive aggregation pool"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_PRUNE: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_agg_pool_prune",
|
||||
"Time spent for the agg pool to prune"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_INSERT: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_agg_pool_insert",
|
||||
"Time spent for the outer pool.insert() function of agg pool"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_CORE_INSERT: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_agg_pool_core_insert",
|
||||
"Time spent for the core map.insert() function of agg pool"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_AGGREGATION: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_agg_pool_aggregation",
|
||||
"Time spent doing signature aggregation when adding to the agg poll"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_CREATE_MAP: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_agg_pool_create_map",
|
||||
"Time spent for creating a map for a new slot"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_APPLY_TO_OP_POOL: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_apply_to_op_pool",
|
||||
"Time spent applying a sync contribution to the block inclusion pool"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_SIGNATURE_SETUP_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_signature_setup_seconds",
|
||||
"Time spent on setting up for the signature verification of sync contribution processing"
|
||||
);
|
||||
pub static ref SYNC_CONTRIBUTION_PROCESSING_SIGNATURE_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_contribution_processing_signature_seconds",
|
||||
"Time spent on the signature verification of sync contribution processing"
|
||||
);
|
||||
|
||||
/*
|
||||
* General Sync Committee Contribution Processing
|
||||
*/
|
||||
pub static ref SYNC_MESSAGE_PROCESSING_SIGNATURE_SETUP_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_committee_message_processing_signature_setup_seconds",
|
||||
"Time spent on setting up for the signature verification of sync message processing"
|
||||
);
|
||||
pub static ref SYNC_MESSAGE_PROCESSING_SIGNATURE_TIMES: Result<Histogram> = try_create_histogram(
|
||||
"beacon_sync_committee_message_processing_signature_seconds",
|
||||
"Time spent on the signature verification of sync message processing"
|
||||
);
|
||||
}
|
||||
|
||||
/// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot,
|
||||
/// head state info, etc) and update the Prometheus `DEFAULT_REGISTRY`.
|
||||
pub fn scrape_for_metrics<T: BeaconChainTypes>(beacon_chain: &BeaconChain<T>) {
|
||||
@@ -659,6 +758,7 @@ pub fn scrape_for_metrics<T: BeaconChainTypes>(beacon_chain: &BeaconChain<T>) {
|
||||
|
||||
if let Some(slot) = beacon_chain.slot_clock.now() {
|
||||
scrape_attestation_observation(slot, beacon_chain);
|
||||
scrape_sync_committee_observation(slot, beacon_chain);
|
||||
}
|
||||
|
||||
set_gauge_by_usize(
|
||||
@@ -677,6 +777,10 @@ pub fn scrape_for_metrics<T: BeaconChainTypes>(beacon_chain: &BeaconChain<T>) {
|
||||
&OP_POOL_NUM_VOLUNTARY_EXITS,
|
||||
beacon_chain.op_pool.num_voluntary_exits(),
|
||||
);
|
||||
set_gauge_by_usize(
|
||||
&OP_POOL_NUM_SYNC_CONTRIBUTIONS,
|
||||
beacon_chain.op_pool.num_sync_contributions(),
|
||||
);
|
||||
|
||||
beacon_chain
|
||||
.validator_monitor
|
||||
@@ -771,6 +875,34 @@ fn scrape_attestation_observation<T: BeaconChainTypes>(slot_now: Slot, chain: &B
|
||||
}
|
||||
}
|
||||
|
||||
fn scrape_sync_committee_observation<T: BeaconChainTypes>(slot_now: Slot, chain: &BeaconChain<T>) {
|
||||
let prev_slot = slot_now - 1;
|
||||
|
||||
let contributors = chain.observed_sync_contributors.read();
|
||||
let mut contributor_sum = 0;
|
||||
for i in 0..SYNC_COMMITTEE_SUBNET_COUNT {
|
||||
if let Some(count) =
|
||||
contributors.observed_validator_count(SlotSubcommitteeIndex::new(prev_slot, i))
|
||||
{
|
||||
contributor_sum += count;
|
||||
}
|
||||
}
|
||||
drop(contributors);
|
||||
set_gauge_by_usize(&SYNC_COMM_OBSERVATION_PREV_SLOT_SIGNERS, contributor_sum);
|
||||
|
||||
let sync_aggregators = chain.observed_sync_aggregators.read();
|
||||
let mut aggregator_sum = 0;
|
||||
for i in 0..SYNC_COMMITTEE_SUBNET_COUNT {
|
||||
if let Some(count) =
|
||||
sync_aggregators.observed_validator_count(SlotSubcommitteeIndex::new(prev_slot, i))
|
||||
{
|
||||
aggregator_sum += count;
|
||||
}
|
||||
}
|
||||
drop(sync_aggregators);
|
||||
set_gauge_by_usize(&SYNC_COMM_OBSERVATION_PREV_SLOT_AGGREGATORS, aggregator_sum);
|
||||
}
|
||||
|
||||
fn set_gauge_by_slot(gauge: &Result<IntGauge>, value: Slot) {
|
||||
set_gauge(gauge, value.as_u64() as i64);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use crate::metrics;
|
||||
use std::collections::HashMap;
|
||||
use tree_hash::TreeHash;
|
||||
use types::{Attestation, AttestationData, EthSpec, Hash256, Slot};
|
||||
use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
|
||||
use types::slot_data::SlotData;
|
||||
use types::sync_committee_contribution::SyncContributionData;
|
||||
use types::{Attestation, AttestationData, EthSpec, Hash256, Slot, SyncCommitteeContribution};
|
||||
|
||||
type AttestationDataRoot = Hash256;
|
||||
type SyncDataRoot = Hash256;
|
||||
|
||||
/// The number of slots that will be stored in the pool.
|
||||
///
|
||||
/// For example, if `SLOTS_RETAINED == 3` and the pool is pruned at slot `6`, then all attestations
|
||||
/// at slots less than `4` will be dropped and any future attestation with a slot less than `4`
|
||||
/// For example, if `SLOTS_RETAINED == 3` and the pool is pruned at slot `6`, then all items
|
||||
/// at slots less than `4` will be dropped and any future item with a slot less than `4`
|
||||
/// will be refused.
|
||||
const SLOTS_RETAINED: usize = 3;
|
||||
|
||||
@@ -16,51 +21,98 @@ const SLOTS_RETAINED: usize = 3;
|
||||
/// This is a DoS protection measure.
|
||||
const MAX_ATTESTATIONS_PER_SLOT: usize = 16_384;
|
||||
|
||||
/// Returned upon successfully inserting an attestation into the pool.
|
||||
/// Returned upon successfully inserting an item into the pool.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InsertOutcome {
|
||||
/// The `attestation.data` had not been seen before and was added to the pool.
|
||||
NewAttestationData { committee_index: usize },
|
||||
/// A validator signature for the given `attestation.data` was already known. No changes were
|
||||
/// The item had not been seen before and was added to the pool.
|
||||
NewItemInserted { committee_index: usize },
|
||||
/// A validator signature for the given item's `Data` was already known. No changes were
|
||||
/// made.
|
||||
SignatureAlreadyKnown { committee_index: usize },
|
||||
/// The `attestation.data` was known, but a signature for the given validator was not yet
|
||||
/// The item's `Data` was known, but a signature for the given validator was not yet
|
||||
/// known. The signature was aggregated into the pool.
|
||||
SignatureAggregated { committee_index: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// The given `attestation.data.slot` was too low to be stored. No changes were made.
|
||||
/// The given `data.slot` was too low to be stored. No changes were made.
|
||||
SlotTooLow {
|
||||
slot: Slot,
|
||||
lowest_permissible_slot: Slot,
|
||||
},
|
||||
/// The given `attestation.aggregation_bits` field was empty.
|
||||
/// The given `aggregation_bits` field was empty.
|
||||
NoAggregationBitsSet,
|
||||
/// The given `attestation.aggregation_bits` field had more than one signature. The number of
|
||||
/// The given `aggregation_bits` field had more than one signature. The number of
|
||||
/// signatures found is included.
|
||||
MoreThanOneAggregationBitSet(usize),
|
||||
/// We have reached the maximum number of unique `AttestationData` that can be stored in a
|
||||
/// We have reached the maximum number of unique items that can be stored in a
|
||||
/// slot. This is a DoS protection function.
|
||||
ReachedMaxAttestationsPerSlot(usize),
|
||||
/// The given `attestation.aggregation_bits` field had a different length to the one currently
|
||||
ReachedMaxItemsPerSlot(usize),
|
||||
/// The given `aggregation_bits` field had a different length to the one currently
|
||||
/// stored. This indicates a fairly serious error somewhere in the code that called this
|
||||
/// function.
|
||||
InconsistentBitfieldLengths,
|
||||
/// The given `attestation` was for the incorrect slot. This is an internal error.
|
||||
IncorrectSlot { expected: Slot, attestation: Slot },
|
||||
/// The given item was for the incorrect slot. This is an internal error.
|
||||
IncorrectSlot { expected: Slot, actual: Slot },
|
||||
}
|
||||
|
||||
/// Implemented for items in the `NaiveAggregationPool`. Requires that items implement `SlotData`,
|
||||
/// which means they have an associated slot. This handles aggregation of items that are inserted.
|
||||
pub trait AggregateMap {
|
||||
/// `Key` should be a hash of `Data`.
|
||||
type Key;
|
||||
|
||||
/// The item stored in the map
|
||||
type Value: Clone + SlotData;
|
||||
|
||||
/// The unique fields of `Value`, hashed to create `Key`.
|
||||
type Data: SlotData;
|
||||
|
||||
/// Create a new `AggregateMap` with capacity `initial_capacity`.
|
||||
fn new(initial_capacity: usize) -> Self;
|
||||
|
||||
/// Insert a `Value` into `Self`, returning a result.
|
||||
fn insert(&mut self, value: &Self::Value) -> Result<InsertOutcome, Error>;
|
||||
|
||||
/// Get a `Value` from `Self` based on `Data`.
|
||||
fn get(&self, data: &Self::Data) -> Option<Self::Value>;
|
||||
|
||||
/// Get a reference to the inner `HashMap`.
|
||||
fn get_map(&self) -> &HashMap<Self::Key, Self::Value>;
|
||||
|
||||
/// Get a `Value` from `Self` based on `Key`, which is a hash of `Data`.
|
||||
fn get_by_root(&self, root: &Self::Key) -> Option<&Self::Value>;
|
||||
|
||||
/// The number of items store in `Self`.
|
||||
fn len(&self) -> usize;
|
||||
|
||||
/// Start a timer observing inserts.
|
||||
fn start_insert_timer() -> Option<metrics::HistogramTimer>;
|
||||
|
||||
/// Start a timer observing the time it takes to create a new map for a new slot.
|
||||
fn start_create_map_timer() -> Option<metrics::HistogramTimer>;
|
||||
|
||||
/// Start a timer observing the time it takes to prune the pool.
|
||||
fn start_prune_timer() -> Option<metrics::HistogramTimer>;
|
||||
|
||||
/// The default capacity of `Self`.
|
||||
fn default_capacity() -> usize;
|
||||
}
|
||||
|
||||
/// A collection of `Attestation` objects, keyed by their `attestation.data`. Enforces that all
|
||||
/// `attestation` are from the same slot.
|
||||
struct AggregatedAttestationMap<E: EthSpec> {
|
||||
pub struct AggregatedAttestationMap<E: EthSpec> {
|
||||
map: HashMap<AttestationDataRoot, Attestation<E>>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> AggregatedAttestationMap<E> {
|
||||
impl<E: EthSpec> AggregateMap for AggregatedAttestationMap<E> {
|
||||
type Key = AttestationDataRoot;
|
||||
type Value = Attestation<E>;
|
||||
type Data = AttestationData;
|
||||
|
||||
/// Create an empty collection with the given `initial_capacity`.
|
||||
pub fn new(initial_capacity: usize) -> Self {
|
||||
fn new(initial_capacity: usize) -> Self {
|
||||
Self {
|
||||
map: HashMap::with_capacity(initial_capacity),
|
||||
}
|
||||
@@ -69,7 +121,7 @@ impl<E: EthSpec> AggregatedAttestationMap<E> {
|
||||
/// Insert an attestation into `self`, aggregating it into the pool.
|
||||
///
|
||||
/// The given attestation (`a`) must only have one signature.
|
||||
pub fn insert(&mut self, a: &Attestation<E>) -> Result<InsertOutcome, Error> {
|
||||
fn insert(&mut self, a: &Self::Value) -> Result<InsertOutcome, Error> {
|
||||
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_CORE_INSERT);
|
||||
|
||||
let set_bits = a
|
||||
@@ -106,65 +158,190 @@ impl<E: EthSpec> AggregatedAttestationMap<E> {
|
||||
}
|
||||
} else {
|
||||
if self.map.len() >= MAX_ATTESTATIONS_PER_SLOT {
|
||||
return Err(Error::ReachedMaxAttestationsPerSlot(
|
||||
MAX_ATTESTATIONS_PER_SLOT,
|
||||
));
|
||||
return Err(Error::ReachedMaxItemsPerSlot(MAX_ATTESTATIONS_PER_SLOT));
|
||||
}
|
||||
|
||||
self.map.insert(attestation_data_root, a.clone());
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index })
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index })
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an aggregated `Attestation` with the given `data`, if any.
|
||||
///
|
||||
/// The given `a.data.slot` must match the slot that `self` was initialized with.
|
||||
pub fn get(&self, data: &AttestationData) -> Option<Attestation<E>> {
|
||||
fn get(&self, data: &Self::Data) -> Option<Self::Value> {
|
||||
self.map.get(&data.tree_hash_root()).cloned()
|
||||
}
|
||||
|
||||
fn get_map(&self) -> &HashMap<Self::Key, Self::Value> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
/// Returns an aggregated `Attestation` with the given `root`, if any.
|
||||
pub fn get_by_root(&self, root: &AttestationDataRoot) -> Option<&Attestation<E>> {
|
||||
fn get_by_root(&self, root: &Self::Key) -> Option<&Self::Value> {
|
||||
self.map.get(root)
|
||||
}
|
||||
|
||||
/// Iterate all attestations in `self`.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Attestation<E>> {
|
||||
self.map.iter().map(|(_key, attestation)| attestation)
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
fn start_insert_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_INSERT)
|
||||
}
|
||||
|
||||
fn start_create_map_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_CREATE_MAP)
|
||||
}
|
||||
|
||||
fn start_prune_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_PRUNE)
|
||||
}
|
||||
|
||||
/// Use the `TARGET_COMMITTEE_SIZE`.
|
||||
///
|
||||
/// Note: hard-coded until `TARGET_COMMITTEE_SIZE` is available via `EthSpec`.
|
||||
fn default_capacity() -> usize {
|
||||
128
|
||||
}
|
||||
}
|
||||
|
||||
/// A pool of `Attestation` that is specially designed to store "unaggregated" attestations from
|
||||
/// the native aggregation scheme.
|
||||
/// A collection of `SyncCommitteeContribution`, keyed by their `SyncContributionData`. Enforces that all
|
||||
/// contributions are from the same slot.
|
||||
pub struct SyncContributionAggregateMap<E: EthSpec> {
|
||||
map: HashMap<SyncDataRoot, SyncCommitteeContribution<E>>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> AggregateMap for SyncContributionAggregateMap<E> {
|
||||
type Key = SyncDataRoot;
|
||||
type Value = SyncCommitteeContribution<E>;
|
||||
type Data = SyncContributionData;
|
||||
|
||||
/// Create an empty collection with the given `initial_capacity`.
|
||||
fn new(initial_capacity: usize) -> Self {
|
||||
Self {
|
||||
map: HashMap::with_capacity(initial_capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a sync committee contribution into `self`, aggregating it into the pool.
|
||||
///
|
||||
/// The given sync contribution must only have one signature.
|
||||
fn insert(
|
||||
&mut self,
|
||||
contribution: &SyncCommitteeContribution<E>,
|
||||
) -> Result<InsertOutcome, Error> {
|
||||
let _timer =
|
||||
metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_CORE_INSERT);
|
||||
|
||||
let set_bits = contribution
|
||||
.aggregation_bits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_i, bit)| *bit)
|
||||
.map(|(i, _bit)| i)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let committee_index = set_bits
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or(Error::NoAggregationBitsSet)?;
|
||||
|
||||
if set_bits.len() > 1 {
|
||||
return Err(Error::MoreThanOneAggregationBitSet(set_bits.len()));
|
||||
}
|
||||
|
||||
let sync_data_root = SyncContributionData::from_contribution(contribution).tree_hash_root();
|
||||
|
||||
if let Some(existing_contribution) = self.map.get_mut(&sync_data_root) {
|
||||
if existing_contribution
|
||||
.aggregation_bits
|
||||
.get(committee_index)
|
||||
.map_err(|_| Error::InconsistentBitfieldLengths)?
|
||||
{
|
||||
Ok(InsertOutcome::SignatureAlreadyKnown { committee_index })
|
||||
} else {
|
||||
let _timer = metrics::start_timer(
|
||||
&metrics::SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_AGGREGATION,
|
||||
);
|
||||
existing_contribution.aggregate(contribution);
|
||||
Ok(InsertOutcome::SignatureAggregated { committee_index })
|
||||
}
|
||||
} else {
|
||||
if self.map.len() >= E::sync_committee_size() {
|
||||
return Err(Error::ReachedMaxItemsPerSlot(E::sync_committee_size()));
|
||||
}
|
||||
|
||||
self.map.insert(sync_data_root, contribution.clone());
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index })
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an aggregated `SyncCommitteeContribution` with the given `data`, if any.
|
||||
///
|
||||
/// The given `data.slot` must match the slot that `self` was initialized with.
|
||||
fn get(&self, data: &SyncContributionData) -> Option<SyncCommitteeContribution<E>> {
|
||||
self.map.get(&data.tree_hash_root()).cloned()
|
||||
}
|
||||
|
||||
fn get_map(&self) -> &HashMap<SyncDataRoot, SyncCommitteeContribution<E>> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
/// Returns an aggregated `SyncCommitteeContribution` with the given `root`, if any.
|
||||
fn get_by_root(&self, root: &SyncDataRoot) -> Option<&SyncCommitteeContribution<E>> {
|
||||
self.map.get(root)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn start_insert_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_INSERT)
|
||||
}
|
||||
|
||||
fn start_create_map_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_CREATE_MAP)
|
||||
}
|
||||
|
||||
fn start_prune_timer() -> Option<metrics::HistogramTimer> {
|
||||
metrics::start_timer(&metrics::SYNC_CONTRIBUTION_PROCESSING_AGG_POOL_PRUNE)
|
||||
}
|
||||
|
||||
/// Default to `SYNC_COMMITTEE_SUBNET_COUNT`.
|
||||
fn default_capacity() -> usize {
|
||||
SYNC_COMMITTEE_SUBNET_COUNT as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// A pool of `Attestation` or `SyncCommitteeContribution` that is specially designed to store
|
||||
/// "unaggregated" messages from the native aggregation scheme.
|
||||
///
|
||||
/// **The `NaiveAggregationPool` does not do any signature or attestation verification. It assumes
|
||||
/// that all `Attestation` objects provided are valid.**
|
||||
/// **The `NaiveAggregationPool` does not do any verification. It assumes that all `Attestation`
|
||||
/// or `SyncCommitteeContribution` objects provided are valid.**
|
||||
///
|
||||
/// ## Details
|
||||
///
|
||||
/// The pool sorts the `Attestation` by `attestation.data.slot`, then by `attestation.data`.
|
||||
/// The pool sorts the items by `slot`, then by `Data`.
|
||||
///
|
||||
/// As each unaggregated attestation is added it is aggregated with any existing `attestation` with
|
||||
/// the same `AttestationData`. Considering that the pool only accepts attestations with a single
|
||||
/// As each item is added it is aggregated with any existing item with the same `Data`. Considering
|
||||
/// that the pool only accepts attestations or sync contributions with a single
|
||||
/// signature, there should only ever be a single aggregated `Attestation` for any given
|
||||
/// `AttestationData`.
|
||||
/// `AttestationData` or a single `SyncCommitteeContribution` for any given `SyncContributionData`.
|
||||
///
|
||||
/// The pool has a capacity for `SLOTS_RETAINED` slots, when a new `attestation.data.slot` is
|
||||
/// The pool has a capacity for `SLOTS_RETAINED` slots, when a new `slot` is
|
||||
/// provided, the oldest slot is dropped and replaced with the new slot. The pool can also be
|
||||
/// pruned by supplying a `current_slot`; all existing attestations with a slot lower than
|
||||
/// `current_slot - SLOTS_RETAINED` will be removed and any future attestation with a slot lower
|
||||
/// than that will also be refused. Pruning is done automatically based upon the attestations it
|
||||
/// pruned by supplying a `current_slot`; all existing items with a slot lower than
|
||||
/// `current_slot - SLOTS_RETAINED` will be removed and any future item with a slot lower
|
||||
/// than that will also be refused. Pruning is done automatically based upon the items it
|
||||
/// receives and it can be triggered manually.
|
||||
pub struct NaiveAggregationPool<E: EthSpec> {
|
||||
pub struct NaiveAggregationPool<T: AggregateMap> {
|
||||
lowest_permissible_slot: Slot,
|
||||
maps: HashMap<Slot, AggregatedAttestationMap<E>>,
|
||||
maps: HashMap<Slot, T>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> Default for NaiveAggregationPool<E> {
|
||||
impl<T: AggregateMap> Default for NaiveAggregationPool<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lowest_permissible_slot: Slot::new(0),
|
||||
@@ -173,20 +350,20 @@ impl<E: EthSpec> Default for NaiveAggregationPool<E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> NaiveAggregationPool<E> {
|
||||
/// Insert an attestation into `self`, aggregating it into the pool.
|
||||
impl<T: AggregateMap> NaiveAggregationPool<T> {
|
||||
/// Insert an item into `self`, aggregating it into the pool.
|
||||
///
|
||||
/// The given attestation (`a`) must only have one signature and have an
|
||||
/// `attestation.data.slot` that is not lower than `self.lowest_permissible_slot`.
|
||||
/// The given item must only have one signature and have an
|
||||
/// `slot` that is not lower than `self.lowest_permissible_slot`.
|
||||
///
|
||||
/// The pool may be pruned if the given `attestation.data` has a slot higher than any
|
||||
/// The pool may be pruned if the given item has a slot higher than any
|
||||
/// previously seen.
|
||||
pub fn insert(&mut self, attestation: &Attestation<E>) -> Result<InsertOutcome, Error> {
|
||||
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_INSERT);
|
||||
let slot = attestation.data.slot;
|
||||
pub fn insert(&mut self, item: &T::Value) -> Result<InsertOutcome, Error> {
|
||||
let _timer = T::start_insert_timer();
|
||||
let slot = item.get_slot();
|
||||
let lowest_permissible_slot = self.lowest_permissible_slot;
|
||||
|
||||
// Reject any attestations that are too old.
|
||||
// Reject any items that are too old.
|
||||
if slot < lowest_permissible_slot {
|
||||
return Err(Error::SlotTooLow {
|
||||
slot,
|
||||
@@ -194,14 +371,10 @@ impl<E: EthSpec> NaiveAggregationPool<E> {
|
||||
});
|
||||
}
|
||||
|
||||
let lock_timer =
|
||||
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_MAPS_WRITE_LOCK);
|
||||
drop(lock_timer);
|
||||
|
||||
let outcome = if let Some(map) = self.maps.get_mut(&slot) {
|
||||
map.insert(attestation)
|
||||
map.insert(item)
|
||||
} else {
|
||||
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_CREATE_MAP);
|
||||
let _timer = T::start_create_map_timer();
|
||||
// To avoid re-allocations, try and determine a rough initial capacity for the new item
|
||||
// by obtaining the mean size of all items in earlier epoch.
|
||||
let (count, sum) = self
|
||||
@@ -213,12 +386,11 @@ impl<E: EthSpec> NaiveAggregationPool<E> {
|
||||
.map(|(_slot, map)| map.len())
|
||||
.fold((0, 0), |(count, sum), len| (count + 1, sum + len));
|
||||
|
||||
// Use the mainnet default committee size if we can't determine an average.
|
||||
let initial_capacity = sum.checked_div(count).unwrap_or(128);
|
||||
let initial_capacity = sum.checked_div(count).unwrap_or_else(T::default_capacity);
|
||||
|
||||
let mut item = AggregatedAttestationMap::new(initial_capacity);
|
||||
let outcome = item.insert(attestation);
|
||||
self.maps.insert(slot, item);
|
||||
let mut aggregate_map = T::new(initial_capacity);
|
||||
let outcome = aggregate_map.insert(item);
|
||||
self.maps.insert(slot, aggregate_map);
|
||||
|
||||
outcome
|
||||
};
|
||||
@@ -228,39 +400,39 @@ impl<E: EthSpec> NaiveAggregationPool<E> {
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Returns the total number of attestations stored in `self`.
|
||||
pub fn num_attestations(&self) -> usize {
|
||||
/// Returns the total number of items stored in `self`.
|
||||
pub fn num_items(&self) -> usize {
|
||||
self.maps.iter().map(|(_, map)| map.len()).sum()
|
||||
}
|
||||
|
||||
/// Returns an aggregated `Attestation` with the given `data`, if any.
|
||||
pub fn get(&self, data: &AttestationData) -> Option<Attestation<E>> {
|
||||
self.maps.get(&data.slot).and_then(|map| map.get(data))
|
||||
/// Returns an aggregated `T::Value` with the given `T::Data`, if any.
|
||||
pub fn get(&self, data: &T::Data) -> Option<T::Value> {
|
||||
self.maps
|
||||
.get(&data.get_slot())
|
||||
.and_then(|map| map.get(data))
|
||||
}
|
||||
|
||||
/// Returns an aggregated `Attestation` with the given `data`, if any.
|
||||
pub fn get_by_slot_and_root(
|
||||
&self,
|
||||
slot: Slot,
|
||||
root: &AttestationDataRoot,
|
||||
) -> Option<Attestation<E>> {
|
||||
/// Returns an aggregated `T::Value` with the given `slot` and `root`, if any.
|
||||
pub fn get_by_slot_and_root(&self, slot: Slot, root: &T::Key) -> Option<T::Value> {
|
||||
self.maps
|
||||
.get(&slot)
|
||||
.and_then(|map| map.get_by_root(root).cloned())
|
||||
}
|
||||
|
||||
/// Iterate all attestations in all slots of `self`.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Attestation<E>> {
|
||||
self.maps.iter().map(|(_slot, map)| map.iter()).flatten()
|
||||
/// Iterate all items in all slots of `self`.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T::Value> {
|
||||
self.maps
|
||||
.iter()
|
||||
.map(|(_slot, map)| map.get_map().iter().map(|(_key, value)| value))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Removes any attestations with a slot lower than `current_slot` and bars any future
|
||||
/// attestations with a slot lower than `current_slot - SLOTS_RETAINED`.
|
||||
/// Removes any items with a slot lower than `current_slot` and bars any future
|
||||
/// items with a slot lower than `current_slot - SLOTS_RETAINED`.
|
||||
pub fn prune(&mut self, current_slot: Slot) {
|
||||
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_PRUNE);
|
||||
let _timer = T::start_prune_timer();
|
||||
|
||||
// Taking advantage of saturating subtraction on `Slot`.
|
||||
let lowest_permissible_slot = current_slot - Slot::from(SLOTS_RETAINED);
|
||||
let lowest_permissible_slot = current_slot.saturating_sub(Slot::from(SLOTS_RETAINED));
|
||||
|
||||
// No need to prune if the lowest permissible slot has not changed and the queue length is
|
||||
// less than the maximum
|
||||
@@ -301,9 +473,10 @@ impl<E: EthSpec> NaiveAggregationPool<E> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ssz_types::BitList;
|
||||
use store::BitVector;
|
||||
use types::{
|
||||
test_utils::{generate_deterministic_keypair, test_random_instance},
|
||||
Fork, Hash256,
|
||||
Fork, Hash256, SyncCommitteeMessage,
|
||||
};
|
||||
|
||||
type E = types::MainnetEthSpec;
|
||||
@@ -315,7 +488,14 @@ mod tests {
|
||||
a
|
||||
}
|
||||
|
||||
fn sign(a: &mut Attestation<E>, i: usize, genesis_validators_root: Hash256) {
|
||||
fn get_sync_contribution(slot: Slot) -> SyncCommitteeContribution<E> {
|
||||
let mut a: SyncCommitteeContribution<E> = test_random_instance();
|
||||
a.slot = slot;
|
||||
a.aggregation_bits = BitVector::new();
|
||||
a
|
||||
}
|
||||
|
||||
fn sign_attestation(a: &mut Attestation<E>, i: usize, genesis_validators_root: Hash256) {
|
||||
a.sign(
|
||||
&generate_deterministic_keypair(i).sk,
|
||||
i,
|
||||
@@ -326,187 +506,294 @@ mod tests {
|
||||
.expect("should sign attestation");
|
||||
}
|
||||
|
||||
fn unset_bit(a: &mut Attestation<E>, i: usize) {
|
||||
fn sign_sync_contribution(
|
||||
a: &mut SyncCommitteeContribution<E>,
|
||||
i: usize,
|
||||
genesis_validators_root: Hash256,
|
||||
) {
|
||||
let sync_message = SyncCommitteeMessage::new::<E>(
|
||||
a.slot,
|
||||
a.beacon_block_root,
|
||||
i as u64,
|
||||
&generate_deterministic_keypair(i).sk,
|
||||
&Fork::default(),
|
||||
genesis_validators_root,
|
||||
&E::default_spec(),
|
||||
);
|
||||
let signed_contribution: SyncCommitteeContribution<E> =
|
||||
SyncCommitteeContribution::from_message(&sync_message, a.subcommittee_index, i)
|
||||
.unwrap();
|
||||
|
||||
a.aggregate(&signed_contribution);
|
||||
}
|
||||
|
||||
fn unset_attestation_bit(a: &mut Attestation<E>, i: usize) {
|
||||
a.aggregation_bits
|
||||
.set(i, false)
|
||||
.expect("should unset aggregation bit")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_attestation() {
|
||||
let mut a = get_attestation(Slot::new(0));
|
||||
|
||||
let mut pool = NaiveAggregationPool::default();
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Err(Error::NoAggregationBitsSet),
|
||||
"should not accept attestation without any signatures"
|
||||
);
|
||||
|
||||
sign(&mut a, 0, Hash256::random());
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index: 0 }),
|
||||
"should accept new attestation"
|
||||
);
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::SignatureAlreadyKnown { committee_index: 0 }),
|
||||
"should acknowledge duplicate signature"
|
||||
);
|
||||
|
||||
let retrieved = pool
|
||||
.get(&a.data)
|
||||
.expect("should not error while getting attestation");
|
||||
assert_eq!(
|
||||
retrieved, a,
|
||||
"retrieved attestation should equal the one inserted"
|
||||
);
|
||||
|
||||
sign(&mut a, 1, Hash256::random());
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Err(Error::MoreThanOneAggregationBitSet(2)),
|
||||
"should not accept attestation with multiple signatures"
|
||||
);
|
||||
fn unset_sync_contribution_bit(a: &mut SyncCommitteeContribution<E>, i: usize) {
|
||||
a.aggregation_bits
|
||||
.set(i, false)
|
||||
.expect("should unset aggregation bit")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_attestations() {
|
||||
let mut a_0 = get_attestation(Slot::new(0));
|
||||
let mut a_1 = a_0.clone();
|
||||
|
||||
let genesis_validators_root = Hash256::random();
|
||||
sign(&mut a_0, 0, genesis_validators_root);
|
||||
sign(&mut a_1, 1, genesis_validators_root);
|
||||
|
||||
let mut pool = NaiveAggregationPool::default();
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a_0),
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index: 0 }),
|
||||
"should accept a_0"
|
||||
);
|
||||
assert_eq!(
|
||||
pool.insert(&a_1),
|
||||
Ok(InsertOutcome::SignatureAggregated { committee_index: 1 }),
|
||||
"should accept a_1"
|
||||
);
|
||||
|
||||
let retrieved = pool
|
||||
.get(&a_0.data)
|
||||
.expect("should not error while getting attestation");
|
||||
|
||||
let mut a_01 = a_0.clone();
|
||||
a_01.aggregate(&a_1);
|
||||
|
||||
assert_eq!(
|
||||
retrieved, a_01,
|
||||
"retrieved attestation should be aggregated"
|
||||
);
|
||||
|
||||
/*
|
||||
* Throw a different attestation data in there and ensure it isn't aggregated
|
||||
*/
|
||||
|
||||
let mut a_different = a_0.clone();
|
||||
let different_root = Hash256::from_low_u64_be(1337);
|
||||
unset_bit(&mut a_different, 0);
|
||||
sign(&mut a_different, 2, genesis_validators_root);
|
||||
assert_ne!(a_different.data.beacon_block_root, different_root);
|
||||
a_different.data.beacon_block_root = different_root;
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a_different),
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index: 2 }),
|
||||
"should accept a_different"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pool.get(&a_0.data)
|
||||
.expect("should not error while getting attestation"),
|
||||
retrieved,
|
||||
"should not have aggregated different attestation data"
|
||||
);
|
||||
fn mutate_attestation_block_root(a: &mut Attestation<E>, block_root: Hash256) {
|
||||
a.data.beacon_block_root = block_root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_pruning() {
|
||||
let mut base = get_attestation(Slot::new(0));
|
||||
sign(&mut base, 0, Hash256::random());
|
||||
fn mutate_attestation_slot(a: &mut Attestation<E>, slot: Slot) {
|
||||
a.data.slot = slot
|
||||
}
|
||||
|
||||
let mut pool = NaiveAggregationPool::default();
|
||||
fn attestation_block_root_comparator(a: &Attestation<E>, block_root: Hash256) -> bool {
|
||||
a.data.beacon_block_root == block_root
|
||||
}
|
||||
|
||||
for i in 0..SLOTS_RETAINED * 2 {
|
||||
let slot = Slot::from(i);
|
||||
let mut a = base.clone();
|
||||
a.data.slot = slot;
|
||||
fn key_from_attestation(a: &Attestation<E>) -> AttestationData {
|
||||
a.data.clone()
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index: 0 }),
|
||||
"should accept new attestation"
|
||||
);
|
||||
fn mutate_sync_contribution_block_root(
|
||||
a: &mut SyncCommitteeContribution<E>,
|
||||
block_root: Hash256,
|
||||
) {
|
||||
a.beacon_block_root = block_root
|
||||
}
|
||||
|
||||
if i < SLOTS_RETAINED {
|
||||
let len = i + 1;
|
||||
assert_eq!(pool.maps.len(), len, "the pool should have length {}", len);
|
||||
} else {
|
||||
assert_eq!(
|
||||
pool.maps.len(),
|
||||
SLOTS_RETAINED,
|
||||
"the pool should have length SLOTS_RETAINED"
|
||||
);
|
||||
fn mutate_sync_contribution_slot(a: &mut SyncCommitteeContribution<E>, slot: Slot) {
|
||||
a.slot = slot
|
||||
}
|
||||
|
||||
let mut pool_slots = pool
|
||||
.maps
|
||||
.iter()
|
||||
.map(|(slot, _map)| *slot)
|
||||
.collect::<Vec<_>>();
|
||||
fn sync_contribution_block_root_comparator(
|
||||
a: &SyncCommitteeContribution<E>,
|
||||
block_root: Hash256,
|
||||
) -> bool {
|
||||
a.beacon_block_root == block_root
|
||||
}
|
||||
|
||||
pool_slots.sort_unstable();
|
||||
fn key_from_sync_contribution(a: &SyncCommitteeContribution<E>) -> SyncContributionData {
|
||||
SyncContributionData::from_contribution(&a)
|
||||
}
|
||||
|
||||
macro_rules! test_suite {
|
||||
(
|
||||
$mod_name: ident,
|
||||
$get_method_name: ident,
|
||||
$sign_method_name: ident,
|
||||
$unset_method_name: ident,
|
||||
$block_root_mutator: ident,
|
||||
$slot_mutator: ident,
|
||||
$block_root_comparator: ident,
|
||||
$key_getter: ident,
|
||||
$map_type: ident,
|
||||
$item_limit: expr
|
||||
) => {
|
||||
#[cfg(test)]
|
||||
mod $mod_name {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_item() {
|
||||
let mut a = $get_method_name(Slot::new(0));
|
||||
|
||||
let mut pool: NaiveAggregationPool<$map_type<E>> =
|
||||
NaiveAggregationPool::default();
|
||||
|
||||
for (j, pool_slot) in pool_slots.iter().enumerate() {
|
||||
let expected_slot = slot - (SLOTS_RETAINED - 1 - j) as u64;
|
||||
assert_eq!(
|
||||
*pool_slot, expected_slot,
|
||||
"the slot of the map should be {}",
|
||||
expected_slot
|
||||
)
|
||||
pool.insert(&a),
|
||||
Err(Error::NoAggregationBitsSet),
|
||||
"should not accept item without any signatures"
|
||||
);
|
||||
|
||||
$sign_method_name(&mut a, 0, Hash256::random());
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index: 0 }),
|
||||
"should accept new item"
|
||||
);
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::SignatureAlreadyKnown { committee_index: 0 }),
|
||||
"should acknowledge duplicate signature"
|
||||
);
|
||||
|
||||
let retrieved = pool
|
||||
.get(&$key_getter(&a))
|
||||
.expect("should not error while getting item");
|
||||
assert_eq!(retrieved, a, "retrieved item should equal the one inserted");
|
||||
|
||||
$sign_method_name(&mut a, 1, Hash256::random());
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Err(Error::MoreThanOneAggregationBitSet(2)),
|
||||
"should not accept item with multiple signatures"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_items() {
|
||||
let mut a_0 = $get_method_name(Slot::new(0));
|
||||
let mut a_1 = a_0.clone();
|
||||
|
||||
let genesis_validators_root = Hash256::random();
|
||||
$sign_method_name(&mut a_0, 0, genesis_validators_root);
|
||||
$sign_method_name(&mut a_1, 1, genesis_validators_root);
|
||||
|
||||
let mut pool: NaiveAggregationPool<$map_type<E>> =
|
||||
NaiveAggregationPool::default();
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a_0),
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index: 0 }),
|
||||
"should accept a_0"
|
||||
);
|
||||
assert_eq!(
|
||||
pool.insert(&a_1),
|
||||
Ok(InsertOutcome::SignatureAggregated { committee_index: 1 }),
|
||||
"should accept a_1"
|
||||
);
|
||||
|
||||
let retrieved = pool
|
||||
.get(&$key_getter(&a_0))
|
||||
.expect("should not error while getting attestation");
|
||||
|
||||
let mut a_01 = a_0.clone();
|
||||
a_01.aggregate(&a_1);
|
||||
|
||||
assert_eq!(retrieved, a_01, "retrieved item should be aggregated");
|
||||
|
||||
/*
|
||||
* Throw different data in there and ensure it isn't aggregated
|
||||
*/
|
||||
|
||||
let mut a_different = a_0.clone();
|
||||
let different_root = Hash256::from_low_u64_be(1337);
|
||||
$unset_method_name(&mut a_different, 0);
|
||||
$sign_method_name(&mut a_different, 2, genesis_validators_root);
|
||||
assert!(!$block_root_comparator(&a_different, different_root));
|
||||
$block_root_mutator(&mut a_different, different_root);
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a_different),
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index: 2 }),
|
||||
"should accept a_different"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pool.get(&$key_getter(&a_0))
|
||||
.expect("should not error while getting item"),
|
||||
retrieved,
|
||||
"should not have aggregated different items with different data"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_pruning_item() {
|
||||
let mut base = $get_method_name(Slot::new(0));
|
||||
$sign_method_name(&mut base, 0, Hash256::random());
|
||||
|
||||
let mut pool: NaiveAggregationPool<$map_type<E>> =
|
||||
NaiveAggregationPool::default();
|
||||
|
||||
for i in 0..SLOTS_RETAINED * 2 {
|
||||
let slot = Slot::from(i);
|
||||
let mut a = base.clone();
|
||||
$slot_mutator(&mut a, slot);
|
||||
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index: 0 }),
|
||||
"should accept new item"
|
||||
);
|
||||
|
||||
if i < SLOTS_RETAINED {
|
||||
let len = i + 1;
|
||||
assert_eq!(pool.maps.len(), len, "the pool should have length {}", len);
|
||||
} else {
|
||||
assert_eq!(
|
||||
pool.maps.len(),
|
||||
SLOTS_RETAINED,
|
||||
"the pool should have length SLOTS_RETAINED"
|
||||
);
|
||||
|
||||
let mut pool_slots = pool
|
||||
.maps
|
||||
.iter()
|
||||
.map(|(slot, _map)| *slot)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
pool_slots.sort_unstable();
|
||||
|
||||
for (j, pool_slot) in pool_slots.iter().enumerate() {
|
||||
let expected_slot = slot - (SLOTS_RETAINED - 1 - j) as u64;
|
||||
assert_eq!(
|
||||
*pool_slot, expected_slot,
|
||||
"the slot of the map should be {}",
|
||||
expected_slot
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_items() {
|
||||
let mut base = $get_method_name(Slot::new(0));
|
||||
$sign_method_name(&mut base, 0, Hash256::random());
|
||||
|
||||
let mut pool: NaiveAggregationPool<$map_type<E>> =
|
||||
NaiveAggregationPool::default();
|
||||
|
||||
for i in 0..=$item_limit {
|
||||
let mut a = base.clone();
|
||||
$block_root_mutator(&mut a, Hash256::from_low_u64_be(i as u64));
|
||||
|
||||
if i < $item_limit {
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewItemInserted { committee_index: 0 }),
|
||||
"should accept item below limit"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Err(Error::ReachedMaxItemsPerSlot($item_limit)),
|
||||
"should not accept item above limit"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_attestations() {
|
||||
let mut base = get_attestation(Slot::new(0));
|
||||
sign(&mut base, 0, Hash256::random());
|
||||
test_suite! {
|
||||
attestation_tests,
|
||||
get_attestation,
|
||||
sign_attestation,
|
||||
unset_attestation_bit,
|
||||
mutate_attestation_block_root,
|
||||
mutate_attestation_slot,
|
||||
attestation_block_root_comparator,
|
||||
key_from_attestation,
|
||||
AggregatedAttestationMap,
|
||||
MAX_ATTESTATIONS_PER_SLOT
|
||||
}
|
||||
|
||||
let mut pool = NaiveAggregationPool::default();
|
||||
|
||||
for i in 0..=MAX_ATTESTATIONS_PER_SLOT {
|
||||
let mut a = base.clone();
|
||||
a.data.beacon_block_root = Hash256::from_low_u64_be(i as u64);
|
||||
|
||||
if i < MAX_ATTESTATIONS_PER_SLOT {
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Ok(InsertOutcome::NewAttestationData { committee_index: 0 }),
|
||||
"should accept attestation below limit"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
pool.insert(&a),
|
||||
Err(Error::ReachedMaxAttestationsPerSlot(
|
||||
MAX_ATTESTATIONS_PER_SLOT
|
||||
)),
|
||||
"should not accept attestation above limit"
|
||||
);
|
||||
}
|
||||
}
|
||||
test_suite! {
|
||||
sync_contribution_tests,
|
||||
get_sync_contribution,
|
||||
sign_sync_contribution,
|
||||
unset_sync_contribution_bit,
|
||||
mutate_sync_contribution_block_root,
|
||||
mutate_sync_contribution_slot,
|
||||
sync_contribution_block_root_comparator,
|
||||
key_from_sync_contribution,
|
||||
SyncContributionAggregateMap,
|
||||
E::sync_committee_size()
|
||||
}
|
||||
}
|
||||
|
||||
507
beacon_node/beacon_chain/src/observed_aggregates.rs
Normal file
507
beacon_node/beacon_chain/src/observed_aggregates.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
//! Provides an `ObservedAggregates` struct which allows us to reject aggregated attestations or
|
||||
//! sync committee contributions if we've already seen them.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use tree_hash::TreeHash;
|
||||
use types::consts::altair::{
|
||||
SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE,
|
||||
};
|
||||
use types::slot_data::SlotData;
|
||||
use types::{Attestation, EthSpec, Hash256, Slot, SyncCommitteeContribution};
|
||||
|
||||
pub type ObservedSyncContributions<E> = ObservedAggregates<SyncCommitteeContribution<E>, E>;
|
||||
pub type ObservedAggregateAttestations<E> = ObservedAggregates<Attestation<E>, E>;
|
||||
|
||||
/// A trait use to associate capacity constants with the type being stored in `ObservedAggregates`.
|
||||
pub trait Consts {
|
||||
/// The default capacity of items stored per slot, in a single `SlotHashSet`.
|
||||
const DEFAULT_PER_SLOT_CAPACITY: usize;
|
||||
|
||||
/// The maximum number of slots
|
||||
fn max_slot_capacity() -> usize;
|
||||
|
||||
/// The maximum number of items stored per slot, in a single `SlotHashSet`.
|
||||
fn max_per_slot_capacity() -> usize;
|
||||
}
|
||||
|
||||
impl<T: EthSpec> Consts for Attestation<T> {
|
||||
/// Use 128 as it's the target committee size for the mainnet spec. This is perhaps a little
|
||||
/// wasteful for the minimal spec, but considering it's approx. 128 * 32 bytes we're not wasting
|
||||
/// much.
|
||||
const DEFAULT_PER_SLOT_CAPACITY: usize = 128;
|
||||
|
||||
/// We need to keep attestations for each slot of the current epoch.
|
||||
fn max_slot_capacity() -> usize {
|
||||
T::slots_per_epoch() as usize
|
||||
}
|
||||
|
||||
/// As a DoS protection measure, the maximum number of distinct `Attestations` or
|
||||
/// `SyncCommitteeContributions` that will be recorded for each slot.
|
||||
///
|
||||
/// Currently this is set to ~524k. If we say that each entry is 40 bytes (Hash256 (32 bytes) + an
|
||||
/// 8 byte hash) then this comes to about 20mb per slot. If we're storing 34 of these slots, then
|
||||
/// we're at 680mb. This is a lot of memory usage, but probably not a show-stopper for most
|
||||
/// reasonable hardware.
|
||||
///
|
||||
/// Upstream conditions should strongly restrict the amount of attestations that can show up in
|
||||
/// this pool. The maximum size with respect to upstream restrictions is more likely on the order
|
||||
/// of the number of validators.
|
||||
fn max_per_slot_capacity() -> usize {
|
||||
1 << 19 // 524,288
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EthSpec> Consts for SyncCommitteeContribution<T> {
|
||||
/// Set to `TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE * SYNC_COMMITTEE_SUBNET_COUNT`. This is the
|
||||
/// expected number of aggregators per slot across all subcommittees.
|
||||
const DEFAULT_PER_SLOT_CAPACITY: usize =
|
||||
(SYNC_COMMITTEE_SUBNET_COUNT * TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE) as usize;
|
||||
|
||||
/// We only need to keep contributions related to the current slot.
|
||||
fn max_slot_capacity() -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
/// We should never receive more aggregates than there are sync committee participants.
|
||||
fn max_per_slot_capacity() -> usize {
|
||||
T::sync_committee_size()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ObserveOutcome {
|
||||
/// This item was already known.
|
||||
AlreadyKnown,
|
||||
/// This was the first time this item was observed.
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
SlotTooLow {
|
||||
slot: Slot,
|
||||
lowest_permissible_slot: Slot,
|
||||
},
|
||||
/// The function to obtain a set index failed, this is an internal error.
|
||||
InvalidSetIndex(usize),
|
||||
/// We have reached the maximum number of unique items that can be observed in a slot.
|
||||
/// This is a DoS protection function.
|
||||
ReachedMaxObservationsPerSlot(usize),
|
||||
IncorrectSlot {
|
||||
expected: Slot,
|
||||
attestation: Slot,
|
||||
},
|
||||
}
|
||||
|
||||
/// A `HashSet` that contains entries related to some `Slot`.
|
||||
struct SlotHashSet {
|
||||
set: HashSet<Hash256>,
|
||||
slot: Slot,
|
||||
max_capacity: usize,
|
||||
}
|
||||
|
||||
impl SlotHashSet {
|
||||
pub fn new(slot: Slot, initial_capacity: usize, max_capacity: usize) -> Self {
|
||||
Self {
|
||||
slot,
|
||||
set: HashSet::with_capacity(initial_capacity),
|
||||
max_capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the items in self so future observations recognise its existence.
|
||||
pub fn observe_item<T: SlotData>(
|
||||
&mut self,
|
||||
item: &T,
|
||||
root: Hash256,
|
||||
) -> Result<ObserveOutcome, Error> {
|
||||
if item.get_slot() != self.slot {
|
||||
return Err(Error::IncorrectSlot {
|
||||
expected: self.slot,
|
||||
attestation: item.get_slot(),
|
||||
});
|
||||
}
|
||||
|
||||
if self.set.contains(&root) {
|
||||
Ok(ObserveOutcome::AlreadyKnown)
|
||||
} else {
|
||||
// Here we check to see if this slot has reached the maximum observation count.
|
||||
//
|
||||
// The resulting behaviour is that we are no longer able to successfully observe new
|
||||
// items, however we will continue to return `is_known` values. We could also
|
||||
// disable `is_known`, however then we would stop forwarding items across the
|
||||
// gossip network and I think that this is a worse case than sending some invalid ones.
|
||||
// The underlying libp2p network is responsible for removing duplicate messages, so
|
||||
// this doesn't risk a broadcast loop.
|
||||
if self.set.len() >= self.max_capacity {
|
||||
return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity));
|
||||
}
|
||||
|
||||
self.set.insert(root);
|
||||
|
||||
Ok(ObserveOutcome::New)
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates if `item` has been observed before.
|
||||
pub fn is_known<T: SlotData>(&self, item: &T, root: Hash256) -> Result<bool, Error> {
|
||||
if item.get_slot() != self.slot {
|
||||
return Err(Error::IncorrectSlot {
|
||||
expected: self.slot,
|
||||
attestation: item.get_slot(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(self.set.contains(&root))
|
||||
}
|
||||
|
||||
/// The number of observed items in `self`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the roots of objects for some number of `Slots`, so we can determine if
|
||||
/// these have previously been seen on the network.
|
||||
pub struct ObservedAggregates<T: TreeHash + SlotData + Consts, E: EthSpec> {
|
||||
lowest_permissible_slot: Slot,
|
||||
sets: Vec<SlotHashSet>,
|
||||
_phantom_spec: PhantomData<E>,
|
||||
_phantom_tree_hash: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: TreeHash + SlotData + Consts, E: EthSpec> Default for ObservedAggregates<T, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lowest_permissible_slot: Slot::new(0),
|
||||
sets: vec![],
|
||||
_phantom_spec: PhantomData,
|
||||
_phantom_tree_hash: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TreeHash + SlotData + Consts, E: EthSpec> ObservedAggregates<T, E> {
|
||||
/// Store the root of `item` in `self`.
|
||||
///
|
||||
/// `root` must equal `item.tree_hash_root()`.
|
||||
pub fn observe_item(
|
||||
&mut self,
|
||||
item: &T,
|
||||
root_opt: Option<Hash256>,
|
||||
) -> Result<ObserveOutcome, Error> {
|
||||
let index = self.get_set_index(item.get_slot())?;
|
||||
let root = root_opt.unwrap_or_else(|| item.tree_hash_root());
|
||||
|
||||
self.sets
|
||||
.get_mut(index)
|
||||
.ok_or(Error::InvalidSetIndex(index))
|
||||
.and_then(|set| set.observe_item(item, root))
|
||||
}
|
||||
|
||||
/// Check to see if the `root` of `item` is in self.
|
||||
///
|
||||
/// `root` must equal `a.tree_hash_root()`.
|
||||
pub fn is_known(&mut self, item: &T, root: Hash256) -> Result<bool, Error> {
|
||||
let index = self.get_set_index(item.get_slot())?;
|
||||
|
||||
self.sets
|
||||
.get(index)
|
||||
.ok_or(Error::InvalidSetIndex(index))
|
||||
.and_then(|set| set.is_known(item, root))
|
||||
}
|
||||
|
||||
/// The maximum number of slots that items are stored for.
|
||||
fn max_capacity(&self) -> u64 {
|
||||
// We add `2` in order to account for one slot either side of the range due to
|
||||
// `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
||||
(T::max_slot_capacity() + 2) as u64
|
||||
}
|
||||
|
||||
/// Removes any items with a slot lower than `current_slot` and bars any future
|
||||
/// item with a slot lower than `current_slot - SLOTS_RETAINED`.
|
||||
pub fn prune(&mut self, current_slot: Slot) {
|
||||
let lowest_permissible_slot = current_slot.saturating_sub(self.max_capacity() - 1);
|
||||
|
||||
self.sets.retain(|set| set.slot >= lowest_permissible_slot);
|
||||
|
||||
self.lowest_permissible_slot = lowest_permissible_slot;
|
||||
}
|
||||
|
||||
/// Returns the index of `self.set` that matches `slot`.
|
||||
///
|
||||
/// If there is no existing set for this slot one will be created. If `self.sets.len() >=
|
||||
/// Self::max_capacity()`, the set with the lowest slot will be replaced.
|
||||
fn get_set_index(&mut self, slot: Slot) -> Result<usize, Error> {
|
||||
let lowest_permissible_slot = self.lowest_permissible_slot;
|
||||
|
||||
if slot < lowest_permissible_slot {
|
||||
return Err(Error::SlotTooLow {
|
||||
slot,
|
||||
lowest_permissible_slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Prune the pool if this item indicates that the current slot has advanced.
|
||||
if lowest_permissible_slot + self.max_capacity() < slot + 1 {
|
||||
self.prune(slot)
|
||||
}
|
||||
|
||||
if let Some(index) = self.sets.iter().position(|set| set.slot == slot) {
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
// To avoid re-allocations, try and determine a rough initial capacity for the new set
|
||||
// by obtaining the mean size of all items in earlier epoch.
|
||||
let (count, sum) = self
|
||||
.sets
|
||||
.iter()
|
||||
// Only include slots that are less than the given slot in the average. This should
|
||||
// generally avoid including recent slots that are still "filling up".
|
||||
.filter(|set| set.slot < slot)
|
||||
.map(|set| set.len())
|
||||
.fold((0, 0), |(count, sum), len| (count + 1, sum + len));
|
||||
// If we are unable to determine an average, just use the `self.default_per_slot_capacity`.
|
||||
let initial_capacity = sum
|
||||
.checked_div(count)
|
||||
.unwrap_or(T::DEFAULT_PER_SLOT_CAPACITY);
|
||||
|
||||
if self.sets.len() < self.max_capacity() as usize || self.sets.is_empty() {
|
||||
let index = self.sets.len();
|
||||
self.sets.push(SlotHashSet::new(
|
||||
slot,
|
||||
initial_capacity,
|
||||
T::max_per_slot_capacity(),
|
||||
));
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
let index = self
|
||||
.sets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_i, set)| set.slot)
|
||||
.map(|(i, _set)| i)
|
||||
.expect("sets cannot be empty due to previous .is_empty() check");
|
||||
|
||||
self.sets[index] = SlotHashSet::new(slot, initial_capacity, T::max_per_slot_capacity());
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tree_hash::TreeHash;
|
||||
use types::{test_utils::test_random_instance, Hash256};
|
||||
|
||||
type E = types::MainnetEthSpec;
|
||||
|
||||
fn get_attestation(slot: Slot, beacon_block_root: u64) -> Attestation<E> {
|
||||
let mut a: Attestation<E> = test_random_instance();
|
||||
a.data.slot = slot;
|
||||
a.data.beacon_block_root = Hash256::from_low_u64_be(beacon_block_root);
|
||||
a
|
||||
}
|
||||
|
||||
fn get_sync_contribution(slot: Slot, beacon_block_root: u64) -> SyncCommitteeContribution<E> {
|
||||
let mut a: SyncCommitteeContribution<E> = test_random_instance();
|
||||
a.slot = slot;
|
||||
a.beacon_block_root = Hash256::from_low_u64_be(beacon_block_root);
|
||||
a
|
||||
}
|
||||
|
||||
macro_rules! test_suite {
|
||||
($mod_name: ident, $type: ident, $method_name: ident) => {
|
||||
#[cfg(test)]
|
||||
mod $mod_name {
|
||||
use super::*;
|
||||
|
||||
const NUM_ELEMENTS: usize = 8;
|
||||
|
||||
fn single_slot_test(store: &mut $type<E>, slot: Slot) {
|
||||
let items = (0..NUM_ELEMENTS as u64)
|
||||
.map(|i| $method_name(slot, i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for a in &items {
|
||||
assert_eq!(
|
||||
store.is_known(a, a.tree_hash_root()),
|
||||
Ok(false),
|
||||
"should indicate an unknown attestation is unknown"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_item(a, None),
|
||||
Ok(ObserveOutcome::New),
|
||||
"should observe new attestation"
|
||||
);
|
||||
}
|
||||
|
||||
for a in &items {
|
||||
assert_eq!(
|
||||
store.is_known(a, a.tree_hash_root()),
|
||||
Ok(true),
|
||||
"should indicate a known attestation is known"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_item(a, Some(a.tree_hash_root())),
|
||||
Ok(ObserveOutcome::AlreadyKnown),
|
||||
"should acknowledge an existing attestation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_slot() {
|
||||
let mut store = $type::default();
|
||||
|
||||
single_slot_test(&mut store, Slot::new(0));
|
||||
|
||||
assert_eq!(store.sets.len(), 1, "should have a single set stored");
|
||||
assert_eq!(
|
||||
store.sets[0].len(),
|
||||
NUM_ELEMENTS,
|
||||
"set should have NUM_ELEMENTS elements"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_contiguous_slots() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
for i in 0..max_cap * 3 {
|
||||
let slot = Slot::new(i);
|
||||
|
||||
single_slot_test(&mut store, slot);
|
||||
|
||||
/*
|
||||
* Ensure that the number of sets is correct.
|
||||
*/
|
||||
|
||||
if i < max_cap {
|
||||
assert_eq!(
|
||||
store.sets.len(),
|
||||
i as usize + 1,
|
||||
"should have a {} sets stored",
|
||||
i + 1
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
store.sets.len(),
|
||||
max_cap as usize,
|
||||
"should have max_capacity sets stored"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that each set contains the correct number of elements.
|
||||
*/
|
||||
|
||||
for set in &store.sets[..] {
|
||||
assert_eq!(
|
||||
set.len(),
|
||||
NUM_ELEMENTS,
|
||||
"each store should have NUM_ELEMENTS elements"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_slots =
|
||||
store.sets.iter().map(|set| set.slot).collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
store_slots.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
store_slots.sort_unstable();
|
||||
|
||||
let expected_slots = (i.saturating_sub(max_cap - 1)..=i)
|
||||
.map(Slot::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(expected_slots, store_slots, "should have expected slots");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_non_contiguous_slots() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
let to_skip = vec![1_u64, 2, 3, 5, 6, 29, 30, 31, 32, 64];
|
||||
let slots = (0..max_cap * 3)
|
||||
.into_iter()
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for &i in &slots {
|
||||
if to_skip.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slot = Slot::from(i);
|
||||
|
||||
single_slot_test(&mut store, slot);
|
||||
|
||||
/*
|
||||
* Ensure that each set contains the correct number of elements.
|
||||
*/
|
||||
|
||||
for set in &store.sets[..] {
|
||||
assert_eq!(
|
||||
set.len(),
|
||||
NUM_ELEMENTS,
|
||||
"each store should have NUM_ELEMENTS elements"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_slots =
|
||||
store.sets.iter().map(|set| set.slot).collect::<Vec<_>>();
|
||||
|
||||
store_slots.sort_unstable();
|
||||
|
||||
assert!(
|
||||
store_slots.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
let lowest = store.lowest_permissible_slot.as_u64();
|
||||
let highest = slot.as_u64();
|
||||
let expected_slots = (lowest..=highest)
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.map(Slot::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_slots,
|
||||
&store_slots[..],
|
||||
"should have expected slots"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
test_suite!(
|
||||
observed_sync_aggregates,
|
||||
ObservedSyncContributions,
|
||||
get_sync_contribution
|
||||
);
|
||||
test_suite!(
|
||||
observed_aggregate_attestations,
|
||||
ObservedAggregateAttestations,
|
||||
get_attestation
|
||||
);
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
//! Provides an `ObservedAttestations` struct which allows us to reject aggregated attestations if
|
||||
//! we've already seen the aggregated attestation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use tree_hash::TreeHash;
|
||||
use types::{Attestation, EthSpec, Hash256, Slot};
|
||||
|
||||
/// As a DoS protection measure, the maximum number of distinct `Attestations` that will be
|
||||
/// recorded for each slot.
|
||||
///
|
||||
/// Currently this is set to ~524k. If we say that each entry is 40 bytes (Hash256 (32 bytes) + an
|
||||
/// 8 byte hash) then this comes to about 20mb per slot. If we're storing 34 of these slots, then
|
||||
/// we're at 680mb. This is a lot of memory usage, but probably not a show-stopper for most
|
||||
/// reasonable hardware.
|
||||
///
|
||||
/// Upstream conditions should strongly restrict the amount of attestations that can show up in
|
||||
/// this pool. The maximum size with respect to upstream restrictions is more likely on the order
|
||||
/// of the number of validators.
|
||||
const MAX_OBSERVATIONS_PER_SLOT: usize = 1 << 19; // 524,288
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ObserveOutcome {
|
||||
/// This attestation was already known.
|
||||
AlreadyKnown,
|
||||
/// This was the first time this attestation was observed.
|
||||
New,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
SlotTooLow {
|
||||
slot: Slot,
|
||||
lowest_permissible_slot: Slot,
|
||||
},
|
||||
/// The function to obtain a set index failed, this is an internal error.
|
||||
InvalidSetIndex(usize),
|
||||
/// We have reached the maximum number of unique `Attestation` that can be observed in a slot.
|
||||
/// This is a DoS protection function.
|
||||
ReachedMaxObservationsPerSlot(usize),
|
||||
IncorrectSlot {
|
||||
expected: Slot,
|
||||
attestation: Slot,
|
||||
},
|
||||
}
|
||||
|
||||
/// A `HashSet` that contains entries related to some `Slot`.
|
||||
struct SlotHashSet {
|
||||
set: HashSet<Hash256>,
|
||||
slot: Slot,
|
||||
}
|
||||
|
||||
impl SlotHashSet {
|
||||
pub fn new(slot: Slot, initial_capacity: usize) -> Self {
|
||||
Self {
|
||||
slot,
|
||||
set: HashSet::with_capacity(initial_capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Store the attestation in self so future observations recognise its existence.
|
||||
pub fn observe_attestation<E: EthSpec>(
|
||||
&mut self,
|
||||
a: &Attestation<E>,
|
||||
root: Hash256,
|
||||
) -> Result<ObserveOutcome, Error> {
|
||||
if a.data.slot != self.slot {
|
||||
return Err(Error::IncorrectSlot {
|
||||
expected: self.slot,
|
||||
attestation: a.data.slot,
|
||||
});
|
||||
}
|
||||
|
||||
if self.set.contains(&root) {
|
||||
Ok(ObserveOutcome::AlreadyKnown)
|
||||
} else {
|
||||
// Here we check to see if this slot has reached the maximum observation count.
|
||||
//
|
||||
// The resulting behaviour is that we are no longer able to successfully observe new
|
||||
// attestations, however we will continue to return `is_known` values. We could also
|
||||
// disable `is_known`, however then we would stop forwarding attestations across the
|
||||
// gossip network and I think that this is a worse case than sending some invalid ones.
|
||||
// The underlying libp2p network is responsible for removing duplicate messages, so
|
||||
// this doesn't risk a broadcast loop.
|
||||
if self.set.len() >= MAX_OBSERVATIONS_PER_SLOT {
|
||||
return Err(Error::ReachedMaxObservationsPerSlot(
|
||||
MAX_OBSERVATIONS_PER_SLOT,
|
||||
));
|
||||
}
|
||||
|
||||
self.set.insert(root);
|
||||
|
||||
Ok(ObserveOutcome::New)
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates if `a` has been observed before.
|
||||
pub fn is_known<E: EthSpec>(&self, a: &Attestation<E>, root: Hash256) -> Result<bool, Error> {
|
||||
if a.data.slot != self.slot {
|
||||
return Err(Error::IncorrectSlot {
|
||||
expected: self.slot,
|
||||
attestation: a.data.slot,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(self.set.contains(&root))
|
||||
}
|
||||
|
||||
/// The number of observed attestations in `self`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the roots of `Attestation` objects for some number of `Slots`, so we can determine if
|
||||
/// these have previously been seen on the network.
|
||||
pub struct ObservedAttestations<E: EthSpec> {
|
||||
lowest_permissible_slot: Slot,
|
||||
sets: Vec<SlotHashSet>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> Default for ObservedAttestations<E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lowest_permissible_slot: Slot::new(0),
|
||||
sets: vec![],
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> ObservedAttestations<E> {
|
||||
/// Store the root of `a` in `self`.
|
||||
///
|
||||
/// `root` must equal `a.tree_hash_root()`.
|
||||
pub fn observe_attestation(
|
||||
&mut self,
|
||||
a: &Attestation<E>,
|
||||
root_opt: Option<Hash256>,
|
||||
) -> Result<ObserveOutcome, Error> {
|
||||
let index = self.get_set_index(a.data.slot)?;
|
||||
let root = root_opt.unwrap_or_else(|| a.tree_hash_root());
|
||||
|
||||
self.sets
|
||||
.get_mut(index)
|
||||
.ok_or(Error::InvalidSetIndex(index))
|
||||
.and_then(|set| set.observe_attestation(a, root))
|
||||
}
|
||||
|
||||
/// Check to see if the `root` of `a` is in self.
|
||||
///
|
||||
/// `root` must equal `a.tree_hash_root()`.
|
||||
pub fn is_known(&mut self, a: &Attestation<E>, root: Hash256) -> Result<bool, Error> {
|
||||
let index = self.get_set_index(a.data.slot)?;
|
||||
|
||||
self.sets
|
||||
.get(index)
|
||||
.ok_or(Error::InvalidSetIndex(index))
|
||||
.and_then(|set| set.is_known(a, root))
|
||||
}
|
||||
|
||||
/// The maximum number of slots that attestations are stored for.
|
||||
fn max_capacity(&self) -> u64 {
|
||||
// We add `2` in order to account for one slot either side of the range due to
|
||||
// `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
||||
E::slots_per_epoch() + 2
|
||||
}
|
||||
|
||||
/// Removes any attestations with a slot lower than `current_slot` and bars any future
|
||||
/// attestations with a slot lower than `current_slot - SLOTS_RETAINED`.
|
||||
pub fn prune(&mut self, current_slot: Slot) {
|
||||
// Taking advantage of saturating subtraction on `Slot`.
|
||||
let lowest_permissible_slot = current_slot - (self.max_capacity() - 1);
|
||||
|
||||
self.sets.retain(|set| set.slot >= lowest_permissible_slot);
|
||||
|
||||
self.lowest_permissible_slot = lowest_permissible_slot;
|
||||
}
|
||||
|
||||
/// Returns the index of `self.set` that matches `slot`.
|
||||
///
|
||||
/// If there is no existing set for this slot one will be created. If `self.sets.len() >=
|
||||
/// Self::max_capacity()`, the set with the lowest slot will be replaced.
|
||||
fn get_set_index(&mut self, slot: Slot) -> Result<usize, Error> {
|
||||
let lowest_permissible_slot = self.lowest_permissible_slot;
|
||||
|
||||
if slot < lowest_permissible_slot {
|
||||
return Err(Error::SlotTooLow {
|
||||
slot,
|
||||
lowest_permissible_slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Prune the pool if this attestation indicates that the current slot has advanced.
|
||||
if lowest_permissible_slot + self.max_capacity() < slot + 1 {
|
||||
self.prune(slot)
|
||||
}
|
||||
|
||||
if let Some(index) = self.sets.iter().position(|set| set.slot == slot) {
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
// To avoid re-allocations, try and determine a rough initial capacity for the new set
|
||||
// by obtaining the mean size of all items in earlier epoch.
|
||||
let (count, sum) = self
|
||||
.sets
|
||||
.iter()
|
||||
// Only include slots that are less than the given slot in the average. This should
|
||||
// generally avoid including recent slots that are still "filling up".
|
||||
.filter(|set| set.slot < slot)
|
||||
.map(|set| set.len())
|
||||
.fold((0, 0), |(count, sum), len| (count + 1, sum + len));
|
||||
// If we are unable to determine an average, just use 128 as it's the target committee
|
||||
// size for the mainnet spec. This is perhaps a little wasteful for the minimal spec,
|
||||
// but considering it's approx. 128 * 32 bytes we're not wasting much.
|
||||
let initial_capacity = sum.checked_div(count).unwrap_or(128);
|
||||
|
||||
if self.sets.len() < self.max_capacity() as usize || self.sets.is_empty() {
|
||||
let index = self.sets.len();
|
||||
self.sets.push(SlotHashSet::new(slot, initial_capacity));
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
let index = self
|
||||
.sets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_i, set)| set.slot)
|
||||
.map(|(i, _set)| i)
|
||||
.expect("sets cannot be empty due to previous .is_empty() check");
|
||||
|
||||
self.sets[index] = SlotHashSet::new(slot, initial_capacity);
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tree_hash::TreeHash;
|
||||
use types::{test_utils::test_random_instance, Hash256};
|
||||
|
||||
type E = types::MainnetEthSpec;
|
||||
|
||||
const NUM_ELEMENTS: usize = 8;
|
||||
|
||||
fn get_attestation(slot: Slot, beacon_block_root: u64) -> Attestation<E> {
|
||||
let mut a: Attestation<E> = test_random_instance();
|
||||
a.data.slot = slot;
|
||||
a.data.beacon_block_root = Hash256::from_low_u64_be(beacon_block_root);
|
||||
a
|
||||
}
|
||||
|
||||
fn single_slot_test(store: &mut ObservedAttestations<E>, slot: Slot) {
|
||||
let attestations = (0..NUM_ELEMENTS as u64)
|
||||
.map(|i| get_attestation(slot, i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for a in &attestations {
|
||||
assert_eq!(
|
||||
store.is_known(a, a.tree_hash_root()),
|
||||
Ok(false),
|
||||
"should indicate an unknown attestation is unknown"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_attestation(a, None),
|
||||
Ok(ObserveOutcome::New),
|
||||
"should observe new attestation"
|
||||
);
|
||||
}
|
||||
|
||||
for a in &attestations {
|
||||
assert_eq!(
|
||||
store.is_known(a, a.tree_hash_root()),
|
||||
Ok(true),
|
||||
"should indicate a known attestation is known"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_attestation(a, Some(a.tree_hash_root())),
|
||||
Ok(ObserveOutcome::AlreadyKnown),
|
||||
"should acknowledge an existing attestation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_slot() {
|
||||
let mut store = ObservedAttestations::default();
|
||||
|
||||
single_slot_test(&mut store, Slot::new(0));
|
||||
|
||||
assert_eq!(store.sets.len(), 1, "should have a single set stored");
|
||||
assert_eq!(
|
||||
store.sets[0].len(),
|
||||
NUM_ELEMENTS,
|
||||
"set should have NUM_ELEMENTS elements"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_contiguous_slots() {
|
||||
let mut store = ObservedAttestations::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
for i in 0..max_cap * 3 {
|
||||
let slot = Slot::new(i);
|
||||
|
||||
single_slot_test(&mut store, slot);
|
||||
|
||||
/*
|
||||
* Ensure that the number of sets is correct.
|
||||
*/
|
||||
|
||||
if i < max_cap {
|
||||
assert_eq!(
|
||||
store.sets.len(),
|
||||
i as usize + 1,
|
||||
"should have a {} sets stored",
|
||||
i + 1
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
store.sets.len(),
|
||||
max_cap as usize,
|
||||
"should have max_capacity sets stored"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that each set contains the correct number of elements.
|
||||
*/
|
||||
|
||||
for set in &store.sets[..] {
|
||||
assert_eq!(
|
||||
set.len(),
|
||||
NUM_ELEMENTS,
|
||||
"each store should have NUM_ELEMENTS elements"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_slots = store.sets.iter().map(|set| set.slot).collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
store_slots.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
store_slots.sort_unstable();
|
||||
|
||||
let expected_slots = (i.saturating_sub(max_cap - 1)..=i)
|
||||
.map(Slot::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(expected_slots, store_slots, "should have expected slots");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_non_contiguous_slots() {
|
||||
let mut store = ObservedAttestations::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
let to_skip = vec![1_u64, 2, 3, 5, 6, 29, 30, 31, 32, 64];
|
||||
let slots = (0..max_cap * 3)
|
||||
.into_iter()
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for &i in &slots {
|
||||
if to_skip.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let slot = Slot::from(i);
|
||||
|
||||
single_slot_test(&mut store, slot);
|
||||
|
||||
/*
|
||||
* Ensure that each set contains the correct number of elements.
|
||||
*/
|
||||
|
||||
for set in &store.sets[..] {
|
||||
assert_eq!(
|
||||
set.len(),
|
||||
NUM_ELEMENTS,
|
||||
"each store should have NUM_ELEMENTS elements"
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_slots = store.sets.iter().map(|set| set.slot).collect::<Vec<_>>();
|
||||
|
||||
store_slots.sort_unstable();
|
||||
|
||||
assert!(
|
||||
store_slots.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
let lowest = store.lowest_permissible_slot.as_u64();
|
||||
let highest = slot.as_u64();
|
||||
let expected_slots = (lowest..=highest)
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.map(Slot::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_slots,
|
||||
&store_slots[..],
|
||||
"should have expected slots"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,29 @@
|
||||
//! the same epoch.
|
||||
//! - `ObservedAggregators`: allows filtering aggregated attestations from the same aggregators in
|
||||
//! the same epoch
|
||||
//!
|
||||
//! Provides an additional two structs that help us filter out sync committee message and
|
||||
//! contribution gossip from validators that have already published messages this slot:
|
||||
//!
|
||||
//! - `ObservedSyncContributors`: allows filtering sync committee messages from the same validator in
|
||||
//! the same slot.
|
||||
//! - `ObservedSyncAggregators`: allows filtering sync committee contributions from the same aggregators in
|
||||
//! the same slot and in the same subcommittee.
|
||||
|
||||
use crate::types::consts::altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE;
|
||||
use bitvec::vec::BitVec;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::marker::PhantomData;
|
||||
use types::{Attestation, Epoch, EthSpec, Unsigned};
|
||||
use types::slot_data::SlotData;
|
||||
use types::{Epoch, EthSpec, Slot, Unsigned};
|
||||
|
||||
pub type ObservedAttesters<E> = AutoPruningContainer<EpochBitfield, E>;
|
||||
pub type ObservedAggregators<E> = AutoPruningContainer<EpochHashSet, E>;
|
||||
pub type ObservedAttesters<E> = AutoPruningEpochContainer<EpochBitfield, E>;
|
||||
pub type ObservedSyncContributors<E> =
|
||||
AutoPruningSlotContainer<SlotSubcommitteeIndex, SyncContributorSlotHashSet<E>, E>;
|
||||
pub type ObservedAggregators<E> = AutoPruningEpochContainer<EpochHashSet, E>;
|
||||
pub type ObservedSyncAggregators<E> =
|
||||
AutoPruningSlotContainer<SlotSubcommitteeIndex, SyncAggregatorSlotHashSet, E>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
@@ -20,7 +35,11 @@ pub enum Error {
|
||||
epoch: Epoch,
|
||||
lowest_permissible_epoch: Epoch,
|
||||
},
|
||||
/// We have reached the maximum number of unique `Attestation` that can be observed in a slot.
|
||||
SlotTooLow {
|
||||
slot: Slot,
|
||||
lowest_permissible_slot: Slot,
|
||||
},
|
||||
/// We have reached the maximum number of unique items that can be observed in a slot.
|
||||
/// This is a DoS protection function.
|
||||
ReachedMaxObservationsPerSlot(usize),
|
||||
/// The function to obtain a set index failed, this is an internal error.
|
||||
@@ -48,7 +67,8 @@ pub trait Item {
|
||||
fn contains(&self, validator_index: usize) -> bool;
|
||||
}
|
||||
|
||||
/// Stores a `BitVec` that represents which validator indices have attested during an epoch.
|
||||
/// Stores a `BitVec` that represents which validator indices have attested or sent sync committee
|
||||
/// signatures during an epoch.
|
||||
pub struct EpochBitfield {
|
||||
bitfield: BitVec,
|
||||
}
|
||||
@@ -99,7 +119,7 @@ impl Item for EpochBitfield {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a `HashSet` of which validator indices have created an aggregate attestation during an
|
||||
/// Stores a `HashSet` of which validator indices have created an aggregate during an
|
||||
/// epoch.
|
||||
pub struct EpochHashSet {
|
||||
set: HashSet<usize>,
|
||||
@@ -138,6 +158,84 @@ impl Item for EpochHashSet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a `HashSet` of which validator indices have created a sync aggregate during a
|
||||
/// slot.
|
||||
pub struct SyncContributorSlotHashSet<E> {
|
||||
set: HashSet<usize>,
|
||||
phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> Item for SyncContributorSlotHashSet<E> {
|
||||
fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
set: HashSet::with_capacity(capacity),
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Defaults to the `SYNC_SUBCOMMITTEE_SIZE`.
|
||||
fn default_capacity() -> usize {
|
||||
E::sync_subcommittee_size()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
fn validator_count(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
/// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was
|
||||
/// already in the set.
|
||||
fn insert(&mut self, validator_index: usize) -> bool {
|
||||
!self.set.insert(validator_index)
|
||||
}
|
||||
|
||||
/// Returns `true` if the `validator_index` is in the set.
|
||||
fn contains(&self, validator_index: usize) -> bool {
|
||||
self.set.contains(&validator_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a `HashSet` of which validator indices have created a sync aggregate during a
|
||||
/// slot.
|
||||
pub struct SyncAggregatorSlotHashSet {
|
||||
set: HashSet<usize>,
|
||||
}
|
||||
|
||||
impl Item for SyncAggregatorSlotHashSet {
|
||||
fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
set: HashSet::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Defaults to the `TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE`.
|
||||
fn default_capacity() -> usize {
|
||||
TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE as usize
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
fn validator_count(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
/// Inserts the `validator_index` in the set. Returns `true` if the `validator_index` was
|
||||
/// already in the set.
|
||||
fn insert(&mut self, validator_index: usize) -> bool {
|
||||
!self.set.insert(validator_index)
|
||||
}
|
||||
|
||||
/// Returns `true` if the `validator_index` is in the set.
|
||||
fn contains(&self, validator_index: usize) -> bool {
|
||||
self.set.contains(&validator_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// A container that stores some number of `T` items.
|
||||
///
|
||||
/// This container is "auto-pruning" since it gets an idea of the current slot by which
|
||||
@@ -146,13 +244,13 @@ impl Item for EpochHashSet {
|
||||
/// attestations with an epoch prior to `a.data.target.epoch - 32` will be cleared from the cache.
|
||||
///
|
||||
/// `T` should be set to a `EpochBitfield` or `EpochHashSet`.
|
||||
pub struct AutoPruningContainer<T, E: EthSpec> {
|
||||
pub struct AutoPruningEpochContainer<T, E: EthSpec> {
|
||||
lowest_permissible_epoch: Epoch,
|
||||
items: HashMap<Epoch, T>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<T, E: EthSpec> Default for AutoPruningContainer<T, E> {
|
||||
impl<T, E: EthSpec> Default for AutoPruningEpochContainer<T, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lowest_permissible_epoch: Epoch::new(0),
|
||||
@@ -162,22 +260,20 @@ impl<T, E: EthSpec> Default for AutoPruningContainer<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item, E: EthSpec> AutoPruningContainer<T, E> {
|
||||
impl<T: Item, E: EthSpec> AutoPruningEpochContainer<T, E> {
|
||||
/// Observe that `validator_index` has produced attestation `a`. Returns `Ok(true)` if `a` has
|
||||
/// previously been observed for `validator_index`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`.
|
||||
/// - `a.data.target.slot` is earlier than `self.earliest_permissible_slot`.
|
||||
/// - `a.data.target.slot` is earlier than `self.lowest_permissible_slot`.
|
||||
pub fn observe_validator(
|
||||
&mut self,
|
||||
a: &Attestation<E>,
|
||||
epoch: Epoch,
|
||||
validator_index: usize,
|
||||
) -> Result<bool, Error> {
|
||||
self.sanitize_request(a, validator_index)?;
|
||||
|
||||
let epoch = a.data.target.epoch;
|
||||
self.sanitize_request(epoch, validator_index)?;
|
||||
|
||||
self.prune(epoch);
|
||||
|
||||
@@ -211,17 +307,17 @@ impl<T: Item, E: EthSpec> AutoPruningContainer<T, E> {
|
||||
/// ## Errors
|
||||
///
|
||||
/// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`.
|
||||
/// - `a.data.target.slot` is earlier than `self.earliest_permissible_slot`.
|
||||
/// - `a.data.target.slot` is earlier than `self.lowest_permissible_slot`.
|
||||
pub fn validator_has_been_observed(
|
||||
&self,
|
||||
a: &Attestation<E>,
|
||||
epoch: Epoch,
|
||||
validator_index: usize,
|
||||
) -> Result<bool, Error> {
|
||||
self.sanitize_request(a, validator_index)?;
|
||||
self.sanitize_request(epoch, validator_index)?;
|
||||
|
||||
let exists = self
|
||||
.items
|
||||
.get(&a.data.target.epoch)
|
||||
.get(&epoch)
|
||||
.map_or(false, |item| item.contains(validator_index));
|
||||
|
||||
Ok(exists)
|
||||
@@ -233,12 +329,11 @@ impl<T: Item, E: EthSpec> AutoPruningContainer<T, E> {
|
||||
self.items.get(&epoch).map(|item| item.validator_count())
|
||||
}
|
||||
|
||||
fn sanitize_request(&self, a: &Attestation<E>, validator_index: usize) -> Result<(), Error> {
|
||||
fn sanitize_request(&self, epoch: Epoch, validator_index: usize) -> Result<(), Error> {
|
||||
if validator_index > E::ValidatorRegistryLimit::to_usize() {
|
||||
return Err(Error::ValidatorIndexTooHigh(validator_index));
|
||||
}
|
||||
|
||||
let epoch = a.data.target.epoch;
|
||||
let lowest_permissible_epoch = self.lowest_permissible_epoch;
|
||||
if epoch < lowest_permissible_epoch {
|
||||
return Err(Error::EpochTooLow {
|
||||
@@ -272,84 +367,240 @@ impl<T: Item, E: EthSpec> AutoPruningContainer<T, E> {
|
||||
/// Also sets `self.lowest_permissible_epoch` with relation to `current_epoch` and
|
||||
/// `Self::max_capacity`.
|
||||
pub fn prune(&mut self, current_epoch: Epoch) {
|
||||
// Taking advantage of saturating subtraction on `Slot`.
|
||||
let lowest_permissible_epoch = current_epoch - (self.max_capacity().saturating_sub(1));
|
||||
let lowest_permissible_epoch =
|
||||
current_epoch.saturating_sub(self.max_capacity().saturating_sub(1));
|
||||
|
||||
self.lowest_permissible_epoch = lowest_permissible_epoch;
|
||||
|
||||
self.items
|
||||
.retain(|epoch, _item| *epoch >= lowest_permissible_epoch);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Returns the `lowest_permissible_epoch`. Used in tests.
|
||||
pub(crate) fn get_lowest_permissible(&self) -> Epoch {
|
||||
self.lowest_permissible_epoch
|
||||
}
|
||||
}
|
||||
|
||||
/// A container that stores some number of `V` items.
|
||||
///
|
||||
/// This container is "auto-pruning" since it gets an idea of the current slot by which
|
||||
/// sync contributions are provided to it and prunes old entries based upon that. For example, if
|
||||
/// `Self::max_capacity == 3` and an attestation with `data.slot` is supplied, then all
|
||||
/// sync contributions with an epoch prior to `data.slot - 3` will be cleared from the cache.
|
||||
///
|
||||
/// `V` should be set to a `SyncAggregatorSlotHashSet` or a `SyncContributorSlotHashSet`.
|
||||
pub struct AutoPruningSlotContainer<K: SlotData + Eq + Hash, V, E: EthSpec> {
|
||||
lowest_permissible_slot: Slot,
|
||||
items: HashMap<K, V>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<K: SlotData + Eq + Hash, V, E: EthSpec> Default for AutoPruningSlotContainer<K, V, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lowest_permissible_slot: Slot::new(0),
|
||||
items: HashMap::new(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: SlotData + Eq + Hash, V: Item, E: EthSpec> AutoPruningSlotContainer<K, V, E> {
|
||||
/// Observe that `validator_index` has produced a sync committee message. Returns `Ok(true)` if
|
||||
/// the sync committee message has previously been observed for `validator_index`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`.
|
||||
/// - `key.slot` is earlier than `self.lowest_permissible_slot`.
|
||||
pub fn observe_validator(&mut self, key: K, validator_index: usize) -> Result<bool, Error> {
|
||||
let slot = key.get_slot();
|
||||
self.sanitize_request(slot, validator_index)?;
|
||||
|
||||
self.prune(slot);
|
||||
|
||||
if let Some(item) = self.items.get_mut(&key) {
|
||||
Ok(item.insert(validator_index))
|
||||
} else {
|
||||
// To avoid re-allocations, try and determine a rough initial capacity for the new item
|
||||
// by obtaining the mean size of all items in earlier slot.
|
||||
let (count, sum) = self
|
||||
.items
|
||||
.iter()
|
||||
// Only include slots that are less than the given slot in the average. This should
|
||||
// generally avoid including recent slots that are still "filling up".
|
||||
.filter(|(item_key, _item)| item_key.get_slot() < slot)
|
||||
.map(|(_, item)| item.len())
|
||||
.fold((0, 0), |(count, sum), len| (count + 1, sum + len));
|
||||
|
||||
let initial_capacity = sum.checked_div(count).unwrap_or_else(V::default_capacity);
|
||||
|
||||
let mut item = V::with_capacity(initial_capacity);
|
||||
item.insert(validator_index);
|
||||
self.items.insert(key, item);
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Ok(true)` if the `validator_index` has already produced a conflicting sync committee message.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - `validator_index` is higher than `VALIDATOR_REGISTRY_LIMIT`.
|
||||
/// - `key.slot` is earlier than `self.lowest_permissible_slot`.
|
||||
pub fn validator_has_been_observed(
|
||||
&self,
|
||||
key: K,
|
||||
validator_index: usize,
|
||||
) -> Result<bool, Error> {
|
||||
self.sanitize_request(key.get_slot(), validator_index)?;
|
||||
|
||||
let exists = self
|
||||
.items
|
||||
.get(&key)
|
||||
.map_or(false, |item| item.contains(validator_index));
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Returns the number of validators that have been observed at the given `slot`. Returns
|
||||
/// `None` if `self` does not have a cache for that slot.
|
||||
pub fn observed_validator_count(&self, key: K) -> Option<usize> {
|
||||
self.items.get(&key).map(|item| item.validator_count())
|
||||
}
|
||||
|
||||
fn sanitize_request(&self, slot: Slot, validator_index: usize) -> Result<(), Error> {
|
||||
if validator_index > E::ValidatorRegistryLimit::to_usize() {
|
||||
return Err(Error::ValidatorIndexTooHigh(validator_index));
|
||||
}
|
||||
|
||||
let lowest_permissible_slot = self.lowest_permissible_slot;
|
||||
if slot < lowest_permissible_slot {
|
||||
return Err(Error::SlotTooLow {
|
||||
slot,
|
||||
lowest_permissible_slot,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The maximum number of slots stored in `self`.
|
||||
fn max_capacity(&self) -> u64 {
|
||||
// The next, current and previous slots. We require the next slot due to the
|
||||
// `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
||||
3
|
||||
}
|
||||
|
||||
/// Updates `self` with the current slot, removing all sync committee messages that become expired
|
||||
/// relative to `Self::max_capacity`.
|
||||
///
|
||||
/// Also sets `self.lowest_permissible_slot` with relation to `current_slot` and
|
||||
/// `Self::max_capacity`.
|
||||
pub fn prune(&mut self, current_slot: Slot) {
|
||||
let lowest_permissible_slot =
|
||||
current_slot.saturating_sub(self.max_capacity().saturating_sub(1));
|
||||
|
||||
self.lowest_permissible_slot = lowest_permissible_slot;
|
||||
|
||||
self.items
|
||||
.retain(|key, _item| key.get_slot() >= lowest_permissible_slot);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Returns the `lowest_permissible_slot`. Used in tests.
|
||||
pub(crate) fn get_lowest_permissible(&self) -> Slot {
|
||||
self.lowest_permissible_slot
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used to key information about sync committee aggregators. We require the
|
||||
/// `subcommittee_index` because it is possible that a validator can aggregate for multiple
|
||||
/// subcommittees in the same slot.
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Copy, PartialOrd, Ord, Debug)]
|
||||
pub struct SlotSubcommitteeIndex {
|
||||
slot: Slot,
|
||||
subcommittee_index: u64,
|
||||
}
|
||||
|
||||
impl SlotData for SlotSubcommitteeIndex {
|
||||
fn get_slot(&self) -> Slot {
|
||||
self.slot
|
||||
}
|
||||
}
|
||||
|
||||
impl SlotSubcommitteeIndex {
|
||||
pub fn new(slot: Slot, subcommittee_index: u64) -> Self {
|
||||
Self {
|
||||
slot,
|
||||
subcommittee_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test_suite {
|
||||
type E = types::MainnetEthSpec;
|
||||
|
||||
macro_rules! test_suite_epoch {
|
||||
($mod_name: ident, $type: ident) => {
|
||||
#[cfg(test)]
|
||||
mod $mod_name {
|
||||
use super::*;
|
||||
use types::test_utils::test_random_instance;
|
||||
|
||||
type E = types::MainnetEthSpec;
|
||||
fn single_period_test(store: &mut $type<E>, period: Epoch) {
|
||||
let validator_indices = [0, 1, 2, 3, 5, 6, 7, 18, 22];
|
||||
|
||||
fn get_attestation(epoch: Epoch) -> Attestation<E> {
|
||||
let mut a: Attestation<E> = test_random_instance();
|
||||
a.data.target.epoch = epoch;
|
||||
a
|
||||
}
|
||||
|
||||
fn single_epoch_test(store: &mut $type<E>, epoch: Epoch) {
|
||||
let attesters = [0, 1, 2, 3, 5, 6, 7, 18, 22];
|
||||
let a = &get_attestation(epoch);
|
||||
|
||||
for &i in &attesters {
|
||||
for &i in &validator_indices {
|
||||
assert_eq!(
|
||||
store.validator_has_been_observed(a, i),
|
||||
store.validator_has_been_observed(period, i),
|
||||
Ok(false),
|
||||
"should indicate an unknown attestation is unknown"
|
||||
"should indicate an unknown item is unknown"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_validator(a, i),
|
||||
store.observe_validator(period, i),
|
||||
Ok(false),
|
||||
"should observe new attestation"
|
||||
"should observe new item"
|
||||
);
|
||||
}
|
||||
|
||||
for &i in &attesters {
|
||||
for &i in &validator_indices {
|
||||
assert_eq!(
|
||||
store.validator_has_been_observed(a, i),
|
||||
store.validator_has_been_observed(period, i),
|
||||
Ok(true),
|
||||
"should indicate a known attestation is known"
|
||||
"should indicate a known item is known"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_validator(a, i),
|
||||
store.observe_validator(period, i),
|
||||
Ok(true),
|
||||
"should acknowledge an existing attestation"
|
||||
"should acknowledge an existing item"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_epoch() {
|
||||
fn single_period() {
|
||||
let mut store = $type::default();
|
||||
|
||||
single_epoch_test(&mut store, Epoch::new(0));
|
||||
single_period_test(&mut store, Epoch::new(0));
|
||||
|
||||
assert_eq!(store.items.len(), 1, "should have a single bitfield stored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_contiguous_epochs() {
|
||||
fn mulitple_contiguous_periods() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
for i in 0..max_cap * 3 {
|
||||
let epoch = Epoch::new(i);
|
||||
let period = Epoch::new(i);
|
||||
|
||||
single_epoch_test(&mut store, epoch);
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that the number of sets is correct.
|
||||
@@ -374,74 +625,77 @@ mod tests {
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_epochs = store
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(epoch, _set)| *epoch)
|
||||
.map(|(period, _set)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
store_epochs.len() <= store.max_capacity() as usize,
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
store_epochs.sort_unstable();
|
||||
store_periods.sort_unstable();
|
||||
|
||||
let expected_epochs = (i.saturating_sub(max_cap - 1)..=i)
|
||||
let expected_periods = (i.saturating_sub(max_cap - 1)..=i)
|
||||
.map(Epoch::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(expected_epochs, store_epochs, "should have expected slots");
|
||||
assert_eq!(
|
||||
expected_periods, store_periods,
|
||||
"should have expected slots"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_non_contiguous_epochs() {
|
||||
fn mulitple_non_contiguous_periods() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
let to_skip = vec![1_u64, 3, 4, 5];
|
||||
let epochs = (0..max_cap * 3)
|
||||
let periods = (0..max_cap * 3)
|
||||
.into_iter()
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for &i in &epochs {
|
||||
for &i in &periods {
|
||||
if to_skip.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let epoch = Epoch::from(i);
|
||||
let period = Epoch::from(i);
|
||||
|
||||
single_epoch_test(&mut store, epoch);
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_epochs = store
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(epoch, _)| *epoch)
|
||||
.map(|(period, _)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
store_epochs.sort_unstable();
|
||||
store_periods.sort_unstable();
|
||||
|
||||
assert!(
|
||||
store_epochs.len() <= store.max_capacity() as usize,
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
let lowest = store.lowest_permissible_epoch.as_u64();
|
||||
let highest = epoch.as_u64();
|
||||
let expected_epochs = (lowest..=highest)
|
||||
let lowest = store.get_lowest_permissible().as_u64();
|
||||
let highest = period.as_u64();
|
||||
let expected_periods = (lowest..=highest)
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.map(Epoch::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_epochs,
|
||||
&store_epochs[..],
|
||||
expected_periods,
|
||||
&store_periods[..],
|
||||
"should have expected epochs"
|
||||
);
|
||||
}
|
||||
@@ -450,6 +704,285 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
test_suite!(observed_attesters, ObservedAttesters);
|
||||
test_suite!(observed_aggregators, ObservedAggregators);
|
||||
test_suite_epoch!(observed_attesters, ObservedAttesters);
|
||||
test_suite_epoch!(observed_aggregators, ObservedAggregators);
|
||||
|
||||
macro_rules! test_suite_slot {
|
||||
($mod_name: ident, $type: ident) => {
|
||||
#[cfg(test)]
|
||||
mod $mod_name {
|
||||
use super::*;
|
||||
|
||||
fn single_period_test(store: &mut $type<E>, key: SlotSubcommitteeIndex) {
|
||||
let validator_indices = [0, 1, 2, 3, 5, 6, 7, 18, 22];
|
||||
|
||||
for &i in &validator_indices {
|
||||
assert_eq!(
|
||||
store.validator_has_been_observed(key, i),
|
||||
Ok(false),
|
||||
"should indicate an unknown item is unknown"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_validator(key, i),
|
||||
Ok(false),
|
||||
"should observe new item"
|
||||
);
|
||||
}
|
||||
|
||||
for &i in &validator_indices {
|
||||
assert_eq!(
|
||||
store.validator_has_been_observed(key, i),
|
||||
Ok(true),
|
||||
"should indicate a known item is known"
|
||||
);
|
||||
assert_eq!(
|
||||
store.observe_validator(key, i),
|
||||
Ok(true),
|
||||
"should acknowledge an existing item"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_period() {
|
||||
let mut store = $type::default();
|
||||
|
||||
single_period_test(&mut store, SlotSubcommitteeIndex::new(Slot::new(0), 0));
|
||||
|
||||
assert_eq!(store.items.len(), 1, "should have a single bitfield stored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_period_multiple_subcommittees() {
|
||||
let mut store = $type::default();
|
||||
|
||||
single_period_test(&mut store, SlotSubcommitteeIndex::new(Slot::new(0), 0));
|
||||
single_period_test(&mut store, SlotSubcommitteeIndex::new(Slot::new(0), 1));
|
||||
single_period_test(&mut store, SlotSubcommitteeIndex::new(Slot::new(0), 2));
|
||||
|
||||
assert_eq!(store.items.len(), 3, "should have three hash sets stored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_contiguous_periods_same_subcommittee() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
for i in 0..max_cap * 3 {
|
||||
let period = SlotSubcommitteeIndex::new(Slot::new(i), 0);
|
||||
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that the number of sets is correct.
|
||||
*/
|
||||
|
||||
if i < max_cap {
|
||||
assert_eq!(
|
||||
store.items.len(),
|
||||
i as usize + 1,
|
||||
"should have a {} items stored",
|
||||
i + 1
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
store.items.len(),
|
||||
max_cap as usize,
|
||||
"should have max_capacity items stored"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(period, _set)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
store_periods.sort_unstable();
|
||||
|
||||
let expected_periods = (i.saturating_sub(max_cap - 1)..=i)
|
||||
.map(|i| SlotSubcommitteeIndex::new(Slot::new(i), 0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_periods, store_periods,
|
||||
"should have expected slots"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_non_contiguous_periods_same_subcommitte() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
let to_skip = vec![1_u64, 3, 4, 5];
|
||||
let periods = (0..max_cap * 3)
|
||||
.into_iter()
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for &i in &periods {
|
||||
if to_skip.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let period = SlotSubcommitteeIndex::new(Slot::from(i), 0);
|
||||
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(period, _)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
store_periods.sort_unstable();
|
||||
|
||||
assert!(
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
let lowest = store.get_lowest_permissible().as_u64();
|
||||
let highest = period.slot.as_u64();
|
||||
let expected_periods = (lowest..=highest)
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.map(|i| SlotSubcommitteeIndex::new(Slot::new(i), 0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_periods,
|
||||
&store_periods[..],
|
||||
"should have expected epochs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_contiguous_periods_different_subcommittee() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
for i in 0..max_cap * 3 {
|
||||
let period = SlotSubcommitteeIndex::new(Slot::new(i), i);
|
||||
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that the number of sets is correct.
|
||||
*/
|
||||
|
||||
if i < max_cap {
|
||||
assert_eq!(
|
||||
store.items.len(),
|
||||
i as usize + 1,
|
||||
"should have a {} items stored",
|
||||
i + 1
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
store.items.len(),
|
||||
max_cap as usize,
|
||||
"should have max_capacity items stored"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(period, _set)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
store_periods.sort_unstable();
|
||||
|
||||
let expected_periods = (i.saturating_sub(max_cap - 1)..=i)
|
||||
.map(|i| SlotSubcommitteeIndex::new(Slot::new(i), i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_periods, store_periods,
|
||||
"should have expected slots"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mulitple_non_contiguous_periods_different_subcommitte() {
|
||||
let mut store = $type::default();
|
||||
let max_cap = store.max_capacity();
|
||||
|
||||
let to_skip = vec![1_u64, 3, 4, 5];
|
||||
let periods = (0..max_cap * 3)
|
||||
.into_iter()
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for &i in &periods {
|
||||
if to_skip.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let period = SlotSubcommitteeIndex::new(Slot::from(i), i);
|
||||
|
||||
single_period_test(&mut store, period);
|
||||
|
||||
/*
|
||||
* Ensure that all the sets have the expected slots
|
||||
*/
|
||||
|
||||
let mut store_periods = store
|
||||
.items
|
||||
.iter()
|
||||
.map(|(period, _)| *period)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
store_periods.sort_unstable();
|
||||
|
||||
assert!(
|
||||
store_periods.len() <= store.max_capacity() as usize,
|
||||
"store size should not exceed max"
|
||||
);
|
||||
|
||||
let lowest = store.get_lowest_permissible().as_u64();
|
||||
let highest = period.slot.as_u64();
|
||||
let expected_periods = (lowest..=highest)
|
||||
.filter(|i| !to_skip.contains(i))
|
||||
.map(|i| SlotSubcommitteeIndex::new(Slot::new(i), i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
expected_periods,
|
||||
&store_periods[..],
|
||||
"should have expected epochs"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
test_suite_slot!(observed_sync_contributors, ObservedSyncContributors);
|
||||
test_suite_slot!(observed_sync_aggregators, ObservedSyncAggregators);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Utilities for managing database schema changes.
|
||||
use crate::beacon_chain::BeaconChainTypes;
|
||||
use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY};
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
use operation_pool::{PersistedOperationPool, PersistedOperationPoolBase};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
@@ -54,6 +55,24 @@ pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Migration for adding sync committee contributions to the persisted op pool.
|
||||
(SchemaVersion(3), SchemaVersion(4)) => {
|
||||
// Deserialize from what exists in the database using the `PersistedOperationPoolBase`
|
||||
// variant and convert it to the Altair variant.
|
||||
let pool_opt = db
|
||||
.get_item::<PersistedOperationPoolBase<T::EthSpec>>(&OP_POOL_DB_KEY)?
|
||||
.map(PersistedOperationPool::Base)
|
||||
.map(PersistedOperationPool::base_to_altair);
|
||||
|
||||
if let Some(pool) = pool_opt {
|
||||
// Store the converted pool under the same key.
|
||||
db.put_item::<PersistedOperationPool<T::EthSpec>>(&OP_POOL_DB_KEY, &pool)?;
|
||||
}
|
||||
|
||||
db.store_schema_version(to)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Anything else is an error.
|
||||
(_, _) => Err(HotColdDBError::UnsupportedSchemaVersion {
|
||||
target_version: to,
|
||||
|
||||
658
beacon_node/beacon_chain/src/sync_committee_verification.rs
Normal file
658
beacon_node/beacon_chain/src/sync_committee_verification.rs
Normal file
@@ -0,0 +1,658 @@
|
||||
//! Provides verification for the following sync committee messages:
|
||||
//!
|
||||
//! - "Unaggregated" `SyncCommitteeMessage` received from either gossip or the HTTP API.
|
||||
//! - "Aggregated" `SignedContributionAndProof` received from gossip or the HTTP API.
|
||||
//!
|
||||
//! For clarity, we define:
|
||||
//!
|
||||
//! - Unaggregated: a `SyncCommitteeMessage` object.
|
||||
//! - Aggregated: a `SignedContributionAndProof` which has zero or more signatures.
|
||||
//! - Note: "zero or more" may soon change to "one or more".
|
||||
//!
|
||||
//! Similar to the `crate::block_verification` module, we try to avoid doing duplicate verification
|
||||
//! work as a sync committee message passes through different stages of verification. We represent these
|
||||
//! different stages of verification with wrapper types. These wrapper-types flow in a particular
|
||||
//! pattern:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! types::SyncCommitteeMessage types::SignedContributionAndProof
|
||||
//! | |
|
||||
//! ▼ ▼
|
||||
//! VerifiedSyncCommitteeMessage VerifiedSyncContribution
|
||||
//! | |
|
||||
//! -------------------------------------
|
||||
//! |
|
||||
//! ▼
|
||||
//! impl SignatureVerifiedSyncContribution
|
||||
//! ```
|
||||
|
||||
use crate::observed_attesters::SlotSubcommitteeIndex;
|
||||
use crate::{
|
||||
beacon_chain::{MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT},
|
||||
metrics,
|
||||
observed_aggregates::ObserveOutcome,
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes,
|
||||
};
|
||||
use bls::{verify_signature_sets, PublicKeyBytes};
|
||||
use derivative::Derivative;
|
||||
use safe_arith::ArithError;
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::per_block_processing::errors::SyncCommitteeMessageValidationError;
|
||||
use state_processing::signature_sets::{
|
||||
signed_sync_aggregate_selection_proof_signature_set, signed_sync_aggregate_signature_set,
|
||||
sync_committee_contribution_signature_set_from_pubkeys,
|
||||
sync_committee_message_set_from_pubkeys,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use strum::AsRefStr;
|
||||
use tree_hash::TreeHash;
|
||||
use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
|
||||
use types::slot_data::SlotData;
|
||||
use types::sync_committee::Error as SyncCommitteeError;
|
||||
use types::{
|
||||
sync_committee_contribution::Error as ContributionError, AggregateSignature, BeaconStateError,
|
||||
EthSpec, Hash256, SignedContributionAndProof, Slot, SyncCommitteeContribution,
|
||||
SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId,
|
||||
};
|
||||
|
||||
/// Returned when a sync committee contribution was not successfully verified. It might not have been verified for
|
||||
/// two reasons:
|
||||
///
|
||||
/// - The sync committee message is malformed or inappropriate for the context (indicated by all variants
|
||||
/// other than `BeaconChainError`).
|
||||
/// - The application encountered an internal error whilst attempting to determine validity
|
||||
/// (the `BeaconChainError` variant)
|
||||
#[derive(Debug, AsRefStr)]
|
||||
pub enum Error {
|
||||
/// The sync committee message is from a slot that is later than the current slot (with respect to the
|
||||
/// gossip clock disparity).
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// Assuming the local clock is correct, the peer has sent an invalid message.
|
||||
FutureSlot {
|
||||
message_slot: Slot,
|
||||
latest_permissible_slot: Slot,
|
||||
},
|
||||
/// The sync committee message is from a slot that is prior to the earliest permissible slot (with
|
||||
/// respect to the gossip clock disparity).
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// Assuming the local clock is correct, the peer has sent an invalid message.
|
||||
PastSlot {
|
||||
message_slot: Slot,
|
||||
earliest_permissible_slot: Slot,
|
||||
},
|
||||
/// The sync committee message's aggregation bits were empty when they shouldn't be.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
EmptyAggregationBitfield,
|
||||
/// The `selection_proof` on the sync contribution does not elect it as an aggregator.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
InvalidSelectionProof { aggregator_index: u64 },
|
||||
/// The `selection_proof` on the sync committee contribution selects it as a validator, however the
|
||||
/// aggregator index is not in the committee for that sync contribution.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
AggregatorNotInCommittee { aggregator_index: u64 },
|
||||
/// The aggregator index refers to a validator index that we have not seen.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
AggregatorPubkeyUnknown(u64),
|
||||
/// The sync contribution has been seen before; either in a block, on the gossip network or from a
|
||||
/// local validator.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It's unclear if this sync contribution is valid, however we have already observed it and do not
|
||||
/// need to observe it again.
|
||||
SyncContributionAlreadyKnown(Hash256),
|
||||
/// There has already been an aggregation observed for this validator, we refuse to process a
|
||||
/// second.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It's unclear if this sync committee message is valid, however we have already observed an aggregate
|
||||
/// sync committee message from this validator for this epoch and should not observe another.
|
||||
AggregatorAlreadyKnown(u64),
|
||||
/// The aggregator index is higher than the maximum possible validator count.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
UnknownValidatorIndex(usize),
|
||||
/// The public key of the validator has not been seen locally.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It's unclear if this sync committee message is valid, however we have already observed an aggregate
|
||||
/// sync committee message from this validator for this epoch and should not observe another.
|
||||
UnknownValidatorPubkey(PublicKeyBytes),
|
||||
/// A signature on the sync committee message is invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
InvalidSignature,
|
||||
/// We have already observed a signature for the `validator_index` and refuse to process
|
||||
/// another.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// It's unclear if this sync message is valid, however we have already observed a
|
||||
/// signature from this validator for this slot and should not observe
|
||||
/// another.
|
||||
PriorSyncCommitteeMessageKnown { validator_index: u64, slot: Slot },
|
||||
/// The sync committee message was received on an invalid sync committee message subnet.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
InvalidSubnetId {
|
||||
received: SyncSubnetId,
|
||||
expected: Vec<SyncSubnetId>,
|
||||
},
|
||||
/// The sync message failed the `state_processing` verification stage.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// The peer has sent an invalid message.
|
||||
Invalid(SyncCommitteeMessageValidationError),
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
BeaconChainError(BeaconChainError),
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
BeaconStateError(BeaconStateError),
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
InvalidSubcommittee {
|
||||
subcommittee_index: u64,
|
||||
subcommittee_size: u64,
|
||||
},
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
ArithError(ArithError),
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
ContributionError(ContributionError),
|
||||
/// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid.
|
||||
///
|
||||
/// ## Peer scoring
|
||||
///
|
||||
/// We were unable to process this sync committee message due to an internal error. It's unclear if the
|
||||
/// sync committee message is valid.
|
||||
SyncCommitteeError(SyncCommitteeError),
|
||||
}
|
||||
|
||||
impl From<BeaconChainError> for Error {
|
||||
fn from(e: BeaconChainError) -> Self {
|
||||
Error::BeaconChainError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BeaconStateError> for Error {
|
||||
fn from(e: BeaconStateError) -> Self {
|
||||
Error::BeaconStateError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SyncCommitteeError> for Error {
|
||||
fn from(e: SyncCommitteeError) -> Self {
|
||||
Error::SyncCommitteeError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArithError> for Error {
|
||||
fn from(e: ArithError) -> Self {
|
||||
Error::ArithError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContributionError> for Error {
|
||||
fn from(e: ContributionError) -> Self {
|
||||
Error::ContributionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a `SignedContributionAndProof` that has been verified for propagation on the gossip network.\
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Clone(bound = "T: BeaconChainTypes"))]
|
||||
pub struct VerifiedSyncContribution<T: BeaconChainTypes> {
|
||||
signed_aggregate: SignedContributionAndProof<T::EthSpec>,
|
||||
}
|
||||
|
||||
/// Wraps a `SyncCommitteeMessage` that has been verified for propagation on the gossip network.
|
||||
#[derive(Clone)]
|
||||
pub struct VerifiedSyncCommitteeMessage {
|
||||
sync_message: SyncCommitteeMessage,
|
||||
subnet_positions: HashMap<SyncSubnetId, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> VerifiedSyncContribution<T> {
|
||||
/// Returns `Ok(Self)` if the `signed_aggregate` is valid to be (re)published on the gossip
|
||||
/// network.
|
||||
pub fn verify(
|
||||
signed_aggregate: SignedContributionAndProof<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<Self, Error> {
|
||||
let aggregator_index = signed_aggregate.message.aggregator_index;
|
||||
let contribution = &signed_aggregate.message.contribution;
|
||||
let subcommittee_index = contribution.subcommittee_index as usize;
|
||||
|
||||
// Ensure sync committee contribution is within the MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance.
|
||||
verify_propagation_slot_range(chain, contribution)?;
|
||||
|
||||
// Validate subcommittee index.
|
||||
if contribution.subcommittee_index >= SYNC_COMMITTEE_SUBNET_COUNT {
|
||||
return Err(Error::InvalidSubcommittee {
|
||||
subcommittee_index: contribution.subcommittee_index,
|
||||
subcommittee_size: SYNC_COMMITTEE_SUBNET_COUNT,
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that the sync committee message has participants.
|
||||
if contribution.aggregation_bits.is_zero() {
|
||||
return Err(Error::EmptyAggregationBitfield);
|
||||
}
|
||||
|
||||
// Ensure the aggregator's pubkey is in the declared subcommittee of the current sync committee
|
||||
let pubkey_bytes = chain
|
||||
.validator_pubkey_bytes(aggregator_index as usize)?
|
||||
.ok_or(Error::UnknownValidatorIndex(aggregator_index as usize))?;
|
||||
let sync_subcommittee_pubkeys = chain
|
||||
.sync_committee_at_next_slot(contribution.get_slot())?
|
||||
.get_subcommittee_pubkeys(subcommittee_index)?;
|
||||
|
||||
if !sync_subcommittee_pubkeys.contains(&pubkey_bytes) {
|
||||
return Err(Error::AggregatorNotInCommittee { aggregator_index });
|
||||
};
|
||||
|
||||
// Ensure the valid sync contribution has not already been seen locally.
|
||||
let contribution_root = contribution.tree_hash_root();
|
||||
if chain
|
||||
.observed_sync_contributions
|
||||
.write()
|
||||
.is_known(contribution, contribution_root)
|
||||
.map_err(|e| Error::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(Error::SyncContributionAlreadyKnown(contribution_root));
|
||||
}
|
||||
|
||||
// Ensure there has been no other observed aggregate for the given `aggregator_index`.
|
||||
//
|
||||
// Note: do not observe yet, only observe once the sync contribution has been verified.
|
||||
let observed_key =
|
||||
SlotSubcommitteeIndex::new(contribution.slot, contribution.subcommittee_index);
|
||||
match chain
|
||||
.observed_sync_aggregators
|
||||
.read()
|
||||
.validator_has_been_observed(observed_key, aggregator_index as usize)
|
||||
{
|
||||
Ok(true) => Err(Error::AggregatorAlreadyKnown(aggregator_index)),
|
||||
Ok(false) => Ok(()),
|
||||
Err(e) => Err(BeaconChainError::from(e).into()),
|
||||
}?;
|
||||
|
||||
// Note: this clones the signature which is known to be a relatively slow operation.
|
||||
//
|
||||
// Future optimizations should remove this clone.
|
||||
let selection_proof =
|
||||
SyncSelectionProof::from(signed_aggregate.message.selection_proof.clone());
|
||||
|
||||
if !selection_proof
|
||||
.is_aggregator::<T::EthSpec>()
|
||||
.map_err(|e| Error::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(Error::InvalidSelectionProof { aggregator_index });
|
||||
}
|
||||
|
||||
// Gather all validator pubkeys that signed this contribution.
|
||||
let participant_pubkeys = sync_subcommittee_pubkeys
|
||||
.into_iter()
|
||||
.zip(contribution.aggregation_bits.iter())
|
||||
.filter_map(|(pubkey, bit)| bit.then(|| pubkey))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Ensure that all signatures are valid.
|
||||
if !verify_signed_aggregate_signatures(
|
||||
chain,
|
||||
&signed_aggregate,
|
||||
participant_pubkeys.as_slice(),
|
||||
)? {
|
||||
return Err(Error::InvalidSignature);
|
||||
}
|
||||
|
||||
let contribution = &signed_aggregate.message.contribution;
|
||||
let aggregator_index = signed_aggregate.message.aggregator_index;
|
||||
|
||||
// Observe the valid sync contribution so we do not re-process it.
|
||||
//
|
||||
// It's important to double check that the contribution is not already known, otherwise two
|
||||
// contribution processed at the same time could be published.
|
||||
if let ObserveOutcome::AlreadyKnown = chain
|
||||
.observed_sync_contributions
|
||||
.write()
|
||||
.observe_item(contribution, Some(contribution_root))
|
||||
.map_err(|e| Error::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(Error::SyncContributionAlreadyKnown(contribution_root));
|
||||
}
|
||||
|
||||
// Observe the aggregator so we don't process another aggregate from them.
|
||||
//
|
||||
// It's important to double check that the sync committee message is not already known, otherwise two
|
||||
// sync committee messages processed at the same time could be published.
|
||||
if chain
|
||||
.observed_sync_aggregators
|
||||
.write()
|
||||
.observe_validator(observed_key, aggregator_index as usize)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorSyncCommitteeMessageKnown {
|
||||
validator_index: aggregator_index,
|
||||
slot: contribution.slot,
|
||||
});
|
||||
}
|
||||
Ok(VerifiedSyncContribution { signed_aggregate })
|
||||
}
|
||||
|
||||
/// A helper function to add this aggregate to `beacon_chain.op_pool`.
|
||||
pub fn add_to_pool(self, chain: &BeaconChain<T>) -> Result<(), Error> {
|
||||
chain.add_contribution_to_block_inclusion_pool(self)
|
||||
}
|
||||
|
||||
/// Returns the underlying `contribution` for the `signed_aggregate`.
|
||||
pub fn contribution(self) -> SyncCommitteeContribution<T::EthSpec> {
|
||||
self.signed_aggregate.message.contribution
|
||||
}
|
||||
|
||||
/// Returns the underlying `signed_aggregate`.
|
||||
pub fn aggregate(&self) -> &SignedContributionAndProof<T::EthSpec> {
|
||||
&self.signed_aggregate
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifiedSyncCommitteeMessage {
|
||||
/// Returns `Ok(Self)` if the `sync_message` is valid to be (re)published on the gossip
|
||||
/// network.
|
||||
///
|
||||
/// `subnet_id` is the subnet from which we received this sync message. This function will
|
||||
/// verify that it was received on the correct subnet.
|
||||
pub fn verify<T: BeaconChainTypes>(
|
||||
sync_message: SyncCommitteeMessage,
|
||||
subnet_id: SyncSubnetId,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<Self, Error> {
|
||||
// Ensure sync committee message is for the current slot (within a
|
||||
// MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance).
|
||||
//
|
||||
// We do not queue future sync committee messages for later processing.
|
||||
verify_propagation_slot_range(chain, &sync_message)?;
|
||||
|
||||
// Ensure the `subnet_id` is valid for the given validator.
|
||||
let pubkey = chain
|
||||
.validator_pubkey_bytes(sync_message.validator_index as usize)?
|
||||
.ok_or(Error::UnknownValidatorIndex(
|
||||
sync_message.validator_index as usize,
|
||||
))?;
|
||||
|
||||
let sync_committee = chain.sync_committee_at_next_slot(sync_message.get_slot())?;
|
||||
let subnet_positions = sync_committee.subcommittee_positions_for_public_key(&pubkey)?;
|
||||
|
||||
if !subnet_positions.contains_key(&subnet_id) {
|
||||
return Err(Error::InvalidSubnetId {
|
||||
received: subnet_id,
|
||||
expected: subnet_positions.keys().cloned().collect::<Vec<_>>(),
|
||||
});
|
||||
}
|
||||
|
||||
// The sync committee message is the first valid message received for the participating validator
|
||||
// for the slot, sync_message.slot.
|
||||
let validator_index = sync_message.validator_index;
|
||||
if chain
|
||||
.observed_sync_contributors
|
||||
.read()
|
||||
.validator_has_been_observed(
|
||||
SlotSubcommitteeIndex::new(sync_message.slot, subnet_id.into()),
|
||||
validator_index as usize,
|
||||
)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorSyncCommitteeMessageKnown {
|
||||
validator_index,
|
||||
slot: sync_message.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// The aggregate signature of the sync committee message is valid.
|
||||
verify_sync_committee_message(chain, &sync_message, &pubkey)?;
|
||||
|
||||
// Now that the sync committee message has been fully verified, store that we have received a valid
|
||||
// sync committee message from this validator.
|
||||
//
|
||||
// It's important to double check that the sync committee message still hasn't been observed, since
|
||||
// there can be a race-condition if we receive two sync committee messages at the same time and
|
||||
// process them in different threads.
|
||||
if chain
|
||||
.observed_sync_contributors
|
||||
.write()
|
||||
.observe_validator(
|
||||
SlotSubcommitteeIndex::new(sync_message.slot, subnet_id.into()),
|
||||
validator_index as usize,
|
||||
)
|
||||
.map_err(BeaconChainError::from)?
|
||||
{
|
||||
return Err(Error::PriorSyncCommitteeMessageKnown {
|
||||
validator_index,
|
||||
slot: sync_message.slot,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
sync_message,
|
||||
subnet_positions,
|
||||
})
|
||||
}
|
||||
|
||||
/// A helper function to add this sync committee message to `beacon_chain.naive_sync_aggregation_pool`.
|
||||
pub fn add_to_pool<T: BeaconChainTypes>(self, chain: &BeaconChain<T>) -> Result<Self, Error> {
|
||||
chain.add_to_naive_sync_aggregation_pool(self)
|
||||
}
|
||||
|
||||
/// Returns the subcommittee positions for the sync message, keyed on the `SyncSubnetId` for
|
||||
/// the subnets the signature should be sent on.
|
||||
pub fn subnet_positions(&self) -> &HashMap<SyncSubnetId, Vec<usize>> {
|
||||
&self.subnet_positions
|
||||
}
|
||||
|
||||
/// Returns the wrapped `SyncCommitteeMessage`.
|
||||
pub fn sync_message(&self) -> &SyncCommitteeMessage {
|
||||
&self.sync_message
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that the `sync_contribution` is within the acceptable gossip propagation range, with reference
|
||||
/// to the current slot of the `chain`.
|
||||
///
|
||||
/// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
||||
pub fn verify_propagation_slot_range<T: BeaconChainTypes, U: SlotData>(
|
||||
chain: &BeaconChain<T>,
|
||||
sync_contribution: &U,
|
||||
) -> Result<(), Error> {
|
||||
let message_slot = sync_contribution.get_slot();
|
||||
|
||||
let latest_permissible_slot = chain
|
||||
.slot_clock
|
||||
.now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY)
|
||||
.ok_or(BeaconChainError::UnableToReadSlot)?;
|
||||
if message_slot > latest_permissible_slot {
|
||||
return Err(Error::FutureSlot {
|
||||
message_slot,
|
||||
latest_permissible_slot,
|
||||
});
|
||||
}
|
||||
|
||||
let earliest_permissible_slot = chain
|
||||
.slot_clock
|
||||
.now_with_past_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY)
|
||||
.ok_or(BeaconChainError::UnableToReadSlot)?;
|
||||
|
||||
if message_slot < earliest_permissible_slot {
|
||||
return Err(Error::PastSlot {
|
||||
message_slot,
|
||||
earliest_permissible_slot,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies all the signatures in a `SignedContributionAndProof` using BLS batch verification. This
|
||||
/// includes three signatures:
|
||||
///
|
||||
/// - `signed_aggregate.signature`
|
||||
/// - `signed_aggregate.message.selection_proof`
|
||||
/// - `signed_aggregate.message.aggregate.signature`
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// - `Ok(true)`: if all signatures are valid.
|
||||
/// - `Ok(false)`: if one or more signatures are invalid.
|
||||
/// - `Err(e)`: if there was an error preventing signature verification.
|
||||
pub fn verify_signed_aggregate_signatures<T: BeaconChainTypes>(
|
||||
chain: &BeaconChain<T>,
|
||||
signed_aggregate: &SignedContributionAndProof<T::EthSpec>,
|
||||
participant_pubkeys: &[PublicKeyBytes],
|
||||
) -> Result<bool, Error> {
|
||||
let pubkey_cache = chain
|
||||
.validator_pubkey_cache
|
||||
.try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT)
|
||||
.ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?;
|
||||
|
||||
let aggregator_index = signed_aggregate.message.aggregator_index;
|
||||
if aggregator_index >= pubkey_cache.len() as u64 {
|
||||
return Err(Error::AggregatorPubkeyUnknown(aggregator_index));
|
||||
}
|
||||
|
||||
let next_slot_epoch =
|
||||
(signed_aggregate.message.contribution.slot + 1).epoch(T::EthSpec::slots_per_epoch());
|
||||
let fork = chain.spec.fork_at_epoch(next_slot_epoch);
|
||||
|
||||
let signature_sets = vec![
|
||||
signed_sync_aggregate_selection_proof_signature_set(
|
||||
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
|
||||
&signed_aggregate,
|
||||
&fork,
|
||||
chain.genesis_validators_root,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(BeaconChainError::SignatureSetError)?,
|
||||
signed_sync_aggregate_signature_set(
|
||||
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
|
||||
&signed_aggregate,
|
||||
&fork,
|
||||
chain.genesis_validators_root,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(BeaconChainError::SignatureSetError)?,
|
||||
sync_committee_contribution_signature_set_from_pubkeys::<T::EthSpec, _>(
|
||||
|validator_index| {
|
||||
pubkey_cache
|
||||
.get_pubkey_from_pubkey_bytes(validator_index)
|
||||
.map(Cow::Borrowed)
|
||||
},
|
||||
participant_pubkeys,
|
||||
&signed_aggregate.message.contribution.signature,
|
||||
signed_aggregate
|
||||
.message
|
||||
.contribution
|
||||
.slot
|
||||
.epoch(T::EthSpec::slots_per_epoch()),
|
||||
signed_aggregate.message.contribution.beacon_block_root,
|
||||
&fork,
|
||||
chain.genesis_validators_root,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(BeaconChainError::SignatureSetError)?,
|
||||
];
|
||||
|
||||
Ok(verify_signature_sets(signature_sets.iter()))
|
||||
}
|
||||
|
||||
/// Verifies that the signature of the `sync_message` is valid.
|
||||
pub fn verify_sync_committee_message<T: BeaconChainTypes>(
|
||||
chain: &BeaconChain<T>,
|
||||
sync_message: &SyncCommitteeMessage,
|
||||
pubkey_bytes: &PublicKeyBytes,
|
||||
) -> Result<(), Error> {
|
||||
let signature_setup_timer =
|
||||
metrics::start_timer(&metrics::SYNC_MESSAGE_PROCESSING_SIGNATURE_SETUP_TIMES);
|
||||
|
||||
let pubkey_cache = chain
|
||||
.validator_pubkey_cache
|
||||
.try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT)
|
||||
.ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?;
|
||||
|
||||
let pubkey = pubkey_cache
|
||||
.get_pubkey_from_pubkey_bytes(pubkey_bytes)
|
||||
.map(Cow::Borrowed)
|
||||
.ok_or_else(|| Error::UnknownValidatorPubkey(*pubkey_bytes))?;
|
||||
|
||||
let next_slot_epoch = (sync_message.get_slot() + 1).epoch(T::EthSpec::slots_per_epoch());
|
||||
let fork = chain.spec.fork_at_epoch(next_slot_epoch);
|
||||
|
||||
let agg_sig = AggregateSignature::from(&sync_message.signature);
|
||||
let signature_set = sync_committee_message_set_from_pubkeys::<T::EthSpec>(
|
||||
pubkey,
|
||||
&agg_sig,
|
||||
sync_message.slot.epoch(T::EthSpec::slots_per_epoch()),
|
||||
sync_message.beacon_block_root,
|
||||
&fork,
|
||||
chain.genesis_validators_root,
|
||||
&chain.spec,
|
||||
)
|
||||
.map_err(BeaconChainError::SignatureSetError)?;
|
||||
|
||||
metrics::stop_timer(signature_setup_timer);
|
||||
|
||||
let _signature_verification_timer =
|
||||
metrics::start_timer(&metrics::SYNC_MESSAGE_PROCESSING_SIGNATURE_TIMES);
|
||||
|
||||
if signature_set.verify() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidSignature)
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,18 @@ use store::{config::StoreConfig, BlockReplay, HotColdDB, ItemStore, LevelDB, Mem
|
||||
use task_executor::ShutdownReason;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use tree_hash::TreeHash;
|
||||
use types::sync_selection_proof::SyncSelectionProof;
|
||||
pub use types::test_utils::generate_deterministic_keypairs;
|
||||
use types::{
|
||||
typenum::U4294967296, AggregateSignature, Attestation, AttestationData, AttesterSlashing,
|
||||
BeaconBlock, BeaconState, BeaconStateHash, ChainSpec, Checkpoint, Deposit, DepositData, Domain,
|
||||
Epoch, EthSpec, ForkName, Graffiti, Hash256, IndexedAttestation, Keypair, ProposerSlashing,
|
||||
PublicKeyBytes, SelectionProof, SignatureBytes, SignedAggregateAndProof, SignedBeaconBlock,
|
||||
SignedBeaconBlockHash, SignedRoot, SignedVoluntaryExit, Slot, SubnetId, VariableList,
|
||||
SignedBeaconBlockHash, SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, Slot,
|
||||
SubnetId, SyncCommittee, SyncCommitteeContribution, SyncCommitteeMessage, VariableList,
|
||||
VoluntaryExit,
|
||||
};
|
||||
|
||||
pub use types::test_utils::generate_deterministic_keypairs;
|
||||
|
||||
// 4th September 2019
|
||||
pub const HARNESS_GENESIS_TIME: u64 = 1_567_552_690;
|
||||
// This parameter is required by a builder but not used because we use the `TestingSlotClock`.
|
||||
@@ -87,6 +88,14 @@ pub enum AttestationStrategy {
|
||||
SomeValidators(Vec<usize>),
|
||||
}
|
||||
|
||||
/// Indicates whether the `BeaconChainHarness` should use the `state.current_sync_committee` or
|
||||
/// `state.next_sync_committee` when creating sync messages or contributions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RelativeSyncCommittee {
|
||||
Current,
|
||||
Next,
|
||||
}
|
||||
|
||||
fn make_rng() -> Mutex<StdRng> {
|
||||
// Nondeterminism in tests is a highly undesirable thing. Seed the RNG to some arbitrary
|
||||
// but fixed value for reproducibility.
|
||||
@@ -155,6 +164,11 @@ pub type HarnessAttestations<E> = Vec<(
|
||||
Option<SignedAggregateAndProof<E>>,
|
||||
)>;
|
||||
|
||||
pub type HarnessSyncContributions<E> = Vec<(
|
||||
Vec<(SyncCommitteeMessage, usize)>,
|
||||
Option<SignedContributionAndProof<E>>,
|
||||
)>;
|
||||
|
||||
impl<E: EthSpec> BeaconChainHarness<EphemeralHarnessType<E>> {
|
||||
pub fn new(
|
||||
eth_spec_instance: E,
|
||||
@@ -596,6 +610,57 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A list of sync messages for the given state.
|
||||
pub fn make_sync_committee_messages(
|
||||
&self,
|
||||
state: &BeaconState<E>,
|
||||
head_block_root: Hash256,
|
||||
message_slot: Slot,
|
||||
relative_sync_committee: RelativeSyncCommittee,
|
||||
) -> Vec<Vec<(SyncCommitteeMessage, usize)>> {
|
||||
let sync_committee: Arc<SyncCommittee<E>> = match relative_sync_committee {
|
||||
RelativeSyncCommittee::Current => state
|
||||
.current_sync_committee()
|
||||
.expect("should be called on altair beacon state")
|
||||
.clone(),
|
||||
RelativeSyncCommittee::Next => state
|
||||
.next_sync_committee()
|
||||
.expect("should be called on altair beacon state")
|
||||
.clone(),
|
||||
};
|
||||
|
||||
sync_committee
|
||||
.pubkeys
|
||||
.as_ref()
|
||||
.chunks(E::sync_subcommittee_size())
|
||||
.map(|subcommittee| {
|
||||
subcommittee
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(subcommittee_position, pubkey)| {
|
||||
let validator_index = self
|
||||
.chain
|
||||
.validator_index(pubkey)
|
||||
.expect("should find validator index")
|
||||
.expect("pubkey should exist in the beacon chain");
|
||||
|
||||
let sync_message = SyncCommitteeMessage::new::<E>(
|
||||
message_slot,
|
||||
head_block_root,
|
||||
validator_index as u64,
|
||||
&self.validator_keypairs[validator_index].sk,
|
||||
&state.fork(),
|
||||
state.genesis_validators_root(),
|
||||
&self.spec,
|
||||
);
|
||||
|
||||
(sync_message, subcommittee_position)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deprecated: Use make_unaggregated_attestations() instead.
|
||||
///
|
||||
/// A list of attestations for each committee for the given slot.
|
||||
@@ -712,6 +777,94 @@ where
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn make_sync_contributions(
|
||||
&self,
|
||||
state: &BeaconState<E>,
|
||||
block_hash: Hash256,
|
||||
slot: Slot,
|
||||
relative_sync_committee: RelativeSyncCommittee,
|
||||
) -> HarnessSyncContributions<E> {
|
||||
let sync_messages =
|
||||
self.make_sync_committee_messages(&state, block_hash, slot, relative_sync_committee);
|
||||
|
||||
let sync_contributions: Vec<Option<SignedContributionAndProof<E>>> = sync_messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(subnet_id, committee_messages)| {
|
||||
// If there are any sync messages in this committee, create an aggregate.
|
||||
if let Some((sync_message, subcommittee_position)) = committee_messages.first() {
|
||||
let sync_committee: Arc<SyncCommittee<E>> = state
|
||||
.current_sync_committee()
|
||||
.expect("should be called on altair beacon state")
|
||||
.clone();
|
||||
|
||||
let aggregator_index = sync_committee
|
||||
.get_subcommittee_pubkeys(subnet_id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find_map(|pubkey| {
|
||||
let validator_index = self
|
||||
.chain
|
||||
.validator_index(pubkey)
|
||||
.expect("should find validator index")
|
||||
.expect("pubkey should exist in the beacon chain");
|
||||
|
||||
let selection_proof = SyncSelectionProof::new::<E>(
|
||||
slot,
|
||||
subnet_id as u64,
|
||||
&self.validator_keypairs[validator_index].sk,
|
||||
&state.fork(),
|
||||
state.genesis_validators_root(),
|
||||
&self.spec,
|
||||
);
|
||||
|
||||
selection_proof
|
||||
.is_aggregator::<E>()
|
||||
.expect("should determine aggregator")
|
||||
.then(|| validator_index)
|
||||
})?;
|
||||
|
||||
let default = SyncCommitteeContribution::from_message(
|
||||
&sync_message,
|
||||
subnet_id as u64,
|
||||
*subcommittee_position,
|
||||
)
|
||||
.expect("should derive sync contribution");
|
||||
|
||||
let aggregate = committee_messages.iter().skip(1).fold(
|
||||
default,
|
||||
|mut agg, (sig, position)| {
|
||||
let contribution = SyncCommitteeContribution::from_message(
|
||||
sig,
|
||||
subnet_id as u64,
|
||||
*position,
|
||||
)
|
||||
.expect("should derive sync contribution");
|
||||
agg.aggregate(&contribution);
|
||||
agg
|
||||
},
|
||||
);
|
||||
|
||||
let signed_aggregate = SignedContributionAndProof::from_aggregate(
|
||||
aggregator_index as u64,
|
||||
aggregate,
|
||||
None,
|
||||
&self.validator_keypairs[aggregator_index].sk,
|
||||
&state.fork(),
|
||||
state.genesis_validators_root(),
|
||||
&self.spec,
|
||||
);
|
||||
|
||||
Some(signed_aggregate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
sync_messages.into_iter().zip(sync_contributions).collect()
|
||||
}
|
||||
|
||||
pub fn make_attester_slashing(&self, validator_indices: Vec<u64>) -> AttesterSlashing<E> {
|
||||
let mut attestation_1 = IndexedAttestation {
|
||||
attesting_indices: VariableList::new(validator_indices).unwrap(),
|
||||
|
||||
@@ -173,6 +173,13 @@ impl<T: BeaconChainTypes> ValidatorPubkeyCache<T> {
|
||||
self.pubkeys.get(i)
|
||||
}
|
||||
|
||||
/// Get the `PublicKey` for a validator with `PublicKeyBytes`.
|
||||
pub fn get_pubkey_from_pubkey_bytes(&self, pubkey: &PublicKeyBytes) -> Option<&PublicKey> {
|
||||
self.get_index(pubkey)
|
||||
.map(|index| self.get(index))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Get the public key (in bytes form) for a validator with index `i`.
|
||||
pub fn get_pubkey_bytes(&self, i: usize) -> Option<&PublicKeyBytes> {
|
||||
self.pubkey_bytes.get(i)
|
||||
|
||||
Reference in New Issue
Block a user