3199b1a6f2
* Update `milagro_bls` to new release (#1183) * Update milagro_bls to new release Signed-off-by: Kirk Baird <baird.k@outlook.com> * Tidy up fake cryptos Signed-off-by: Kirk Baird <baird.k@outlook.com> * move SecretHash to bls and put plaintext back Signed-off-by: Kirk Baird <baird.k@outlook.com> * Update v0.12.0 to v0.12.1 * Add compute_subnet_for_attestation * Replace CommitteeIndex topic with Attestation * Fix warnings * Fix attestation service tests * fmt * Appease clippy * return error from validator_subscriptions * move state out of loop * Fix early break on error * Get state from slot clock * Fix beacon state in attestation tests * Add failing test for lookahead > 1 * Minor change * Address some review comments * Add subnet verification to beacon chain * Move subnet verification to processor * Pass committee_count_at_slot to ValidatorDuty and ValidatorSubscription * Pass subnet id for publishing attestations * Fix attestation service tests * Fix more tests * Fix fork choice test * Remove unused code * Remove more unused and expensive code Co-authored-by: Kirk Baird <baird.k@outlook.com> Co-authored-by: Michael Sproul <michael@sigmaprime.io> Co-authored-by: Age Manning <Age@AgeManning.com> Co-authored-by: Paul Hauner <paul@paulhauner.com>
813 lines
27 KiB
Rust
813 lines
27 KiB
Rust
#![cfg(not(debug_assertions))]
|
|
|
|
use beacon_chain::{
|
|
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, HarnessType},
|
|
BeaconChain, BeaconChainError, BeaconForkChoiceStore, ForkChoiceError,
|
|
};
|
|
use fork_choice::{
|
|
ForkChoiceStore, InvalidAttestation, InvalidBlock, QueuedAttestation,
|
|
SAFE_SLOTS_TO_UPDATE_JUSTIFIED,
|
|
};
|
|
use std::sync::Mutex;
|
|
use store::{MemoryStore, StoreConfig};
|
|
use types::{
|
|
test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs},
|
|
Epoch, EthSpec, IndexedAttestation, MainnetEthSpec, Slot, SubnetId,
|
|
};
|
|
use types::{BeaconBlock, BeaconState, Hash256, SignedBeaconBlock};
|
|
|
|
pub type E = MainnetEthSpec;
|
|
|
|
pub const VALIDATOR_COUNT: usize = 16;
|
|
|
|
/// Defines some delay between when an attestation is created and when it is mutated.
|
|
pub enum MutationDelay {
|
|
/// No delay between creation and mutation.
|
|
NoDelay,
|
|
/// Create `n` blocks before mutating the attestation.
|
|
Blocks(usize),
|
|
}
|
|
|
|
/// A helper struct to make testing fork choice more ergonomic and less repetitive.
|
|
struct ForkChoiceTest {
|
|
harness: BeaconChainHarness<HarnessType<E>>,
|
|
}
|
|
|
|
impl ForkChoiceTest {
|
|
/// Creates a new tester.
|
|
pub fn new() -> Self {
|
|
let harness = BeaconChainHarness::new_with_target_aggregators(
|
|
MainnetEthSpec,
|
|
generate_deterministic_keypairs(VALIDATOR_COUNT),
|
|
// Ensure we always have an aggregator for each slot.
|
|
u64::max_value(),
|
|
StoreConfig::default(),
|
|
);
|
|
|
|
Self { harness }
|
|
}
|
|
|
|
/// Get a value from the `ForkChoice` instantiation.
|
|
fn get<T, U>(&self, func: T) -> U
|
|
where
|
|
T: Fn(&BeaconForkChoiceStore<E, MemoryStore<E>, MemoryStore<E>>) -> U,
|
|
{
|
|
func(&self.harness.chain.fork_choice.read().fc_store())
|
|
}
|
|
|
|
/// Assert the epochs match.
|
|
pub fn assert_finalized_epoch(self, epoch: u64) -> Self {
|
|
assert_eq!(
|
|
self.get(|fc_store| fc_store.finalized_checkpoint().epoch),
|
|
Epoch::new(epoch),
|
|
"finalized_epoch"
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Assert the epochs match.
|
|
pub fn assert_justified_epoch(self, epoch: u64) -> Self {
|
|
assert_eq!(
|
|
self.get(|fc_store| fc_store.justified_checkpoint().epoch),
|
|
Epoch::new(epoch),
|
|
"justified_epoch"
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Assert the epochs match.
|
|
pub fn assert_best_justified_epoch(self, epoch: u64) -> Self {
|
|
assert_eq!(
|
|
self.get(|fc_store| fc_store.best_justified_checkpoint().epoch),
|
|
Epoch::new(epoch),
|
|
"best_justified_epoch"
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Inspect the queued attestations in fork choice.
|
|
pub fn inspect_queued_attestations<F>(self, mut func: F) -> Self
|
|
where
|
|
F: FnMut(&[QueuedAttestation]),
|
|
{
|
|
self.harness
|
|
.chain
|
|
.fork_choice
|
|
.write()
|
|
.update_time(self.harness.chain.slot().unwrap())
|
|
.unwrap();
|
|
func(self.harness.chain.fork_choice.read().queued_attestations());
|
|
self
|
|
}
|
|
|
|
/// Skip a slot, without producing a block.
|
|
pub fn skip_slot(self) -> Self {
|
|
self.harness.advance_slot();
|
|
self
|
|
}
|
|
|
|
/// Build the chain whilst `predicate` returns `true`.
|
|
pub fn apply_blocks_while<F>(self, mut predicate: F) -> Self
|
|
where
|
|
F: FnMut(&BeaconBlock<E>, &BeaconState<E>) -> bool,
|
|
{
|
|
self.harness.advance_slot();
|
|
self.harness.extend_chain_while(
|
|
|block, state| predicate(&block.message, state),
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::AllValidators,
|
|
);
|
|
|
|
self
|
|
}
|
|
|
|
/// Apply `count` blocks to the chain (with attestations).
|
|
pub fn apply_blocks(self, count: usize) -> Self {
|
|
self.harness.advance_slot();
|
|
self.harness.extend_chain(
|
|
count,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::AllValidators,
|
|
);
|
|
|
|
self
|
|
}
|
|
|
|
/// Apply `count` blocks to the chain (without attestations).
|
|
pub fn apply_blocks_without_new_attestations(self, count: usize) -> Self {
|
|
self.harness.advance_slot();
|
|
self.harness.extend_chain(
|
|
count,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::SomeValidators(vec![]),
|
|
);
|
|
|
|
self
|
|
}
|
|
|
|
/// Moves to the next slot that is *outside* the `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` range.
|
|
///
|
|
/// If the chain is presently in an unsafe period, transition through it and the following safe
|
|
/// period.
|
|
pub fn move_to_next_unsafe_period(self) -> Self {
|
|
self.move_inside_safe_to_update()
|
|
.move_outside_safe_to_update()
|
|
}
|
|
|
|
/// Moves to the next slot that is *outside* the `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` range.
|
|
pub fn move_outside_safe_to_update(self) -> Self {
|
|
while is_safe_to_update(self.harness.chain.slot().unwrap()) {
|
|
self.harness.advance_slot()
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Moves to the next slot that is *inside* the `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` range.
|
|
pub fn move_inside_safe_to_update(self) -> Self {
|
|
while !is_safe_to_update(self.harness.chain.slot().unwrap()) {
|
|
self.harness.advance_slot()
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Applies a block directly to fork choice, bypassing the beacon chain.
|
|
///
|
|
/// Asserts the block was applied successfully.
|
|
pub fn apply_block_directly_to_fork_choice<F>(self, mut func: F) -> Self
|
|
where
|
|
F: FnMut(&mut BeaconBlock<E>, &mut BeaconState<E>),
|
|
{
|
|
let (mut block, mut state) = self.harness.get_block();
|
|
func(&mut block.message, &mut state);
|
|
let current_slot = self.harness.chain.slot().unwrap();
|
|
self.harness
|
|
.chain
|
|
.fork_choice
|
|
.write()
|
|
.on_block(current_slot, &block.message, block.canonical_root(), &state)
|
|
.unwrap();
|
|
self
|
|
}
|
|
|
|
/// Applies a block directly to fork choice, bypassing the beacon chain.
|
|
///
|
|
/// Asserts that an error occurred and allows inspecting it via `comparison_func`.
|
|
pub fn apply_invalid_block_directly_to_fork_choice<F, G>(
|
|
self,
|
|
mut mutation_func: F,
|
|
mut comparison_func: G,
|
|
) -> Self
|
|
where
|
|
F: FnMut(&mut BeaconBlock<E>, &mut BeaconState<E>),
|
|
G: FnMut(ForkChoiceError),
|
|
{
|
|
let (mut block, mut state) = self.harness.get_block();
|
|
mutation_func(&mut block.message, &mut state);
|
|
let current_slot = self.harness.chain.slot().unwrap();
|
|
let err = self
|
|
.harness
|
|
.chain
|
|
.fork_choice
|
|
.write()
|
|
.on_block(current_slot, &block.message, block.canonical_root(), &state)
|
|
.err()
|
|
.expect("on_block did not return an error");
|
|
comparison_func(err);
|
|
self
|
|
}
|
|
|
|
/// Compares the justified balances in the `ForkChoiceStore` verses a direct lookup from the
|
|
/// database.
|
|
fn check_justified_balances(&self) {
|
|
let harness = &self.harness;
|
|
let fc = self.harness.chain.fork_choice.read();
|
|
|
|
let state_root = harness
|
|
.chain
|
|
.store
|
|
.get_item::<SignedBeaconBlock<E>>(&fc.fc_store().justified_checkpoint().root)
|
|
.unwrap()
|
|
.unwrap()
|
|
.message
|
|
.state_root;
|
|
let state = harness
|
|
.chain
|
|
.store
|
|
.get_state(&state_root, None)
|
|
.unwrap()
|
|
.unwrap();
|
|
let balances = state
|
|
.validators
|
|
.into_iter()
|
|
.map(|v| {
|
|
if v.is_active_at(state.current_epoch()) {
|
|
v.effective_balance
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(
|
|
&balances[..],
|
|
fc.fc_store().justified_balances(),
|
|
"balances should match"
|
|
)
|
|
}
|
|
|
|
/// Returns an attestation that is valid for some slot in the given `chain`.
|
|
///
|
|
/// Also returns some info about who created it.
|
|
fn apply_attestation_to_chain<F, G>(
|
|
self,
|
|
delay: MutationDelay,
|
|
mut mutation_func: F,
|
|
mut comparison_func: G,
|
|
) -> Self
|
|
where
|
|
F: FnMut(&mut IndexedAttestation<E>, &BeaconChain<HarnessType<E>>),
|
|
G: FnMut(Result<(), BeaconChainError>),
|
|
{
|
|
let chain = &self.harness.chain;
|
|
let head = chain.head().expect("should get head");
|
|
let current_slot = chain.slot().expect("should get slot");
|
|
|
|
let mut attestation = chain
|
|
.produce_unaggregated_attestation(current_slot, 0)
|
|
.expect("should not error while producing attestation");
|
|
|
|
let validator_committee_index = 0;
|
|
let validator_index = *head
|
|
.beacon_state
|
|
.get_beacon_committee(current_slot, attestation.data.index)
|
|
.expect("should get committees")
|
|
.committee
|
|
.get(validator_committee_index)
|
|
.expect("there should be an attesting validator");
|
|
|
|
let committee_count = head
|
|
.beacon_state
|
|
.get_committee_count_at_slot(current_slot)
|
|
.expect("should not error while getting committee count");
|
|
|
|
let subnet_id =
|
|
SubnetId::compute_subnet::<E>(current_slot, 0, committee_count, &chain.spec)
|
|
.expect("should compute subnet id");
|
|
|
|
let validator_sk = generate_deterministic_keypair(validator_index).sk;
|
|
|
|
attestation
|
|
.sign(
|
|
&validator_sk,
|
|
validator_committee_index,
|
|
&head.beacon_state.fork,
|
|
chain.genesis_validators_root,
|
|
&chain.spec,
|
|
)
|
|
.expect("should sign attestation");
|
|
|
|
let mut verified_attestation = chain
|
|
.verify_unaggregated_attestation_for_gossip(attestation, subnet_id)
|
|
.expect("precondition: should gossip verify attestation");
|
|
|
|
if let MutationDelay::Blocks(slots) = delay {
|
|
self.harness.advance_slot();
|
|
self.harness.extend_chain(
|
|
slots,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::SomeValidators(vec![]),
|
|
);
|
|
}
|
|
|
|
mutation_func(verified_attestation.__indexed_attestation_mut(), chain);
|
|
|
|
let result = chain.apply_attestation_to_fork_choice(&verified_attestation);
|
|
|
|
comparison_func(result);
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
fn is_safe_to_update(slot: Slot) -> bool {
|
|
slot % E::slots_per_epoch() < SAFE_SLOTS_TO_UPDATE_JUSTIFIED
|
|
}
|
|
|
|
/// - The new justified checkpoint descends from the current.
|
|
/// - Current slot is within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`
|
|
#[test]
|
|
fn justified_checkpoint_updates_with_descendent_inside_safe_slots() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.move_inside_safe_to_update()
|
|
.assert_justified_epoch(0)
|
|
.apply_blocks(1)
|
|
.assert_justified_epoch(2);
|
|
}
|
|
|
|
/// - The new justified checkpoint descends from the current.
|
|
/// - Current slot is **not** within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`
|
|
/// - This is **not** the first justification since genesis
|
|
#[test]
|
|
fn justified_checkpoint_updates_with_descendent_outside_safe_slots() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch <= 2)
|
|
.move_outside_safe_to_update()
|
|
.assert_justified_epoch(2)
|
|
.assert_best_justified_epoch(2)
|
|
.apply_blocks(1)
|
|
.assert_justified_epoch(3);
|
|
}
|
|
|
|
/// - The new justified checkpoint descends from the current.
|
|
/// - Current slot is **not** within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`
|
|
/// - This is the first justification since genesis
|
|
#[test]
|
|
fn justified_checkpoint_updates_first_justification_outside_safe_to_update() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.move_to_next_unsafe_period()
|
|
.assert_justified_epoch(0)
|
|
.assert_best_justified_epoch(0)
|
|
.apply_blocks(1)
|
|
.assert_justified_epoch(2)
|
|
.assert_best_justified_epoch(2);
|
|
}
|
|
|
|
/// - The new justified checkpoint **does not** descend from the current.
|
|
/// - Current slot is within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`
|
|
/// - Finalized epoch has **not** increased.
|
|
#[test]
|
|
fn justified_checkpoint_updates_with_non_descendent_inside_safe_slots_without_finality() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.move_inside_safe_to_update()
|
|
.assert_justified_epoch(2)
|
|
.apply_block_directly_to_fork_choice(|_, state| {
|
|
// The finalized checkpoint should not change.
|
|
state.finalized_checkpoint.epoch = Epoch::new(0);
|
|
|
|
// The justified checkpoint has changed.
|
|
state.current_justified_checkpoint.epoch = Epoch::new(3);
|
|
// The new block should **not** include the current justified block as an ancestor.
|
|
state.current_justified_checkpoint.root = *state
|
|
.get_block_root(Epoch::new(1).start_slot(E::slots_per_epoch()))
|
|
.unwrap();
|
|
})
|
|
.assert_justified_epoch(3)
|
|
.assert_best_justified_epoch(3);
|
|
}
|
|
|
|
/// - The new justified checkpoint **does not** descend from the current.
|
|
/// - Current slot is **not** within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`.
|
|
/// - Finalized epoch has **not** increased.
|
|
#[test]
|
|
fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_without_finality() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.move_to_next_unsafe_period()
|
|
.assert_justified_epoch(2)
|
|
.apply_block_directly_to_fork_choice(|_, state| {
|
|
// The finalized checkpoint should not change.
|
|
state.finalized_checkpoint.epoch = Epoch::new(0);
|
|
|
|
// The justified checkpoint has changed.
|
|
state.current_justified_checkpoint.epoch = Epoch::new(3);
|
|
// The new block should **not** include the current justified block as an ancestor.
|
|
state.current_justified_checkpoint.root = *state
|
|
.get_block_root(Epoch::new(1).start_slot(E::slots_per_epoch()))
|
|
.unwrap();
|
|
})
|
|
.assert_justified_epoch(2)
|
|
.assert_best_justified_epoch(3);
|
|
}
|
|
|
|
/// - The new justified checkpoint **does not** descend from the current.
|
|
/// - Current slot is **not** within `SAFE_SLOTS_TO_UPDATE_JUSTIFIED`
|
|
/// - Finalized epoch has increased.
|
|
#[test]
|
|
fn justified_checkpoint_updates_with_non_descendent_outside_safe_slots_with_finality() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.move_to_next_unsafe_period()
|
|
.assert_justified_epoch(2)
|
|
.apply_block_directly_to_fork_choice(|_, state| {
|
|
// The finalized checkpoint should change.
|
|
state.finalized_checkpoint.epoch = Epoch::new(1);
|
|
|
|
// The justified checkpoint has changed.
|
|
state.current_justified_checkpoint.epoch = Epoch::new(3);
|
|
// The new block should **not** include the current justified block as an ancestor.
|
|
state.current_justified_checkpoint.root = *state
|
|
.get_block_root(Epoch::new(1).start_slot(E::slots_per_epoch()))
|
|
.unwrap();
|
|
})
|
|
.assert_justified_epoch(3)
|
|
.assert_best_justified_epoch(3);
|
|
}
|
|
|
|
/// Check that the balances are obtained correctly.
|
|
#[test]
|
|
fn justified_balances() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.assert_justified_epoch(2)
|
|
.check_justified_balances()
|
|
}
|
|
|
|
macro_rules! assert_invalid_block {
|
|
($err: tt, $($error: pat) |+ $( if $guard: expr )?) => {
|
|
assert!(
|
|
matches!(
|
|
$err,
|
|
$( ForkChoiceError::InvalidBlock($error) ) |+ $( if $guard )?
|
|
),
|
|
);
|
|
};
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert block.parent_root in store.block_states
|
|
#[test]
|
|
fn invalid_block_unknown_parent() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks(2)
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
block.parent_root = junk;
|
|
},
|
|
|err| {
|
|
assert_invalid_block!(
|
|
err,
|
|
InvalidBlock::UnknownParent(parent)
|
|
if parent == junk
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert get_current_slot(store) >= block.slot
|
|
#[test]
|
|
fn invalid_block_future_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks(2)
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
block.slot = block.slot + 1;
|
|
},
|
|
|err| {
|
|
assert_invalid_block!(
|
|
err,
|
|
InvalidBlock::FutureSlot { .. }
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert block.slot > finalized_slot
|
|
#[test]
|
|
fn invalid_block_finalized_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
block.slot = Epoch::new(2).start_slot(E::slots_per_epoch()) - 1;
|
|
},
|
|
|err| {
|
|
assert_invalid_block!(
|
|
err,
|
|
InvalidBlock::FinalizedSlot { finalized_slot, .. }
|
|
if finalized_slot == Epoch::new(2).start_slot(E::slots_per_epoch())
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert get_ancestor(store, hash_tree_root(block), finalized_slot) ==
|
|
/// store.finalized_checkpoint.root
|
|
///
|
|
/// Note: we technically don't do this exact check, but an equivalent check. Reference:
|
|
///
|
|
/// https://github.com/ethereum/eth2.0-specs/pull/1884
|
|
#[test]
|
|
fn invalid_block_finalized_descendant() {
|
|
let invalid_ancestor = Mutex::new(Hash256::zero());
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint.epoch == 0)
|
|
.apply_blocks(1)
|
|
.assert_finalized_epoch(2)
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, state| {
|
|
block.parent_root = *state
|
|
.get_block_root(Epoch::new(1).start_slot(E::slots_per_epoch()))
|
|
.unwrap();
|
|
*invalid_ancestor.lock().unwrap() = block.parent_root;
|
|
},
|
|
|err| {
|
|
assert_invalid_block!(
|
|
err,
|
|
InvalidBlock::NotFinalizedDescendant { block_ancestor, .. }
|
|
if block_ancestor == Some(*invalid_ancestor.lock().unwrap())
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
macro_rules! assert_invalid_attestation {
|
|
($err: tt, $($error: pat) |+ $( if $guard: expr )?) => {
|
|
assert!(
|
|
matches!(
|
|
$err,
|
|
$( Err(BeaconChainError::ForkChoiceError(ForkChoiceError::InvalidAttestation($error))) ) |+ $( if $guard )?
|
|
),
|
|
"{:?}",
|
|
$err
|
|
);
|
|
};
|
|
}
|
|
|
|
/// Ensure we can process a valid attestation.
|
|
#[test]
|
|
fn valid_attestation() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|_, _| {},
|
|
|result| assert_eq!(result.unwrap(), ()),
|
|
);
|
|
}
|
|
|
|
/// This test is not in the specification, however we reject an attestation with an empty
|
|
/// aggregation bitfield since it has no purpose beyond wasting our time.
|
|
#[test]
|
|
fn invalid_attestation_empty_bitfield() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.attesting_indices = vec![].into();
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(result, InvalidAttestation::EmptyAggregationBitfield)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch in [expected_current_epoch, previous_epoch]
|
|
///
|
|
/// (tests epoch after current epoch)
|
|
#[test]
|
|
fn invalid_attestation_future_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data.target.epoch = Epoch::new(2);
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::FutureEpoch { attestation_epoch, current_epoch }
|
|
if attestation_epoch == Epoch::new(2) && current_epoch == Epoch::new(0)
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch in [expected_current_epoch, previous_epoch]
|
|
///
|
|
/// (tests epoch prior to previous epoch)
|
|
#[test]
|
|
fn invalid_attestation_past_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(E::slots_per_epoch() as usize * 3 + 1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data.target.epoch = Epoch::new(0);
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::PastEpoch { attestation_epoch, current_epoch }
|
|
if attestation_epoch == Epoch::new(0) && current_epoch == Epoch::new(3)
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch == compute_epoch_at_slot(attestation.data.slot)
|
|
#[test]
|
|
fn invalid_attestation_target_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(E::slots_per_epoch() as usize + 1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data.slot = Slot::new(1);
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::BadTargetEpoch { target, slot }
|
|
if target == Epoch::new(1) && slot == Slot::new(1)
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.root in store.blocks
|
|
#[test]
|
|
fn invalid_attestation_unknown_target_root() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data.target.root = junk;
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::UnknownTargetRoot(root)
|
|
if root == junk
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert attestation.data.beacon_block_root in store.blocks
|
|
#[test]
|
|
fn invalid_attestation_unknown_beacon_block_root() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data.beacon_block_root = junk;
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::UnknownHeadBlock { beacon_block_root }
|
|
if beacon_block_root == junk
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot
|
|
#[test]
|
|
fn invalid_attestation_future_block() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::Blocks(1),
|
|
|attestation, chain| {
|
|
attestation.data.beacon_block_root = chain
|
|
.block_at_slot(chain.slot().unwrap())
|
|
.unwrap()
|
|
.unwrap()
|
|
.canonical_root();
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::AttestsToFutureBlock { block, attestation }
|
|
if block == 2 && attestation == 1
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.root == get_ancestor(store, attestation.data.beacon_block_root, target_slot)
|
|
#[test]
|
|
fn invalid_attestation_inconsistent_ffg_vote() {
|
|
let local_opt = Mutex::new(None);
|
|
let attestation_opt = Mutex::new(None);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, chain| {
|
|
attestation.data.target.root = chain
|
|
.block_at_slot(Slot::new(1))
|
|
.unwrap()
|
|
.unwrap()
|
|
.canonical_root();
|
|
|
|
*attestation_opt.lock().unwrap() = Some(attestation.data.target.root);
|
|
*local_opt.lock().unwrap() = Some(
|
|
chain
|
|
.block_at_slot(Slot::new(0))
|
|
.unwrap()
|
|
.unwrap()
|
|
.canonical_root(),
|
|
);
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::InvalidTarget { attestation, local }
|
|
if attestation == attestation_opt.lock().unwrap().unwrap()
|
|
&& local == local_opt.lock().unwrap().unwrap()
|
|
)
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert get_current_slot(store) >= attestation.data.slot + 1
|
|
#[test]
|
|
fn invalid_attestation_delayed_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|_, _| {},
|
|
|result| assert_eq!(result.unwrap(), ()),
|
|
)
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 1))
|
|
.skip_slot()
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0));
|
|
}
|