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 state1 = build_state::<T>(validator_count); | ||||||
|     let state2 = state1.clone(); |     let state2 = state1.clone(); | ||||||
|     let mut state3 = state1.clone(); |     let mut state3 = state1.clone(); | ||||||
|     state3.build_tree_hash_cache().unwrap(); |     state3.update_tree_hash_cache().unwrap(); | ||||||
| 
 | 
 | ||||||
|     c.bench( |     c.bench( | ||||||
|         &format!("{}/{}_validators/no_cache", spec_desc, validator_count), |         &format!("{}/{}_validators/no_cache", spec_desc, validator_count), | ||||||
|  | |||||||
| @ -70,6 +70,11 @@ pub enum Error { | |||||||
|     CommitteeCacheUninitialized(Option<RelativeEpoch>), |     CommitteeCacheUninitialized(Option<RelativeEpoch>), | ||||||
|     SszTypesError(ssz_types::Error), |     SszTypesError(ssz_types::Error), | ||||||
|     TreeHashCacheNotInitialized, |     TreeHashCacheNotInitialized, | ||||||
|  |     NonLinearTreeHashCacheHistory, | ||||||
|  |     TreeHashCacheSkippedSlot { | ||||||
|  |         cache: Slot, | ||||||
|  |         state: Slot, | ||||||
|  |     }, | ||||||
|     TreeHashError(tree_hash::Error), |     TreeHashError(tree_hash::Error), | ||||||
|     CachedTreeHashError(cached_tree_hash::Error), |     CachedTreeHashError(cached_tree_hash::Error), | ||||||
|     InvalidValidatorPubkey(ssz::DecodeError), |     InvalidValidatorPubkey(ssz::DecodeError), | ||||||
| @ -217,7 +222,7 @@ where | |||||||
|     #[ssz(skip_deserializing)] |     #[ssz(skip_deserializing)] | ||||||
|     #[tree_hash(skip_hashing)] |     #[tree_hash(skip_hashing)] | ||||||
|     #[test_random(default)] |     #[test_random(default)] | ||||||
|     pub tree_hash_cache: Option<BeaconTreeHashCache>, |     pub tree_hash_cache: Option<BeaconTreeHashCache<T>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: EthSpec> BeaconState<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> { |     pub fn build_all_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { | ||||||
|         self.build_all_committee_caches(spec)?; |         self.build_all_committee_caches(spec)?; | ||||||
|         self.update_pubkey_cache()?; |         self.update_pubkey_cache()?; | ||||||
|         self.build_tree_hash_cache()?; |  | ||||||
|         self.exit_cache.build(&self.validators, spec)?; |         self.exit_cache.build(&self.validators, spec)?; | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         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.
 |     /// Compute the tree hash root of the state using the tree hash cache.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Initialize the tree hash cache if it isn't already initialized.
 |     /// 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
 | /// This implementation primarily exists to satisfy some testing requirements (ef_tests). It is
 | ||||||
| /// recommended to use the methods directly on the beacon state instead.
 | /// recommended to use the methods directly on the beacon state instead.
 | ||||||
| impl<T: EthSpec> CachedTreeHash<BeaconTreeHashCache> for BeaconState<T> { | impl<T: EthSpec> CachedTreeHash<BeaconTreeHashCache<T>> for BeaconState<T> { | ||||||
|     fn new_tree_hash_cache(&self, _arena: &mut CacheArena) -> BeaconTreeHashCache { |     fn new_tree_hash_cache(&self, _arena: &mut CacheArena) -> BeaconTreeHashCache<T> { | ||||||
|         BeaconTreeHashCache::new(self) |         BeaconTreeHashCache::new(self) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn recalculate_tree_hash_root( |     fn recalculate_tree_hash_root( | ||||||
|         &self, |         &self, | ||||||
|         _arena: &mut CacheArena, |         _arena: &mut CacheArena, | ||||||
|         cache: &mut BeaconTreeHashCache, |         cache: &mut BeaconTreeHashCache<T>, | ||||||
|     ) -> Result<Hash256, cached_tree_hash::Error> { |     ) -> Result<Hash256, cached_tree_hash::Error> { | ||||||
|         cache |         cache | ||||||
|             .recalculate_tree_hash_root(self) |             .recalculate_tree_hash_root(self) | ||||||
|  | |||||||
| @ -180,6 +180,9 @@ fn clone_config() { | |||||||
|     let (mut state, _keypairs) = builder.build(); |     let (mut state, _keypairs) = builder.build(); | ||||||
| 
 | 
 | ||||||
|     state.build_all_caches(&spec).unwrap(); |     state.build_all_caches(&spec).unwrap(); | ||||||
|  |     state | ||||||
|  |         .update_tree_hash_cache() | ||||||
|  |         .expect("should update tree hash cache"); | ||||||
| 
 | 
 | ||||||
|     let num_caches = 4; |     let num_caches = 4; | ||||||
|     let all_configs = (0..2u8.pow(num_caches)).map(|i| CloneConfig { |     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()[..]); |     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; |     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(); |     let root = state.update_tree_hash_cache().unwrap(); | ||||||
|     assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]); |     assert_eq!(root.as_bytes(), &state.tree_hash_root()[..]); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| #![allow(clippy::integer_arithmetic)] | #![allow(clippy::integer_arithmetic)] | ||||||
| 
 | 
 | ||||||
| use super::Error; | 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 cached_tree_hash::{int_log, CacheArena, CachedTreeHash, TreeHashCache}; | ||||||
| use rayon::prelude::*; | use rayon::prelude::*; | ||||||
| use ssz_derive::{Decode, Encode}; | use ssz_derive::{Decode, Encode}; | ||||||
|  | use ssz_types::VariableList; | ||||||
| use std::cmp::Ordering; | use std::cmp::Ordering; | ||||||
| use tree_hash::{mix_in_length, MerkleHasher, TreeHash}; | use tree_hash::{mix_in_length, MerkleHasher, TreeHash}; | ||||||
| 
 | 
 | ||||||
| @ -22,9 +23,66 @@ const NODES_PER_VALIDATOR: usize = 15; | |||||||
| /// Do not set to 0.
 | /// Do not set to 0.
 | ||||||
| const VALIDATORS_PER_ARENA: usize = 4_096; | 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.
 | /// A cache that performs a caching tree hash of the entire `BeaconState` struct.
 | ||||||
| #[derive(Debug, PartialEq, Clone, Default, Encode, Decode)] | #[derive(Debug, PartialEq, Clone, Encode, Decode)] | ||||||
| pub struct BeaconTreeHashCache { | 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 cache
 | ||||||
|     validators: ValidatorsListTreeHashCache, |     validators: ValidatorsListTreeHashCache, | ||||||
|     // Arenas
 |     // Arenas
 | ||||||
| @ -38,14 +96,15 @@ pub struct BeaconTreeHashCache { | |||||||
|     balances: TreeHashCache, |     balances: TreeHashCache, | ||||||
|     randao_mixes: TreeHashCache, |     randao_mixes: TreeHashCache, | ||||||
|     slashings: TreeHashCache, |     slashings: TreeHashCache, | ||||||
|  |     eth1_data_votes: Eth1DataVotesTreeHashCache<T>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl BeaconTreeHashCache { | impl<T: EthSpec> BeaconTreeHashCache<T> { | ||||||
|     /// Instantiates a new cache.
 |     /// Instantiates a new cache.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// Allocates the necessary memory to store all of the cached Merkle trees but does perform any
 |     /// Allocates the necessary memory to store all of the cached Merkle trees. Only the leaves are
 | ||||||
|     /// hashing.
 |     /// hashed, leaving the internal nodes as all-zeros.
 | ||||||
|     pub fn new<T: EthSpec>(state: &BeaconState<T>) -> Self { |     pub fn new(state: &BeaconState<T>) -> Self { | ||||||
|         let mut fixed_arena = CacheArena::default(); |         let mut fixed_arena = CacheArena::default(); | ||||||
|         let block_roots = state.block_roots.new_tree_hash_cache(&mut fixed_arena); |         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); |         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); |         let slashings = state.slashings.new_tree_hash_cache(&mut slashings_arena); | ||||||
| 
 | 
 | ||||||
|         Self { |         Self { | ||||||
|  |             previous_state: None, | ||||||
|             validators, |             validators, | ||||||
|             fixed_arena, |             fixed_arena, | ||||||
|             balances_arena, |             balances_arena, | ||||||
| @ -71,6 +131,7 @@ impl BeaconTreeHashCache { | |||||||
|             balances, |             balances, | ||||||
|             randao_mixes, |             randao_mixes, | ||||||
|             slashings, |             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 provided `state` should be a descendant of the last `state` given to this function, or
 | ||||||
|     /// the `Self::new` function.
 |     /// the `Self::new` function.
 | ||||||
|     pub fn recalculate_tree_hash_root<T: EthSpec>( |     pub fn recalculate_tree_hash_root(&mut self, state: &BeaconState<T>) -> Result<Hash256, Error> { | ||||||
|         &mut self, |         // If this cache has previously produced a root, ensure that it is in the state root
 | ||||||
|         state: &BeaconState<T>, |         // history of this state.
 | ||||||
|     ) -> Result<Hash256, Error> { |         //
 | ||||||
|  |         // 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); |         let mut hasher = MerkleHasher::with_leaves(NUM_BEACON_STATE_HASHING_FIELDS); | ||||||
| 
 | 
 | ||||||
|         hasher.write(state.genesis_time.tree_hash_root().as_bytes())?; |         hasher.write(state.genesis_time.tree_hash_root().as_bytes())?; | ||||||
| @ -108,7 +188,11 @@ impl BeaconTreeHashCache { | |||||||
|                 .as_bytes(), |                 .as_bytes(), | ||||||
|         )?; |         )?; | ||||||
|         hasher.write(state.eth1_data.tree_hash_root().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(state.eth1_deposit_index.tree_hash_root().as_bytes())?; | ||||||
|         hasher.write( |         hasher.write( | ||||||
|             self.validators |             self.validators | ||||||
| @ -155,7 +239,11 @@ impl BeaconTreeHashCache { | |||||||
|         )?; |         )?; | ||||||
|         hasher.write(state.finalized_checkpoint.tree_hash_root().as_bytes())?; |         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`.
 |     /// Updates the cache and provides the root of the given `validators`.
 | ||||||
|  | |||||||
| @ -180,8 +180,8 @@ mod ssz_static { | |||||||
|     ssz_static_test!( |     ssz_static_test!( | ||||||
|         beacon_state, |         beacon_state, | ||||||
|         SszStaticTHCHandler, { |         SszStaticTHCHandler, { | ||||||
|             (BeaconState<MinimalEthSpec>, BeaconTreeHashCache, MinimalEthSpec), |             (BeaconState<MinimalEthSpec>, BeaconTreeHashCache<_>, MinimalEthSpec), | ||||||
|             (BeaconState<MainnetEthSpec>, BeaconTreeHashCache, MainnetEthSpec) |             (BeaconState<MainnetEthSpec>, BeaconTreeHashCache<_>, MainnetEthSpec) | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|     ssz_static_test!(checkpoint, Checkpoint); |     ssz_static_test!(checkpoint, Checkpoint); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user