mirror of
https://github.com/sigp/lighthouse.git
synced 2026-07-01 11:54:40 +00:00
`dequeue_attestations` released votes by splitting the queue at the first entry with `slot >= current_slot`, which assumes the queue is sorted by slot. It isn't: `on_attestation` pushes attestations in arrival order and never sorts. When a future-slot vote sits ahead of a vote that is already due, the split happens at the future-slot vote and the due vote stays stuck behind it and is never applied to fork choice, even after its slot is in the past. The PR current uses a naive solution to solve the bug and also adds regression tests to exercise the bug. There are other competing solutions which can be used which also optimize this path at the same time. https://github.com/sigp/lighthouse/pull/8378 https://github.com/sigp/lighthouse/pull/8378#discussion_r2543322106 Co-Authored-By: hopinheimer <knmanas6@gmail.com> Co-Authored-By: hopinheimer <48147533+hopinheimer@users.noreply.github.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
1465 lines
48 KiB
Rust
1465 lines
48 KiB
Rust
#![cfg(not(debug_assertions))]
|
|
|
|
use beacon_chain::test_utils::{
|
|
AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType,
|
|
};
|
|
use beacon_chain::{
|
|
BeaconChain, BeaconChainError, BeaconForkChoiceStore, ChainConfig, ForkChoiceError,
|
|
StateSkipConfig, WhenSlotSkipped,
|
|
};
|
|
use bls::AggregateSignature;
|
|
use fixed_bytes::FixedBytesExtended;
|
|
use fork_choice::{
|
|
AttestationFromBlock, ForkChoiceStore, InvalidAttestation, InvalidBlock,
|
|
InvalidPayloadAttestation, PayloadVerificationStatus, QueuedAttestation,
|
|
};
|
|
use state_processing::state_advance::complete_state_advance;
|
|
use std::fmt;
|
|
use std::sync::Mutex;
|
|
use std::time::Duration;
|
|
use store::MemoryStore;
|
|
use types::SingleAttestation;
|
|
use types::{
|
|
BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, ForkName, Hash256,
|
|
IndexedAttestation, IndexedPayloadAttestation, MainnetEthSpec, PayloadAttestationData,
|
|
RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, test_utils::generate_deterministic_keypair,
|
|
};
|
|
|
|
pub type E = MainnetEthSpec;
|
|
|
|
pub const VALIDATOR_COUNT: usize = 64;
|
|
|
|
// When set to true, cache any states fetched from the db.
|
|
pub const CACHE_STATE_IN_TESTS: bool = true;
|
|
|
|
/// 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<EphemeralHarnessType<E>>,
|
|
}
|
|
|
|
/// Allows us to use `unwrap` in some cases.
|
|
impl fmt::Debug for ForkChoiceTest {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("ForkChoiceTest").finish()
|
|
}
|
|
}
|
|
|
|
impl ForkChoiceTest {
|
|
/// Creates a new tester.
|
|
pub fn new() -> Self {
|
|
Self::new_with_chain_config(ChainConfig::default())
|
|
}
|
|
|
|
/// Creates a new tester with a custom chain config.
|
|
pub fn new_with_chain_config(chain_config: ChainConfig) -> Self {
|
|
// Run fork choice tests against the latest fork.
|
|
let spec = ForkName::latest_stable().make_genesis_spec(ChainSpec::default());
|
|
let harness = BeaconChainHarness::builder(MainnetEthSpec)
|
|
.spec(spec.into())
|
|
.chain_config(chain_config)
|
|
.deterministic_keypairs(VALIDATOR_COUNT)
|
|
.fresh_ephemeral_store()
|
|
.mock_execution_layer()
|
|
.build();
|
|
|
|
Self { harness }
|
|
}
|
|
|
|
/// Creates a new tester with the Gloas fork active at epoch 1.
|
|
/// Genesis is a standard Fulu block (epoch 0), so block production works normally.
|
|
/// Tests that need Gloas semantics should advance the chain into epoch 1 first.
|
|
/// Get a value from the `ForkChoice` instantiation.
|
|
fn get<T, U>(&self, func: T) -> U
|
|
where
|
|
T: Fn(&BeaconForkChoiceStore<E, MemoryStore, MemoryStore>) -> U,
|
|
{
|
|
func(
|
|
self.harness
|
|
.chain
|
|
.canonical_head
|
|
.fork_choice_read_lock()
|
|
.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 given slot is greater than the head slot.
|
|
pub fn assert_finalized_epoch_is_less_than(self, epoch: Epoch) -> Self {
|
|
assert!(self.harness.finalized_checkpoint().epoch < epoch);
|
|
self
|
|
}
|
|
|
|
/// Assert there was a shutdown signal sent by the beacon chain.
|
|
pub fn shutdown_signal_sent(&self) -> bool {
|
|
let mutex = self.harness.shutdown_receiver.clone();
|
|
let mut shutdown_receiver = mutex.lock();
|
|
|
|
shutdown_receiver.close();
|
|
let msg = shutdown_receiver.try_next().unwrap();
|
|
msg.is_some()
|
|
}
|
|
|
|
/// Assert there was a shutdown signal sent by the beacon chain.
|
|
pub fn assert_shutdown_signal_sent(self) -> Self {
|
|
assert!(self.shutdown_signal_sent());
|
|
self
|
|
}
|
|
|
|
/// Assert no shutdown was signal sent by the beacon chain.
|
|
pub fn assert_shutdown_signal_not_sent(self) -> Self {
|
|
assert!(!self.shutdown_signal_sent());
|
|
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
|
|
.canonical_head
|
|
.fork_choice_write_lock()
|
|
.update_time(self.harness.chain.slot().unwrap())
|
|
.unwrap();
|
|
let queued = self
|
|
.harness
|
|
.chain
|
|
.canonical_head
|
|
.fork_choice_read_lock()
|
|
.queued_attestations()
|
|
.values()
|
|
.flatten()
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
func(&queued);
|
|
self
|
|
}
|
|
|
|
/// Skip a slot, without producing a block.
|
|
pub fn skip_slot(self) -> Self {
|
|
self.harness.advance_slot();
|
|
self
|
|
}
|
|
|
|
/// Skips `count` slots, without producing a block.
|
|
pub fn skip_slots(self, count: usize) -> Self {
|
|
for _ in 0..count {
|
|
self.harness.advance_slot();
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Build the chain whilst `predicate` returns `true` and `process_block_result` does not error.
|
|
pub async fn apply_blocks_while<F>(self, mut predicate: F) -> Result<Self, Self>
|
|
where
|
|
F: FnMut(BeaconBlockRef<'_, E>, &BeaconState<E>) -> bool,
|
|
{
|
|
self.harness.advance_slot();
|
|
let mut state = self.harness.get_current_state();
|
|
let validators = self.harness.get_all_validators();
|
|
loop {
|
|
let slot = self.harness.get_current_slot();
|
|
|
|
// Skip slashed proposers, as we expect validators to get slashed in these tests.
|
|
// Presently `make_block` will panic if the proposer is slashed, so we just avoid
|
|
// calling it in this case.
|
|
complete_state_advance(&mut state, None, slot, &self.harness.spec).unwrap();
|
|
state.build_caches(&self.harness.spec).unwrap();
|
|
let proposer_index = state
|
|
.get_beacon_proposer_index(slot, &self.harness.chain.spec)
|
|
.unwrap();
|
|
if state.validators().get(proposer_index).unwrap().slashed {
|
|
self.harness.advance_slot();
|
|
continue;
|
|
}
|
|
|
|
let (block_contents, state_) = self.harness.make_block(state, slot).await;
|
|
state = state_;
|
|
if !predicate(block_contents.0.message(), &state) {
|
|
break;
|
|
}
|
|
let block = block_contents.0.clone();
|
|
if let Ok(block_hash) = self.harness.process_block_result(block_contents).await {
|
|
self.harness.attest_block(
|
|
&state,
|
|
block.state_root(),
|
|
block_hash,
|
|
&block,
|
|
&validators,
|
|
);
|
|
self.harness.advance_slot();
|
|
} else {
|
|
return Err(self);
|
|
}
|
|
}
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
/// Apply `count` blocks to the chain (with attestations).
|
|
///
|
|
/// Note that in the case of slashed validators, their proposals will be skipped and the chain
|
|
/// may be advanced by *more than* `count` slots.
|
|
pub async fn apply_blocks(self, count: usize) -> Self {
|
|
// Use `Self::apply_blocks_while` which gracefully handles slashed validators.
|
|
let mut blocks_applied = 0;
|
|
self.apply_blocks_while(|_, _| {
|
|
// Blocks are applied after the predicate is called, so continue applying the block if
|
|
// less than *or equal* to the count.
|
|
blocks_applied += 1;
|
|
blocks_applied <= count
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
/// Slash a validator from the previous epoch committee.
|
|
pub async fn add_previous_epoch_attester_slashing(self) -> Self {
|
|
let state = self.harness.get_current_state();
|
|
let previous_epoch_shuffling = state.get_shuffling(RelativeEpoch::Previous).unwrap();
|
|
let validator_indices = previous_epoch_shuffling
|
|
.iter()
|
|
.map(|idx| *idx as u64)
|
|
.take(1)
|
|
.collect();
|
|
|
|
self.harness
|
|
.add_attester_slashing(validator_indices)
|
|
.unwrap();
|
|
|
|
self
|
|
}
|
|
|
|
/// Slash the proposer of a block in the previous epoch.
|
|
pub async fn add_previous_epoch_proposer_slashing(self, slots_per_epoch: u64) -> Self {
|
|
let previous_epoch_slot = self.harness.get_current_slot() - slots_per_epoch;
|
|
let previous_epoch_block = self
|
|
.harness
|
|
.chain
|
|
.block_at_slot(previous_epoch_slot, WhenSlotSkipped::None)
|
|
.unwrap()
|
|
.unwrap();
|
|
let proposer_index: u64 = previous_epoch_block.message().proposer_index();
|
|
|
|
self.harness.add_proposer_slashing(proposer_index).unwrap();
|
|
|
|
self
|
|
}
|
|
|
|
/// Apply `count` blocks to the chain (without attestations).
|
|
pub async fn apply_blocks_without_new_attestations(self, count: usize) -> Self {
|
|
// This function does not gracefully handle slashed proposers, but may need to in future.
|
|
self.harness.advance_slot();
|
|
self.harness
|
|
.extend_chain(
|
|
count,
|
|
BlockStrategy::OnCanonicalHead,
|
|
AttestationStrategy::SomeValidators(vec![]),
|
|
)
|
|
.await;
|
|
|
|
self
|
|
}
|
|
|
|
/// Applies a block directly to fork choice, bypassing the beacon chain.
|
|
///
|
|
/// Asserts the block was applied successfully.
|
|
pub async fn apply_block_directly_to_fork_choice<F>(self, mut func: F) -> Self
|
|
where
|
|
F: FnMut(&mut SignedBeaconBlock<E>, &mut BeaconState<E>),
|
|
{
|
|
let state = self
|
|
.harness
|
|
.chain
|
|
.state_at_slot(
|
|
self.harness.get_current_slot() - 1,
|
|
StateSkipConfig::WithStateRoots,
|
|
)
|
|
.unwrap();
|
|
let slot = self.harness.get_current_slot();
|
|
let ((block_arc, _block_blobs), mut state) = self.harness.make_block(state, slot).await;
|
|
let mut block = (*block_arc).clone();
|
|
func(&mut block, &mut state);
|
|
let current_slot = self.harness.get_current_slot();
|
|
self.harness
|
|
.chain
|
|
.canonical_head
|
|
.fork_choice_write_lock()
|
|
.on_block(
|
|
current_slot,
|
|
block.message(),
|
|
block.canonical_root(),
|
|
Duration::from_secs(0),
|
|
&state,
|
|
PayloadVerificationStatus::Verified,
|
|
&self.harness.chain.spec,
|
|
)
|
|
.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 async fn apply_invalid_block_directly_to_fork_choice<F, G>(
|
|
self,
|
|
mut mutation_func: F,
|
|
mut comparison_func: G,
|
|
) -> Self
|
|
where
|
|
F: FnMut(&mut SignedBeaconBlock<E>, &mut BeaconState<E>),
|
|
G: FnMut(ForkChoiceError),
|
|
{
|
|
let state = self
|
|
.harness
|
|
.chain
|
|
.state_at_slot(
|
|
self.harness.get_current_slot() - 1,
|
|
StateSkipConfig::WithStateRoots,
|
|
)
|
|
.unwrap();
|
|
let slot = self.harness.get_current_slot();
|
|
let ((block_arc, _block_blobs), mut state) = self.harness.make_block(state, slot).await;
|
|
let mut block = (*block_arc).clone();
|
|
mutation_func(&mut block, &mut state);
|
|
let current_slot = self.harness.get_current_slot();
|
|
let err = self
|
|
.harness
|
|
.chain
|
|
.canonical_head
|
|
.fork_choice_write_lock()
|
|
.on_block(
|
|
current_slot,
|
|
block.message(),
|
|
block.canonical_root(),
|
|
Duration::from_secs(0),
|
|
&state,
|
|
PayloadVerificationStatus::Verified,
|
|
&self.harness.chain.spec,
|
|
)
|
|
.expect_err("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.canonical_head.fork_choice_read_lock();
|
|
|
|
let state_root = harness
|
|
.chain
|
|
.store
|
|
.get_blinded_block(&fc.fc_store().justified_checkpoint().root)
|
|
.unwrap()
|
|
.unwrap()
|
|
.message()
|
|
.state_root();
|
|
let state = harness
|
|
.chain
|
|
.store
|
|
.get_state(&state_root, None, CACHE_STATE_IN_TESTS)
|
|
.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().effective_balances,
|
|
"balances should match"
|
|
);
|
|
assert_eq!(
|
|
balances.iter().sum::<u64>(),
|
|
fc.fc_store().justified_balances().total_effective_balance
|
|
);
|
|
}
|
|
|
|
/// Returns an attestation that is valid for some slot in the given `chain`.
|
|
///
|
|
/// Also returns some info about who created it.
|
|
async fn apply_attestation_to_chain<F, G>(
|
|
self,
|
|
delay: MutationDelay,
|
|
mutation_func: F,
|
|
comparison_func: G,
|
|
) -> Self
|
|
where
|
|
F: FnMut(&mut IndexedAttestation<E>, &BeaconChain<EphemeralHarnessType<E>>),
|
|
G: FnMut(Result<(), BeaconChainError>),
|
|
{
|
|
self.apply_nth_attestation_to_chain(0, delay, mutation_func, comparison_func)
|
|
.await
|
|
}
|
|
|
|
/// Like `apply_attestation_to_chain`, but attests with the validator at
|
|
/// `validator_index_in_committee` within the committee. Lets a test enqueue multiple distinct
|
|
/// votes for the same slot without tripping `PriorAttestationKnown`.
|
|
async fn apply_nth_attestation_to_chain<F, G>(
|
|
self,
|
|
validator_index_in_committee: usize,
|
|
delay: MutationDelay,
|
|
mut mutation_func: F,
|
|
mut comparison_func: G,
|
|
) -> Self
|
|
where
|
|
F: FnMut(&mut IndexedAttestation<E>, &BeaconChain<EphemeralHarnessType<E>>),
|
|
G: FnMut(Result<(), BeaconChainError>),
|
|
{
|
|
let head = self.harness.chain.head_snapshot();
|
|
let current_slot = self.harness.chain.slot().expect("should get slot");
|
|
|
|
let mut attestation = self
|
|
.harness
|
|
.chain
|
|
.produce_unaggregated_attestation(current_slot, 0)
|
|
.expect("should not error while producing attestation");
|
|
|
|
// For these tests we always use committee index 0, which also matches the "dummy" committee
|
|
// index used post-Electra.
|
|
let committee_index = 0;
|
|
|
|
let validator_index = *head
|
|
.beacon_state
|
|
.get_beacon_committee(current_slot, committee_index)
|
|
.expect("should get committees")
|
|
.committee
|
|
.get(validator_index_in_committee)
|
|
.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,
|
|
committee_index,
|
|
committee_count,
|
|
&self.harness.chain.spec,
|
|
)
|
|
.expect("should compute subnet id");
|
|
|
|
let validator_sk = generate_deterministic_keypair(validator_index).sk;
|
|
|
|
attestation
|
|
.sign(
|
|
&validator_sk,
|
|
committee_index as usize,
|
|
&head.beacon_state.fork(),
|
|
self.harness.chain.genesis_validators_root,
|
|
&self.harness.chain.spec,
|
|
)
|
|
.expect("should sign attestation");
|
|
|
|
let single_attestation = SingleAttestation {
|
|
attester_index: validator_index as u64,
|
|
committee_index,
|
|
data: attestation.data().clone(),
|
|
signature: attestation.signature().clone(),
|
|
};
|
|
|
|
let mut verified_attestation = self
|
|
.harness
|
|
.chain
|
|
.verify_unaggregated_attestation_for_gossip(&single_attestation, Some(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![]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
mutation_func(
|
|
verified_attestation.__indexed_attestation_mut(),
|
|
&self.harness.chain,
|
|
);
|
|
|
|
let result = self
|
|
.harness
|
|
.chain
|
|
.apply_attestation_to_fork_choice(&verified_attestation);
|
|
|
|
comparison_func(result);
|
|
|
|
self
|
|
}
|
|
|
|
/// Check to ensure that we can read the finalized block. This is a regression test.
|
|
pub fn check_finalized_block_is_accessible(self) -> Self {
|
|
self.harness
|
|
.chain
|
|
.canonical_head
|
|
.fork_choice_read_lock()
|
|
.get_block(&self.harness.finalized_checkpoint().root)
|
|
.unwrap();
|
|
|
|
self
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn justified_and_finalized_blocks() {
|
|
let tester = ForkChoiceTest::new();
|
|
let fork_choice = tester.harness.chain.canonical_head.fork_choice_read_lock();
|
|
|
|
let justified_checkpoint = fork_choice.justified_checkpoint();
|
|
assert_eq!(justified_checkpoint.epoch, 0);
|
|
assert!(justified_checkpoint.root != Hash256::zero());
|
|
assert!(fork_choice.get_justified_block().is_ok());
|
|
|
|
let finalized_checkpoint = fork_choice.finalized_checkpoint();
|
|
assert_eq!(finalized_checkpoint.epoch, 0);
|
|
assert!(finalized_checkpoint.root != Hash256::zero());
|
|
assert!(fork_choice.get_finalized_block().is_ok());
|
|
}
|
|
|
|
/// - The new justified checkpoint descends from the current. Near genesis.
|
|
#[tokio::test]
|
|
async fn justified_checkpoint_updates_with_descendent_first_justification() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.assert_justified_epoch(0)
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_justified_epoch(2);
|
|
}
|
|
|
|
/// - The new justified checkpoint descends from the current.
|
|
/// - This is **not** the first justification since genesis
|
|
#[tokio::test]
|
|
async fn justified_checkpoint_updates_with_descendent() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint().epoch <= 2)
|
|
.await
|
|
.unwrap()
|
|
.assert_justified_epoch(2)
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_justified_epoch(3);
|
|
}
|
|
|
|
/// - The new justified checkpoint **does not** descend from the current.
|
|
/// - Finalized epoch has **not** increased.
|
|
#[tokio::test]
|
|
async fn justified_checkpoint_updates_with_non_descendent() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.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_mut().epoch = Epoch::new(3);
|
|
// The new block should **not** include the current justified block as an ancestor.
|
|
state.current_justified_checkpoint_mut().root = *state
|
|
.get_block_root(Epoch::new(1).start_slot(E::slots_per_epoch()))
|
|
.unwrap();
|
|
})
|
|
.await
|
|
.assert_justified_epoch(3);
|
|
}
|
|
|
|
/// Check that the balances are obtained correctly.
|
|
#[tokio::test]
|
|
async fn justified_balances() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.current_justified_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_justified_epoch(2)
|
|
.check_justified_balances()
|
|
}
|
|
|
|
macro_rules! assert_invalid_block {
|
|
($err: tt, $($error: pat_param) |+ $( if $guard: expr )?) => {
|
|
assert!(
|
|
matches!(
|
|
$err,
|
|
$( ForkChoiceError::InvalidBlock($error) ) |+ $( if $guard )?
|
|
),
|
|
)
|
|
};
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert block.parent_root in store.block_states
|
|
#[tokio::test]
|
|
async fn invalid_block_unknown_parent() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks(2)
|
|
.await
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
*block.message_mut().parent_root_mut() = junk;
|
|
},
|
|
|err| {
|
|
assert_invalid_block!(
|
|
err,
|
|
InvalidBlock::UnknownParent(parent)
|
|
if parent == junk
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert get_current_slot(store) >= block.slot
|
|
#[tokio::test]
|
|
async fn invalid_block_future_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks(2)
|
|
.await
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
*block.message_mut().slot_mut() += 1;
|
|
},
|
|
|err| assert_invalid_block!(err, InvalidBlock::FutureSlot { .. }),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1
|
|
///
|
|
/// assert block.slot > finalized_slot
|
|
#[tokio::test]
|
|
async fn invalid_block_finalized_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, _| {
|
|
*block.message_mut().slot_mut() =
|
|
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())
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// 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
|
|
#[tokio::test]
|
|
async fn invalid_block_finalized_descendant() {
|
|
let invalid_ancestor = Mutex::new(Hash256::zero());
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2)
|
|
.apply_invalid_block_directly_to_fork_choice(
|
|
|block, state| {
|
|
*block.message_mut().parent_root_mut() = *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())
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
macro_rules! assert_invalid_attestation {
|
|
($err: tt, $($error: pat_param) |+ $( if $guard: expr )?) => {
|
|
assert!(
|
|
matches!(
|
|
$err,
|
|
$( Err(BeaconChainError::ForkChoiceError(ForkChoiceError::InvalidAttestation($error))) ) |+ $( if $guard )?
|
|
),
|
|
"{:?}",
|
|
$err
|
|
)
|
|
};
|
|
}
|
|
|
|
/// Ensure we can process a valid attestation.
|
|
#[tokio::test]
|
|
async fn valid_attestation() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|_, _| {},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// 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.
|
|
#[tokio::test]
|
|
async fn invalid_attestation_empty_bitfield() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| match attestation {
|
|
IndexedAttestation::Base(att) => {
|
|
att.attesting_indices = vec![].try_into().unwrap();
|
|
}
|
|
IndexedAttestation::Electra(att) => {
|
|
att.attesting_indices = vec![].try_into().unwrap();
|
|
}
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(result, InvalidAttestation::EmptyAggregationBitfield)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch in [expected_current_epoch, previous_epoch]
|
|
///
|
|
/// (tests epoch after current epoch)
|
|
#[tokio::test]
|
|
async fn invalid_attestation_future_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data_mut().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)
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch in [expected_current_epoch, previous_epoch]
|
|
///
|
|
/// (tests epoch prior to previous epoch)
|
|
#[tokio::test]
|
|
async fn invalid_attestation_past_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(E::slots_per_epoch() as usize * 3 + 1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data_mut().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)
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.epoch == compute_epoch_at_slot(attestation.data.slot)
|
|
#[tokio::test]
|
|
async fn invalid_attestation_target_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(E::slots_per_epoch() as usize + 1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data_mut().slot = Slot::new(1);
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::BadTargetEpoch { target, slot }
|
|
if target == Epoch::new(1) && slot == Slot::new(1)
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.root in store.blocks
|
|
#[tokio::test]
|
|
async fn invalid_attestation_unknown_target_root() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data_mut().target.root = junk;
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::UnknownTargetRoot(root)
|
|
if root == junk
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert attestation.data.beacon_block_root in store.blocks
|
|
#[tokio::test]
|
|
async fn invalid_attestation_unknown_beacon_block_root() {
|
|
let junk = Hash256::from_low_u64_be(42);
|
|
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
attestation.data_mut().beacon_block_root = junk;
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::UnknownHeadBlock { beacon_block_root }
|
|
if beacon_block_root == junk
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot
|
|
#[tokio::test]
|
|
async fn invalid_attestation_future_block() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::Blocks(1),
|
|
|attestation, chain| {
|
|
attestation.data_mut().beacon_block_root = chain
|
|
.block_at_slot(chain.slot().unwrap(), WhenSlotSkipped::Prev)
|
|
.unwrap()
|
|
.unwrap()
|
|
.canonical_root();
|
|
},
|
|
|result| {
|
|
assert_invalid_attestation!(
|
|
result,
|
|
InvalidAttestation::AttestsToFutureBlock { block, attestation }
|
|
if block == 2 && attestation == 1
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Gossip payload attestations must be for the current slot. A payload attestation for slot S
|
|
/// received at slot S+1 should be rejected per the spec.
|
|
#[tokio::test]
|
|
async fn non_block_payload_attestation_for_previous_slot_is_rejected() {
|
|
let test = ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await;
|
|
|
|
let chain = &test.harness.chain;
|
|
let block_a = chain
|
|
.block_at_slot(Slot::new(1), WhenSlotSkipped::Prev)
|
|
.expect("lookup should succeed")
|
|
.expect("block A should exist");
|
|
let block_a_root = block_a.canonical_root();
|
|
let s_plus_1 = block_a.slot().saturating_add(1_u64);
|
|
|
|
let payload_attestation = IndexedPayloadAttestation::<E> {
|
|
attesting_indices: vec![0_u64].try_into().expect("valid attesting indices"),
|
|
data: PayloadAttestationData {
|
|
beacon_block_root: block_a_root,
|
|
slot: Slot::new(1),
|
|
payload_present: true,
|
|
blob_data_available: true,
|
|
},
|
|
signature: AggregateSignature::empty(),
|
|
};
|
|
|
|
let ptc = &[0_usize];
|
|
|
|
let result = chain
|
|
.canonical_head
|
|
.fork_choice_write_lock()
|
|
.on_payload_attestation(
|
|
s_plus_1,
|
|
&payload_attestation,
|
|
AttestationFromBlock::False,
|
|
ptc,
|
|
);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
Err(ForkChoiceError::InvalidPayloadAttestation(
|
|
InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { .. }
|
|
))
|
|
),
|
|
"gossip payload attestation for previous slot should be rejected, got: {:?}",
|
|
result
|
|
);
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert target.root == get_ancestor(store, attestation.data.beacon_block_root, target_slot)
|
|
#[tokio::test]
|
|
async 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)
|
|
.await
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, chain| {
|
|
attestation.data_mut().target.root = chain
|
|
.block_at_slot(Slot::new(1), WhenSlotSkipped::Prev)
|
|
.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), WhenSlotSkipped::Prev)
|
|
.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()
|
|
)
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
/// Specification v0.12.1:
|
|
///
|
|
/// assert get_current_slot(store) >= attestation.data.slot + 1
|
|
#[tokio::test]
|
|
async fn invalid_attestation_delayed_slot() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|_, _| {},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 1))
|
|
.skip_slot()
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0));
|
|
}
|
|
|
|
/// Regression test for dequeuing when votes for two different future slots are queued.
|
|
///
|
|
/// With votes queued for consecutive slots, advancing the clock past the earlier one must release
|
|
/// only that vote and leave the later one queued until its own slot is in the past.
|
|
#[tokio::test]
|
|
async fn dequeue_attestations_consecutive_slot_divergence() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
|
// Queue a vote for `slot + 2`.
|
|
.apply_nth_attestation_to_chain(
|
|
0,
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
let slot = attestation.data().slot;
|
|
attestation.data_mut().slot = slot + 2;
|
|
},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await
|
|
// Queue a vote for `slot + 1`, which becomes due sooner.
|
|
// A different committee position avoids `PriorAttestationKnown`.
|
|
.apply_nth_attestation_to_chain(
|
|
1,
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
let slot = attestation.data().slot;
|
|
attestation.data_mut().slot = slot + 1;
|
|
},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 2))
|
|
// Advance so the slot+1 vote is due (in the past) but the slot+2 vote is not yet.
|
|
.skip_slots(2)
|
|
.inspect_queued_attestations(|queue| {
|
|
assert_eq!(
|
|
queue.len(),
|
|
1,
|
|
"only the due slot+1 vote should be dequeued"
|
|
);
|
|
assert_eq!(
|
|
queue[0].slot,
|
|
Slot::new(3),
|
|
"the surviving vote must be the not-yet-due slot+2 vote"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Companion to `dequeue_attestations_consecutive_slot_divergence`: votes for two different slots
|
|
/// are queued, but the clock is advanced far enough that *both* are due at dequeue time.
|
|
///
|
|
/// When every queued vote is in the past, the whole queue drains in a single dequeue.
|
|
#[tokio::test]
|
|
async fn dequeue_attestations_conciliation() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_without_new_attestations(1)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
|
// Queue a vote for `slot + 2`.
|
|
.apply_nth_attestation_to_chain(
|
|
0,
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
let slot = attestation.data().slot;
|
|
attestation.data_mut().slot = slot + 2;
|
|
},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await
|
|
// Queue a vote for `slot + 1`.
|
|
.apply_nth_attestation_to_chain(
|
|
1,
|
|
MutationDelay::NoDelay,
|
|
|attestation, _| {
|
|
let slot = attestation.data().slot;
|
|
attestation.data_mut().slot = slot + 1;
|
|
},
|
|
|result| assert!(result.is_ok()),
|
|
)
|
|
.await
|
|
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 2))
|
|
// Advance past both votes (to slot + 3) so the whole queue drains.
|
|
.skip_slots(3)
|
|
.inspect_queued_attestations(|queue| {
|
|
assert_eq!(
|
|
queue.len(),
|
|
0,
|
|
"all votes are due, so the entire queue must drain"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Tests that the correct target root is used when the attested-to block is in a prior epoch to
|
|
/// the attestation.
|
|
#[tokio::test]
|
|
async fn valid_attestation_skip_across_epoch() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks(E::slots_per_epoch() as usize - 1)
|
|
.await
|
|
.skip_slots(2)
|
|
.apply_attestation_to_chain(
|
|
MutationDelay::NoDelay,
|
|
|attestation, _chain| {
|
|
assert_eq!(
|
|
attestation.data().target.root,
|
|
attestation.data().beacon_block_root
|
|
)
|
|
},
|
|
|result| result.unwrap(),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn can_read_finalized_block() {
|
|
ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.check_finalized_block_is_accessible();
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn weak_subjectivity_fail_on_startup() {
|
|
let epoch = Epoch::new(0);
|
|
let root = Hash256::from_low_u64_le(1);
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(Checkpoint { epoch, root }),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_pass_on_startup() {
|
|
let epoch = Epoch::new(0);
|
|
let root = Hash256::zero();
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(Checkpoint { epoch, root }),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config)
|
|
.apply_blocks(E::slots_per_epoch() as usize)
|
|
.await
|
|
.assert_shutdown_signal_not_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_passes() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2);
|
|
|
|
let checkpoint = setup_harness.harness.finalized_checkpoint();
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2)
|
|
.assert_shutdown_signal_not_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_fails_early_epoch() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2);
|
|
|
|
let mut checkpoint = setup_harness.harness.finalized_checkpoint();
|
|
|
|
checkpoint.epoch -= 1;
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 3)
|
|
.await
|
|
.unwrap_err()
|
|
.assert_finalized_epoch_is_less_than(checkpoint.epoch)
|
|
.assert_shutdown_signal_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_fails_late_epoch() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2);
|
|
|
|
let mut checkpoint = setup_harness.harness.finalized_checkpoint();
|
|
|
|
checkpoint.epoch += 1;
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 4)
|
|
.await
|
|
.unwrap_err()
|
|
.assert_finalized_epoch_is_less_than(checkpoint.epoch)
|
|
.assert_shutdown_signal_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_fails_incorrect_root() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(2);
|
|
|
|
let mut checkpoint = setup_harness.harness.finalized_checkpoint();
|
|
|
|
checkpoint.root = Hash256::zero();
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 3)
|
|
.await
|
|
.unwrap_err()
|
|
.assert_finalized_epoch_is_less_than(checkpoint.epoch)
|
|
.assert_shutdown_signal_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_epoch_boundary_is_skip_slot() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
// first two epochs
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// get the head, it will become the finalized root of epoch 4
|
|
let checkpoint_root = setup_harness.harness.head_block_root();
|
|
|
|
setup_harness
|
|
// epoch 3 will be entirely skip slots
|
|
.skip_slots(E::slots_per_epoch() as usize)
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(5);
|
|
|
|
// the checkpoint at epoch 4 should become the root of last block of epoch 2
|
|
let checkpoint = Checkpoint {
|
|
epoch: Epoch::new(4),
|
|
root: checkpoint_root,
|
|
};
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
// recreate the chain exactly
|
|
Box::pin(
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.skip_slots(E::slots_per_epoch() as usize)
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1),
|
|
)
|
|
.await
|
|
.assert_finalized_epoch(5)
|
|
.assert_shutdown_signal_not_sent();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn weak_subjectivity_check_epoch_boundary_is_skip_slot_failure() {
|
|
let setup_harness = ForkChoiceTest::new()
|
|
// first two epochs
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// get the head, it will become the finalized root of epoch 4
|
|
let checkpoint_root = setup_harness.harness.head_block_root();
|
|
|
|
setup_harness
|
|
// epoch 3 will be entirely skip slots
|
|
.skip_slots(E::slots_per_epoch() as usize)
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5)
|
|
.await
|
|
.unwrap()
|
|
.apply_blocks(1)
|
|
.await
|
|
.assert_finalized_epoch(5);
|
|
|
|
// Invalid checkpoint (epoch too early)
|
|
let checkpoint = Checkpoint {
|
|
epoch: Epoch::new(1),
|
|
root: checkpoint_root,
|
|
};
|
|
|
|
let chain_config = ChainConfig {
|
|
weak_subjectivity_checkpoint: Some(checkpoint),
|
|
..ChainConfig::default()
|
|
};
|
|
|
|
// recreate the chain exactly
|
|
ForkChoiceTest::new_with_chain_config(chain_config.clone())
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.skip_slots(E::slots_per_epoch() as usize)
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 6)
|
|
.await
|
|
.unwrap_err()
|
|
.assert_finalized_epoch_is_less_than(checkpoint.epoch)
|
|
.assert_shutdown_signal_sent();
|
|
}
|
|
|
|
/// Checks that `ProgressiveBalancesCache` is updated correctly after an attester slashing event,
|
|
/// where the slashed validator is a target attester in previous / current epoch.
|
|
#[tokio::test]
|
|
async fn progressive_balances_cache_attester_slashing() {
|
|
ForkChoiceTest::new()
|
|
// first two epochs
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.add_previous_epoch_attester_slashing()
|
|
.await
|
|
// expect fork choice to import blocks successfully after a previous epoch attester is
|
|
// slashed, i.e. the slashed attester's balance is correctly excluded from
|
|
// the previous epoch total balance in `ProgressiveBalancesCache`.
|
|
.apply_blocks(1)
|
|
.await
|
|
// expect fork choice to import another epoch of blocks successfully - the slashed
|
|
// attester's balance should be excluded from the current epoch total balance in
|
|
// `ProgressiveBalancesCache` as well.
|
|
.apply_blocks(E::slots_per_epoch() as usize)
|
|
.await;
|
|
}
|
|
|
|
/// Checks that `ProgressiveBalancesCache` is updated correctly after a proposer slashing event,
|
|
/// where the slashed validator is a target attester in previous / current epoch.
|
|
#[tokio::test]
|
|
async fn progressive_balances_cache_proposer_slashing() {
|
|
ForkChoiceTest::new()
|
|
// first two epochs
|
|
.apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0)
|
|
.await
|
|
.unwrap()
|
|
.add_previous_epoch_proposer_slashing(E::slots_per_epoch())
|
|
.await
|
|
// expect fork choice to import blocks successfully after a previous epoch proposer is
|
|
// slashed, i.e. the slashed proposer's balance is correctly excluded from
|
|
// the previous epoch total balance in `ProgressiveBalancesCache`.
|
|
.apply_blocks(1)
|
|
.await
|
|
// expect fork choice to import another epoch of blocks successfully - the slashed
|
|
// proposer's balance should be excluded from the current epoch total balance in
|
|
// `ProgressiveBalancesCache` as well.
|
|
.apply_blocks(E::slots_per_epoch() as usize)
|
|
.await;
|
|
}
|