mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-29 10:54:24 +00:00
v0.12 fork choice update (#1229)
* Incomplete scraps
* Add progress on new fork choice impl
* Further progress
* First complete compiling version
* Remove chain reference
* Add new lmd_ghost crate
* Start integrating into beacon chain
* 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 state processing for v0.12
* Fix EF test runners for v0.12
* Fix some tests
* Fix broken attestation verification test
* More test fixes
* Rough beacon chain impl working
* Remove fork_choice_2
* Remove checkpoint manager
* Half finished ssz impl
* Add missed file
* Add persistence
* Tidy, fix some compile errors
* Remove RwLock from ProtoArrayForkChoice
* Fix store-based compile errors
* Add comments, tidy
* Move function out of ForkChoice struct
* Start testing
* More testing
* Fix compile error
* Tidy beacon_chain::fork_choice
* Queue attestations from the current slot
* Allow fork choice to handle prior-to-genesis start
* Improve error granularity
* Test attestation dequeuing
* Process attestations during block
* Store target root in fork choice
* Move fork choice verification into new crate
* Update tests
* Consensus updates for v0.12 (#1228)
* Update state processing for v0.12
* Fix EF test runners for v0.12
* Fix some tests
* Fix broken attestation verification test
* More test fixes
* Fix typo found in review
* Add `Block` struct to ProtoArray
* Start fixing get_ancestor
* Add rough progress on testing
* Get fork choice tests working
* Progress with testing
* Fix partialeq impl
* Move slot clock from fc_store
* Improve testing
* Add testing for best justified
* Add clone back to SystemTimeSlotClock
* Add balances test
* Start adding balances cache again
* Wire-in balances cache
* Improve tests
* Remove commented-out tests
* Remove beacon_chain::ForkChoice
* Rename crates
* Update wider codebase to new fork_choice layout
* Move advance_slot in test harness
* Tidy ForkChoice::update_time
* Fix verification tests
* Fix compile error with iter::once
* Fix fork choice tests
* Ensure block attestations are processed
* Fix failing beacon_chain tests
* Add first invalid block check
* Add finalized block check
* Progress with testing, new store builder
* Add fixes to get_ancestor
* Fix old genesis justification test
* Fix remaining fork choice tests
* Change root iteration method
* Move on_verified_block
* Remove unused method
* Start adding attestation verification tests
* Add invalid ffg target test
* Add target epoch test
* Add queued attestation test
* Remove old fork choice verification tests
* Tidy, add test
* Move fork choice lock drop
* Rename BeaconForkChoiceStore
* Add comments, tidy BeaconForkChoiceStore
* Update metrics, rename fork_choice_store.rs
* Remove genesis_block_root from ForkChoice
* Tidy
* Update fork_choice comments
* Tidy, add comments
* Tidy, simplify ForkChoice, fix compile issue
* Tidy, removed dead file
* Increase http request timeout
* Fix failing rest_api test
* Set HTTP timeout back to 5s
* Apply fix to get_ancestor
* Address Michael's comments
* Fix typo
* Revert "Fix broken attestation verification test"
This reverts commit 722cdc903b.
Co-authored-by: Kirk Baird <baird.k@outlook.com>
Co-authored-by: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
20
consensus/fork_choice/Cargo.toml
Normal file
20
consensus/fork_choice/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "fork_choice"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
types = { path = "../types" }
|
||||
proto_array = { path = "../proto_array" }
|
||||
eth2_ssz = { path = "../ssz" }
|
||||
eth2_ssz_derive = { path = "../ssz_derive" }
|
||||
|
||||
[dev-dependencies]
|
||||
state_processing = { path = "../../consensus/state_processing" }
|
||||
beacon_chain = { path = "../../beacon_node/beacon_chain" }
|
||||
store = { path = "../../beacon_node/store" }
|
||||
tree_hash = { path = "../../consensus/tree_hash" }
|
||||
slot_clock = { path = "../../common/slot_clock" }
|
||||
884
consensus/fork_choice/src/fork_choice.rs
Normal file
884
consensus/fork_choice/src/fork_choice.rs
Normal file
@@ -0,0 +1,884 @@
|
||||
use crate::ForkChoiceStore;
|
||||
use proto_array::{Block as ProtoBlock, ProtoArrayForkChoice};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::marker::PhantomData;
|
||||
use types::{
|
||||
BeaconBlock, BeaconState, BeaconStateError, Epoch, EthSpec, Hash256, IndexedAttestation, Slot,
|
||||
};
|
||||
|
||||
/// Defined here:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#configuration
|
||||
pub const SAFE_SLOTS_TO_UPDATE_JUSTIFIED: u64 = 8;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<T> {
|
||||
InvalidAttestation(InvalidAttestation),
|
||||
InvalidBlock(InvalidBlock),
|
||||
ProtoArrayError(String),
|
||||
InvalidProtoArrayBytes(String),
|
||||
MissingProtoArrayBlock(Hash256),
|
||||
UnknownAncestor {
|
||||
ancestor_slot: Slot,
|
||||
descendant_root: Hash256,
|
||||
},
|
||||
InconsistentOnTick {
|
||||
previous_slot: Slot,
|
||||
time: Slot,
|
||||
},
|
||||
BeaconStateError(BeaconStateError),
|
||||
AttemptToRevertJustification {
|
||||
store: Slot,
|
||||
state: Slot,
|
||||
},
|
||||
ForkChoiceStoreError(T),
|
||||
UnableToSetJustifiedCheckpoint(T),
|
||||
AfterBlockFailed(T),
|
||||
}
|
||||
|
||||
impl<T> From<InvalidAttestation> for Error<T> {
|
||||
fn from(e: InvalidAttestation) -> Self {
|
||||
Error::InvalidAttestation(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InvalidBlock {
|
||||
UnknownParent(Hash256),
|
||||
FutureSlot {
|
||||
current_slot: Slot,
|
||||
block_slot: Slot,
|
||||
},
|
||||
FinalizedSlot {
|
||||
finalized_slot: Slot,
|
||||
block_slot: Slot,
|
||||
},
|
||||
NotFinalizedDescendant {
|
||||
finalized_root: Hash256,
|
||||
block_ancestor: Option<Hash256>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InvalidAttestation {
|
||||
/// The attestations aggregation bits were empty when they shouldn't be.
|
||||
EmptyAggregationBitfield,
|
||||
/// The `attestation.data.beacon_block_root` block is unknown.
|
||||
UnknownHeadBlock { beacon_block_root: Hash256 },
|
||||
/// The `attestation.data.slot` is not from the same epoch as `data.target.epoch` and therefore
|
||||
/// the attestation is invalid.
|
||||
BadTargetEpoch { target: Epoch, slot: Slot },
|
||||
/// The target root of the attestation points to a block that we have not verified.
|
||||
UnknownTargetRoot(Hash256),
|
||||
/// The attestation is for an epoch in the future (with respect to the gossip clock disparity).
|
||||
FutureEpoch {
|
||||
attestation_epoch: Epoch,
|
||||
current_epoch: Epoch,
|
||||
},
|
||||
/// The attestation is for an epoch in the past (with respect to the gossip clock disparity).
|
||||
PastEpoch {
|
||||
attestation_epoch: Epoch,
|
||||
current_epoch: Epoch,
|
||||
},
|
||||
/// The attestation references a target root that does not match what is stored in our
|
||||
/// database.
|
||||
InvalidTarget {
|
||||
attestation: Hash256,
|
||||
local: Hash256,
|
||||
},
|
||||
/// The attestation is attesting to a state that is later than itself. (Viz., attesting to the
|
||||
/// future).
|
||||
AttestsToFutureBlock { block: Slot, attestation: Slot },
|
||||
}
|
||||
|
||||
impl<T> From<String> for Error<T> {
|
||||
fn from(e: String) -> Self {
|
||||
Error::ProtoArrayError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate how far `slot` lies from the start of its epoch.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#compute_slots_since_epoch_start
|
||||
pub fn compute_slots_since_epoch_start<E: EthSpec>(slot: Slot) -> Slot {
|
||||
slot - slot
|
||||
.epoch(E::slots_per_epoch())
|
||||
.start_slot(E::slots_per_epoch())
|
||||
}
|
||||
|
||||
/// Calculate the first slot in `epoch`.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch
|
||||
fn compute_start_slot_at_epoch<E: EthSpec>(epoch: Epoch) -> Slot {
|
||||
epoch.start_slot(E::slots_per_epoch())
|
||||
}
|
||||
|
||||
/// Called whenever the current time increases.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#on_tick
|
||||
fn on_tick<T, E>(store: &mut T, time: Slot) -> Result<(), Error<T::Error>>
|
||||
where
|
||||
T: ForkChoiceStore<E>,
|
||||
E: EthSpec,
|
||||
{
|
||||
let previous_slot = store.get_current_slot();
|
||||
|
||||
if time > previous_slot + 1 {
|
||||
return Err(Error::InconsistentOnTick {
|
||||
previous_slot,
|
||||
time,
|
||||
});
|
||||
}
|
||||
|
||||
// Update store time.
|
||||
store.set_current_slot(time);
|
||||
|
||||
let current_slot = store.get_current_slot();
|
||||
if !(current_slot > previous_slot && compute_slots_since_epoch_start::<E>(current_slot) == 0) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if store.best_justified_checkpoint().epoch > store.justified_checkpoint().epoch {
|
||||
store
|
||||
.set_justified_checkpoint(*store.best_justified_checkpoint())
|
||||
.map_err(Error::ForkChoiceStoreError)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Used for queuing attestations from the current slot. Only contains the minimum necessary
|
||||
/// information about the attestation.
|
||||
#[derive(Clone, PartialEq, Encode, Decode)]
|
||||
pub struct QueuedAttestation {
|
||||
slot: Slot,
|
||||
attesting_indices: Vec<u64>,
|
||||
block_root: Hash256,
|
||||
target_epoch: Epoch,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> From<&IndexedAttestation<E>> for QueuedAttestation {
|
||||
fn from(a: &IndexedAttestation<E>) -> Self {
|
||||
Self {
|
||||
slot: a.data.slot,
|
||||
attesting_indices: a.attesting_indices[..].to_vec(),
|
||||
block_root: a.data.beacon_block_root,
|
||||
target_epoch: a.data.target.epoch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all values in `self.queued_attestations` that have a slot that is earlier than the
|
||||
/// current slot. Also removes those values from `self.queued_attestations`.
|
||||
fn dequeue_attestations(
|
||||
current_slot: Slot,
|
||||
queued_attestations: &mut Vec<QueuedAttestation>,
|
||||
) -> Vec<QueuedAttestation> {
|
||||
let remaining = queued_attestations.split_off(
|
||||
queued_attestations
|
||||
.iter()
|
||||
.position(|a| a.slot >= current_slot)
|
||||
.unwrap_or(queued_attestations.len()),
|
||||
);
|
||||
|
||||
std::mem::replace(queued_attestations, remaining)
|
||||
}
|
||||
|
||||
/// Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#ethereum-20-phase-0----beacon-chain-fork-choice
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// This struct wraps `ProtoArrayForkChoice` and provides:
|
||||
///
|
||||
/// - Management of the justified state and caching of balances.
|
||||
/// - Queuing of attestations from the current slot.
|
||||
pub struct ForkChoice<T, E> {
|
||||
/// Storage for `ForkChoice`, modelled off the spec `Store` object.
|
||||
fc_store: T,
|
||||
/// The underlying representation of the block DAG.
|
||||
proto_array: ProtoArrayForkChoice,
|
||||
/// Attestations that arrived at the current slot and must be queued for later processing.
|
||||
queued_attestations: Vec<QueuedAttestation>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<T, E> PartialEq for ForkChoice<T, E>
|
||||
where
|
||||
T: ForkChoiceStore<E> + PartialEq,
|
||||
E: EthSpec,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.fc_store == other.fc_store
|
||||
&& self.proto_array == other.proto_array
|
||||
&& self.queued_attestations == other.queued_attestations
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ForkChoice<T, E>
|
||||
where
|
||||
T: ForkChoiceStore<E>,
|
||||
E: EthSpec,
|
||||
{
|
||||
/// Instantiates `Self` from the genesis parameters.
|
||||
pub fn from_genesis(
|
||||
fc_store: T,
|
||||
genesis_block: &BeaconBlock<E>,
|
||||
) -> Result<Self, Error<T::Error>> {
|
||||
let finalized_block_slot = genesis_block.slot;
|
||||
let finalized_block_state_root = genesis_block.state_root;
|
||||
|
||||
let proto_array = ProtoArrayForkChoice::new(
|
||||
finalized_block_slot,
|
||||
finalized_block_state_root,
|
||||
fc_store.justified_checkpoint().epoch,
|
||||
fc_store.finalized_checkpoint().epoch,
|
||||
fc_store.finalized_checkpoint().root,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
fc_store,
|
||||
proto_array,
|
||||
queued_attestations: vec![],
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Instantiates `Self` from some existing components.
|
||||
///
|
||||
/// This is useful if the existing components have been loaded from disk after a process
|
||||
/// restart.
|
||||
pub fn from_components(
|
||||
fc_store: T,
|
||||
proto_array: ProtoArrayForkChoice,
|
||||
queued_attestations: Vec<QueuedAttestation>,
|
||||
) -> Self {
|
||||
Self {
|
||||
fc_store,
|
||||
proto_array,
|
||||
queued_attestations,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers
|
||||
/// to the block that is *returned*, not the one that is supplied.)
|
||||
///
|
||||
/// The result may be `Ok(None)` if the block does not descend from the finalized block. This
|
||||
/// is an artifact of proto-array, sometimes it contains descendants of blocks that have been
|
||||
/// pruned.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#get_ancestor
|
||||
fn get_ancestor(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
ancestor_slot: Slot,
|
||||
) -> Result<Option<Hash256>, Error<T::Error>>
|
||||
where
|
||||
T: ForkChoiceStore<E>,
|
||||
E: EthSpec,
|
||||
{
|
||||
let block = self
|
||||
.proto_array
|
||||
.get_block(&block_root)
|
||||
.ok_or_else(|| Error::MissingProtoArrayBlock(block_root))?;
|
||||
|
||||
if block.slot > ancestor_slot {
|
||||
Ok(self
|
||||
.proto_array
|
||||
.core_proto_array()
|
||||
.iter_block_roots(&block_root)
|
||||
// Search for a slot that is **less than or equal to** the target slot. We check
|
||||
// for lower slots to account for skip slots.
|
||||
.find(|(_, slot)| *slot <= ancestor_slot)
|
||||
.map(|(root, _)| root))
|
||||
} else if block.slot == ancestor_slot {
|
||||
Ok(Some(block_root))
|
||||
} else {
|
||||
// Root is older than queried slot, thus a skip slot. Return most recent root prior to
|
||||
// slot.
|
||||
Ok(Some(block_root))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the fork choice rule to determine the head.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Is equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#get_head
|
||||
pub fn get_head(&mut self, current_slot: Slot) -> Result<Hash256, Error<T::Error>> {
|
||||
self.update_time(current_slot)?;
|
||||
|
||||
let store = &mut self.fc_store;
|
||||
|
||||
let result = self
|
||||
.proto_array
|
||||
.find_head(
|
||||
store.justified_checkpoint().epoch,
|
||||
store.justified_checkpoint().root,
|
||||
store.finalized_checkpoint().epoch,
|
||||
store.justified_balances(),
|
||||
)
|
||||
.map_err(Into::into);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns `true` if the given `store` should be updated to set
|
||||
/// `state.current_justified_checkpoint` its `justified_checkpoint`.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Is equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#should_update_justified_checkpoint
|
||||
fn should_update_justified_checkpoint(
|
||||
&mut self,
|
||||
current_slot: Slot,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<bool, Error<T::Error>> {
|
||||
self.update_time(current_slot)?;
|
||||
|
||||
let new_justified_checkpoint = &state.current_justified_checkpoint;
|
||||
|
||||
if compute_slots_since_epoch_start::<E>(self.fc_store.get_current_slot())
|
||||
< SAFE_SLOTS_TO_UPDATE_JUSTIFIED
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let justified_slot =
|
||||
compute_start_slot_at_epoch::<E>(self.fc_store.justified_checkpoint().epoch);
|
||||
|
||||
// This sanity check is not in the spec, but the invariant is implied.
|
||||
if justified_slot >= state.slot {
|
||||
return Err(Error::AttemptToRevertJustification {
|
||||
store: justified_slot,
|
||||
state: state.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// We know that the slot for `new_justified_checkpoint.root` is not greater than
|
||||
// `state.slot`, since a state cannot justify its own slot.
|
||||
//
|
||||
// We know that `new_justified_checkpoint.root` is an ancestor of `state`, since a `state`
|
||||
// only ever justifies ancestors.
|
||||
//
|
||||
// A prior `if` statement protects against a justified_slot that is greater than
|
||||
// `state.slot`
|
||||
let justified_ancestor =
|
||||
self.get_ancestor(new_justified_checkpoint.root, justified_slot)?;
|
||||
if justified_ancestor != Some(self.fc_store.justified_checkpoint().root) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Add `block` to the fork choice DAG.
|
||||
///
|
||||
/// - `block_root` is the root of `block.
|
||||
/// - The root of `state` matches `block.state_root`.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Approximates:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#on_block
|
||||
///
|
||||
/// It only approximates the specification since it does not run the `state_transition` check.
|
||||
/// That should have already been called upstream and it's too expensive to call again.
|
||||
///
|
||||
/// ## Notes:
|
||||
///
|
||||
/// The supplied block **must** pass the `state_transition` function as it will not be run
|
||||
/// here.
|
||||
pub fn on_block(
|
||||
&mut self,
|
||||
current_slot: Slot,
|
||||
block: &BeaconBlock<E>,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<(), Error<T::Error>> {
|
||||
let current_slot = self.update_time(current_slot)?;
|
||||
|
||||
// Parent block must be known.
|
||||
if !self.proto_array.contains_block(&block.parent_root) {
|
||||
return Err(Error::InvalidBlock(InvalidBlock::UnknownParent(
|
||||
block.parent_root,
|
||||
)));
|
||||
}
|
||||
|
||||
// Blocks cannot be in the future. If they are, their consideration must be delayed until
|
||||
// the are in the past.
|
||||
//
|
||||
// Note: presently, we do not delay consideration. We just drop the block.
|
||||
if block.slot > current_slot {
|
||||
return Err(Error::InvalidBlock(InvalidBlock::FutureSlot {
|
||||
current_slot,
|
||||
block_slot: block.slot,
|
||||
}));
|
||||
}
|
||||
|
||||
// Check that block is later than the finalized epoch slot (optimization to reduce calls to
|
||||
// get_ancestor).
|
||||
let finalized_slot =
|
||||
compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
|
||||
if block.slot <= finalized_slot {
|
||||
return Err(Error::InvalidBlock(InvalidBlock::FinalizedSlot {
|
||||
finalized_slot,
|
||||
block_slot: block.slot,
|
||||
}));
|
||||
}
|
||||
|
||||
// Check block is a descendant of the finalized block at the checkpoint finalized slot.
|
||||
//
|
||||
// Note: the specification uses `hash_tree_root(block)` instead of `block.parent_root` for
|
||||
// the start of this search. I claim that since `block.slot > finalized_slot` it is
|
||||
// equivalent to use the parent root for this search. Doing so reduces a single lookup
|
||||
// (trivial), but more importantly, it means we don't need to have added `block` to
|
||||
// `self.proto_array` to do this search. See:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-specs/pull/1884
|
||||
let block_ancestor = self.get_ancestor(block.parent_root, finalized_slot)?;
|
||||
let finalized_root = self.fc_store.finalized_checkpoint().root;
|
||||
if block_ancestor != Some(finalized_root) {
|
||||
return Err(Error::InvalidBlock(InvalidBlock::NotFinalizedDescendant {
|
||||
finalized_root,
|
||||
block_ancestor,
|
||||
}));
|
||||
}
|
||||
|
||||
// Update justified checkpoint.
|
||||
if state.current_justified_checkpoint.epoch > self.fc_store.justified_checkpoint().epoch {
|
||||
if state.current_justified_checkpoint.epoch
|
||||
> self.fc_store.best_justified_checkpoint().epoch
|
||||
{
|
||||
self.fc_store
|
||||
.set_best_justified_checkpoint(state.current_justified_checkpoint);
|
||||
}
|
||||
if self.should_update_justified_checkpoint(current_slot, state)? {
|
||||
self.fc_store
|
||||
.set_justified_checkpoint(state.current_justified_checkpoint)
|
||||
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update finalized checkpoint.
|
||||
if state.finalized_checkpoint.epoch > self.fc_store.finalized_checkpoint().epoch {
|
||||
self.fc_store
|
||||
.set_finalized_checkpoint(state.finalized_checkpoint);
|
||||
let finalized_slot =
|
||||
compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
|
||||
|
||||
// Note: the `if` statement here is not part of the specification, but I claim that it
|
||||
// is an optimization and equivalent to the specification. See this PR for more
|
||||
// information:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-specs/pull/1880
|
||||
if *self.fc_store.justified_checkpoint() != state.current_justified_checkpoint {
|
||||
if state.current_justified_checkpoint.epoch
|
||||
> self.fc_store.justified_checkpoint().epoch
|
||||
|| self
|
||||
.get_ancestor(self.fc_store.justified_checkpoint().root, finalized_slot)?
|
||||
!= Some(self.fc_store.finalized_checkpoint().root)
|
||||
{
|
||||
self.fc_store
|
||||
.set_justified_checkpoint(state.current_justified_checkpoint)
|
||||
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let target_slot = block
|
||||
.slot
|
||||
.epoch(E::slots_per_epoch())
|
||||
.start_slot(E::slots_per_epoch());
|
||||
let target_root = if block.slot == target_slot {
|
||||
block_root
|
||||
} else {
|
||||
*state
|
||||
.get_block_root(target_slot)
|
||||
.map_err(Error::BeaconStateError)?
|
||||
};
|
||||
|
||||
self.fc_store
|
||||
.on_verified_block(block, block_root, state)
|
||||
.map_err(Error::AfterBlockFailed)?;
|
||||
|
||||
// This does not apply a vote to the block, it just makes fork choice aware of the block so
|
||||
// it can still be identified as the head even if it doesn't have any votes.
|
||||
self.proto_array.process_block(ProtoBlock {
|
||||
slot: block.slot,
|
||||
root: block_root,
|
||||
parent_root: Some(block.parent_root),
|
||||
target_root,
|
||||
state_root: block.state_root,
|
||||
justified_epoch: state.current_justified_checkpoint.epoch,
|
||||
finalized_epoch: state.finalized_checkpoint.epoch,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates the `indexed_attestation` for application to fork choice.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#validate_on_attestation
|
||||
fn validate_on_attestation(
|
||||
&self,
|
||||
indexed_attestation: &IndexedAttestation<E>,
|
||||
) -> Result<(), InvalidAttestation> {
|
||||
// There is no point in processing an attestation with an empty bitfield. Reject
|
||||
// it immediately.
|
||||
//
|
||||
// This is not in the specification, however it should be transparent to other nodes. We
|
||||
// return early here to avoid wasting precious resources verifying the rest of it.
|
||||
if indexed_attestation.attesting_indices.len() == 0 {
|
||||
return Err(InvalidAttestation::EmptyAggregationBitfield);
|
||||
}
|
||||
|
||||
let slot_now = self.fc_store.get_current_slot();
|
||||
let epoch_now = slot_now.epoch(E::slots_per_epoch());
|
||||
let target = indexed_attestation.data.target.clone();
|
||||
|
||||
// Attestation must be from the current or previous epoch.
|
||||
if target.epoch > epoch_now {
|
||||
return Err(InvalidAttestation::FutureEpoch {
|
||||
attestation_epoch: target.epoch,
|
||||
current_epoch: epoch_now,
|
||||
});
|
||||
} else if target.epoch + 1 < epoch_now {
|
||||
return Err(InvalidAttestation::PastEpoch {
|
||||
attestation_epoch: target.epoch,
|
||||
current_epoch: epoch_now,
|
||||
});
|
||||
}
|
||||
|
||||
if target.epoch != indexed_attestation.data.slot.epoch(E::slots_per_epoch()) {
|
||||
return Err(InvalidAttestation::BadTargetEpoch {
|
||||
target: target.epoch,
|
||||
slot: indexed_attestation.data.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Attestation target must be for a known block.
|
||||
//
|
||||
// We do not delay the block for later processing to reduce complexity and DoS attack
|
||||
// surface.
|
||||
if !self.proto_array.contains_block(&target.root) {
|
||||
return Err(InvalidAttestation::UnknownTargetRoot(target.root));
|
||||
}
|
||||
|
||||
// Load the block for `attestation.data.beacon_block_root`.
|
||||
//
|
||||
// This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork
|
||||
// choice. Any known, non-finalized block should be in fork choice, so this check
|
||||
// immediately filters out attestations that attest to a block that has not been processed.
|
||||
//
|
||||
// Attestations must be for a known block. If the block is unknown, we simply drop the
|
||||
// attestation and do not delay consideration for later.
|
||||
let block = self
|
||||
.proto_array
|
||||
.get_block(&indexed_attestation.data.beacon_block_root)
|
||||
.ok_or_else(|| InvalidAttestation::UnknownHeadBlock {
|
||||
beacon_block_root: indexed_attestation.data.beacon_block_root,
|
||||
})?;
|
||||
|
||||
if block.target_root != target.root {
|
||||
return Err(InvalidAttestation::InvalidTarget {
|
||||
attestation: target.root,
|
||||
local: block.target_root,
|
||||
});
|
||||
}
|
||||
|
||||
// Attestations must not be for blocks in the future. If this is the case, the attestation
|
||||
// should not be considered.
|
||||
if block.slot > indexed_attestation.data.slot {
|
||||
return Err(InvalidAttestation::AttestsToFutureBlock {
|
||||
block: block.slot,
|
||||
attestation: indexed_attestation.data.slot,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register `attestation` with the fork choice DAG so that it may influence future calls to
|
||||
/// `Self::get_head`.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Approximates:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#on_attestation
|
||||
///
|
||||
/// It only approximates the specification since it does not perform
|
||||
/// `is_valid_indexed_attestation` since that should already have been called upstream and it's
|
||||
/// too expensive to call again.
|
||||
///
|
||||
/// ## Notes:
|
||||
///
|
||||
/// The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it
|
||||
/// will not be run here.
|
||||
pub fn on_attestation(
|
||||
&mut self,
|
||||
current_slot: Slot,
|
||||
attestation: &IndexedAttestation<E>,
|
||||
) -> Result<(), Error<T::Error>> {
|
||||
// Ensure the store is up-to-date.
|
||||
self.update_time(current_slot)?;
|
||||
|
||||
// Ignore any attestations to the zero hash.
|
||||
//
|
||||
// This is an edge case that results from the spec aliasing the zero hash to the genesis
|
||||
// block. Attesters may attest to the zero hash if they have never seen a block.
|
||||
//
|
||||
// We have two options here:
|
||||
//
|
||||
// 1. Apply all zero-hash attestations to the genesis block.
|
||||
// 2. Ignore all attestations to the zero hash.
|
||||
//
|
||||
// (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is
|
||||
// fine because votes to the genesis block are not useful; all validators implicitly attest
|
||||
// to genesis just by being present in the chain.
|
||||
if attestation.data.beacon_block_root == Hash256::zero() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.validate_on_attestation(attestation)?;
|
||||
|
||||
if attestation.data.slot < self.fc_store.get_current_slot() {
|
||||
for validator_index in attestation.attesting_indices.iter() {
|
||||
self.proto_array.process_attestation(
|
||||
*validator_index as usize,
|
||||
attestation.data.beacon_block_root,
|
||||
attestation.data.target.epoch,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// The spec declares:
|
||||
//
|
||||
// ```
|
||||
// Attestations can only affect the fork choice of subsequent slots.
|
||||
// Delay consideration in the fork choice until their slot is in the past.
|
||||
// ```
|
||||
self.queued_attestations
|
||||
.push(QueuedAttestation::from(attestation));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call `on_tick` for all slots between `fc_store.get_current_slot()` and the provided
|
||||
/// `current_slot`. Returns the value of `self.fc_store.get_current_slot`.
|
||||
pub fn update_time(&mut self, current_slot: Slot) -> Result<Slot, Error<T::Error>> {
|
||||
while self.fc_store.get_current_slot() < current_slot {
|
||||
let previous_slot = self.fc_store.get_current_slot();
|
||||
// Note: we are relying upon `on_tick` to update `fc_store.time` to ensure we don't
|
||||
// get stuck in a loop.
|
||||
on_tick(&mut self.fc_store, previous_slot + 1)?
|
||||
}
|
||||
|
||||
// Process any attestations that might now be eligible.
|
||||
self.process_attestation_queue()?;
|
||||
|
||||
Ok(self.fc_store.get_current_slot())
|
||||
}
|
||||
|
||||
/// Processes and removes from the queue any queued attestations which may now be eligible for
|
||||
/// processing due to the slot clock incrementing.
|
||||
fn process_attestation_queue(&mut self) -> Result<(), Error<T::Error>> {
|
||||
for attestation in dequeue_attestations(
|
||||
self.fc_store.get_current_slot(),
|
||||
&mut self.queued_attestations,
|
||||
) {
|
||||
for validator_index in attestation.attesting_indices.iter() {
|
||||
self.proto_array.process_attestation(
|
||||
*validator_index as usize,
|
||||
attestation.block_root,
|
||||
attestation.target_epoch,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the block is known.
|
||||
pub fn contains_block(&self, block_root: &Hash256) -> bool {
|
||||
self.proto_array.contains_block(block_root)
|
||||
}
|
||||
|
||||
/// Returns a `ProtoBlock` if the block is known.
|
||||
pub fn get_block(&self, block_root: &Hash256) -> Option<ProtoBlock> {
|
||||
self.proto_array.get_block(block_root)
|
||||
}
|
||||
|
||||
/// Returns the latest message for a given validator, if any.
|
||||
///
|
||||
/// Returns `(block_root, block_slot)`.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// It may be prudent to call `Self::update_time` before calling this function,
|
||||
/// since some attestations might be queued and awaiting processing.
|
||||
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> {
|
||||
self.proto_array.latest_message(validator_index)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying fork choice DAG.
|
||||
pub fn proto_array(&self) -> &ProtoArrayForkChoice {
|
||||
&self.proto_array
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying `fc_store`.
|
||||
pub fn fc_store(&self) -> &T {
|
||||
&self.fc_store
|
||||
}
|
||||
|
||||
/// Returns a reference to the currently queued attestations.
|
||||
pub fn queued_attestations(&self) -> &[QueuedAttestation] {
|
||||
&self.queued_attestations
|
||||
}
|
||||
|
||||
/// Prunes the underlying fork choice DAG.
|
||||
pub fn prune(&mut self) -> Result<(), Error<T::Error>> {
|
||||
let finalized_root = self.fc_store.finalized_checkpoint().root;
|
||||
|
||||
self.proto_array
|
||||
.maybe_prune(finalized_root)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Instantiate `Self` from some `PersistedForkChoice` generated by a earlier call to
|
||||
/// `Self::to_persisted`.
|
||||
pub fn from_persisted(
|
||||
persisted: PersistedForkChoice,
|
||||
fc_store: T,
|
||||
) -> Result<Self, Error<T::Error>> {
|
||||
let proto_array = ProtoArrayForkChoice::from_bytes(&persisted.proto_array_bytes)
|
||||
.map_err(Error::InvalidProtoArrayBytes)?;
|
||||
|
||||
Ok(Self {
|
||||
fc_store,
|
||||
proto_array,
|
||||
queued_attestations: persisted.queued_attestations,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Takes a snapshot of `Self` and stores it in `PersistedForkChoice`, allowing this struct to
|
||||
/// be instantiated again later.
|
||||
pub fn to_persisted(&self) -> PersistedForkChoice {
|
||||
PersistedForkChoice {
|
||||
proto_array_bytes: self.proto_array().as_bytes(),
|
||||
queued_attestations: self.queued_attestations().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct that is used to encode/decode the state of the `ForkChoice` as SSZ bytes.
|
||||
///
|
||||
/// This is used when persisting the state of the fork choice to disk.
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct PersistedForkChoice {
|
||||
proto_array_bytes: Vec<u8>,
|
||||
queued_attestations: Vec<QueuedAttestation>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use types::{EthSpec, MainnetEthSpec};
|
||||
|
||||
type E = MainnetEthSpec;
|
||||
|
||||
#[test]
|
||||
fn slots_since_epoch_start() {
|
||||
for epoch in 0..3 {
|
||||
for slot in 0..E::slots_per_epoch() {
|
||||
let input = epoch * E::slots_per_epoch() + slot;
|
||||
assert_eq!(compute_slots_since_epoch_start::<E>(Slot::new(input)), slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_slot_at_epoch() {
|
||||
for epoch in 0..3 {
|
||||
assert_eq!(
|
||||
compute_start_slot_at_epoch::<E>(Epoch::new(epoch)),
|
||||
epoch * E::slots_per_epoch()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_queued_attestations() -> Vec<QueuedAttestation> {
|
||||
(1..4)
|
||||
.into_iter()
|
||||
.map(|i| QueuedAttestation {
|
||||
slot: Slot::new(i),
|
||||
attesting_indices: vec![],
|
||||
block_root: Hash256::zero(),
|
||||
target_epoch: Epoch::new(0),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_slots(queued_attestations: &[QueuedAttestation]) -> Vec<u64> {
|
||||
queued_attestations.iter().map(|a| a.slot.into()).collect()
|
||||
}
|
||||
|
||||
fn test_queued_attestations(current_time: Slot) -> (Vec<u64>, Vec<u64>) {
|
||||
let mut queued = get_queued_attestations();
|
||||
let dequeued = dequeue_attestations(current_time, &mut queued);
|
||||
|
||||
(get_slots(&queued), get_slots(&dequeued))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dequeing_attestations() {
|
||||
let (queued, dequeued) = test_queued_attestations(Slot::new(0));
|
||||
assert_eq!(queued, vec![1, 2, 3]);
|
||||
assert!(dequeued.is_empty());
|
||||
|
||||
let (queued, dequeued) = test_queued_attestations(Slot::new(1));
|
||||
assert_eq!(queued, vec![1, 2, 3]);
|
||||
assert!(dequeued.is_empty());
|
||||
|
||||
let (queued, dequeued) = test_queued_attestations(Slot::new(2));
|
||||
assert_eq!(queued, vec![2, 3]);
|
||||
assert_eq!(dequeued, vec![1]);
|
||||
|
||||
let (queued, dequeued) = test_queued_attestations(Slot::new(3));
|
||||
assert_eq!(queued, vec![3]);
|
||||
assert_eq!(dequeued, vec![1, 2]);
|
||||
|
||||
let (queued, dequeued) = test_queued_attestations(Slot::new(4));
|
||||
assert!(queued.is_empty());
|
||||
assert_eq!(dequeued, vec![1, 2, 3]);
|
||||
}
|
||||
}
|
||||
61
consensus/fork_choice/src/fork_choice_store.rs
Normal file
61
consensus/fork_choice/src/fork_choice_store.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use types::{BeaconBlock, BeaconState, Checkpoint, EthSpec, Hash256, Slot};
|
||||
|
||||
/// Approximates the `Store` in "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.0/specs/phase0/fork-choice.md#store
|
||||
///
|
||||
/// ## Detail
|
||||
///
|
||||
/// This is only an approximation for two reasons:
|
||||
///
|
||||
/// - This crate stores the actual block DAG in `ProtoArrayForkChoice`.
|
||||
/// - `time` is represented using `Slot` instead of UNIX epoch `u64`.
|
||||
///
|
||||
/// ## Motiviation
|
||||
///
|
||||
/// The primary motivation for defining this as a trait to be implemented upstream rather than a
|
||||
/// concrete struct is to allow this crate to be free from "impure" on-disk database logic,
|
||||
/// hopefully making auditing easier.
|
||||
pub trait ForkChoiceStore<T: EthSpec>: Sized {
|
||||
type Error;
|
||||
|
||||
/// Returns the last value passed to `Self::update_time`.
|
||||
fn get_current_slot(&self) -> Slot;
|
||||
|
||||
/// Set the value to be returned by `Self::get_current_slot`.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// This should only ever be called from within `ForkChoice::on_tick`.
|
||||
fn set_current_slot(&mut self, slot: Slot);
|
||||
|
||||
/// Called whenever `ForkChoice::on_block` has verified a block, but not yet added it to fork
|
||||
/// choice. Allows the implementer to performing caching or other housekeeping duties.
|
||||
fn on_verified_block(
|
||||
&mut self,
|
||||
block: &BeaconBlock<T>,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<T>,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Returns the `justified_checkpoint`.
|
||||
fn justified_checkpoint(&self) -> &Checkpoint;
|
||||
|
||||
/// Returns balances from the `state` identified by `justified_checkpoint.root`.
|
||||
fn justified_balances(&self) -> &[u64];
|
||||
|
||||
/// Returns the `best_justified_checkpoint`.
|
||||
fn best_justified_checkpoint(&self) -> &Checkpoint;
|
||||
|
||||
/// Returns the `finalized_checkpoint`.
|
||||
fn finalized_checkpoint(&self) -> &Checkpoint;
|
||||
|
||||
/// Sets `finalized_checkpoint`.
|
||||
fn set_finalized_checkpoint(&mut self, checkpoint: Checkpoint);
|
||||
|
||||
/// Sets the `justified_checkpoint`.
|
||||
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Self::Error>;
|
||||
|
||||
/// Sets the `best_justified_checkpoint`.
|
||||
fn set_best_justified_checkpoint(&mut self, checkpoint: Checkpoint);
|
||||
}
|
||||
8
consensus/fork_choice/src/lib.rs
Normal file
8
consensus/fork_choice/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod fork_choice;
|
||||
mod fork_choice_store;
|
||||
|
||||
pub use crate::fork_choice::{
|
||||
Error, ForkChoice, InvalidAttestation, InvalidBlock, PersistedForkChoice, QueuedAttestation,
|
||||
SAFE_SLOTS_TO_UPDATE_JUSTIFIED,
|
||||
};
|
||||
pub use fork_choice_store::ForkChoiceStore;
|
||||
802
consensus/fork_choice/tests/tests.rs
Normal file
802
consensus/fork_choice/tests/tests.rs
Normal file
@@ -0,0 +1,802 @@
|
||||
#![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, Store};
|
||||
use types::{
|
||||
test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs},
|
||||
Epoch, EthSpec, IndexedAttestation, MainnetEthSpec, Slot,
|
||||
};
|
||||
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(),
|
||||
);
|
||||
|
||||
Self { harness }
|
||||
}
|
||||
|
||||
/// Get a value from the `ForkChoice` instantiation.
|
||||
fn get<T, U>(&self, func: T) -> U
|
||||
where
|
||||
T: Fn(&BeaconForkChoiceStore<MemoryStore<E>, 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 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)
|
||||
.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));
|
||||
}
|
||||
Reference in New Issue
Block a user