mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-07 08:52:54 +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:
@@ -40,15 +40,13 @@ futures = "0.3.5"
|
||||
genesis = { path = "../genesis" }
|
||||
integer-sqrt = "0.1.3"
|
||||
rand = "0.7.3"
|
||||
proto_array_fork_choice = { path = "../../consensus/proto_array_fork_choice" }
|
||||
proto_array = { path = "../../consensus/proto_array" }
|
||||
lru = "0.5.1"
|
||||
tempfile = "3.1.0"
|
||||
bitvec = "0.17.4"
|
||||
bls = { path = "../../crypto/bls" }
|
||||
safe_arith = { path = "../../consensus/safe_arith" }
|
||||
fork_choice = { path = "../../consensus/fork_choice" }
|
||||
environment = { path = "../../lighthouse/environment" }
|
||||
bus = "2.2.3"
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
//! -------------------------------------
|
||||
//! |
|
||||
//! ▼
|
||||
//! ForkChoiceVerifiedAttestation
|
||||
//! impl SignatureVerifiedAttestation
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
@@ -158,65 +158,21 @@ impl<T: BeaconChainTypes> Clone for VerifiedUnaggregatedAttestation<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps an `indexed_attestation` that is valid for application to fork choice. The
|
||||
/// `indexed_attestation` will have been generated via the `VerifiedAggregatedAttestation` or
|
||||
/// `VerifiedUnaggregatedAttestation` wrappers.
|
||||
pub struct ForkChoiceVerifiedAttestation<'a, T: BeaconChainTypes> {
|
||||
indexed_attestation: &'a IndexedAttestation<T::EthSpec>,
|
||||
}
|
||||
|
||||
/// A helper trait implemented on wrapper types that can be progressed to a state where they can be
|
||||
/// verified for application to fork choice.
|
||||
pub trait IntoForkChoiceVerifiedAttestation<'a, T: BeaconChainTypes> {
|
||||
fn into_fork_choice_verified_attestation(
|
||||
&'a self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<'a, T>, Error>;
|
||||
pub trait SignatureVerifiedAttestation<T: BeaconChainTypes> {
|
||||
fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec>;
|
||||
}
|
||||
|
||||
impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T>
|
||||
for VerifiedAggregatedAttestation<T>
|
||||
{
|
||||
/// Progresses the `VerifiedAggregatedAttestation` to a stage where it is valid for application
|
||||
/// to the fork-choice rule (or not).
|
||||
fn into_fork_choice_verified_attestation(
|
||||
&'a self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<T>, Error> {
|
||||
ForkChoiceVerifiedAttestation::from_signature_verified_components(
|
||||
&self.indexed_attestation,
|
||||
chain,
|
||||
)
|
||||
impl<'a, T: BeaconChainTypes> SignatureVerifiedAttestation<T> for VerifiedAggregatedAttestation<T> {
|
||||
fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec> {
|
||||
&self.indexed_attestation
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T>
|
||||
for VerifiedUnaggregatedAttestation<T>
|
||||
{
|
||||
/// Progresses the `Attestation` to a stage where it is valid for application to the
|
||||
/// fork-choice rule (or not).
|
||||
fn into_fork_choice_verified_attestation(
|
||||
&'a self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<T>, Error> {
|
||||
ForkChoiceVerifiedAttestation::from_signature_verified_components(
|
||||
&self.indexed_attestation,
|
||||
chain,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: BeaconChainTypes> IntoForkChoiceVerifiedAttestation<'a, T>
|
||||
for ForkChoiceVerifiedAttestation<'a, T>
|
||||
{
|
||||
/// Simply returns itself.
|
||||
fn into_fork_choice_verified_attestation(
|
||||
&'a self,
|
||||
_: &BeaconChain<T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<T>, Error> {
|
||||
Ok(Self {
|
||||
indexed_attestation: self.indexed_attestation,
|
||||
})
|
||||
impl<T: BeaconChainTypes> SignatureVerifiedAttestation<T> for VerifiedUnaggregatedAttestation<T> {
|
||||
fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec> {
|
||||
&self.indexed_attestation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,14 +300,6 @@ impl<T: BeaconChainTypes> VerifiedAggregatedAttestation<T> {
|
||||
chain.add_to_block_inclusion_pool(self)
|
||||
}
|
||||
|
||||
/// A helper function to add this aggregate to `beacon_chain.fork_choice`.
|
||||
pub fn add_to_fork_choice(
|
||||
&self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<T>, Error> {
|
||||
chain.apply_attestation_to_fork_choice(self)
|
||||
}
|
||||
|
||||
/// Returns the underlying `attestation` for the `signed_aggregate`.
|
||||
pub fn attestation(&self) -> &Attestation<T::EthSpec> {
|
||||
&self.signed_aggregate.message.aggregate
|
||||
@@ -449,114 +397,6 @@ impl<T: BeaconChainTypes> VerifiedUnaggregatedAttestation<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: BeaconChainTypes> ForkChoiceVerifiedAttestation<'a, T> {
|
||||
/// Returns `Ok(Self)` if the `attestation` is valid to be applied to the beacon chain fork
|
||||
/// choice.
|
||||
///
|
||||
/// The supplied `indexed_attestation` MUST have a valid signature, this function WILL NOT
|
||||
/// CHECK THE SIGNATURE. Use the `VerifiedAggregatedAttestation` or
|
||||
/// `VerifiedUnaggregatedAttestation` structs to do signature verification.
|
||||
fn from_signature_verified_components(
|
||||
indexed_attestation: &'a IndexedAttestation<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<Self, Error> {
|
||||
// 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(Error::EmptyAggregationBitfield);
|
||||
}
|
||||
|
||||
let slot_now = chain.slot()?;
|
||||
let epoch_now = slot_now.epoch(T::EthSpec::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(Error::FutureEpoch {
|
||||
attestation_epoch: target.epoch,
|
||||
current_epoch: epoch_now,
|
||||
});
|
||||
} else if target.epoch + 1 < epoch_now {
|
||||
return Err(Error::PastEpoch {
|
||||
attestation_epoch: target.epoch,
|
||||
current_epoch: epoch_now,
|
||||
});
|
||||
}
|
||||
|
||||
if target.epoch
|
||||
!= indexed_attestation
|
||||
.data
|
||||
.slot
|
||||
.epoch(T::EthSpec::slots_per_epoch())
|
||||
{
|
||||
return Err(Error::BadTargetEpoch);
|
||||
}
|
||||
|
||||
// Attestation target must be for a known block.
|
||||
if !chain.fork_choice.contains_block(&target.root) {
|
||||
return Err(Error::UnknownTargetRoot(target.root));
|
||||
}
|
||||
|
||||
// TODO: we're not testing an assert from the spec:
|
||||
//
|
||||
// `assert get_current_slot(store) >= compute_start_slot_at_epoch(target.epoch)`
|
||||
//
|
||||
// I think this check is redundant and I've raised an issue here:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-specs/pull/1755
|
||||
//
|
||||
// To resolve this todo, observe the outcome of the above PR.
|
||||
|
||||
// Load the slot and state root 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_slot, _state_root) = chain
|
||||
.fork_choice
|
||||
.block_slot_and_state_root(&indexed_attestation.data.beacon_block_root)
|
||||
.ok_or_else(|| Error::UnknownHeadBlock {
|
||||
beacon_block_root: indexed_attestation.data.beacon_block_root,
|
||||
})?;
|
||||
|
||||
// TODO: currently we do not check the FFG source/target. This is what the spec dictates
|
||||
// but it seems wrong.
|
||||
//
|
||||
// I have opened an issue on the specs repo for this:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-specs/issues/1636
|
||||
//
|
||||
// We should revisit this code once that issue has been resolved.
|
||||
|
||||
// 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(Error::AttestsToFutureBlock {
|
||||
block: block_slot,
|
||||
attestation: indexed_attestation.data.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// Note: we're not checking the "attestations can only affect the fork choice of subsequent
|
||||
// slots" part of the spec, we do this upstream.
|
||||
|
||||
Ok(Self {
|
||||
indexed_attestation,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the wrapped `IndexedAttestation`.
|
||||
pub fn indexed_attestation(&self) -> &IndexedAttestation<T::EthSpec> {
|
||||
&self.indexed_attestation
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain.
|
||||
///
|
||||
/// The block root may not be known for two reasons:
|
||||
@@ -573,6 +413,7 @@ fn verify_head_block_is_known<T: BeaconChainTypes>(
|
||||
) -> Result<(), Error> {
|
||||
if chain
|
||||
.fork_choice
|
||||
.read()
|
||||
.contains_block(&attestation.data.beacon_block_root)
|
||||
{
|
||||
Ok(())
|
||||
@@ -765,9 +606,10 @@ where
|
||||
// processing an attestation that does not include our latest finalized block in its chain.
|
||||
//
|
||||
// We do not delay consideration for later, we simply drop the attestation.
|
||||
let (target_block_slot, target_block_state_root) = chain
|
||||
let target_block = chain
|
||||
.fork_choice
|
||||
.block_slot_and_state_root(&target.root)
|
||||
.read()
|
||||
.get_block(&target.root)
|
||||
.ok_or_else(|| Error::UnknownTargetRoot(target.root))?;
|
||||
|
||||
// Obtain the shuffling cache, timing how long we wait.
|
||||
@@ -800,15 +642,15 @@ where
|
||||
chain.log,
|
||||
"Attestation processing cache miss";
|
||||
"attn_epoch" => attestation_epoch.as_u64(),
|
||||
"target_block_epoch" => target_block_slot.epoch(T::EthSpec::slots_per_epoch()).as_u64(),
|
||||
"target_block_epoch" => target_block.slot.epoch(T::EthSpec::slots_per_epoch()).as_u64(),
|
||||
);
|
||||
|
||||
let state_read_timer =
|
||||
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES);
|
||||
|
||||
let mut state = chain
|
||||
.get_state(&target_block_state_root, Some(target_block_slot))?
|
||||
.ok_or_else(|| BeaconChainError::MissingBeaconState(target_block_state_root))?;
|
||||
.get_state(&target_block.state_root, Some(target_block.slot))?
|
||||
.ok_or_else(|| BeaconChainError::MissingBeaconState(target_block.state_root))?;
|
||||
|
||||
metrics::stop_timer(state_read_timer);
|
||||
let state_skip_timer =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::attestation_verification::{
|
||||
Error as AttestationError, ForkChoiceVerifiedAttestation, IntoForkChoiceVerifiedAttestation,
|
||||
VerifiedAggregatedAttestation, VerifiedUnaggregatedAttestation,
|
||||
Error as AttestationError, SignatureVerifiedAttestation, VerifiedAggregatedAttestation,
|
||||
VerifiedUnaggregatedAttestation,
|
||||
};
|
||||
use crate::block_verification::{
|
||||
check_block_relevancy, get_block_root, signature_verify_chain_segment, BlockError,
|
||||
@@ -9,7 +9,6 @@ use crate::block_verification::{
|
||||
use crate::errors::{BeaconChainError as Error, BlockProductionError};
|
||||
use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend};
|
||||
use crate::events::{EventHandler, EventKind};
|
||||
use crate::fork_choice::{Error as ForkChoiceError, ForkChoice};
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::metrics;
|
||||
use crate::migrate::Migrate;
|
||||
@@ -18,17 +17,24 @@ use crate::observed_attestations::{Error as AttestationObservationError, Observe
|
||||
use crate::observed_attesters::{ObservedAggregators, ObservedAttesters};
|
||||
use crate::observed_block_producers::ObservedBlockProducers;
|
||||
use crate::persisted_beacon_chain::PersistedBeaconChain;
|
||||
use crate::persisted_fork_choice::PersistedForkChoice;
|
||||
use crate::shuffling_cache::ShufflingCache;
|
||||
use crate::snapshot_cache::SnapshotCache;
|
||||
use crate::timeout_rw_lock::TimeoutRwLock;
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
use crate::BeaconForkChoiceStore;
|
||||
use crate::BeaconSnapshot;
|
||||
use fork_choice::ForkChoice;
|
||||
use operation_pool::{OperationPool, PersistedOperationPool};
|
||||
use parking_lot::RwLock;
|
||||
use slog::{crit, debug, error, info, trace, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::per_block_processing::errors::{
|
||||
AttestationValidationError, AttesterSlashingValidationError, ExitValidationError,
|
||||
ProposerSlashingValidationError,
|
||||
use state_processing::{
|
||||
common::get_indexed_attestation,
|
||||
per_block_processing::errors::{
|
||||
AttestationValidationError, AttesterSlashingValidationError, ExitValidationError,
|
||||
ProposerSlashingValidationError,
|
||||
},
|
||||
};
|
||||
use state_processing::{per_block_processing, per_slot_processing, BlockSignatureStrategy};
|
||||
use std::borrow::Cow;
|
||||
@@ -42,6 +48,8 @@ use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterato
|
||||
use store::{Error as DBError, Store};
|
||||
use types::*;
|
||||
|
||||
pub type ForkChoiceError = fork_choice::Error<crate::ForkChoiceStoreError>;
|
||||
|
||||
// Text included in blocks.
|
||||
// Must be 32-bytes or panic.
|
||||
//
|
||||
@@ -193,7 +201,7 @@ pub struct BeaconChain<T: BeaconChainTypes> {
|
||||
pub genesis_validators_root: Hash256,
|
||||
/// A state-machine that is updated with information from the network and chooses a canonical
|
||||
/// head block.
|
||||
pub fork_choice: ForkChoice<T>,
|
||||
pub fork_choice: RwLock<ForkChoice<BeaconForkChoiceStore<T::Store, T::EthSpec>, T::EthSpec>>,
|
||||
/// A handler for events generated by the beacon chain.
|
||||
pub event_handler: T::EventHandler,
|
||||
/// Used to track the heads of the beacon chain.
|
||||
@@ -238,11 +246,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
let fork_choice_timer = metrics::start_timer(&metrics::PERSIST_FORK_CHOICE);
|
||||
|
||||
let fork_choice = self.fork_choice.read();
|
||||
|
||||
self.store.put_item(
|
||||
&Hash256::from_slice(&FORK_CHOICE_DB_KEY),
|
||||
&self.fork_choice.as_ssz_container(),
|
||||
&PersistedForkChoice {
|
||||
fork_choice: fork_choice.to_persisted(),
|
||||
fork_choice_store: fork_choice.fc_store().to_persisted(),
|
||||
},
|
||||
)?;
|
||||
|
||||
drop(fork_choice);
|
||||
|
||||
metrics::stop_timer(fork_choice_timer);
|
||||
let head_timer = metrics::start_timer(&metrics::PERSIST_HEAD);
|
||||
|
||||
@@ -261,21 +276,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// This operation is typically slow and causes a lot of allocations. It should be used
|
||||
/// sparingly.
|
||||
pub fn persist_op_pool(&self) -> Result<(), Error> {
|
||||
let timer = metrics::start_timer(&metrics::PERSIST_OP_POOL);
|
||||
let _timer = metrics::start_timer(&metrics::PERSIST_OP_POOL);
|
||||
|
||||
self.store.put_item(
|
||||
&Hash256::from_slice(&OP_POOL_DB_KEY),
|
||||
&PersistedOperationPool::from_operation_pool(&self.op_pool),
|
||||
)?;
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persists `self.eth1_chain` and its caches to disk.
|
||||
pub fn persist_eth1_cache(&self) -> Result<(), Error> {
|
||||
let timer = metrics::start_timer(&metrics::PERSIST_OP_POOL);
|
||||
let _timer = metrics::start_timer(&metrics::PERSIST_OP_POOL);
|
||||
|
||||
if let Some(eth1_chain) = self.eth1_chain.as_ref() {
|
||||
self.store.put_item(
|
||||
@@ -284,8 +297,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
)?;
|
||||
}
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -876,23 +887,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// Accepts some attestation-type object and attempts to verify it in the context of fork
|
||||
/// choice. If it is valid it is applied to `self.fork_choice`.
|
||||
///
|
||||
/// Common items that implement `IntoForkChoiceVerifiedAttestation`:
|
||||
/// Common items that implement `SignatureVerifiedAttestation`:
|
||||
///
|
||||
/// - `VerifiedUnaggregatedAttestation`
|
||||
/// - `VerifiedAggregatedAttestation`
|
||||
/// - `ForkChoiceVerifiedAttestation`
|
||||
pub fn apply_attestation_to_fork_choice<'a>(
|
||||
&self,
|
||||
unverified_attestation: &'a impl IntoForkChoiceVerifiedAttestation<'a, T>,
|
||||
) -> Result<ForkChoiceVerifiedAttestation<'a, T>, AttestationError> {
|
||||
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_APPLY_TO_FORK_CHOICE);
|
||||
verified: &'a impl SignatureVerifiedAttestation<T>,
|
||||
) -> Result<(), Error> {
|
||||
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_ATTESTATION_TIMES);
|
||||
|
||||
let verified = unverified_attestation.into_fork_choice_verified_attestation(self)?;
|
||||
let indexed_attestation = verified.indexed_attestation();
|
||||
self.fork_choice
|
||||
.process_indexed_attestation(indexed_attestation)
|
||||
.map_err(|e| Error::from(e))?;
|
||||
Ok(verified)
|
||||
.write()
|
||||
.on_attestation(self.slot()?, verified.indexed_attestation())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Accepts an `VerifiedUnaggregatedAttestation` and attempts to apply it to the "naive
|
||||
@@ -1028,8 +1036,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// pivot block is the same as the current state's pivot block. If it is, then the
|
||||
// attestation's shuffling is the same as the current state's.
|
||||
// To account for skipped slots, find the first block at *or before* the pivot slot.
|
||||
let fork_choice_lock = self.fork_choice.core_proto_array();
|
||||
let fork_choice_lock = self.fork_choice.read();
|
||||
let pivot_block_root = fork_choice_lock
|
||||
.proto_array()
|
||||
.core_proto_array()
|
||||
.iter_block_roots(block_root)
|
||||
.find(|(_, slot)| *slot <= pivot_slot)
|
||||
.map(|(block_root, _)| block_root);
|
||||
@@ -1325,7 +1335,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
unverified_block: B,
|
||||
) -> Result<Hash256, BlockError> {
|
||||
// Start the Prometheus timer.
|
||||
let full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES);
|
||||
let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES);
|
||||
|
||||
// Increment the Prometheus counter for block processing requests.
|
||||
metrics::inc_counter(&metrics::BLOCK_PROCESSING_REQUESTS);
|
||||
@@ -1393,9 +1403,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
};
|
||||
|
||||
// Stop the Prometheus timer.
|
||||
metrics::stop_timer(full_timer);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1414,6 +1421,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let state = fully_verified_block.state;
|
||||
let parent_block = fully_verified_block.parent_block;
|
||||
let intermediate_states = fully_verified_block.intermediate_states;
|
||||
let current_slot = self.slot()?;
|
||||
|
||||
let attestation_observation_timer =
|
||||
metrics::start_timer(&metrics::BLOCK_PROCESSING_ATTESTATION_OBSERVATION);
|
||||
@@ -1433,9 +1441,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
metrics::stop_timer(attestation_observation_timer);
|
||||
|
||||
let fork_choice_register_timer =
|
||||
metrics::start_timer(&metrics::BLOCK_PROCESSING_FORK_CHOICE_REGISTER);
|
||||
|
||||
// If there are new validators in this block, update our pubkey cache.
|
||||
//
|
||||
// We perform this _before_ adding the block to fork choice because the pubkey cache is
|
||||
@@ -1471,20 +1476,35 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
shuffling_cache.insert(state.current_epoch(), target_root, committee_cache);
|
||||
}
|
||||
|
||||
let mut fork_choice = self.fork_choice.write();
|
||||
|
||||
// Register the new block with the fork choice service.
|
||||
if let Err(e) = self
|
||||
.fork_choice
|
||||
.process_block(self, &state, block, block_root)
|
||||
{
|
||||
error!(
|
||||
self.log,
|
||||
"Add block to fork choice failed";
|
||||
"block_root" => format!("{}", block_root),
|
||||
"error" => format!("{:?}", e),
|
||||
)
|
||||
let _fork_choice_block_timer =
|
||||
metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_BLOCK_TIMES);
|
||||
fork_choice
|
||||
.on_block(current_slot, block, block_root, &state)
|
||||
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
|
||||
}
|
||||
|
||||
metrics::stop_timer(fork_choice_register_timer);
|
||||
// Register each attestation in the block with the fork choice service.
|
||||
for attestation in &block.body.attestations[..] {
|
||||
let _fork_choice_attestation_timer =
|
||||
metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_ATTESTATION_TIMES);
|
||||
|
||||
let committee =
|
||||
state.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
|
||||
let indexed_attestation = get_indexed_attestation(committee.committee, attestation)
|
||||
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
|
||||
|
||||
match fork_choice.on_attestation(current_slot, &indexed_attestation) {
|
||||
Ok(()) => Ok(()),
|
||||
// Ignore invalid attestations whilst importing attestations from a block. The
|
||||
// block might be very old and therefore the attestations useless to fork choice.
|
||||
Err(ForkChoiceError::InvalidAttestation(_)) => Ok(()),
|
||||
Err(e) => Err(BlockError::BeaconChainError(e.into())),
|
||||
}?;
|
||||
}
|
||||
|
||||
metrics::observe(
|
||||
&metrics::OPERATIONS_PER_BLOCK_ATTESTATION,
|
||||
@@ -1506,6 +1526,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
self.store.put_state(&block.state_root, &state)?;
|
||||
self.store.put_block(&block_root, signed_block.clone())?;
|
||||
|
||||
// The fork choice write-lock is dropped *after* the on-disk database has been updated.
|
||||
// This prevents inconsistency between the two at the expense of concurrency.
|
||||
drop(fork_choice);
|
||||
|
||||
let parent_root = block.parent_root;
|
||||
let slot = block.slot;
|
||||
|
||||
@@ -1674,7 +1698,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// Execute the fork choice algorithm and enthrone the result as the canonical head.
|
||||
pub fn fork_choice(&self) -> Result<(), Error> {
|
||||
metrics::inc_counter(&metrics::FORK_CHOICE_REQUESTS);
|
||||
let overall_timer = metrics::start_timer(&metrics::FORK_CHOICE_TIMES);
|
||||
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_TIMES);
|
||||
|
||||
let result = self.fork_choice_internal();
|
||||
|
||||
@@ -1682,14 +1706,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
metrics::inc_counter(&metrics::FORK_CHOICE_ERRORS);
|
||||
}
|
||||
|
||||
metrics::stop_timer(overall_timer);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn fork_choice_internal(&self) -> Result<(), Error> {
|
||||
// Determine the root of the block that is the head of the chain.
|
||||
let beacon_block_root = self.fork_choice.find_head(&self)?;
|
||||
let beacon_block_root = self.fork_choice.write().get_head(self.slot()?)?;
|
||||
|
||||
let current_head = self.head_info()?;
|
||||
let old_finalized_root = current_head.finalized_checkpoint.root;
|
||||
@@ -1869,7 +1891,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
new_epoch: new_finalized_epoch,
|
||||
})
|
||||
} else {
|
||||
self.fork_choice.prune()?;
|
||||
self.fork_choice.write().prune()?;
|
||||
|
||||
self.observed_block_producers
|
||||
.prune(new_finalized_epoch.start_slot(T::EthSpec::slots_per_epoch()));
|
||||
|
||||
334
beacon_node/beacon_chain/src/beacon_fork_choice_store.rs
Normal file
334
beacon_node/beacon_chain/src/beacon_fork_choice_store.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
//! Defines the `BeaconForkChoiceStore` which provides the persistent storage for the `ForkChoice`
|
||||
//! struct.
|
||||
//!
|
||||
//! Additionally, the private `BalancesCache` struct is defined; a cache designed to avoid database
|
||||
//! reads when fork choice requires the validator balances of the justified state.
|
||||
|
||||
use crate::{metrics, BeaconSnapshot};
|
||||
use fork_choice::ForkChoiceStore;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use store::{Error as StoreError, Store};
|
||||
use types::{
|
||||
BeaconBlock, BeaconState, BeaconStateError, Checkpoint, EthSpec, Hash256, SignedBeaconBlock,
|
||||
Slot,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
UnableToReadSlot,
|
||||
UnableToReadTime,
|
||||
InvalidGenesisSnapshot(Slot),
|
||||
AncestorUnknown { ancestor_slot: Slot },
|
||||
UninitializedBestJustifiedBalances,
|
||||
FailedToReadBlock(StoreError),
|
||||
MissingBlock(Hash256),
|
||||
FailedToReadState(StoreError),
|
||||
MissingState(Hash256),
|
||||
InvalidPersistedBytes(ssz::DecodeError),
|
||||
BeaconStateError(BeaconStateError),
|
||||
}
|
||||
|
||||
impl From<BeaconStateError> for Error {
|
||||
fn from(e: BeaconStateError) -> Self {
|
||||
Error::BeaconStateError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of validator balance sets that are cached within `BalancesCache`.
|
||||
const MAX_BALANCE_CACHE_SIZE: usize = 4;
|
||||
|
||||
/// Returns the effective balances for every validator in the given `state`.
|
||||
///
|
||||
/// Any validator who is not active in the epoch of the given `state` is assigned a balance of
|
||||
/// zero.
|
||||
pub fn get_effective_balances<T: EthSpec>(state: &BeaconState<T>) -> Vec<u64> {
|
||||
state
|
||||
.validators
|
||||
.iter()
|
||||
.map(|validator| {
|
||||
if validator.is_active_at(state.current_epoch()) {
|
||||
validator.effective_balance
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An item that is stored in the `BalancesCache`.
|
||||
#[derive(PartialEq, Clone, Debug, Encode, Decode)]
|
||||
struct CacheItem {
|
||||
/// The block root at which `self.balances` are valid.
|
||||
block_root: Hash256,
|
||||
/// The effective balances from a `BeaconState` validator registry.
|
||||
balances: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Provides a cache to avoid reading `BeaconState` from disk when updating the current justified
|
||||
/// checkpoint.
|
||||
///
|
||||
/// It is effectively a mapping of `epoch_boundary_block_root -> state.balances`.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Encode, Decode)]
|
||||
struct BalancesCache {
|
||||
items: Vec<CacheItem>,
|
||||
}
|
||||
|
||||
impl BalancesCache {
|
||||
/// Inspect the given `state` and determine the root of the block at the first slot of
|
||||
/// `state.current_epoch`. If there is not already some entry for the given block root, then
|
||||
/// add the effective balances from the `state` to the cache.
|
||||
pub fn process_state<E: EthSpec>(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<(), Error> {
|
||||
// We are only interested in balances from states that are at the start of an epoch,
|
||||
// because this is where the `current_justified_checkpoint.root` will point.
|
||||
if !Self::is_first_block_in_epoch(block_root, state)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let epoch_boundary_slot = state.current_epoch().start_slot(E::slots_per_epoch());
|
||||
let epoch_boundary_root = if epoch_boundary_slot == state.slot {
|
||||
block_root
|
||||
} else {
|
||||
// This call remains sensible as long as `state.block_roots` is larger than a single
|
||||
// epoch.
|
||||
*state.get_block_root(epoch_boundary_slot)?
|
||||
};
|
||||
|
||||
if self.position(epoch_boundary_root).is_none() {
|
||||
let item = CacheItem {
|
||||
block_root: epoch_boundary_root,
|
||||
balances: get_effective_balances(state),
|
||||
};
|
||||
|
||||
if self.items.len() == MAX_BALANCE_CACHE_SIZE {
|
||||
self.items.remove(0);
|
||||
}
|
||||
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the given `block_root` is the first/only block to have been processed in
|
||||
/// the epoch of the given `state`.
|
||||
///
|
||||
/// We can determine if it is the first block by looking back through `state.block_roots` to
|
||||
/// see if there is a block in the current epoch with a different root.
|
||||
fn is_first_block_in_epoch<E: EthSpec>(
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut prior_block_found = false;
|
||||
|
||||
for slot in state.current_epoch().slot_iter(E::slots_per_epoch()) {
|
||||
if slot < state.slot {
|
||||
if *state.get_block_root(slot)? != block_root {
|
||||
prior_block_found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(!prior_block_found)
|
||||
}
|
||||
|
||||
fn position(&self, block_root: Hash256) -> Option<usize> {
|
||||
self.items
|
||||
.iter()
|
||||
.position(|item| item.block_root == block_root)
|
||||
}
|
||||
|
||||
/// Get the balances for the given `block_root`, if any.
|
||||
///
|
||||
/// If some balances are found, they are removed from the cache.
|
||||
pub fn get(&mut self, block_root: Hash256) -> Option<Vec<u64>> {
|
||||
let i = self.position(block_root)?;
|
||||
Some(self.items.remove(i).balances)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `fork_choice::ForkChoiceStore` in order to provide a persistent backing to the
|
||||
/// `fork_choice::ForkChoice` struct.
|
||||
#[derive(Debug)]
|
||||
pub struct BeaconForkChoiceStore<S, E> {
|
||||
store: Arc<S>,
|
||||
balances_cache: BalancesCache,
|
||||
time: Slot,
|
||||
finalized_checkpoint: Checkpoint,
|
||||
justified_checkpoint: Checkpoint,
|
||||
justified_balances: Vec<u64>,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<S, E> PartialEq for BeaconForkChoiceStore<S, E> {
|
||||
/// This implementation ignores the `store` and `slot_clock`.
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.balances_cache == other.balances_cache
|
||||
&& self.time == other.time
|
||||
&& self.finalized_checkpoint == other.finalized_checkpoint
|
||||
&& self.justified_checkpoint == other.justified_checkpoint
|
||||
&& self.justified_balances == other.justified_balances
|
||||
&& self.best_justified_checkpoint == other.best_justified_checkpoint
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Store<E>, E: EthSpec> BeaconForkChoiceStore<S, E> {
|
||||
/// Initialize `Self` from some `anchor` checkpoint which may or may not be the genesis state.
|
||||
///
|
||||
/// ## Specification
|
||||
///
|
||||
/// Equivalent to:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_forkchoice_store
|
||||
///
|
||||
/// ## Notes:
|
||||
///
|
||||
/// It is assumed that `anchor` is already persisted in `store`.
|
||||
pub fn get_forkchoice_store(store: Arc<S>, anchor: &BeaconSnapshot<E>) -> Self {
|
||||
let anchor_state = &anchor.beacon_state;
|
||||
let mut anchor_block_header = anchor_state.latest_block_header.clone();
|
||||
if anchor_block_header.state_root == Hash256::zero() {
|
||||
anchor_block_header.state_root = anchor.beacon_state_root;
|
||||
}
|
||||
let anchor_root = anchor_block_header.canonical_root();
|
||||
let anchor_epoch = anchor_state.current_epoch();
|
||||
let justified_checkpoint = Checkpoint {
|
||||
epoch: anchor_epoch,
|
||||
root: anchor_root,
|
||||
};
|
||||
let finalized_checkpoint = justified_checkpoint;
|
||||
|
||||
Self {
|
||||
store,
|
||||
balances_cache: <_>::default(),
|
||||
time: anchor_state.slot,
|
||||
justified_checkpoint,
|
||||
justified_balances: anchor_state.balances.clone().into(),
|
||||
finalized_checkpoint,
|
||||
best_justified_checkpoint: justified_checkpoint,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the current state of `Self` to a `PersistedForkChoiceStore` which can be stored to the
|
||||
/// on-disk database.
|
||||
pub fn to_persisted(&self) -> PersistedForkChoiceStore {
|
||||
PersistedForkChoiceStore {
|
||||
balances_cache: self.balances_cache.clone(),
|
||||
time: self.time,
|
||||
finalized_checkpoint: self.finalized_checkpoint,
|
||||
justified_checkpoint: self.justified_checkpoint,
|
||||
justified_balances: self.justified_balances.clone(),
|
||||
best_justified_checkpoint: self.best_justified_checkpoint,
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore `Self` from a previously-generated `PersistedForkChoiceStore`.
|
||||
pub fn from_persisted(
|
||||
persisted: PersistedForkChoiceStore,
|
||||
store: Arc<S>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
store,
|
||||
balances_cache: persisted.balances_cache,
|
||||
time: persisted.time,
|
||||
finalized_checkpoint: persisted.finalized_checkpoint,
|
||||
justified_checkpoint: persisted.justified_checkpoint,
|
||||
justified_balances: persisted.justified_balances,
|
||||
best_justified_checkpoint: persisted.best_justified_checkpoint,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Store<E>, E: EthSpec> ForkChoiceStore<E> for BeaconForkChoiceStore<S, E> {
|
||||
type Error = Error;
|
||||
|
||||
fn get_current_slot(&self) -> Slot {
|
||||
self.time
|
||||
}
|
||||
|
||||
fn set_current_slot(&mut self, slot: Slot) {
|
||||
self.time = slot
|
||||
}
|
||||
|
||||
fn on_verified_block(
|
||||
&mut self,
|
||||
_block: &BeaconBlock<E>,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.balances_cache.process_state(block_root, state)
|
||||
}
|
||||
|
||||
fn justified_checkpoint(&self) -> &Checkpoint {
|
||||
&self.justified_checkpoint
|
||||
}
|
||||
|
||||
fn justified_balances(&self) -> &[u64] {
|
||||
&self.justified_balances
|
||||
}
|
||||
|
||||
fn best_justified_checkpoint(&self) -> &Checkpoint {
|
||||
&self.best_justified_checkpoint
|
||||
}
|
||||
|
||||
fn finalized_checkpoint(&self) -> &Checkpoint {
|
||||
&self.finalized_checkpoint
|
||||
}
|
||||
|
||||
fn set_finalized_checkpoint(&mut self, checkpoint: Checkpoint) {
|
||||
self.finalized_checkpoint = checkpoint
|
||||
}
|
||||
|
||||
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> {
|
||||
self.justified_checkpoint = checkpoint;
|
||||
|
||||
if let Some(balances) = self.balances_cache.get(self.justified_checkpoint.root) {
|
||||
metrics::inc_counter(&metrics::BALANCES_CACHE_HITS);
|
||||
self.justified_balances = balances;
|
||||
} else {
|
||||
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
|
||||
let justified_block = self
|
||||
.store
|
||||
.get_item::<SignedBeaconBlock<E>>(&self.justified_checkpoint.root)
|
||||
.map_err(Error::FailedToReadBlock)?
|
||||
.ok_or_else(|| Error::MissingBlock(self.justified_checkpoint.root))?
|
||||
.message;
|
||||
|
||||
self.justified_balances = self
|
||||
.store
|
||||
.get_state(&justified_block.state_root, Some(justified_block.slot))
|
||||
.map_err(Error::FailedToReadState)?
|
||||
.ok_or_else(|| Error::MissingState(justified_block.state_root))?
|
||||
.balances
|
||||
.into();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_best_justified_checkpoint(&mut self, checkpoint: Checkpoint) {
|
||||
self.best_justified_checkpoint = checkpoint
|
||||
}
|
||||
}
|
||||
|
||||
/// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database.
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct PersistedForkChoiceStore {
|
||||
balances_cache: BalancesCache,
|
||||
time: Slot,
|
||||
finalized_checkpoint: Checkpoint,
|
||||
justified_checkpoint: Checkpoint,
|
||||
justified_balances: Vec<u64>,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
}
|
||||
@@ -530,7 +530,11 @@ impl<T: BeaconChainTypes> FullyVerifiedBlock<T> {
|
||||
// because it will revert finalization. Note that the finalized block is stored in fork
|
||||
// choice, so we will not reject any child of the finalized block (this is relevant during
|
||||
// genesis).
|
||||
if !chain.fork_choice.contains_block(&block.parent_root()) {
|
||||
if !chain
|
||||
.fork_choice
|
||||
.read()
|
||||
.contains_block(&block.parent_root())
|
||||
{
|
||||
return Err(BlockError::ParentUnknown(block.parent_root()));
|
||||
}
|
||||
|
||||
@@ -727,7 +731,7 @@ pub fn check_block_relevancy<T: BeaconChainTypes>(
|
||||
|
||||
// Check if the block is already known. We know it is post-finalization, so it is
|
||||
// sufficient to check the fork choice.
|
||||
if chain.fork_choice.contains_block(&block_root) {
|
||||
if chain.fork_choice.read().contains_block(&block_root) {
|
||||
return Err(BlockError::BlockIsAlreadyKnown);
|
||||
}
|
||||
|
||||
@@ -767,7 +771,7 @@ fn load_parent<T: BeaconChainTypes>(
|
||||
// because it will revert finalization. Note that the finalized block is stored in fork
|
||||
// choice, so we will not reject any child of the finalized block (this is relevant during
|
||||
// genesis).
|
||||
if !chain.fork_choice.contains_block(&block.parent_root) {
|
||||
if !chain.fork_choice.read().contains_block(&block.parent_root) {
|
||||
return Err(BlockError::ParentUnknown(block.parent_root));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,22 @@ use crate::beacon_chain::{
|
||||
};
|
||||
use crate::eth1_chain::{CachingEth1Backend, SszEth1};
|
||||
use crate::events::NullEventHandler;
|
||||
use crate::fork_choice::SszForkChoice;
|
||||
use crate::head_tracker::HeadTracker;
|
||||
use crate::migrate::Migrate;
|
||||
use crate::persisted_beacon_chain::PersistedBeaconChain;
|
||||
use crate::persisted_fork_choice::PersistedForkChoice;
|
||||
use crate::shuffling_cache::ShufflingCache;
|
||||
use crate::snapshot_cache::{SnapshotCache, DEFAULT_SNAPSHOT_CACHE_SIZE};
|
||||
use crate::timeout_rw_lock::TimeoutRwLock;
|
||||
use crate::validator_pubkey_cache::ValidatorPubkeyCache;
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainTypes, BeaconSnapshot, Eth1Chain, Eth1ChainBackend, EventHandler,
|
||||
ForkChoice,
|
||||
BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, BeaconSnapshot, Eth1Chain,
|
||||
Eth1ChainBackend, EventHandler,
|
||||
};
|
||||
use eth1::Config as Eth1Config;
|
||||
use fork_choice::ForkChoice;
|
||||
use operation_pool::{OperationPool, PersistedOperationPool};
|
||||
use proto_array_fork_choice::ProtoArrayForkChoice;
|
||||
use parking_lot::RwLock;
|
||||
use slog::{info, Logger};
|
||||
use slot_clock::{SlotClock, TestingSlotClock};
|
||||
use std::marker::PhantomData;
|
||||
@@ -79,7 +80,6 @@ pub struct BeaconChainBuilder<T: BeaconChainTypes> {
|
||||
pub finalized_snapshot: Option<BeaconSnapshot<T::EthSpec>>,
|
||||
genesis_block_root: Option<Hash256>,
|
||||
op_pool: Option<OperationPool<T::EthSpec>>,
|
||||
fork_choice: Option<ForkChoice<T>>,
|
||||
eth1_chain: Option<Eth1Chain<T::Eth1Chain, T::EthSpec, T::Store>>,
|
||||
event_handler: Option<T::EventHandler>,
|
||||
slot_clock: Option<T::SlotClock>,
|
||||
@@ -116,7 +116,6 @@ where
|
||||
finalized_snapshot: None,
|
||||
genesis_block_root: None,
|
||||
op_pool: None,
|
||||
fork_choice: None,
|
||||
eth1_chain: None,
|
||||
event_handler: None,
|
||||
slot_clock: None,
|
||||
@@ -386,6 +385,13 @@ where
|
||||
let log = self
|
||||
.log
|
||||
.ok_or_else(|| "Cannot build without a logger".to_string())?;
|
||||
let slot_clock = self
|
||||
.slot_clock
|
||||
.ok_or_else(|| "Cannot build without a slot_clock.".to_string())?;
|
||||
let store = self
|
||||
.store
|
||||
.clone()
|
||||
.ok_or_else(|| "Cannot build without a store.".to_string())?;
|
||||
|
||||
// If this beacon chain is being loaded from disk, use the stored head. Otherwise, just use
|
||||
// the finalized checkpoint (which is probably genesis).
|
||||
@@ -417,17 +423,33 @@ where
|
||||
.map_err(|e| format!("Unable to init validator pubkey cache: {:?}", e))
|
||||
})?;
|
||||
|
||||
let persisted_fork_choice = store
|
||||
.get_item::<PersistedForkChoice>(&Hash256::from_slice(&FORK_CHOICE_DB_KEY))
|
||||
.map_err(|e| format!("DB error when reading persisted fork choice: {:?}", e))?;
|
||||
|
||||
let fork_choice = if let Some(persisted) = persisted_fork_choice {
|
||||
let fc_store =
|
||||
BeaconForkChoiceStore::from_persisted(persisted.fork_choice_store, store.clone())
|
||||
.map_err(|e| format!("Unable to load ForkChoiceStore: {:?}", e))?;
|
||||
|
||||
ForkChoice::from_persisted(persisted.fork_choice, fc_store)
|
||||
.map_err(|e| format!("Unable to parse persisted fork choice from disk: {:?}", e))?
|
||||
} else {
|
||||
let genesis = &canonical_head;
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), genesis);
|
||||
|
||||
ForkChoice::from_genesis(fc_store, &genesis.beacon_block.message)
|
||||
.map_err(|e| format!("Unable to build initialize ForkChoice: {:?}", e))?
|
||||
};
|
||||
|
||||
let beacon_chain = BeaconChain {
|
||||
spec: self.spec,
|
||||
store: self
|
||||
.store
|
||||
.ok_or_else(|| "Cannot build without store".to_string())?,
|
||||
store,
|
||||
store_migrator: self
|
||||
.store_migrator
|
||||
.ok_or_else(|| "Cannot build without store migrator".to_string())?,
|
||||
slot_clock: self
|
||||
.slot_clock
|
||||
.ok_or_else(|| "Cannot build without slot clock".to_string())?,
|
||||
slot_clock,
|
||||
op_pool: self
|
||||
.op_pool
|
||||
.ok_or_else(|| "Cannot build without op pool".to_string())?,
|
||||
@@ -447,9 +469,7 @@ where
|
||||
genesis_block_root: self
|
||||
.genesis_block_root
|
||||
.ok_or_else(|| "Cannot build without a genesis block root".to_string())?,
|
||||
fork_choice: self
|
||||
.fork_choice
|
||||
.ok_or_else(|| "Cannot build without a fork choice".to_string())?,
|
||||
fork_choice: RwLock::new(fork_choice),
|
||||
event_handler: self
|
||||
.event_handler
|
||||
.ok_or_else(|| "Cannot build without an event handler".to_string())?,
|
||||
@@ -480,69 +500,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<TStore, TStoreMigrator, TSlotClock, TEth1Backend, TEthSpec, TEventHandler>
|
||||
BeaconChainBuilder<
|
||||
Witness<TStore, TStoreMigrator, TSlotClock, TEth1Backend, TEthSpec, TEventHandler>,
|
||||
>
|
||||
where
|
||||
TStore: Store<TEthSpec> + 'static,
|
||||
TStoreMigrator: Migrate<TEthSpec> + 'static,
|
||||
TSlotClock: SlotClock + 'static,
|
||||
TEth1Backend: Eth1ChainBackend<TEthSpec, TStore> + 'static,
|
||||
TEthSpec: EthSpec + 'static,
|
||||
TEventHandler: EventHandler<TEthSpec> + 'static,
|
||||
{
|
||||
/// Initializes a fork choice with the `ThreadSafeReducedTree` backend.
|
||||
///
|
||||
/// If this builder is being "resumed" from disk, then rebuild the last fork choice stored to
|
||||
/// the database. Otherwise, create a new, empty fork choice.
|
||||
pub fn reduced_tree_fork_choice(mut self) -> Result<Self, String> {
|
||||
let store = self
|
||||
.store
|
||||
.clone()
|
||||
.ok_or_else(|| "reduced_tree_fork_choice requires a store.".to_string())?;
|
||||
|
||||
let persisted_fork_choice = store
|
||||
.get_item::<SszForkChoice>(&Hash256::from_slice(&FORK_CHOICE_DB_KEY))
|
||||
.map_err(|e| format!("DB error when reading persisted fork choice: {:?}", e))?;
|
||||
|
||||
let fork_choice = if let Some(persisted) = persisted_fork_choice {
|
||||
ForkChoice::from_ssz_container(persisted)
|
||||
.map_err(|e| format!("Unable to read persisted fork choice from disk: {:?}", e))?
|
||||
} else {
|
||||
let finalized_snapshot = &self
|
||||
.finalized_snapshot
|
||||
.as_ref()
|
||||
.ok_or_else(|| "reduced_tree_fork_choice requires a finalized_snapshot")?;
|
||||
let genesis_block_root = self
|
||||
.genesis_block_root
|
||||
.ok_or_else(|| "reduced_tree_fork_choice requires a genesis_block_root")?;
|
||||
|
||||
let backend = ProtoArrayForkChoice::new(
|
||||
finalized_snapshot.beacon_block.message.slot,
|
||||
finalized_snapshot.beacon_block.message.state_root,
|
||||
// Note: here we set the `justified_epoch` to be the same as the epoch of the
|
||||
// finalized checkpoint. Whilst this finalized checkpoint may actually point to
|
||||
// a _later_ justified checkpoint, that checkpoint won't yet exist in the fork
|
||||
// choice.
|
||||
finalized_snapshot.beacon_state.current_epoch(),
|
||||
finalized_snapshot.beacon_state.current_epoch(),
|
||||
finalized_snapshot.beacon_block_root,
|
||||
)?;
|
||||
|
||||
ForkChoice::new(
|
||||
backend,
|
||||
genesis_block_root,
|
||||
&finalized_snapshot.beacon_state,
|
||||
)
|
||||
};
|
||||
|
||||
self.fork_choice = Some(fork_choice);
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<TStore, TStoreMigrator, TSlotClock, TEthSpec, TEventHandler>
|
||||
BeaconChainBuilder<
|
||||
Witness<
|
||||
@@ -710,8 +667,6 @@ mod test {
|
||||
.null_event_handler()
|
||||
.testing_slot_clock(Duration::from_secs(1))
|
||||
.expect("should configure testing slot clock")
|
||||
.reduced_tree_fork_choice()
|
||||
.expect("should add fork choice to builder")
|
||||
.build()
|
||||
.expect("should build");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::beacon_chain::ForkChoiceError;
|
||||
use crate::eth1_chain::Error as Eth1ChainError;
|
||||
use crate::fork_choice::Error as ForkChoiceError;
|
||||
use crate::naive_aggregation_pool::Error as NaiveAggregationError;
|
||||
use crate::observed_attestations::Error as ObservedAttestationsError;
|
||||
use crate::observed_attesters::Error as ObservedAttestersError;
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
mod checkpoint_manager;
|
||||
|
||||
use crate::{errors::BeaconChainError, metrics, BeaconChain, BeaconChainTypes};
|
||||
use checkpoint_manager::{get_effective_balances, CheckpointManager, CheckpointWithBalances};
|
||||
use parking_lot::{RwLock, RwLockReadGuard};
|
||||
use proto_array_fork_choice::{core::ProtoArray, ProtoArrayForkChoice};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use state_processing::common::get_indexed_attestation;
|
||||
use std::marker::PhantomData;
|
||||
use store::{DBColumn, Error as StoreError, StoreItem};
|
||||
use types::{BeaconBlock, BeaconState, BeaconStateError, Epoch, Hash256, IndexedAttestation, Slot};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
MissingBlock(Hash256),
|
||||
MissingState(Hash256),
|
||||
BackendError(String),
|
||||
BeaconStateError(BeaconStateError),
|
||||
StoreError(StoreError),
|
||||
BeaconChainError(Box<BeaconChainError>),
|
||||
UnknownBlockSlot(Hash256),
|
||||
UnknownJustifiedBlock(Hash256),
|
||||
UnknownJustifiedState(Hash256),
|
||||
UnableToJsonEncode(String),
|
||||
InvalidAttestation,
|
||||
}
|
||||
|
||||
pub struct ForkChoice<T: BeaconChainTypes> {
|
||||
backend: ProtoArrayForkChoice,
|
||||
/// Used for resolving the `0x00..00` alias back to genesis.
|
||||
///
|
||||
/// Does not necessarily need to be the _actual_ genesis, it suffices to be the finalized root
|
||||
/// whenever the struct was instantiated.
|
||||
genesis_block_root: Hash256,
|
||||
checkpoint_manager: RwLock<CheckpointManager>,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> PartialEq for ForkChoice<T> {
|
||||
/// This implementation ignores the `store`.
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.backend == other.backend
|
||||
&& self.genesis_block_root == other.genesis_block_root
|
||||
&& *self.checkpoint_manager.read() == *other.checkpoint_manager.read()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> ForkChoice<T> {
|
||||
/// Instantiate a new fork chooser.
|
||||
///
|
||||
/// "Genesis" does not necessarily need to be the absolute genesis, it can be some finalized
|
||||
/// block.
|
||||
pub fn new(
|
||||
backend: ProtoArrayForkChoice,
|
||||
genesis_block_root: Hash256,
|
||||
genesis_state: &BeaconState<T::EthSpec>,
|
||||
) -> Self {
|
||||
let genesis_checkpoint = CheckpointWithBalances {
|
||||
epoch: genesis_state.current_epoch(),
|
||||
root: genesis_block_root,
|
||||
balances: get_effective_balances(genesis_state),
|
||||
};
|
||||
|
||||
Self {
|
||||
backend,
|
||||
genesis_block_root,
|
||||
checkpoint_manager: RwLock::new(CheckpointManager::new(genesis_checkpoint)),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the fork choice rule to determine the head.
|
||||
pub fn find_head(&self, chain: &BeaconChain<T>) -> Result<Hash256> {
|
||||
let timer = metrics::start_timer(&metrics::FORK_CHOICE_FIND_HEAD_TIMES);
|
||||
|
||||
let remove_alias = |root| {
|
||||
if root == Hash256::zero() {
|
||||
self.genesis_block_root
|
||||
} else {
|
||||
root
|
||||
}
|
||||
};
|
||||
|
||||
let mut manager = self.checkpoint_manager.write();
|
||||
manager.maybe_update(chain.slot()?, chain)?;
|
||||
|
||||
let result = self
|
||||
.backend
|
||||
.find_head(
|
||||
manager.current.justified.epoch,
|
||||
remove_alias(manager.current.justified.root),
|
||||
manager.current.finalized.epoch,
|
||||
&manager.current.justified.balances,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns true if the given block is known to fork choice.
|
||||
pub fn contains_block(&self, block_root: &Hash256) -> bool {
|
||||
self.backend.contains_block(block_root)
|
||||
}
|
||||
|
||||
/// Returns the state root for the given block root.
|
||||
pub fn block_slot_and_state_root(&self, block_root: &Hash256) -> Option<(Slot, Hash256)> {
|
||||
self.backend.block_slot_and_state_root(block_root)
|
||||
}
|
||||
|
||||
/// Process all attestations in the given `block`.
|
||||
///
|
||||
/// Assumes the block (and therefore its attestations) are valid. It is a logic error to
|
||||
/// provide an invalid block.
|
||||
pub fn process_block(
|
||||
&self,
|
||||
chain: &BeaconChain<T>,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
block: &BeaconBlock<T::EthSpec>,
|
||||
block_root: Hash256,
|
||||
) -> Result<()> {
|
||||
let timer = metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_BLOCK_TIMES);
|
||||
|
||||
self.checkpoint_manager
|
||||
.write()
|
||||
.process_state(block_root, state, chain, &self.backend)?;
|
||||
self.checkpoint_manager
|
||||
.write()
|
||||
.maybe_update(chain.slot()?, chain)?;
|
||||
|
||||
// Note: we never count the block as a latest message, only attestations.
|
||||
for attestation in &block.body.attestations {
|
||||
// If the `data.beacon_block_root` block is not known to the fork choice, simply ignore
|
||||
// the vote.
|
||||
if self
|
||||
.backend
|
||||
.contains_block(&attestation.data.beacon_block_root)
|
||||
{
|
||||
let committee =
|
||||
state.get_beacon_committee(attestation.data.slot, attestation.data.index)?;
|
||||
let indexed_attestation =
|
||||
get_indexed_attestation(committee.committee, &attestation)
|
||||
.map_err(|_| Error::InvalidAttestation)?;
|
||||
self.process_indexed_attestation(&indexed_attestation)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.backend.process_block(
|
||||
block.slot,
|
||||
block_root,
|
||||
block.parent_root,
|
||||
block.state_root,
|
||||
state.current_justified_checkpoint.epoch,
|
||||
state.finalized_checkpoint.epoch,
|
||||
)?;
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process an attestation which references `block` in `attestation.data.beacon_block_root`.
|
||||
///
|
||||
/// Assumes the attestation is valid.
|
||||
pub fn process_indexed_attestation(
|
||||
&self,
|
||||
attestation: &IndexedAttestation<T::EthSpec>,
|
||||
) -> Result<()> {
|
||||
let timer = metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_ATTESTATION_TIMES);
|
||||
|
||||
let block_hash = attestation.data.beacon_block_root;
|
||||
|
||||
// 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 zero hash.
|
||||
// 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.
|
||||
//
|
||||
// Additionally, don't add any block hash to fork choice unless we have imported the block.
|
||||
if block_hash != Hash256::zero() {
|
||||
for validator_index in attestation.attesting_indices.iter() {
|
||||
self.backend.process_attestation(
|
||||
*validator_index as usize,
|
||||
block_hash,
|
||||
attestation.data.target.epoch,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
metrics::stop_timer(timer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the latest message for a given validator, if any.
|
||||
///
|
||||
/// Returns `(block_root, block_slot)`.
|
||||
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> {
|
||||
self.backend.latest_message(validator_index)
|
||||
}
|
||||
|
||||
/// Trigger a prune on the underlying fork choice backend.
|
||||
pub fn prune(&self) -> Result<()> {
|
||||
let finalized_root = self.checkpoint_manager.read().current.finalized.root;
|
||||
|
||||
self.backend.maybe_prune(finalized_root).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns a read-lock to the core `ProtoArray` struct.
|
||||
///
|
||||
/// Should only be used when encoding/decoding during troubleshooting.
|
||||
pub fn core_proto_array(&self) -> RwLockReadGuard<ProtoArray> {
|
||||
self.backend.core_proto_array()
|
||||
}
|
||||
|
||||
/// Returns a `SszForkChoice` which contains the current state of `Self`.
|
||||
pub fn as_ssz_container(&self) -> SszForkChoice {
|
||||
SszForkChoice {
|
||||
genesis_block_root: self.genesis_block_root.clone(),
|
||||
checkpoint_manager: self.checkpoint_manager.read().clone(),
|
||||
backend_bytes: self.backend.as_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiates `Self` from a prior `SszForkChoice`.
|
||||
///
|
||||
/// The created `Self` will have the same state as the `Self` that created the `SszForkChoice`.
|
||||
pub fn from_ssz_container(ssz_container: SszForkChoice) -> Result<Self> {
|
||||
let backend = ProtoArrayForkChoice::from_bytes(&ssz_container.backend_bytes)?;
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
genesis_block_root: ssz_container.genesis_block_root,
|
||||
checkpoint_manager: RwLock::new(ssz_container.checkpoint_manager),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 `BeaconChain` to disk.
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct SszForkChoice {
|
||||
genesis_block_root: Hash256,
|
||||
checkpoint_manager: CheckpointManager,
|
||||
backend_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<BeaconStateError> for Error {
|
||||
fn from(e: BeaconStateError) -> Error {
|
||||
Error::BeaconStateError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BeaconChainError> for Error {
|
||||
fn from(e: BeaconChainError) -> Error {
|
||||
Error::BeaconChainError(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreError> for Error {
|
||||
fn from(e: StoreError) -> Error {
|
||||
Error::StoreError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(e: String) -> Error {
|
||||
Error::BackendError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreItem for SszForkChoice {
|
||||
fn db_column() -> DBColumn {
|
||||
DBColumn::ForkChoice
|
||||
}
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
self.as_ssz_bytes()
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> std::result::Result<Self, StoreError> {
|
||||
Self::from_ssz_bytes(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
use super::Error;
|
||||
use crate::{metrics, BeaconChain, BeaconChainTypes};
|
||||
use proto_array_fork_choice::ProtoArrayForkChoice;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use types::{BeaconState, Checkpoint, Epoch, EthSpec, Hash256, Slot};
|
||||
|
||||
const MAX_BALANCE_CACHE_SIZE: usize = 4;
|
||||
|
||||
/// An item that is stored in the `BalancesCache`.
|
||||
#[derive(PartialEq, Clone, Encode, Decode)]
|
||||
struct CacheItem {
|
||||
/// The block root at which `self.balances` are valid.
|
||||
block_root: Hash256,
|
||||
/// The `state.balances` list.
|
||||
balances: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Provides a cache to avoid reading `BeaconState` from disk when updating the current justified
|
||||
/// checkpoint.
|
||||
///
|
||||
/// It should store a mapping of `epoch_boundary_block_root -> state.balances`.
|
||||
#[derive(PartialEq, Clone, Default, Encode, Decode)]
|
||||
struct BalancesCache {
|
||||
items: Vec<CacheItem>,
|
||||
}
|
||||
|
||||
impl BalancesCache {
|
||||
/// Inspect the given `state` and determine the root of the block at the first slot of
|
||||
/// `state.current_epoch`. If there is not already some entry for the given block root, then
|
||||
/// add `state.balances` to the cache.
|
||||
pub fn process_state<E: EthSpec>(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<(), Error> {
|
||||
// We are only interested in balances from states that are at the start of an epoch,
|
||||
// because this is where the `current_justified_checkpoint.root` will point.
|
||||
if !Self::is_first_block_in_epoch(block_root, state)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let epoch_boundary_slot = state.current_epoch().start_slot(E::slots_per_epoch());
|
||||
let epoch_boundary_root = if epoch_boundary_slot == state.slot {
|
||||
block_root
|
||||
} else {
|
||||
// This call remains sensible as long as `state.block_roots` is larger than a single
|
||||
// epoch.
|
||||
*state.get_block_root(epoch_boundary_slot)?
|
||||
};
|
||||
|
||||
if self.position(epoch_boundary_root).is_none() {
|
||||
let item = CacheItem {
|
||||
block_root: epoch_boundary_root,
|
||||
balances: get_effective_balances(state),
|
||||
};
|
||||
|
||||
if self.items.len() == MAX_BALANCE_CACHE_SIZE {
|
||||
self.items.remove(0);
|
||||
}
|
||||
|
||||
self.items.push(item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the given `block_root` is the first/only block to have been processed in
|
||||
/// the epoch of the given `state`.
|
||||
///
|
||||
/// We can determine if it is the first block by looking back through `state.block_roots` to
|
||||
/// see if there is a block in the current epoch with a different root.
|
||||
fn is_first_block_in_epoch<E: EthSpec>(
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut prior_block_found = false;
|
||||
|
||||
for slot in state.current_epoch().slot_iter(E::slots_per_epoch()) {
|
||||
if slot < state.slot {
|
||||
if *state.get_block_root(slot)? != block_root {
|
||||
prior_block_found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(!prior_block_found)
|
||||
}
|
||||
|
||||
fn position(&self, block_root: Hash256) -> Option<usize> {
|
||||
self.items
|
||||
.iter()
|
||||
.position(|item| item.block_root == block_root)
|
||||
}
|
||||
|
||||
/// Get the balances for the given `block_root`, if any.
|
||||
///
|
||||
/// If some balances are found, they are removed from the cache.
|
||||
pub fn get(&mut self, block_root: Hash256) -> Option<Vec<u64>> {
|
||||
let i = self.position(block_root)?;
|
||||
Some(self.items.remove(i).balances)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the effective balances for every validator in the given `state`.
|
||||
///
|
||||
/// Any validator who is not active in the epoch of the given `state` is assigned a balance of
|
||||
/// zero.
|
||||
pub fn get_effective_balances<T: EthSpec>(state: &BeaconState<T>) -> Vec<u64> {
|
||||
state
|
||||
.validators
|
||||
.iter()
|
||||
.map(|validator| {
|
||||
if validator.is_active_at(state.current_epoch()) {
|
||||
validator.effective_balance
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `types::Checkpoint` that also stores the validator balances from a `BeaconState`.
|
||||
///
|
||||
/// Useful because we need to track the justified checkpoint balances.
|
||||
#[derive(PartialEq, Clone, Encode, Decode)]
|
||||
pub struct CheckpointWithBalances {
|
||||
pub epoch: Epoch,
|
||||
pub root: Hash256,
|
||||
/// These are the balances of the state with `self.root`.
|
||||
///
|
||||
/// Importantly, these are _not_ the balances of the first state that we saw that has
|
||||
/// `self.epoch` and `self.root` as `state.current_justified_checkpoint`. These are the
|
||||
/// balances of the state from the block with `state.current_justified_checkpoint.root`.
|
||||
pub balances: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Into<Checkpoint> for CheckpointWithBalances {
|
||||
fn into(self) -> Checkpoint {
|
||||
Checkpoint {
|
||||
epoch: self.epoch,
|
||||
root: self.root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pair of checkpoints, representing `state.current_justified_checkpoint` and
|
||||
/// `state.finalized_checkpoint` for some `BeaconState`.
|
||||
#[derive(PartialEq, Clone, Encode, Decode)]
|
||||
pub struct FFGCheckpoints {
|
||||
pub justified: CheckpointWithBalances,
|
||||
pub finalized: Checkpoint,
|
||||
}
|
||||
|
||||
/// A struct to manage the justified and finalized checkpoints to be used for `ForkChoice`.
|
||||
///
|
||||
/// This struct exists to manage the `should_update_justified_checkpoint` logic in the fork choice
|
||||
/// section of the spec:
|
||||
///
|
||||
/// https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/fork-choice.md#should_update_justified_checkpoint
|
||||
#[derive(PartialEq, Clone, Encode, Decode)]
|
||||
pub struct CheckpointManager {
|
||||
/// The current FFG checkpoints that should be used for finding the head.
|
||||
pub current: FFGCheckpoints,
|
||||
/// The best-known checkpoints that should be moved to `self.current` when the time is right.
|
||||
best: FFGCheckpoints,
|
||||
/// The epoch at which `self.current` should become `self.best`, if any.
|
||||
update_at: Option<Epoch>,
|
||||
/// A cached used to try and avoid DB reads when updating `self.current` and `self.best`.
|
||||
balances_cache: BalancesCache,
|
||||
}
|
||||
|
||||
impl CheckpointManager {
|
||||
/// Create a new checkpoint cache from `genesis_checkpoint` derived from the genesis block.
|
||||
pub fn new(genesis_checkpoint: CheckpointWithBalances) -> Self {
|
||||
let ffg_checkpoint = FFGCheckpoints {
|
||||
justified: genesis_checkpoint.clone(),
|
||||
finalized: genesis_checkpoint.into(),
|
||||
};
|
||||
Self {
|
||||
current: ffg_checkpoint.clone(),
|
||||
best: ffg_checkpoint,
|
||||
update_at: None,
|
||||
balances_cache: BalancesCache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Potentially updates `self.current`, if the conditions are correct.
|
||||
///
|
||||
/// Should be called before running the fork choice `find_head` function to ensure
|
||||
/// `self.current` is up-to-date.
|
||||
pub fn maybe_update<T: BeaconChainTypes>(
|
||||
&mut self,
|
||||
current_slot: Slot,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<(), Error> {
|
||||
if self.best.justified.epoch > self.current.justified.epoch {
|
||||
let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
match self.update_at {
|
||||
None => {
|
||||
if self.best.justified.epoch > self.current.justified.epoch {
|
||||
if Self::compute_slots_since_epoch_start::<T>(current_slot)
|
||||
< chain.spec.safe_slots_to_update_justified
|
||||
{
|
||||
self.current = self.best.clone();
|
||||
} else {
|
||||
self.update_at = Some(current_epoch + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(epoch) if epoch <= current_epoch => {
|
||||
self.current = self.best.clone();
|
||||
self.update_at = None
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks the given `state` (must correspond to the given `block_root`) to see if it contains
|
||||
/// a `current_justified_checkpoint` that is better than `self.best_justified_checkpoint`. If
|
||||
/// so, the value is updated.
|
||||
///
|
||||
/// Note: this does not update `self.justified_checkpoint`.
|
||||
pub fn process_state<T: BeaconChainTypes>(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
proto_array: &ProtoArrayForkChoice,
|
||||
) -> Result<(), Error> {
|
||||
// Only proceed if the new checkpoint is better than our current checkpoint.
|
||||
if state.current_justified_checkpoint.epoch > self.current.justified.epoch
|
||||
&& state.finalized_checkpoint.epoch >= self.current.finalized.epoch
|
||||
{
|
||||
let candidate = FFGCheckpoints {
|
||||
justified: CheckpointWithBalances {
|
||||
epoch: state.current_justified_checkpoint.epoch,
|
||||
root: state.current_justified_checkpoint.root,
|
||||
balances: self
|
||||
.get_balances_for_block(state.current_justified_checkpoint.root, chain)?,
|
||||
},
|
||||
finalized: state.finalized_checkpoint.clone(),
|
||||
};
|
||||
|
||||
// Using the given `state`, determine its ancestor at the slot of our current justified
|
||||
// epoch. Later, this will be compared to the root of the current justified checkpoint
|
||||
// to determine if this state is descendant of our current justified state.
|
||||
let new_checkpoint_ancestor = Self::get_block_root_at_slot(
|
||||
state,
|
||||
chain,
|
||||
candidate.justified.root,
|
||||
self.current
|
||||
.justified
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch()),
|
||||
)?;
|
||||
|
||||
let candidate_justified_block_slot = proto_array
|
||||
.block_slot(&candidate.justified.root)
|
||||
.ok_or_else(|| Error::UnknownBlockSlot(candidate.justified.root))?;
|
||||
|
||||
// If the new justified checkpoint is an ancestor of the current justified checkpoint,
|
||||
// it is always safe to change it.
|
||||
if new_checkpoint_ancestor == Some(self.current.justified.root)
|
||||
&& candidate_justified_block_slot
|
||||
>= candidate
|
||||
.justified
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch())
|
||||
{
|
||||
self.current = candidate.clone()
|
||||
}
|
||||
|
||||
if candidate.justified.epoch > self.best.justified.epoch {
|
||||
// Always update the best checkpoint, if it's better.
|
||||
self.best = candidate;
|
||||
}
|
||||
|
||||
// Add the state's balances to the balances cache to avoid a state read later.
|
||||
self.balances_cache.process_state(block_root, state)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_balances_for_block<T: BeaconChainTypes>(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<Vec<u64>, Error> {
|
||||
if let Some(balances) = self.balances_cache.get(block_root) {
|
||||
metrics::inc_counter(&metrics::BALANCES_CACHE_HITS);
|
||||
|
||||
Ok(balances)
|
||||
} else {
|
||||
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
|
||||
|
||||
let block = chain
|
||||
.get_block(&block_root)?
|
||||
.ok_or_else(|| Error::UnknownJustifiedBlock(block_root))?;
|
||||
|
||||
let state = chain
|
||||
.get_state(&block.state_root(), Some(block.slot()))?
|
||||
.ok_or_else(|| Error::UnknownJustifiedState(block.state_root()))?;
|
||||
|
||||
Ok(get_effective_balances(&state))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to get the block root for the given `slot`.
|
||||
///
|
||||
/// First, the `state` is used to see if the slot is within the distance of its historical
|
||||
/// lists. Then, the `chain` is used which will anchor the search at the given
|
||||
/// `justified_root`.
|
||||
fn get_block_root_at_slot<T: BeaconChainTypes>(
|
||||
state: &BeaconState<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
justified_root: Hash256,
|
||||
slot: Slot,
|
||||
) -> Result<Option<Hash256>, Error> {
|
||||
match state.get_block_root(slot) {
|
||||
Ok(root) => Ok(Some(*root)),
|
||||
Err(_) => chain
|
||||
.get_ancestor_block_root(justified_root, slot)
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate how far `slot` lies from the start of its epoch.
|
||||
fn compute_slots_since_epoch_start<T: BeaconChainTypes>(slot: Slot) -> u64 {
|
||||
let slots_per_epoch = T::EthSpec::slots_per_epoch();
|
||||
(slot - slot.epoch(slots_per_epoch).start_slot(slots_per_epoch)).as_u64()
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ extern crate lazy_static;
|
||||
|
||||
pub mod attestation_verification;
|
||||
mod beacon_chain;
|
||||
mod beacon_fork_choice_store;
|
||||
mod beacon_snapshot;
|
||||
mod block_verification;
|
||||
pub mod builder;
|
||||
mod errors;
|
||||
pub mod eth1_chain;
|
||||
pub mod events;
|
||||
mod fork_choice;
|
||||
mod head_tracker;
|
||||
mod metrics;
|
||||
pub mod migrate;
|
||||
@@ -19,6 +19,7 @@ mod observed_attestations;
|
||||
mod observed_attesters;
|
||||
mod observed_block_producers;
|
||||
mod persisted_beacon_chain;
|
||||
mod persisted_fork_choice;
|
||||
mod shuffling_cache;
|
||||
mod snapshot_cache;
|
||||
pub mod test_utils;
|
||||
@@ -27,15 +28,15 @@ mod validator_pubkey_cache;
|
||||
|
||||
pub use self::beacon_chain::{
|
||||
AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, ChainSegmentResult,
|
||||
StateSkipConfig,
|
||||
ForkChoiceError, StateSkipConfig,
|
||||
};
|
||||
pub use self::beacon_snapshot::BeaconSnapshot;
|
||||
pub use self::errors::{BeaconChainError, BlockProductionError};
|
||||
pub use attestation_verification::Error as AttestationError;
|
||||
pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError};
|
||||
pub use block_verification::{BlockError, BlockProcessingOutcome, GossipVerifiedBlock};
|
||||
pub use eth1_chain::{Eth1Chain, Eth1ChainBackend};
|
||||
pub use events::EventHandler;
|
||||
pub use fork_choice::ForkChoice;
|
||||
pub use metrics::scrape_for_metrics;
|
||||
pub use parking_lot;
|
||||
pub use slot_clock;
|
||||
|
||||
@@ -49,10 +49,6 @@ lazy_static! {
|
||||
"beacon_block_processing_db_write_seconds",
|
||||
"Time spent writing a newly processed block and state to DB"
|
||||
);
|
||||
pub static ref BLOCK_PROCESSING_FORK_CHOICE_REGISTER: Result<Histogram> = try_create_histogram(
|
||||
"beacon_block_processing_fork_choice_register_seconds",
|
||||
"Time spent registering the new block with fork choice (but not finding head)"
|
||||
);
|
||||
pub static ref BLOCK_PROCESSING_ATTESTATION_OBSERVATION: Result<Histogram> = try_create_histogram(
|
||||
"beacon_block_processing_attestation_observation_seconds",
|
||||
"Time spent hashing and remembering all the attestations in the block"
|
||||
@@ -115,10 +111,6 @@ lazy_static! {
|
||||
/*
|
||||
* General Attestation Processing
|
||||
*/
|
||||
pub static ref ATTESTATION_PROCESSING_APPLY_TO_FORK_CHOICE: Result<Histogram> = try_create_histogram(
|
||||
"beacon_attestation_processing_apply_to_fork_choice",
|
||||
"Time spent applying an attestation to fork choice"
|
||||
);
|
||||
pub static ref ATTESTATION_PROCESSING_APPLY_TO_AGG_POOL: Result<Histogram> = try_create_histogram(
|
||||
"beacon_attestation_processing_apply_to_agg_pool",
|
||||
"Time spent applying an attestation to the naive aggregation pool"
|
||||
|
||||
25
beacon_node/beacon_chain/src/persisted_fork_choice.rs
Normal file
25
beacon_node/beacon_chain/src/persisted_fork_choice.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::beacon_fork_choice_store::PersistedForkChoiceStore as ForkChoiceStore;
|
||||
use fork_choice::PersistedForkChoice as ForkChoice;
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use store::{DBColumn, Error, StoreItem};
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct PersistedForkChoice {
|
||||
pub fork_choice: ForkChoice,
|
||||
pub fork_choice_store: ForkChoiceStore,
|
||||
}
|
||||
|
||||
impl StoreItem for PersistedForkChoice {
|
||||
fn db_column() -> DBColumn {
|
||||
DBColumn::ForkChoice
|
||||
}
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
self.as_ssz_bytes()
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> std::result::Result<Self, Error> {
|
||||
Self::from_ssz_bytes(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -122,8 +122,6 @@ impl<E: EthSpec> BeaconChainHarness<HarnessType<E>> {
|
||||
.null_event_handler()
|
||||
.testing_slot_clock(HARNESS_SLOT_TIME)
|
||||
.expect("should configure testing slot clock")
|
||||
.reduced_tree_fork_choice()
|
||||
.expect("should add fork choice to builder")
|
||||
.build()
|
||||
.expect("should build");
|
||||
|
||||
@@ -164,8 +162,6 @@ impl<E: EthSpec> BeaconChainHarness<DiskHarnessType<E>> {
|
||||
.null_event_handler()
|
||||
.testing_slot_clock(HARNESS_SLOT_TIME)
|
||||
.expect("should configure testing slot clock")
|
||||
.reduced_tree_fork_choice()
|
||||
.expect("should add fork choice to builder")
|
||||
.build()
|
||||
.expect("should build");
|
||||
|
||||
@@ -201,8 +197,6 @@ impl<E: EthSpec> BeaconChainHarness<DiskHarnessType<E>> {
|
||||
.null_event_handler()
|
||||
.testing_slot_clock(Duration::from_secs(1))
|
||||
.expect("should configure testing slot clock")
|
||||
.reduced_tree_fork_choice()
|
||||
.expect("should add fork choice to builder")
|
||||
.build()
|
||||
.expect("should build");
|
||||
|
||||
@@ -243,6 +237,35 @@ where
|
||||
block_strategy: BlockStrategy,
|
||||
attestation_strategy: AttestationStrategy,
|
||||
) -> Hash256 {
|
||||
let mut i = 0;
|
||||
self.extend_chain_while(
|
||||
|_, _| {
|
||||
i += 1;
|
||||
i <= num_blocks
|
||||
},
|
||||
block_strategy,
|
||||
attestation_strategy,
|
||||
)
|
||||
}
|
||||
|
||||
/// Extend the `BeaconChain` with some blocks and attestations. Returns the root of the
|
||||
/// last-produced block (the head of the chain).
|
||||
///
|
||||
/// Chain will be extended while `predidcate` returns `true`.
|
||||
///
|
||||
/// The `block_strategy` dictates where the new blocks will be placed.
|
||||
///
|
||||
/// The `attestation_strategy` dictates which validators will attest to the newly created
|
||||
/// blocks.
|
||||
pub fn extend_chain_while<F>(
|
||||
&self,
|
||||
mut predicate: F,
|
||||
block_strategy: BlockStrategy,
|
||||
attestation_strategy: AttestationStrategy,
|
||||
) -> Hash256
|
||||
where
|
||||
F: FnMut(&SignedBeaconBlock<E>, &BeaconState<E>) -> bool,
|
||||
{
|
||||
let mut state = {
|
||||
// Determine the slot for the first block (or skipped block).
|
||||
let state_slot = match block_strategy {
|
||||
@@ -265,13 +288,17 @@ where
|
||||
|
||||
let mut head_block_root = None;
|
||||
|
||||
for _ in 0..num_blocks {
|
||||
loop {
|
||||
let (block, new_state) = self.build_block(state.clone(), slot, block_strategy);
|
||||
|
||||
if !predicate(&block, &new_state) {
|
||||
break;
|
||||
}
|
||||
|
||||
while self.chain.slot().expect("should have a slot") < slot {
|
||||
self.advance_slot();
|
||||
}
|
||||
|
||||
let (block, new_state) = self.build_block(state.clone(), slot, block_strategy);
|
||||
|
||||
let block_root = self
|
||||
.chain
|
||||
.process_block(block)
|
||||
@@ -289,6 +316,39 @@ where
|
||||
head_block_root.expect("did not produce any blocks")
|
||||
}
|
||||
|
||||
/// A simple method to produce a block at the current slot without applying it to the chain.
|
||||
///
|
||||
/// Always uses `BlockStrategy::OnCanonicalHead`.
|
||||
pub fn get_block(&self) -> (SignedBeaconBlock<E>, BeaconState<E>) {
|
||||
let state = self
|
||||
.chain
|
||||
.state_at_slot(
|
||||
self.chain.slot().unwrap() - 1,
|
||||
StateSkipConfig::WithStateRoots,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let slot = self.chain.slot().unwrap();
|
||||
|
||||
self.build_block(state, slot, BlockStrategy::OnCanonicalHead)
|
||||
}
|
||||
|
||||
/// A simple method to produce and process all attestation at the current slot. Always uses
|
||||
/// `AttestationStrategy::AllValidators`.
|
||||
pub fn generate_all_attestations(&self) {
|
||||
let slot = self.chain.slot().unwrap();
|
||||
let (state, block_root) = {
|
||||
let head = self.chain.head().unwrap();
|
||||
(head.beacon_state.clone(), head.beacon_block_root)
|
||||
};
|
||||
self.add_attestations_for_slot(
|
||||
&AttestationStrategy::AllValidators,
|
||||
&state,
|
||||
block_root,
|
||||
slot,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns current canonical head slot
|
||||
pub fn get_chain_slot(&self) -> Slot {
|
||||
self.chain.slot().unwrap()
|
||||
@@ -626,14 +686,16 @@ where
|
||||
spec,
|
||||
);
|
||||
|
||||
self.chain
|
||||
let attn = self.chain
|
||||
.verify_aggregated_attestation_for_gossip(signed_aggregate)
|
||||
.expect("should not error during attestation processing")
|
||||
.add_to_pool(&self.chain)
|
||||
.expect("should add attestation to naive aggregation pool")
|
||||
.add_to_fork_choice(&self.chain)
|
||||
.expect("should not error during attestation processing");
|
||||
|
||||
self.chain.apply_attestation_to_fork_choice(&attn)
|
||||
.expect("should add attestation to fork choice");
|
||||
}
|
||||
|
||||
self.chain.add_to_block_inclusion_pool(attn)
|
||||
.expect("should add attestation to op pool");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -721,244 +721,6 @@ fn unaggregated_gossip_verification() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests the verification conditions for an unaggregated attestation on the gossip network.
|
||||
#[test]
|
||||
fn fork_choice_verification() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
let chain = &harness.chain;
|
||||
|
||||
// Extend the chain out a few epochs so we have some chain depth to play with.
|
||||
harness.extend_chain(
|
||||
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
);
|
||||
|
||||
// Advance into a slot where there have not been blocks or attestations produced.
|
||||
harness.advance_slot();
|
||||
|
||||
// We're going to produce the attestations at the first slot of the epoch.
|
||||
let (valid_attestation, _validator_index, _validator_committee_index, _validator_sk) =
|
||||
get_valid_unaggregated_attestation(&harness.chain);
|
||||
|
||||
// Extend the chain two more blocks, but without any attestations so we don't trigger the
|
||||
// "already seen" caches.
|
||||
//
|
||||
// Because of this, the attestation we're dealing with was made one slot prior to the current
|
||||
// slot. This allows us to test the `AttestsToFutureBlock` condition.
|
||||
harness.extend_chain(
|
||||
2,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::SomeValidators(vec![]),
|
||||
);
|
||||
|
||||
let current_slot = chain.slot().expect("should get slot");
|
||||
let expected_current_epoch = chain.epoch().expect("should get epoch");
|
||||
|
||||
let attestation = harness
|
||||
.chain
|
||||
.verify_unaggregated_attestation_for_gossip(valid_attestation.clone())
|
||||
.expect("precondition: should gossip verify attestation");
|
||||
|
||||
macro_rules! assert_invalid {
|
||||
($desc: tt, $attn_getter: expr, $($error: pat) |+ $( if $guard: expr )?) => {
|
||||
assert!(
|
||||
matches!(
|
||||
harness
|
||||
.chain
|
||||
.apply_attestation_to_fork_choice(&$attn_getter)
|
||||
.err()
|
||||
.expect(&format!(
|
||||
"{} should error during apply_attestation_to_fork_choice",
|
||||
$desc
|
||||
)),
|
||||
$( $error ) |+ $( if $guard )?
|
||||
),
|
||||
"case: {}",
|
||||
$desc,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
assert_invalid!(
|
||||
"attestation without any aggregation bits set",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
a.__indexed_attestation_mut().attesting_indices = vec![].into();
|
||||
a
|
||||
},
|
||||
AttnError::EmptyAggregationBitfield
|
||||
);
|
||||
|
||||
/*
|
||||
* The following two tests ensure that:
|
||||
*
|
||||
* Spec v0.11.2
|
||||
*
|
||||
* assert target.epoch in [expected_current_epoch, previous_epoch]
|
||||
*/
|
||||
|
||||
let future_epoch = expected_current_epoch + 1;
|
||||
assert_invalid!(
|
||||
"attestation from future epoch",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
a.__indexed_attestation_mut().data.target.epoch = future_epoch;
|
||||
a
|
||||
},
|
||||
AttnError::FutureEpoch {
|
||||
attestation_epoch,
|
||||
current_epoch,
|
||||
}
|
||||
if attestation_epoch == future_epoch && current_epoch == expected_current_epoch
|
||||
);
|
||||
|
||||
assert!(
|
||||
expected_current_epoch > 1,
|
||||
"precondition: must be able to have a past epoch"
|
||||
);
|
||||
|
||||
let past_epoch = expected_current_epoch - 2;
|
||||
assert_invalid!(
|
||||
"attestation from past epoch",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
a.__indexed_attestation_mut().data.target.epoch = past_epoch;
|
||||
a
|
||||
},
|
||||
AttnError::PastEpoch {
|
||||
attestation_epoch,
|
||||
current_epoch,
|
||||
}
|
||||
if attestation_epoch == past_epoch && current_epoch == expected_current_epoch
|
||||
);
|
||||
|
||||
/*
|
||||
* This test ensures that:
|
||||
*
|
||||
* Spec v0.11.2
|
||||
*
|
||||
* assert target.epoch == compute_epoch_at_slot(attestation.data.slot)
|
||||
*/
|
||||
|
||||
assert_invalid!(
|
||||
"attestation with bad target epoch",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
|
||||
let indexed = a.__indexed_attestation_mut();
|
||||
indexed.data.target.epoch = indexed.data.slot.epoch(E::slots_per_epoch()) - 1;
|
||||
a
|
||||
},
|
||||
AttnError::BadTargetEpoch
|
||||
);
|
||||
|
||||
/*
|
||||
* This test ensures that:
|
||||
*
|
||||
* Spec v0.11.2
|
||||
*
|
||||
* Attestations target be for a known block. If target block is unknown, delay consideration
|
||||
* until the block is found
|
||||
*
|
||||
* assert target.root in store.blocks
|
||||
*/
|
||||
|
||||
let unknown_root = Hash256::from_low_u64_le(42);
|
||||
assert_invalid!(
|
||||
"attestation with unknown target root",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
|
||||
let indexed = a.__indexed_attestation_mut();
|
||||
indexed.data.target.root = unknown_root;
|
||||
a
|
||||
},
|
||||
AttnError::UnknownTargetRoot(hash) if hash == unknown_root
|
||||
);
|
||||
|
||||
// NOTE: we're not testing an assert from the spec:
|
||||
//
|
||||
// `assert get_current_slot(store) >= compute_start_slot_at_epoch(target.epoch)`
|
||||
//
|
||||
// I think this check is redundant and I've raised an issue here:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-specs/pull/1755
|
||||
|
||||
/*
|
||||
* This test asserts that:
|
||||
*
|
||||
* Spec v0.11.2
|
||||
*
|
||||
* # Attestations must be for a known block. If block is unknown, delay consideration until the
|
||||
* block is found
|
||||
*
|
||||
* assert attestation.data.beacon_block_root in store.blocks
|
||||
*/
|
||||
|
||||
assert_invalid!(
|
||||
"attestation with unknown beacon block root",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
|
||||
let indexed = a.__indexed_attestation_mut();
|
||||
indexed.data.beacon_block_root = unknown_root;
|
||||
a
|
||||
},
|
||||
AttnError::UnknownHeadBlock {
|
||||
beacon_block_root
|
||||
}
|
||||
if beacon_block_root == unknown_root
|
||||
);
|
||||
|
||||
let future_block = harness
|
||||
.chain
|
||||
.block_at_slot(current_slot)
|
||||
.expect("should not error getting block")
|
||||
.expect("should find block at current slot");
|
||||
assert_invalid!(
|
||||
"attestation to future block",
|
||||
{
|
||||
let mut a = attestation.clone();
|
||||
|
||||
let indexed = a.__indexed_attestation_mut();
|
||||
|
||||
assert!(
|
||||
future_block.slot() > indexed.data.slot,
|
||||
"precondition: the attestation must attest to the future"
|
||||
);
|
||||
|
||||
indexed.data.beacon_block_root = future_block.canonical_root();
|
||||
a
|
||||
},
|
||||
AttnError::AttestsToFutureBlock {
|
||||
block: current_slot,
|
||||
attestation: slot,
|
||||
}
|
||||
if slot == current_slot - 1
|
||||
);
|
||||
|
||||
// Note: we're not checking the "attestations can only affect the fork choice of subsequent
|
||||
// slots" part of the spec, we do this upstream.
|
||||
|
||||
assert!(
|
||||
harness
|
||||
.chain
|
||||
.apply_attestation_to_fork_choice(&attestation.clone())
|
||||
.is_ok(),
|
||||
"should verify valid attestation"
|
||||
);
|
||||
|
||||
// There's nothing stopping fork choice from accepting the same attestation twice.
|
||||
assert!(
|
||||
harness
|
||||
.chain
|
||||
.apply_attestation_to_fork_choice(&attestation)
|
||||
.is_ok(),
|
||||
"should verify valid attestation a second time"
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensures that an attestation that skips epochs can still be processed.
|
||||
///
|
||||
/// This also checks that we can do a state lookup if we don't get a hit from the shuffling cache.
|
||||
|
||||
@@ -154,7 +154,7 @@ fn assert_chains_pretty_much_the_same<T: BeaconChainTypes>(a: &BeaconChain<T>, b
|
||||
"genesis_block_root should be equal"
|
||||
);
|
||||
assert!(
|
||||
a.fork_choice == b.fork_choice,
|
||||
*a.fork_choice.read() == *b.fork_choice.read(),
|
||||
"fork_choice should be equal"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -375,7 +375,13 @@ fn unaggregated_attestations_added_to_fork_choice_some_none() {
|
||||
);
|
||||
|
||||
let state = &harness.chain.head().expect("should get head").beacon_state;
|
||||
let fork_choice = &harness.chain.fork_choice;
|
||||
let mut fork_choice = harness.chain.fork_choice.write();
|
||||
|
||||
// Move forward a slot so all queued attestations can be processed.
|
||||
harness.advance_slot();
|
||||
fork_choice
|
||||
.update_time(harness.chain.slot().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let validator_slots: Vec<(usize, Slot)> = (0..VALIDATOR_COUNT)
|
||||
.into_iter()
|
||||
@@ -397,7 +403,7 @@ fn unaggregated_attestations_added_to_fork_choice_some_none() {
|
||||
assert_eq!(
|
||||
latest_message.unwrap().1,
|
||||
slot.epoch(MinimalEthSpec::slots_per_epoch()),
|
||||
"Latest message slot for {} should be equal to slot {}.",
|
||||
"Latest message epoch for {} should be equal to epoch {}.",
|
||||
validator,
|
||||
slot
|
||||
)
|
||||
@@ -485,7 +491,13 @@ fn unaggregated_attestations_added_to_fork_choice_all_updated() {
|
||||
);
|
||||
|
||||
let state = &harness.chain.head().expect("should get head").beacon_state;
|
||||
let fork_choice = &harness.chain.fork_choice;
|
||||
let mut fork_choice = harness.chain.fork_choice.write();
|
||||
|
||||
// Move forward a slot so all queued attestations can be processed.
|
||||
harness.advance_slot();
|
||||
fork_choice
|
||||
.update_time(harness.chain.slot().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let validators: Vec<usize> = (0..VALIDATOR_COUNT).collect();
|
||||
let slots: Vec<Slot> = validators
|
||||
|
||||
Reference in New Issue
Block a user