lighthouse/beacon_node/operation_pool/src/lib.rs
Michael Sproul 3b901dc5ec Pack attestations into blocks in parallel (#2307)
## Proposed Changes

Use two instances of max cover when packing attestations into blocks: one for the previous epoch, and one for the current epoch. This reduces the amount of computation done by roughly half due to the `O(n^2)` running time of max cover (`2 * (n/2)^2 = n^2/2`). This should help alleviate some load on block proposal, particularly on Prater.
2021-04-13 05:27:42 +00:00

1247 lines
45 KiB
Rust

mod attestation;
mod attestation_id;
mod attester_slashing;
mod max_cover;
mod metrics;
mod persistence;
pub use persistence::PersistedOperationPool;
use attestation::AttMaxCover;
use attestation_id::AttestationId;
use attester_slashing::AttesterSlashingMaxCover;
use max_cover::{maximum_cover, MaxCover};
use parking_lot::RwLock;
use state_processing::per_block_processing::errors::AttestationValidationError;
use state_processing::per_block_processing::{
get_slashable_indices_modular, verify_attestation_for_block_inclusion, verify_exit,
VerifySignatures,
};
use state_processing::SigVerifiedOp;
use std::collections::{hash_map, HashMap, HashSet};
use std::marker::PhantomData;
use std::ptr;
use types::{
typenum::Unsigned, Attestation, AttesterSlashing, BeaconState, BeaconStateError, ChainSpec,
Epoch, EthSpec, Fork, ForkVersion, Hash256, ProposerSlashing, RelativeEpoch,
SignedVoluntaryExit, Validator,
};
#[derive(Default, Debug)]
pub struct OperationPool<T: EthSpec + Default> {
/// Map from attestation ID (see below) to vectors of attestations.
attestations: RwLock<HashMap<AttestationId, Vec<Attestation<T>>>>,
/// Set of attester slashings, and the fork version they were verified against.
attester_slashings: RwLock<HashSet<(AttesterSlashing<T>, ForkVersion)>>,
/// Map from proposer index to slashing.
proposer_slashings: RwLock<HashMap<u64, ProposerSlashing>>,
/// Map from exiting validator to their exit data.
voluntary_exits: RwLock<HashMap<u64, SignedVoluntaryExit>>,
_phantom: PhantomData<T>,
}
#[derive(Debug, PartialEq)]
pub enum OpPoolError {
GetAttestationsTotalBalanceError(BeaconStateError),
}
impl<T: EthSpec> OperationPool<T> {
/// Create a new operation pool.
pub fn new() -> Self {
Self::default()
}
/// Insert an attestation into the pool, aggregating it with existing attestations if possible.
///
/// ## Note
///
/// This function assumes the given `attestation` is valid.
pub fn insert_attestation(
&self,
attestation: Attestation<T>,
fork: &Fork,
genesis_validators_root: Hash256,
spec: &ChainSpec,
) -> Result<(), AttestationValidationError> {
let id = AttestationId::from_data(&attestation.data, fork, genesis_validators_root, spec);
// Take a write lock on the attestations map.
let mut attestations = self.attestations.write();
let existing_attestations = match attestations.entry(id) {
hash_map::Entry::Vacant(entry) => {
entry.insert(vec![attestation]);
return Ok(());
}
hash_map::Entry::Occupied(entry) => entry.into_mut(),
};
let mut aggregated = false;
for existing_attestation in existing_attestations.iter_mut() {
if existing_attestation.signers_disjoint_from(&attestation) {
existing_attestation.aggregate(&attestation);
aggregated = true;
} else if *existing_attestation == attestation {
aggregated = true;
}
}
if !aggregated {
existing_attestations.push(attestation);
}
Ok(())
}
/// Total number of attestations in the pool, including attestations for the same data.
pub fn num_attestations(&self) -> usize {
self.attestations.read().values().map(Vec::len).sum()
}
/// Return all valid attestations for the given epoch, for use in max cover.
fn get_valid_attestations_for_epoch<'a>(
&'a self,
epoch: Epoch,
all_attestations: &'a HashMap<AttestationId, Vec<Attestation<T>>>,
state: &'a BeaconState<T>,
total_active_balance: u64,
validity_filter: impl FnMut(&&Attestation<T>) -> bool + Send,
spec: &'a ChainSpec,
) -> impl Iterator<Item = AttMaxCover<'a, T>> + Send {
let domain_bytes = AttestationId::compute_domain_bytes(
epoch,
&state.fork,
state.genesis_validators_root,
spec,
);
all_attestations
.iter()
.filter(move |(key, _)| key.domain_bytes_match(&domain_bytes))
.flat_map(|(_, attestations)| attestations)
.filter(move |attestation| attestation.data.target.epoch == epoch)
.filter(move |attestation| {
// Ensure attestations are valid for block inclusion
verify_attestation_for_block_inclusion(
state,
attestation,
VerifySignatures::False,
spec,
)
.is_ok()
})
.filter(validity_filter)
.filter_map(move |att| AttMaxCover::new(att, state, total_active_balance, spec))
}
/// Get a list of attestations for inclusion in a block.
///
/// The `validity_filter` is a closure that provides extra filtering of the attestations
/// before an approximately optimal bundle is constructed. We use it to provide access
/// to the fork choice data from the `BeaconChain` struct that doesn't logically belong
/// in the operation pool.
pub fn get_attestations(
&self,
state: &BeaconState<T>,
prev_epoch_validity_filter: impl FnMut(&&Attestation<T>) -> bool + Send,
curr_epoch_validity_filter: impl FnMut(&&Attestation<T>) -> bool + Send,
spec: &ChainSpec,
) -> Result<Vec<Attestation<T>>, OpPoolError> {
// Attestations for the current fork, which may be from the current or previous epoch.
let prev_epoch = state.previous_epoch();
let current_epoch = state.current_epoch();
let all_attestations = self.attestations.read();
let active_indices = state
.get_cached_active_validator_indices(RelativeEpoch::Current)
.map_err(OpPoolError::GetAttestationsTotalBalanceError)?;
let total_active_balance = state
.get_total_balance(&active_indices, spec)
.map_err(OpPoolError::GetAttestationsTotalBalanceError)?;
// Split attestations for the previous & current epochs, so that we
// can optimise them individually in parallel.
let prev_epoch_att = self.get_valid_attestations_for_epoch(
prev_epoch,
&*all_attestations,
state,
total_active_balance,
prev_epoch_validity_filter,
spec,
);
let curr_epoch_att = self.get_valid_attestations_for_epoch(
current_epoch,
&*all_attestations,
state,
total_active_balance,
curr_epoch_validity_filter,
spec,
);
let prev_epoch_limit = std::cmp::min(
T::MaxPendingAttestations::to_usize()
.saturating_sub(state.previous_epoch_attestations.len()),
T::MaxAttestations::to_usize(),
);
let (prev_cover, curr_cover) = rayon::join(
move || {
let _timer = metrics::start_timer(&metrics::ATTESTATION_PREV_EPOCH_PACKING_TIME);
// If we're in the genesis epoch, just use the current epoch attestations.
if prev_epoch == current_epoch {
vec![]
} else {
maximum_cover(prev_epoch_att, prev_epoch_limit)
}
},
move || {
let _timer = metrics::start_timer(&metrics::ATTESTATION_CURR_EPOCH_PACKING_TIME);
maximum_cover(curr_epoch_att, T::MaxAttestations::to_usize())
},
);
Ok(max_cover::merge_solutions(
curr_cover,
prev_cover,
T::MaxAttestations::to_usize(),
))
}
/// Remove attestations which are too old to be included in a block.
pub fn prune_attestations(&self, current_epoch: Epoch) {
// Prune attestations that are from before the previous epoch.
self.attestations.write().retain(|_, attestations| {
// All the attestations in this bucket have the same data, so we only need to
// check the first one.
attestations
.first()
.map_or(false, |att| current_epoch <= att.data.target.epoch + 1)
});
}
/// Insert a proposer slashing into the pool.
pub fn insert_proposer_slashing(
&self,
verified_proposer_slashing: SigVerifiedOp<ProposerSlashing>,
) {
let slashing = verified_proposer_slashing.into_inner();
self.proposer_slashings
.write()
.insert(slashing.signed_header_1.message.proposer_index, slashing);
}
/// Insert an attester slashing into the pool.
pub fn insert_attester_slashing(
&self,
verified_slashing: SigVerifiedOp<AttesterSlashing<T>>,
fork: Fork,
) {
self.attester_slashings
.write()
.insert((verified_slashing.into_inner(), fork.current_version));
}
/// Get proposer and attester slashings for inclusion in a block.
///
/// This function computes both types of slashings together, because
/// attester slashings may be invalidated by proposer slashings included
/// earlier in the block.
pub fn get_slashings(
&self,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> (Vec<ProposerSlashing>, Vec<AttesterSlashing<T>>) {
let proposer_slashings = filter_limit_operations(
self.proposer_slashings.read().values(),
|slashing| {
state
.validators
.get(slashing.signed_header_1.message.proposer_index as usize)
.map_or(false, |validator| !validator.slashed)
},
T::MaxProposerSlashings::to_usize(),
);
// Set of validators to be slashed, so we don't attempt to construct invalid attester
// slashings.
let to_be_slashed = proposer_slashings
.iter()
.map(|s| s.signed_header_1.message.proposer_index)
.collect::<HashSet<_>>();
let reader = self.attester_slashings.read();
let relevant_attester_slashings = reader.iter().flat_map(|(slashing, fork)| {
if *fork == state.fork.previous_version || *fork == state.fork.current_version {
AttesterSlashingMaxCover::new(&slashing, &to_be_slashed, state, spec)
} else {
None
}
});
let attester_slashings = maximum_cover(
relevant_attester_slashings,
T::MaxAttesterSlashings::to_usize(),
)
.into_iter()
.map(|cover| cover.object().clone())
.collect();
(proposer_slashings, attester_slashings)
}
/// Prune proposer slashings for validators which are exited in the finalized epoch.
pub fn prune_proposer_slashings(&self, head_state: &BeaconState<T>) {
prune_validator_hash_map(
&mut self.proposer_slashings.write(),
|validator| validator.exit_epoch <= head_state.finalized_checkpoint.epoch,
head_state,
);
}
/// Prune attester slashings for all slashed or withdrawn validators, or attestations on another
/// fork.
pub fn prune_attester_slashings(&self, head_state: &BeaconState<T>) {
self.attester_slashings
.write()
.retain(|(slashing, fork_version)| {
let previous_fork_is_finalized =
head_state.finalized_checkpoint.epoch >= head_state.fork.epoch;
// Prune any slashings which don't match the current fork version, or the previous
// fork version if it is not finalized yet.
let fork_ok = (fork_version == &head_state.fork.current_version)
|| (fork_version == &head_state.fork.previous_version
&& !previous_fork_is_finalized);
// Slashings that don't slash any validators can also be dropped.
let slashing_ok =
get_slashable_indices_modular(head_state, slashing, |_, validator| {
// Declare that a validator is still slashable if they have not exited prior
// to the finalized epoch.
//
// We cannot check the `slashed` field since the `head` is not finalized and
// a fork could un-slash someone.
validator.exit_epoch > head_state.finalized_checkpoint.epoch
})
.map_or(false, |indices| !indices.is_empty());
fork_ok && slashing_ok
});
}
/// Total number of attester slashings in the pool.
pub fn num_attester_slashings(&self) -> usize {
self.attester_slashings.read().len()
}
/// Total number of proposer slashings in the pool.
pub fn num_proposer_slashings(&self) -> usize {
self.proposer_slashings.read().len()
}
/// Insert a voluntary exit that has previously been checked elsewhere.
pub fn insert_voluntary_exit(&self, verified_exit: SigVerifiedOp<SignedVoluntaryExit>) {
let exit = verified_exit.into_inner();
self.voluntary_exits
.write()
.insert(exit.message.validator_index, exit);
}
/// Get a list of voluntary exits for inclusion in a block.
pub fn get_voluntary_exits(
&self,
state: &BeaconState<T>,
spec: &ChainSpec,
) -> Vec<SignedVoluntaryExit> {
filter_limit_operations(
self.voluntary_exits.read().values(),
|exit| verify_exit(state, exit, VerifySignatures::False, spec).is_ok(),
T::MaxVoluntaryExits::to_usize(),
)
}
/// Prune if validator has already exited at or before the finalized checkpoint of the head.
pub fn prune_voluntary_exits(&self, head_state: &BeaconState<T>) {
prune_validator_hash_map(
&mut self.voluntary_exits.write(),
// This condition is slightly too loose, since there will be some finalized exits that
// are missed here.
//
// We choose simplicity over the gain of pruning more exits since they are small and
// should not be seen frequently.
|validator| validator.exit_epoch <= head_state.finalized_checkpoint.epoch,
head_state,
);
}
/// Prune all types of transactions given the latest head state and head fork.
pub fn prune_all(&self, head_state: &BeaconState<T>, current_epoch: Epoch) {
self.prune_attestations(current_epoch);
self.prune_proposer_slashings(head_state);
self.prune_attester_slashings(head_state);
self.prune_voluntary_exits(head_state);
}
/// Total number of voluntary exits in the pool.
pub fn num_voluntary_exits(&self) -> usize {
self.voluntary_exits.read().len()
}
/// Returns all known `Attestation` objects.
///
/// This method may return objects that are invalid for block inclusion.
pub fn get_all_attestations(&self) -> Vec<Attestation<T>> {
self.attestations
.read()
.iter()
.map(|(_, attns)| attns.iter().cloned())
.flatten()
.collect()
}
/// Returns all known `Attestation` objects that pass the provided filter.
///
/// This method may return objects that are invalid for block inclusion.
pub fn get_filtered_attestations<F>(&self, filter: F) -> Vec<Attestation<T>>
where
F: Fn(&Attestation<T>) -> bool,
{
self.attestations
.read()
.iter()
.map(|(_, attns)| attns.iter().cloned())
.flatten()
.filter(filter)
.collect()
}
/// Returns all known `AttesterSlashing` objects.
///
/// This method may return objects that are invalid for block inclusion.
pub fn get_all_attester_slashings(&self) -> Vec<AttesterSlashing<T>> {
self.attester_slashings
.read()
.iter()
.map(|(slashing, _)| slashing.clone())
.collect()
}
/// Returns all known `ProposerSlashing` objects.
///
/// This method may return objects that are invalid for block inclusion.
pub fn get_all_proposer_slashings(&self) -> Vec<ProposerSlashing> {
self.proposer_slashings
.read()
.iter()
.map(|(_, slashing)| slashing.clone())
.collect()
}
/// Returns all known `SignedVoluntaryExit` objects.
///
/// This method may return objects that are invalid for block inclusion.
pub fn get_all_voluntary_exits(&self) -> Vec<SignedVoluntaryExit> {
self.voluntary_exits
.read()
.iter()
.map(|(_, exit)| exit.clone())
.collect()
}
}
/// Filter up to a maximum number of operations out of an iterator.
fn filter_limit_operations<'a, T: 'a, I, F>(operations: I, filter: F, limit: usize) -> Vec<T>
where
I: IntoIterator<Item = &'a T>,
F: Fn(&T) -> bool,
T: Clone,
{
operations
.into_iter()
.filter(|x| filter(*x))
.take(limit)
.cloned()
.collect()
}
/// Remove all entries from the given hash map for which `prune_if` returns true.
///
/// The keys in the map should be validator indices, which will be looked up
/// in the state's validator registry and then passed to `prune_if`.
/// Entries for unknown validators will be kept.
fn prune_validator_hash_map<T, F, E: EthSpec>(
map: &mut HashMap<u64, T>,
prune_if: F,
head_state: &BeaconState<E>,
) where
F: Fn(&Validator) -> bool,
{
map.retain(|&validator_index, _| {
head_state
.validators
.get(validator_index as usize)
.map_or(true, |validator| !prune_if(validator))
});
}
/// Compare two operation pools.
impl<T: EthSpec + Default> PartialEq for OperationPool<T> {
fn eq(&self, other: &Self) -> bool {
if ptr::eq(self, other) {
return true;
}
*self.attestations.read() == *other.attestations.read()
&& *self.attester_slashings.read() == *other.attester_slashings.read()
&& *self.proposer_slashings.read() == *other.proposer_slashings.read()
&& *self.voluntary_exits.read() == *other.voluntary_exits.read()
}
}
// TODO: more tests
#[cfg(all(test, not(debug_assertions)))]
mod release_tests {
use super::attestation::earliest_attestation_validators;
use super::*;
use state_processing::{
common::{get_attesting_indices, get_base_reward},
VerifyOperation,
};
use std::collections::BTreeSet;
use std::iter::FromIterator;
use types::test_utils::*;
use types::*;
/// Create a signed attestation for use in tests.
/// Signed by all validators in `committee[signing_range]` and `committee[extra_signer]`.
fn signed_attestation<R: std::slice::SliceIndex<[usize], Output = [usize]>, E: EthSpec>(
committee: &[usize],
index: u64,
keypairs: &[Keypair],
signing_range: R,
slot: Slot,
state: &BeaconState<E>,
spec: &ChainSpec,
extra_signer: Option<usize>,
) -> Attestation<E> {
let mut builder = TestingAttestationBuilder::new(
AttestationTestTask::Valid,
state,
committee,
slot,
index,
spec,
);
let signers = &committee[signing_range];
let committee_keys = signers.iter().map(|&i| &keypairs[i].sk).collect::<Vec<_>>();
builder.sign(
AttestationTestTask::Valid,
signers,
&committee_keys,
&state.fork,
state.genesis_validators_root,
spec,
);
extra_signer.map(|c_idx| {
let validator_index = committee[c_idx];
builder.sign(
AttestationTestTask::Valid,
&[validator_index],
&[&keypairs[validator_index].sk],
&state.fork,
state.genesis_validators_root,
spec,
)
});
builder.build()
}
/// Test state for attestation-related tests.
fn attestation_test_state<E: EthSpec>(
num_committees: usize,
) -> (BeaconState<E>, Vec<Keypair>, ChainSpec) {
let spec = E::default_spec();
let num_validators =
num_committees * E::slots_per_epoch() as usize * spec.target_committee_size;
let mut state_builder =
TestingBeaconStateBuilder::from_deterministic_keypairs(num_validators, &spec);
let slot_offset = 1000 * E::slots_per_epoch() + E::slots_per_epoch() / 2;
let slot = spec.genesis_slot + slot_offset;
state_builder.teleport_to_slot(slot);
state_builder.build_caches(&spec).unwrap();
let (state, keypairs) = state_builder.build();
(state, keypairs, spec)
}
#[test]
fn test_earliest_attestation() {
let (ref mut state, ref keypairs, ref spec) = attestation_test_state::<MainnetEthSpec>(1);
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
for bc in committees {
let att1 = signed_attestation(
&bc.committee,
bc.index,
keypairs,
..2,
slot,
state,
spec,
None,
);
let att2 = signed_attestation(
&bc.committee,
bc.index,
keypairs,
..,
slot,
state,
spec,
None,
);
assert_eq!(
att1.aggregation_bits.num_set_bits(),
earliest_attestation_validators(&att1, state).num_set_bits()
);
state
.current_epoch_attestations
.push(PendingAttestation {
aggregation_bits: att1.aggregation_bits.clone(),
data: att1.data.clone(),
inclusion_delay: 0,
proposer_index: 0,
})
.unwrap();
assert_eq!(
bc.committee.len() - 2,
earliest_attestation_validators(&att2, state).num_set_bits()
);
}
}
/// End-to-end test of basic attestation handling.
#[test]
fn attestation_aggregation_insert_get_prune() {
let (ref mut state, ref keypairs, ref spec) = attestation_test_state::<MainnetEthSpec>(1);
let op_pool = OperationPool::new();
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
assert_eq!(
committees.len(),
1,
"we expect just one committee with this many validators"
);
for bc in &committees {
let step_size = 2;
for i in (0..bc.committee.len()).step_by(step_size) {
let att = signed_attestation(
&bc.committee,
bc.index,
keypairs,
i..i + step_size,
slot,
state,
spec,
None,
);
op_pool
.insert_attestation(att, &state.fork, state.genesis_validators_root, spec)
.unwrap();
}
}
assert_eq!(op_pool.attestations.read().len(), committees.len());
assert_eq!(op_pool.num_attestations(), committees.len());
// Before the min attestation inclusion delay, get_attestations shouldn't return anything.
state.slot -= 1;
assert_eq!(
op_pool
.get_attestations(state, |_| true, |_| true, spec)
.expect("should have attestations")
.len(),
0
);
// Then once the delay has elapsed, we should get a single aggregated attestation.
state.slot += spec.min_attestation_inclusion_delay;
let block_attestations = op_pool
.get_attestations(state, |_| true, |_| true, spec)
.expect("Should have block attestations");
assert_eq!(block_attestations.len(), committees.len());
let agg_att = &block_attestations[0];
assert_eq!(
agg_att.aggregation_bits.num_set_bits(),
spec.target_committee_size as usize
);
// Prune attestations shouldn't do anything at this point.
op_pool.prune_attestations(state.current_epoch());
assert_eq!(op_pool.num_attestations(), committees.len());
// But once we advance to more than an epoch after the attestation, it should prune it
// out of existence.
state.slot += 2 * MainnetEthSpec::slots_per_epoch();
op_pool.prune_attestations(state.current_epoch());
assert_eq!(op_pool.num_attestations(), 0);
}
/// Adding an attestation already in the pool should not increase the size of the pool.
#[test]
fn attestation_duplicate() {
let (ref mut state, ref keypairs, ref spec) = attestation_test_state::<MainnetEthSpec>(1);
let op_pool = OperationPool::new();
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
for bc in &committees {
let att = signed_attestation(
&bc.committee,
bc.index,
keypairs,
..,
slot,
state,
spec,
None,
);
op_pool
.insert_attestation(
att.clone(),
&state.fork,
state.genesis_validators_root,
spec,
)
.unwrap();
op_pool
.insert_attestation(att, &state.fork, state.genesis_validators_root, spec)
.unwrap();
}
assert_eq!(op_pool.num_attestations(), committees.len());
}
/// Adding lots of attestations that only intersect pairwise should lead to two aggregate
/// attestations.
#[test]
fn attestation_pairwise_overlapping() {
let (ref mut state, ref keypairs, ref spec) = attestation_test_state::<MainnetEthSpec>(1);
let op_pool = OperationPool::new();
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
let step_size = 2;
for bc in &committees {
// Create attestations that overlap on `step_size` validators, like:
// {0,1,2,3}, {2,3,4,5}, {4,5,6,7}, ...
for i in (0..bc.committee.len() - step_size).step_by(step_size) {
let att = signed_attestation(
&bc.committee,
bc.index,
keypairs,
i..i + 2 * step_size,
slot,
state,
spec,
None,
);
op_pool
.insert_attestation(att, &state.fork, state.genesis_validators_root, spec)
.unwrap();
}
}
// The attestations should get aggregated into two attestations that comprise all
// validators.
assert_eq!(op_pool.attestations.read().len(), committees.len());
assert_eq!(op_pool.num_attestations(), 2 * committees.len());
}
/// Create a bunch of attestations signed by a small number of validators, and another
/// bunch signed by a larger number, such that there are at least `max_attestations`
/// signed by the larger number. Then, check that `get_attestations` only returns the
/// high-quality attestations. To ensure that no aggregation occurs, ALL attestations
/// are also signed by the 0th member of the committee.
#[test]
fn attestation_get_max() {
let small_step_size = 2;
let big_step_size = 4;
let (ref mut state, ref keypairs, ref spec) =
attestation_test_state::<MainnetEthSpec>(big_step_size);
let op_pool = OperationPool::new();
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
let max_attestations = <MainnetEthSpec as EthSpec>::MaxAttestations::to_usize();
let target_committee_size = spec.target_committee_size as usize;
let insert_attestations = |bc: &OwnedBeaconCommittee, step_size| {
for i in (0..target_committee_size).step_by(step_size) {
let att = signed_attestation(
&bc.committee,
bc.index,
keypairs,
i..i + step_size,
slot,
state,
spec,
if i == 0 { None } else { Some(0) },
);
op_pool
.insert_attestation(att, &state.fork, state.genesis_validators_root, spec)
.unwrap();
}
};
for committee in &committees {
assert_eq!(committee.committee.len(), target_committee_size);
// Attestations signed by only 2-3 validators
insert_attestations(committee, small_step_size);
// Attestations signed by 4+ validators
insert_attestations(committee, big_step_size);
}
let num_small = target_committee_size / small_step_size;
let num_big = target_committee_size / big_step_size;
assert_eq!(op_pool.attestations.read().len(), committees.len());
assert_eq!(
op_pool.num_attestations(),
(num_small + num_big) * committees.len()
);
assert!(op_pool.num_attestations() > max_attestations);
state.slot += spec.min_attestation_inclusion_delay;
let best_attestations = op_pool
.get_attestations(state, |_| true, |_| true, spec)
.expect("should have best attestations");
assert_eq!(best_attestations.len(), max_attestations);
// All the best attestations should be signed by at least `big_step_size` (4) validators.
for att in &best_attestations {
assert!(att.aggregation_bits.num_set_bits() >= big_step_size);
}
}
#[test]
fn attestation_rewards() {
let small_step_size = 2;
let big_step_size = 4;
let (ref mut state, ref keypairs, ref spec) =
attestation_test_state::<MainnetEthSpec>(big_step_size);
let op_pool = OperationPool::new();
let slot = state.slot - 1;
let committees = state
.get_beacon_committees_at_slot(slot)
.unwrap()
.into_iter()
.map(BeaconCommittee::into_owned)
.collect::<Vec<_>>();
let max_attestations = <MainnetEthSpec as EthSpec>::MaxAttestations::to_usize();
let target_committee_size = spec.target_committee_size as usize;
// Each validator will have a multiple of 1_000_000_000 wei.
// Safe from overflow unless there are about 18B validators (2^64 / 1_000_000_000).
for i in 0..state.validators.len() {
state.validators[i].effective_balance = 1_000_000_000 * i as u64;
}
let insert_attestations = |bc: &OwnedBeaconCommittee, step_size| {
for i in (0..target_committee_size).step_by(step_size) {
let att = signed_attestation(
&bc.committee,
bc.index,
keypairs,
i..i + step_size,
slot,
state,
spec,
if i == 0 { None } else { Some(0) },
);
op_pool
.insert_attestation(att, &state.fork, state.genesis_validators_root, spec)
.unwrap();
}
};
for committee in &committees {
assert_eq!(committee.committee.len(), target_committee_size);
// Attestations signed by only 2-3 validators
insert_attestations(committee, small_step_size);
// Attestations signed by 4+ validators
insert_attestations(committee, big_step_size);
}
let num_small = target_committee_size / small_step_size;
let num_big = target_committee_size / big_step_size;
assert_eq!(op_pool.attestations.read().len(), committees.len());
assert_eq!(
op_pool.num_attestations(),
(num_small + num_big) * committees.len()
);
assert!(op_pool.num_attestations() > max_attestations);
state.slot += spec.min_attestation_inclusion_delay;
let best_attestations = op_pool
.get_attestations(state, |_| true, |_| true, spec)
.expect("should have valid best attestations");
assert_eq!(best_attestations.len(), max_attestations);
let active_indices = state
.get_cached_active_validator_indices(RelativeEpoch::Current)
.unwrap();
let total_active_balance = state.get_total_balance(&active_indices, spec).unwrap();
// Set of indices covered by previous attestations in `best_attestations`.
let mut seen_indices = BTreeSet::new();
// Used for asserting that rewards are in decreasing order.
let mut prev_reward = u64::max_value();
for att in &best_attestations {
let fresh_validators_bitlist = earliest_attestation_validators(att, state);
let committee = state
.get_beacon_committee(att.data.slot, att.data.index)
.expect("should get beacon committee");
let att_indices = BTreeSet::from_iter(
get_attesting_indices::<MainnetEthSpec>(
committee.committee,
&fresh_validators_bitlist,
)
.unwrap(),
);
let fresh_indices = &att_indices - &seen_indices;
let rewards = fresh_indices
.iter()
.map(|validator_index| {
get_base_reward(state, *validator_index as usize, total_active_balance, spec)
.unwrap()
/ spec.proposer_reward_quotient
})
.sum();
// Check that rewards are in decreasing order
assert!(prev_reward >= rewards);
prev_reward = rewards;
seen_indices.extend(fresh_indices);
}
}
struct TestContext {
spec: ChainSpec,
state: BeaconState<MainnetEthSpec>,
keypairs: Vec<Keypair>,
op_pool: OperationPool<MainnetEthSpec>,
}
impl TestContext {
fn new() -> Self {
let spec = MainnetEthSpec::default_spec();
let num_validators = 32;
let mut state_builder =
TestingBeaconStateBuilder::<MainnetEthSpec>::from_deterministic_keypairs(
num_validators,
&spec,
);
state_builder.build_caches(&spec).unwrap();
let (state, keypairs) = state_builder.build();
let op_pool = OperationPool::new();
TestContext {
spec,
state,
keypairs,
op_pool,
}
}
fn proposer_slashing(&self, proposer_index: u64) -> ProposerSlashing {
TestingProposerSlashingBuilder::double_vote::<MainnetEthSpec>(
ProposerSlashingTestTask::Valid,
proposer_index,
&self.keypairs[proposer_index as usize].sk,
&self.state.fork,
self.state.genesis_validators_root,
&self.spec,
)
}
fn attester_slashing(&self, slashed_indices: &[u64]) -> AttesterSlashing<MainnetEthSpec> {
let signer = |idx: u64, message: &[u8]| {
self.keypairs[idx as usize]
.sk
.sign(Hash256::from_slice(&message))
};
TestingAttesterSlashingBuilder::double_vote(
AttesterSlashingTestTask::Valid,
slashed_indices,
signer,
&self.state.fork,
self.state.genesis_validators_root,
&self.spec,
)
}
fn attester_slashing_two_indices(
&self,
slashed_indices_1: &[u64],
slashed_indices_2: &[u64],
) -> AttesterSlashing<MainnetEthSpec> {
let signer = |idx: u64, message: &[u8]| {
self.keypairs[idx as usize]
.sk
.sign(Hash256::from_slice(&message))
};
TestingAttesterSlashingBuilder::double_vote_with_additional_indices(
AttesterSlashingTestTask::Valid,
slashed_indices_1,
Some(slashed_indices_2),
signer,
&self.state.fork,
self.state.genesis_validators_root,
&self.spec,
)
}
}
/// Insert two slashings for the same proposer and ensure only one is returned.
#[test]
fn duplicate_proposer_slashing() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let proposer_index = 0;
let slashing1 = ctxt.proposer_slashing(proposer_index);
let slashing2 = ProposerSlashing {
signed_header_1: slashing1.signed_header_2.clone(),
signed_header_2: slashing1.signed_header_1.clone(),
};
// Both slashings should be valid and accepted by the pool.
op_pool.insert_proposer_slashing(slashing1.clone().validate(state, spec).unwrap());
op_pool.insert_proposer_slashing(slashing2.clone().validate(state, spec).unwrap());
// Should only get the second slashing back.
assert_eq!(op_pool.get_slashings(state, spec).0, vec![slashing2]);
}
// Sanity check on the pruning of proposer slashings
#[test]
fn prune_proposer_slashing_noop() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing = ctxt.proposer_slashing(0);
op_pool.insert_proposer_slashing(slashing.clone().validate(state, spec).unwrap());
op_pool.prune_proposer_slashings(state);
assert_eq!(op_pool.get_slashings(state, spec).0, vec![slashing]);
}
// Sanity check on the pruning of attester slashings
#[test]
fn prune_attester_slashing_noop() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing = ctxt.attester_slashing(&[1, 3, 5, 7, 9]);
op_pool
.insert_attester_slashing(slashing.clone().validate(state, spec).unwrap(), state.fork);
op_pool.prune_attester_slashings(state);
assert_eq!(op_pool.get_slashings(state, spec).1, vec![slashing]);
}
// Check that we get maximum coverage for attester slashings (highest qty of validators slashed)
#[test]
fn simple_max_cover_attester_slashing() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing_1 = ctxt.attester_slashing(&[1]);
let slashing_2 = ctxt.attester_slashing(&[2, 3]);
let slashing_3 = ctxt.attester_slashing(&[4, 5, 6]);
let slashing_4 = ctxt.attester_slashing(&[7, 8, 9, 10]);
op_pool.insert_attester_slashing(
slashing_1.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_2.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_3.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_4.clone().validate(state, spec).unwrap(),
state.fork,
);
let best_slashings = op_pool.get_slashings(state, spec);
assert_eq!(best_slashings.1, vec![slashing_4, slashing_3]);
}
// Check that we get maximum coverage for attester slashings with overlapping indices
#[test]
fn overlapping_max_cover_attester_slashing() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing_1 = ctxt.attester_slashing(&[1, 2, 3, 4]);
let slashing_2 = ctxt.attester_slashing(&[1, 2, 5]);
let slashing_3 = ctxt.attester_slashing(&[5, 6]);
let slashing_4 = ctxt.attester_slashing(&[6]);
op_pool.insert_attester_slashing(
slashing_1.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_2.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_3.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_4.clone().validate(state, spec).unwrap(),
state.fork,
);
let best_slashings = op_pool.get_slashings(state, spec);
assert_eq!(best_slashings.1, vec![slashing_1, slashing_3]);
}
// Max coverage of attester slashings taking into account proposer slashings
#[test]
fn max_coverage_attester_proposer_slashings() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let p_slashing = ctxt.proposer_slashing(1);
let a_slashing_1 = ctxt.attester_slashing(&[1, 2, 3, 4]);
let a_slashing_2 = ctxt.attester_slashing(&[1, 3, 4]);
let a_slashing_3 = ctxt.attester_slashing(&[5, 6]);
op_pool.insert_proposer_slashing(p_slashing.clone().validate(state, spec).unwrap());
op_pool.insert_attester_slashing(
a_slashing_1.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
a_slashing_2.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
a_slashing_3.clone().validate(state, spec).unwrap(),
state.fork,
);
let best_slashings = op_pool.get_slashings(state, spec);
assert_eq!(best_slashings.1, vec![a_slashing_1, a_slashing_3]);
}
//Max coverage checking that non overlapping indices are still recognized for their value
#[test]
fn max_coverage_different_indices_set() {
let ctxt = TestContext::new();
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing_1 =
ctxt.attester_slashing_two_indices(&[1, 2, 3, 4, 5, 6], &[3, 4, 5, 6, 7, 8]);
let slashing_2 = ctxt.attester_slashing(&[5, 6]);
let slashing_3 = ctxt.attester_slashing(&[1, 2, 3]);
op_pool.insert_attester_slashing(
slashing_1.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_2.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_3.clone().validate(state, spec).unwrap(),
state.fork,
);
let best_slashings = op_pool.get_slashings(state, spec);
assert_eq!(best_slashings.1, vec![slashing_1, slashing_3]);
}
//Max coverage should be affected by the overall effective balances
#[test]
fn max_coverage_effective_balances() {
let mut ctxt = TestContext::new();
ctxt.state.validators[1].effective_balance = 17_000_000_000;
ctxt.state.validators[2].effective_balance = 17_000_000_000;
ctxt.state.validators[3].effective_balance = 17_000_000_000;
let (op_pool, state, spec) = (&ctxt.op_pool, &ctxt.state, &ctxt.spec);
let slashing_1 = ctxt.attester_slashing(&[1, 2, 3]);
let slashing_2 = ctxt.attester_slashing(&[4, 5, 6]);
let slashing_3 = ctxt.attester_slashing(&[7, 8]);
op_pool.insert_attester_slashing(
slashing_1.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_2.clone().validate(state, spec).unwrap(),
state.fork,
);
op_pool.insert_attester_slashing(
slashing_3.clone().validate(state, spec).unwrap(),
state.fork,
);
let best_slashings = op_pool.get_slashings(state, spec);
assert_eq!(best_slashings.1, vec![slashing_2, slashing_3]);
}
}