Add caching for state.eth1_data_votes (#919)
## Issue Addressed NA ## Proposed Changes Adds additional tree hash caching for `state.eth1_data_votes`. Presently, each time we tree hash the `BeaconState`, we recompute the `state.eth1_data_votes` tree in it's entirety. This is because we only previous had support for caching fixed-length lists. This PR adds the `Eth1DataVotesTreeHashCache` which provides caching for the `state.eth1_data_votes` list. The cache is aware of `SLOTS_PER_ETH1_VOTING_PERIOD` and will reset itself whenever that boundary is crossed. This cache adds a new (but somewhat fundamental) restriction to tree hash caching: *For some state `s`, `s.tree_hash_cache` is only valid for `s` or descendants of `s` that have been reached via state transitions that are faithful to the specification (invalid blocks are permitted, as long as they are faithfully processed).*
This commit is contained in:
parent
23a8f31f83
commit
21bcc8848d
@ -34,7 +34,7 @@ fn bench_suite<T: EthSpec>(c: &mut Criterion, spec_desc: &str, validator_count:
|
||||
let state1 = build_state::<T>(validator_count);
|
||||
let state2 = state1.clone();
|
||||
let mut state3 = state1.clone();
|
||||
state3.build_tree_hash_cache().unwrap();
|
||||
state3.update_tree_hash_cache().unwrap();
|
||||
|
||||
c.bench(
|
||||
&format!("{}/{}_validators/no_cache", spec_desc, validator_count),
|
||||
|
@ -70,6 +70,11 @@ pub enum Error {
|
||||
CommitteeCacheUninitialized(Option<RelativeEpoch>),
|
||||
SszTypesError(ssz_types::Error),
|
||||
TreeHashCacheNotInitialized,
|
||||
NonLinearTreeHashCacheHistory,
|
||||
TreeHashCacheSkippedSlot {
|
||||
cache: Slot,
|
||||
state: Slot,
|
||||
},
|
||||
TreeHashError(tree_hash::Error),
|
||||
CachedTreeHashError(cached_tree_hash::Error),
|
||||
InvalidValidatorPubkey(ssz::DecodeError),
|
||||
@ -217,7 +222,7 @@ where
|
||||
#[ssz(skip_deserializing)]
|
||||
#[tree_hash(skip_hashing)]
|
||||
#[test_random(default)]
|
||||
pub tree_hash_cache: Option<BeaconTreeHashCache>,
|
||||
pub tree_hash_cache: Option<BeaconTreeHashCache<T>>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> BeaconState<T> {
|
||||
@ -879,7 +884,6 @@ impl<T: EthSpec> BeaconState<T> {
|
||||
pub fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> {
|
||||
self.build_all_committee_caches(spec)?;
|
||||
self.update_pubkey_cache()?;
|
||||
self.build_tree_hash_cache()?;
|
||||
self.exit_cache.build(&self.validators, spec)?;
|
||||
|
||||
Ok(())
|
||||
@ -1013,17 +1017,6 @@ impl<T: EthSpec> BeaconState<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build and update the tree hash cache if it isn't already initialized.
|
||||
pub fn build_tree_hash_cache(&mut self) -> Result<(), Error> {
|
||||
self.update_tree_hash_cache().map(|_| ())
|
||||
}
|
||||
|
||||
/// Build the tree hash cache, with blatant disregard for any existing cache.
|
||||
pub fn force_build_tree_hash_cache(&mut self) -> Result<(), Error> {
|
||||
self.tree_hash_cache = None;
|
||||
self.build_tree_hash_cache()
|
||||
}
|
||||
|
||||
/// Compute the tree hash root of the state using the tree hash cache.
|
||||
///
|
||||
/// Initialize the tree hash cache if it isn't already initialized.
|
||||
@ -1125,15 +1118,15 @@ impl<T: EthSpec> BeaconState<T> {
|
||||
|
||||
/// This implementation primarily exists to satisfy some testing requirements (ef_tests). It is
|
||||
/// recommended to use the methods directly on the beacon state instead.
|
||||
impl<T: EthSpec> CachedTreeHash<BeaconTreeHashCache> for BeaconState<T> {
|
||||
fn new_tree_hash_cache(&self, _arena: &mut CacheArena) -> BeaconTreeHashCache {
|
||||
impl<T: EthSpec> CachedTreeHash<BeaconTreeHashCache<T>> for BeaconState<T> {
|
||||
fn new_tree_hash_cache(&self, _arena: &mut CacheArena) -> BeaconTreeHashCache<T> {
|
||||
BeaconTreeHashCache::new(self)
|
||||
}
|
||||
|
||||
fn recalculate_tree_hash_root(
|
||||
&self,
|
||||
_arena: &mut CacheArena,
|
||||
cache: &mut BeaconTreeHashCache,
|
||||
cache: &mut BeaconTreeHashCache<T>,
|
||||
) -> Result<Hash256, cached_tree_hash::Error> {
|
||||
cache
|
||||
.recalculate_tree_hash_root(self)
|
||||
|
@ -180,6 +180,9 @@ fn clone_config() {
|
||||
let (mut state, _keypairs) = builder.build();
|
||||
|
||||
state.build_all_caches(&spec).unwrap();
|
||||
state
|
||||
.update_tree_hash_cache()
|
||||
.expect("should update tree hash cache");
|
||||
|
||||
let num_caches = 4;
|
||||
let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig {
|
||||
@ -207,8 +210,47 @@ fn tree_hash_cache() {
|
||||
|
||||
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
|
||||
|
||||
/*
|
||||
* A cache should hash twice without updating the slot.
|
||||
*/
|
||||
|
||||
assert_eq!(
|
||||
state.update_tree_hash_cache().unwrap(),
|
||||
root,
|
||||
"tree hash result should be identical on the same slot"
|
||||
);
|
||||
|
||||
/*
|
||||
* A cache should not hash after updating the slot but not updating the state roots.
|
||||
*/
|
||||
|
||||
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
|
||||
state
|
||||
.update_tree_hash_cache()
|
||||
.expect("should rebuild cache");
|
||||
|
||||
state.slot += 1;
|
||||
|
||||
assert_eq!(
|
||||
state.update_tree_hash_cache(),
|
||||
Err(BeaconStateError::NonLinearTreeHashCacheHistory),
|
||||
"should not build hash without updating the state root"
|
||||
);
|
||||
|
||||
/*
|
||||
* The cache should update if the slot and state root are updated.
|
||||
*/
|
||||
|
||||
// The tree hash cache needs to be rebuilt since it was dropped when it failed.
|
||||
let root = state
|
||||
.update_tree_hash_cache()
|
||||
.expect("should rebuild cache");
|
||||
|
||||
state.slot += 1;
|
||||
state
|
||||
.set_state_root(state.slot - 1, root)
|
||||
.expect("should set state root");
|
||||
|
||||
let root = state.update_tree_hash_cache().unwrap();
|
||||
assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]);
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
|
||||
use super::Error;
|
||||
use crate::{BeaconState, EthSpec, Hash256, Unsigned, Validator};
|
||||
use crate::{BeaconState, EthSpec, Hash256, Slot, Unsigned, Validator};
|
||||
use cached_tree_hash::{int_log, CacheArena, CachedTreeHash, TreeHashCache};
|
||||
use rayon::prelude::*;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use ssz_types::VariableList;
|
||||
use std::cmp::Ordering;
|
||||
use tree_hash::{mix_in_length, MerkleHasher, TreeHash};
|
||||
|
||||
@ -22,9 +23,66 @@ const NODES_PER_VALIDATOR: usize = 15;
|
||||
/// Do not set to 0.
|
||||
const VALIDATORS_PER_ARENA: usize = 4_096;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Encode, Decode)]
|
||||
pub struct Eth1DataVotesTreeHashCache<T: EthSpec> {
|
||||
arena: CacheArena,
|
||||
tree_hash_cache: TreeHashCache,
|
||||
voting_period: u64,
|
||||
roots: VariableList<Hash256, T::SlotsPerEth1VotingPeriod>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> Eth1DataVotesTreeHashCache<T> {
|
||||
/// Instantiates a new cache.
|
||||
///
|
||||
/// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are
|
||||
/// hashed, leaving the internal nodes as all-zeros.
|
||||
pub fn new(state: &BeaconState<T>) -> Self {
|
||||
let mut arena = CacheArena::default();
|
||||
let roots: VariableList<_, _> = state
|
||||
.eth1_data_votes
|
||||
.iter()
|
||||
.map(|eth1_data| eth1_data.tree_hash_root())
|
||||
.collect::<Vec<_>>()
|
||||
.into();
|
||||
let tree_hash_cache = roots.new_tree_hash_cache(&mut arena);
|
||||
|
||||
Self {
|
||||
arena,
|
||||
tree_hash_cache,
|
||||
voting_period: Self::voting_period(state.slot),
|
||||
roots,
|
||||
}
|
||||
}
|
||||
|
||||
fn voting_period(slot: Slot) -> u64 {
|
||||
slot.as_u64() / T::SlotsPerEth1VotingPeriod::to_u64()
|
||||
}
|
||||
|
||||
pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState<T>) -> Result<Hash256, Error> {
|
||||
if state.eth1_data_votes.len() < self.roots.len()
|
||||
|| Self::voting_period(state.slot) != self.voting_period
|
||||
{
|
||||
*self = Self::new(state);
|
||||
}
|
||||
|
||||
state
|
||||
.eth1_data_votes
|
||||
.iter()
|
||||
.skip(self.roots.len())
|
||||
.try_for_each(|eth1_data| self.roots.push(eth1_data.tree_hash_root()))?;
|
||||
|
||||
self.roots
|
||||
.recalculate_tree_hash_root(&mut self.arena, &mut self.tree_hash_cache)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache that performs a caching tree hash of the entire `BeaconState` struct.
|
||||
#[derive(Debug, PartialEq, Clone, Default, Encode, Decode)]
|
||||
pub struct BeaconTreeHashCache {
|
||||
#[derive(Debug, PartialEq, Clone, Encode, Decode)]
|
||||
pub struct BeaconTreeHashCache<T: EthSpec> {
|
||||
/// Tracks the previously generated state root to ensure the next state root provided descends
|
||||
/// directly from this state.
|
||||
previous_state: Option<(Hash256, Slot)>,
|
||||
// Validators cache
|
||||
validators: ValidatorsListTreeHashCache,
|
||||
// Arenas
|
||||
@ -38,14 +96,15 @@ pub struct BeaconTreeHashCache {
|
||||
balances: TreeHashCache,
|
||||
randao_mixes: TreeHashCache,
|
||||
slashings: TreeHashCache,
|
||||
eth1_data_votes: Eth1DataVotesTreeHashCache<T>,
|
||||
}
|
||||
|
||||
impl BeaconTreeHashCache {
|
||||
impl<T: EthSpec> BeaconTreeHashCache<T> {
|
||||
/// Instantiates a new cache.
|
||||
///
|
||||
/// Allocates the necessary memory to store all of the cached Merkle trees but does perform any
|
||||
/// hashing.
|
||||
pub fn new<T: EthSpec>(state: &BeaconState<T>) -> Self {
|
||||
/// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are
|
||||
/// hashed, leaving the internal nodes as all-zeros.
|
||||
pub fn new(state: &BeaconState<T>) -> Self {
|
||||
let mut fixed_arena = CacheArena::default();
|
||||
let block_roots = state.block_roots.new_tree_hash_cache(&mut fixed_arena);
|
||||
let state_roots = state.state_roots.new_tree_hash_cache(&mut fixed_arena);
|
||||
@ -61,6 +120,7 @@ impl BeaconTreeHashCache {
|
||||
let slashings = state.slashings.new_tree_hash_cache(&mut slashings_arena);
|
||||
|
||||
Self {
|
||||
previous_state: None,
|
||||
validators,
|
||||
fixed_arena,
|
||||
balances_arena,
|
||||
@ -71,6 +131,7 @@ impl BeaconTreeHashCache {
|
||||
balances,
|
||||
randao_mixes,
|
||||
slashings,
|
||||
eth1_data_votes: Eth1DataVotesTreeHashCache::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,10 +139,29 @@ impl BeaconTreeHashCache {
|
||||
///
|
||||
/// The provided `state` should be a descendant of the last `state` given to this function, or
|
||||
/// the `Self::new` function.
|
||||
pub fn recalculate_tree_hash_root<T: EthSpec>(
|
||||
&mut self,
|
||||
state: &BeaconState<T>,
|
||||
) -> Result<Hash256, Error> {
|
||||
pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState<T>) -> Result<Hash256, Error> {
|
||||
// If this cache has previously produced a root, ensure that it is in the state root
|
||||
// history of this state.
|
||||
//
|
||||
// This ensures that the states applied have a linear history, this
|
||||
// allows us to make assumptions about how the state changes over times and produce a more
|
||||
// efficient algorithm.
|
||||
if let Some((previous_root, previous_slot)) = self.previous_state {
|
||||
// The previously-hashed state must not be newer than `state`.
|
||||
if previous_slot > state.slot {
|
||||
return Err(Error::TreeHashCacheSkippedSlot {
|
||||
cache: previous_slot,
|
||||
state: state.slot,
|
||||
});
|
||||
}
|
||||
|
||||
// If the state is newer, the previous root must be in the history of the given state.
|
||||
if previous_slot < state.slot && *state.get_state_root(previous_slot)? != previous_root
|
||||
{
|
||||
return Err(Error::NonLinearTreeHashCacheHistory);
|
||||
}
|
||||
}
|
||||
|
||||
let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASHING_FIELDS);
|
||||
|
||||
hasher.write(state.genesis_time.tree_hash_root().as_bytes())?;
|
||||
@ -108,7 +188,11 @@ impl BeaconTreeHashCache {
|
||||
.as_bytes(),
|
||||
)?;
|
||||
hasher.write(state.eth1_data.tree_hash_root().as_bytes())?;
|
||||
hasher.write(state.eth1_data_votes.tree_hash_root().as_bytes())?;
|
||||
hasher.write(
|
||||
self.eth1_data_votes
|
||||
.recalculate_tree_hash_root(&state)?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
hasher.write(state.eth1_deposit_index.tree_hash_root().as_bytes())?;
|
||||
hasher.write(
|
||||
self.validators
|
||||
@ -155,7 +239,11 @@ impl BeaconTreeHashCache {
|
||||
)?;
|
||||
hasher.write(state.finalized_checkpoint.tree_hash_root().as_bytes())?;
|
||||
|
||||
hasher.finish().map_err(Into::into)
|
||||
let root = hasher.finish()?;
|
||||
|
||||
self.previous_state = Some((root, state.slot));
|
||||
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
/// Updates the cache and provides the root of the given `validators`.
|
||||
|
@ -180,8 +180,8 @@ mod ssz_static {
|
||||
ssz_static_test!(
|
||||
beacon_state,
|
||||
SszStaticTHCHandler, {
|
||||
(BeaconState<MinimalEthSpec>, BeaconTreeHashCache, MinimalEthSpec),
|
||||
(BeaconState<MainnetEthSpec>, BeaconTreeHashCache, MainnetEthSpec)
|
||||
(BeaconState<MinimalEthSpec>, BeaconTreeHashCache<_>, MinimalEthSpec),
|
||||
(BeaconState<MainnetEthSpec>, BeaconTreeHashCache<_>, MainnetEthSpec)
|
||||
}
|
||||
);
|
||||
ssz_static_test!(checkpoint, Checkpoint);
|
||||
|
Loading…
Reference in New Issue
Block a user