mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-21 05:44:44 +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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user