Add block/state caching on beacon chain (#677)

* Add basic block/state caching on beacon chain

* Adds checkpoint cache

* Stop storing the tree hash cache in the db

* Remove dedunant beacon state write

* Use caching get methods in fork choice

* Use caching state getting in state_by_slot

* Add state.cacheless_clone

* Attempt to improve attestation processing times

* Introduce HeadInfo struct

* Used cache tree hash for block processing

* Use cached tree hash for block production too
This commit is contained in:
Paul Hauner 2019-12-09 14:20:25 +11:00 committed by GitHub
parent 36624b1d14
commit 2bfc512fb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 340 additions and 101 deletions

View File

@ -1,4 +1,5 @@
use crate::checkpoint::CheckPoint;
use crate::checkpoint_cache::CheckPointCache;
use crate::errors::{BeaconChainError as Error, BlockProductionError};
use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend};
use crate::events::{EventHandler, EventKind};
@ -93,6 +94,13 @@ pub enum AttestationProcessingOutcome {
Invalid(AttestationValidationError),
}
pub struct HeadInfo {
pub slot: Slot,
pub block_root: Hash256,
pub state_root: Hash256,
pub finalized_checkpoint: types::Checkpoint,
}
pub trait BeaconChainTypes: Send + Sync + 'static {
type Store: store::Store<Self::EthSpec>;
type StoreMigrator: store::Migrate<Self::Store, Self::EthSpec>;
@ -129,6 +137,8 @@ pub struct BeaconChain<T: BeaconChainTypes> {
pub event_handler: T::EventHandler,
/// Used to track the heads of the beacon chain.
pub(crate) head_tracker: HeadTracker,
/// Provides a small cache of `BeaconState` and `BeaconBlock`.
pub(crate) checkpoint_cache: CheckPointCache<T::EthSpec>,
/// Logging to CLI, etc.
pub(crate) log: Logger,
}
@ -264,11 +274,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
block_root: Hash256,
) -> Result<ReverseBlockRootIterator<T::EthSpec, T::Store>, Error> {
let block = self
.get_block(&block_root)?
.get_block_caching(&block_root)?
.ok_or_else(|| Error::MissingBeaconBlock(block_root))?;
let state = self
.store
.get_state(&block.state_root, Some(block.slot))?
.get_state_caching(&block.state_root, Some(block.slot))?
.ok_or_else(|| Error::MissingBeaconState(block.state_root))?;
let iter = BlockRootsIterator::owned(self.store.clone(), state);
Ok(ReverseBlockRootIterator::new(
@ -349,6 +358,63 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
Ok(self.store.get(block_root)?)
}
/// Returns the block at the given root, if any.
///
/// ## Errors
///
/// May return a database error.
fn get_block_caching(
&self,
block_root: &Hash256,
) -> Result<Option<BeaconBlock<T::EthSpec>>, Error> {
if let Some(block) = self.checkpoint_cache.get_block(block_root) {
Ok(Some(block))
} else {
Ok(self.store.get(block_root)?)
}
}
/// Returns the state at the given root, if any.
///
/// ## Errors
///
/// May return a database error.
fn get_state_caching(
&self,
state_root: &Hash256,
slot: Option<Slot>,
) -> Result<Option<BeaconState<T::EthSpec>>, Error> {
if let Some(state) = self.checkpoint_cache.get_state(state_root) {
Ok(Some(state))
} else {
Ok(self.store.get_state(state_root, slot)?)
}
}
/// Returns the state at the given root, if any.
///
/// The return state does not contain any caches other than the committee caches. This method
/// is much faster than `Self::get_state_caching` because it does not clone the tree hash cache
/// when the state is found in the checkpoint cache.
///
/// ## Errors
///
/// May return a database error.
fn get_state_caching_only_with_committee_caches(
&self,
state_root: &Hash256,
slot: Option<Slot>,
) -> Result<Option<BeaconState<T::EthSpec>>, Error> {
if let Some(state) = self
.checkpoint_cache
.get_state_only_with_committee_cache(state_root)
{
Ok(Some(state))
} else {
Ok(self.store.get_state(state_root, slot)?)
}
}
/// Returns a `Checkpoint` representing the head block and state. Contains the "best block";
/// the head of the canonical `BeaconChain`.
///
@ -359,6 +425,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
self.canonical_head.read().clone()
}
/// Returns info representing the head block and state.
///
/// A summarized version of `Self::head` that involves less cloning.
pub fn head_info(&self) -> HeadInfo {
let head = self.canonical_head.read();
HeadInfo {
slot: head.beacon_block.slot,
block_root: head.beacon_block_root,
state_root: head.beacon_state_root,
finalized_checkpoint: head.beacon_state.finalized_checkpoint.clone(),
}
}
/// Returns the current heads of the `BeaconChain`. For the canonical head, see `Self::head`.
///
/// Returns `(block_root, block_slot)`.
@ -428,8 +508,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or_else(|| Error::NoStateForSlot(slot))?;
Ok(self
.store
.get_state(&state_root, Some(slot))?
.get_state_caching(&state_root, Some(slot))?
.ok_or_else(|| Error::NoStateForSlot(slot))?)
}
}
@ -745,50 +824,16 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
//
// An honest validator would have set this block to be the head of the chain (i.e., the
// result of running fork choice).
let result = if let Some(attestation_head_block) = self
.store
.get::<BeaconBlock<T::EthSpec>>(&attestation.data.beacon_block_root)?
let result = if let Some(attestation_head_block) =
self.get_block_caching(&attestation.data.beacon_block_root)?
{
// Attempt to process the attestation using the `self.head()` state.
//
// This is purely an effort to avoid loading a `BeaconState` unnecessarily from the DB.
let state = &self.head().beacon_state;
// If it turns out that the attestation was made using the head state, then there
// is no need to load a state from the database to process the attestation.
//
// Note: use the epoch of the target because it indicates which epoch the
// attestation was created in. You cannot use the epoch of the head block, because
// the block doesn't necessarily need to be in the same epoch as the attestation
// (e.g., if there are skip slots between the epoch the block was created in and
// the epoch for the attestation).
//
// This check also ensures that the slot for `data.beacon_block_root` is not higher
// than `state.root` by ensuring that the block is in the history of `state`.
if state.current_epoch() == attestation.data.target.epoch
&& (attestation.data.beacon_block_root == self.head().beacon_block_root
|| state
.get_block_root(attestation_head_block.slot)
.map(|root| *root == attestation.data.beacon_block_root)
.unwrap_or_else(|_| false))
{
// The head state is able to be used to validate this attestation. No need to load
// anything from the database.
return self.process_attestation_for_state_and_block(
attestation.clone(),
state,
&attestation_head_block,
);
}
// Use the `data.beacon_block_root` to load the state from the latest non-skipped
// slot preceding the attestation's creation.
//
// This state is guaranteed to be in the same chain as the attestation, but it's
// not guaranteed to be from the same slot or epoch as the attestation.
let mut state: BeaconState<T::EthSpec> = self
.store
.get_state(
.get_state_caching_only_with_committee_caches(
&attestation_head_block.state_root,
Some(attestation_head_block.slot),
)?
@ -837,11 +882,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
//
// This is likely overly restrictive, we could store the attestation for later
// processing.
let head_epoch = self
.head()
.beacon_block
.slot
.epoch(T::EthSpec::slots_per_epoch());
let head_epoch = self.head_info().slot.epoch(T::EthSpec::slots_per_epoch());
let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch());
// Only log a warning if our head is in a reasonable place to verify this attestation.
@ -903,7 +944,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// - The highest valid finalized epoch we've ever seen (i.e., the head).
// - The finalized epoch that this attestation was created against.
let finalized_epoch = std::cmp::max(
self.head().beacon_state.finalized_checkpoint.epoch,
self.head_info().finalized_checkpoint.epoch,
state.finalized_checkpoint.epoch,
);
@ -1110,8 +1151,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES);
let finalized_slot = self
.head()
.beacon_state
.head_info()
.finalized_checkpoint
.epoch
.start_slot(T::EthSpec::slots_per_epoch());
@ -1156,7 +1196,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// Load the blocks parent block from the database, returning invalid if that block is not
// found.
let parent_block: BeaconBlock<T::EthSpec> = match self.store.get(&block.parent_root)? {
let parent_block: BeaconBlock<T::EthSpec> =
match self.get_block_caching(&block.parent_root)? {
Some(block) => block,
None => {
return Ok(BlockProcessingOutcome::ParentUnknown {
@ -1169,8 +1210,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// It is an error because if we know the parent block we should also know the parent state.
let parent_state_root = parent_block.state_root;
let parent_state = self
.store
.get_state(&parent_state_root, Some(parent_block.slot))?
.get_state_caching(&parent_state_root, Some(parent_block.slot))?
.ok_or_else(|| {
Error::DBInconsistent(format!("Missing state {:?}", parent_state_root))
})?;
@ -1187,7 +1227,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// Transition the parent state to the block slot.
let mut state: BeaconState<T::EthSpec> = parent_state;
for i in state.slot.as_u64()..block.slot.as_u64() {
let distance = block.slot.as_u64().saturating_sub(state.slot.as_u64());
for i in 0..distance {
if i > 0 {
intermediate_states.push(state.clone());
}
@ -1231,7 +1272,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let state_root_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_STATE_ROOT);
let state_root = state.canonical_root();
let state_root = state.update_tree_hash_cache()?;
metrics::stop_timer(state_root_timer);
write_state(
&format!("state_post_block_{}", block_root),
@ -1246,8 +1289,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
});
}
metrics::stop_timer(state_root_timer);
let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE);
// Store all the states between the parent block state and this blocks slot before storing
@ -1315,6 +1356,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&metrics::OPERATIONS_PER_BLOCK_ATTESTATION,
block.body.attestations.len() as f64,
);
// Store the block in the checkpoint cache.
//
// A block that was just imported is likely to be referenced by the next block that we
// import.
self.checkpoint_cache.insert(&CheckPoint {
beacon_block_root: block_root,
beacon_block: block,
beacon_state_root: state_root,
beacon_state: state,
});
metrics::stop_timer(full_timer);
Ok(BlockProcessingOutcome::Processed { block_root })
@ -1410,7 +1463,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
&self.spec,
)?;
let state_root = state.canonical_root();
let state_root = state.update_tree_hash_cache()?;
block.state_root = state_root;
@ -1439,24 +1492,22 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let beacon_block_root = self.fork_choice.find_head(&self)?;
// If a new head was chosen.
let result = if beacon_block_root != self.head().beacon_block_root {
let result = if beacon_block_root != self.head_info().block_root {
metrics::inc_counter(&metrics::FORK_CHOICE_CHANGED_HEAD);
let beacon_block: BeaconBlock<T::EthSpec> = self
.store
.get(&beacon_block_root)?
.get_block_caching(&beacon_block_root)?
.ok_or_else(|| Error::MissingBeaconBlock(beacon_block_root))?;
let beacon_state_root = beacon_block.state_root;
let beacon_state: BeaconState<T::EthSpec> = self
.store
.get_state(&beacon_state_root, Some(beacon_block.slot))?
.get_state_caching(&beacon_state_root, Some(beacon_block.slot))?
.ok_or_else(|| Error::MissingBeaconState(beacon_state_root))?;
let previous_slot = self.head().beacon_block.slot;
let previous_slot = self.head_info().slot;
let new_slot = beacon_block.slot;
let is_reorg = self.head().beacon_block_root != beacon_block.parent_root;
let is_reorg = self.head_info().block_root != beacon_block.parent_root;
// If we switched to a new chain (instead of building atop the present chain).
if is_reorg {
@ -1464,7 +1515,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
warn!(
self.log,
"Beacon chain re-org";
"previous_head" => format!("{}", self.head().beacon_block_root),
"previous_head" => format!("{}", self.head_info().block_root),
"previous_slot" => previous_slot,
"new_head_parent" => format!("{}", beacon_block.parent_root),
"new_head" => format!("{}", beacon_block_root),
@ -1483,7 +1534,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
);
};
let old_finalized_epoch = self.head().beacon_state.finalized_checkpoint.epoch;
let old_finalized_epoch = self.head_info().finalized_checkpoint.epoch;
let new_finalized_epoch = beacon_state.finalized_checkpoint.epoch;
let finalized_root = beacon_state.finalized_checkpoint.root;
@ -1508,6 +1559,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let timer = metrics::start_timer(&metrics::UPDATE_HEAD_TIMES);
// Store the head in the checkpoint cache.
//
// The head block is likely to be referenced by the next imported block.
self.checkpoint_cache.insert(&new_head);
// Update the checkpoint that stores the head of the chain at the time it received the
// block.
*self.canonical_head.write() = new_head;

View File

@ -1,3 +1,4 @@
use crate::checkpoint_cache::CheckPointCache;
use crate::eth1_chain::CachingEth1Backend;
use crate::events::NullEventHandler;
use crate::head_tracker::HeadTracker;
@ -374,6 +375,7 @@ where
.event_handler
.ok_or_else(|| "Cannot build without an event handler".to_string())?,
head_tracker: self.head_tracker.unwrap_or_default(),
checkpoint_cache: CheckPointCache::default(),
log: log.clone(),
};

View File

@ -0,0 +1,124 @@
use crate::checkpoint::CheckPoint;
use crate::metrics;
use parking_lot::RwLock;
use types::{BeaconBlock, BeaconState, EthSpec, Hash256};
const CACHE_SIZE: usize = 4;
struct Inner<T: EthSpec> {
oldest: usize,
limit: usize,
checkpoints: Vec<CheckPoint<T>>,
}
impl<T: EthSpec> Default for Inner<T> {
fn default() -> Self {
Self {
oldest: 0,
limit: CACHE_SIZE,
checkpoints: vec![],
}
}
}
pub struct CheckPointCache<T: EthSpec> {
inner: RwLock<Inner<T>>,
}
impl<T: EthSpec> Default for CheckPointCache<T> {
fn default() -> Self {
Self {
inner: RwLock::new(Inner::default()),
}
}
}
impl<T: EthSpec> CheckPointCache<T> {
pub fn insert(&self, checkpoint: &CheckPoint<T>) {
if self
.inner
.read()
.checkpoints
.iter()
// This is `O(n)` but whilst `n == 4` it ain't no thing.
.any(|local| local.beacon_state_root == checkpoint.beacon_state_root)
{
// Adding a known checkpoint to the cache should be a no-op.
return;
}
let mut inner = self.inner.write();
if inner.checkpoints.len() < inner.limit {
inner.checkpoints.push(checkpoint.clone())
} else {
let i = inner.oldest; // to satisfy the borrow checker.
inner.checkpoints[i] = checkpoint.clone();
inner.oldest += 1;
inner.oldest %= inner.limit;
}
}
pub fn get_state(&self, state_root: &Hash256) -> Option<BeaconState<T>> {
self.inner
.read()
.checkpoints
.iter()
// Also `O(n)`.
.find(|checkpoint| checkpoint.beacon_state_root == *state_root)
.map(|checkpoint| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_HITS);
checkpoint.beacon_state.clone()
})
.or_else(|| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_MISSES);
None
})
}
pub fn get_state_only_with_committee_cache(
&self,
state_root: &Hash256,
) -> Option<BeaconState<T>> {
self.inner
.read()
.checkpoints
.iter()
// Also `O(n)`.
.find(|checkpoint| checkpoint.beacon_state_root == *state_root)
.map(|checkpoint| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_HITS);
let mut state = checkpoint.beacon_state.clone_without_caches();
state.committee_caches = checkpoint.beacon_state.committee_caches.clone();
state
})
.or_else(|| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_MISSES);
None
})
}
pub fn get_block(&self, block_root: &Hash256) -> Option<BeaconBlock<T>> {
self.inner
.read()
.checkpoints
.iter()
// Also `O(n)`.
.find(|checkpoint| checkpoint.beacon_block_root == *block_root)
.map(|checkpoint| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_HITS);
checkpoint.beacon_block.clone()
})
.or_else(|| {
metrics::inc_counter(&metrics::CHECKPOINT_CACHE_MISSES);
None
})
}
}

View File

@ -5,6 +5,7 @@ extern crate lazy_static;
mod beacon_chain;
pub mod builder;
mod checkpoint;
mod checkpoint_cache;
mod errors;
pub mod eth1_chain;
pub mod events;

View File

@ -149,6 +149,14 @@ lazy_static! {
pub static ref PERSIST_CHAIN: Result<Histogram> =
try_create_histogram("beacon_persist_chain", "Time taken to update the canonical head");
/*
* Checkpoint cache
*/
pub static ref CHECKPOINT_CACHE_HITS: Result<IntCounter> =
try_create_int_counter("beacon_checkpoint_cache_hits_total", "Count of times checkpoint cache fulfils request");
pub static ref CHECKPOINT_CACHE_MISSES: Result<IntCounter> =
try_create_int_counter("beacon_checkpoint_cache_misses_total", "Count of times checkpoint cache fulfils request");
/*
* Chain Head
*/

View File

@ -2,7 +2,7 @@ use crate::*;
use ssz::{Decode, DecodeError, Encode};
use ssz_derive::{Decode, Encode};
use std::convert::TryInto;
use types::beacon_state::{BeaconTreeHashCache, CommitteeCache, CACHED_EPOCHS};
use types::beacon_state::{CommitteeCache, CACHED_EPOCHS};
pub fn store_full_state<S: Store<E>, E: EthSpec>(
store: &S,
@ -47,27 +47,14 @@ pub fn get_full_state<S: Store<E>, E: EthSpec>(
pub struct StorageContainer<T: EthSpec> {
state: BeaconState<T>,
committee_caches: Vec<CommitteeCache>,
tree_hash_cache: BeaconTreeHashCache,
}
impl<T: EthSpec> StorageContainer<T> {
/// Create a new instance for storing a `BeaconState`.
pub fn new(state: &BeaconState<T>) -> Self {
let mut state = state.clone();
let mut committee_caches = vec![CommitteeCache::default(); CACHED_EPOCHS];
for i in 0..CACHED_EPOCHS {
std::mem::swap(&mut state.committee_caches[i], &mut committee_caches[i]);
}
let tree_hash_cache =
std::mem::replace(&mut state.tree_hash_cache, BeaconTreeHashCache::default());
Self {
state,
committee_caches,
tree_hash_cache,
state: state.clone_without_caches(),
committee_caches: state.committee_caches.to_vec(),
}
}
}
@ -88,8 +75,6 @@ impl<T: EthSpec> TryInto<BeaconState<T>> for StorageContainer<T> {
state.committee_caches[i] = self.committee_caches.remove(i);
}
state.tree_hash_cache = self.tree_hash_cache;
Ok(state)
}
}

View File

@ -3,7 +3,7 @@ use criterion::{black_box, criterion_group, criterion_main, Benchmark};
use rayon::prelude::*;
use ssz::{Decode, Encode};
use types::{
test_utils::generate_deterministic_keypair, BeaconState, Eth1Data, EthSpec, Hash256,
test_utils::generate_deterministic_keypair, BeaconState, Epoch, Eth1Data, EthSpec, Hash256,
MainnetEthSpec, Validator,
};
@ -28,12 +28,12 @@ fn get_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
.map(|&i| Validator {
pubkey: generate_deterministic_keypair(i).pk.into(),
withdrawal_credentials: Hash256::from_low_u64_le(i as u64),
effective_balance: i as u64,
slashed: i % 2 == 0,
activation_eligibility_epoch: i.into(),
activation_epoch: i.into(),
exit_epoch: i.into(),
withdrawable_epoch: i.into(),
effective_balance: spec.max_effective_balance,
slashed: false,
activation_eligibility_epoch: Epoch::new(0),
activation_epoch: Epoch::new(0),
exit_epoch: Epoch::from(u64::max_value()),
withdrawable_epoch: Epoch::from(u64::max_value()),
})
.collect::<Vec<_>>()
.into();
@ -43,14 +43,18 @@ fn get_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
fn all_benches(c: &mut Criterion) {
let validator_count = 16_384;
let state = get_state::<MainnetEthSpec>(validator_count);
let spec = &MainnetEthSpec::default_spec();
let mut state = get_state::<MainnetEthSpec>(validator_count);
state.build_all_caches(spec).expect("should build caches");
let state_bytes = state.as_ssz_bytes();
let inner_state = state.clone();
c.bench(
&format!("{}_validators", validator_count),
Benchmark::new("encode/beacon_state", move |b| {
b.iter_batched_ref(
|| state.clone(),
|| inner_state.clone(),
|state| black_box(state.as_ssz_bytes()),
criterion::BatchSize::SmallInput,
)
@ -73,6 +77,32 @@ fn all_benches(c: &mut Criterion) {
})
.sample_size(10),
);
let inner_state = state.clone();
c.bench(
&format!("{}_validators", validator_count),
Benchmark::new("clone/beacon_state", move |b| {
b.iter_batched_ref(
|| inner_state.clone(),
|state| black_box(state.clone()),
criterion::BatchSize::SmallInput,
)
})
.sample_size(10),
);
let inner_state = state.clone();
c.bench(
&format!("{}_validators", validator_count),
Benchmark::new("clone_without_caches/beacon_state", move |b| {
b.iter_batched_ref(
|| inner_state.clone(),
|state| black_box(state.clone_without_caches()),
criterion::BatchSize::SmallInput,
)
})
.sample_size(10),
);
}
criterion_group!(benches, all_benches,);

View File

@ -909,6 +909,39 @@ impl<T: EthSpec> BeaconState<T> {
pub fn drop_tree_hash_cache(&mut self) {
self.tree_hash_cache = BeaconTreeHashCache::default();
}
pub fn clone_without_caches(&self) -> Self {
BeaconState {
genesis_time: self.genesis_time,
slot: self.slot,
fork: self.fork.clone(),
latest_block_header: self.latest_block_header.clone(),
block_roots: self.block_roots.clone(),
state_roots: self.state_roots.clone(),
historical_roots: self.historical_roots.clone(),
eth1_data: self.eth1_data.clone(),
eth1_data_votes: self.eth1_data_votes.clone(),
eth1_deposit_index: self.eth1_deposit_index,
validators: self.validators.clone(),
balances: self.balances.clone(),
randao_mixes: self.randao_mixes.clone(),
slashings: self.slashings.clone(),
previous_epoch_attestations: self.previous_epoch_attestations.clone(),
current_epoch_attestations: self.current_epoch_attestations.clone(),
justification_bits: self.justification_bits.clone(),
previous_justified_checkpoint: self.previous_justified_checkpoint.clone(),
current_justified_checkpoint: self.current_justified_checkpoint.clone(),
finalized_checkpoint: self.finalized_checkpoint.clone(),
committee_caches: [
CommitteeCache::default(),
CommitteeCache::default(),
CommitteeCache::default(),
],
pubkey_cache: PubkeyCache::default(),
exit_cache: ExitCache::default(),
tree_hash_cache: BeaconTreeHashCache::default(),
}
}
}
impl From<RelativeEpochError> for Error {