use crate::{DepositLog, Eth1Block}; use ssz_derive::{Decode, Encode}; use state_processing::common::DepositDataTree; use std::cmp::Ordering; use superstruct::superstruct; use tree_hash::TreeHash; use types::{Deposit, DepositTreeSnapshot, Hash256, DEPOSIT_TREE_DEPTH}; #[derive(Debug, PartialEq)] pub enum Error { /// A deposit log was added when a prior deposit was not already in the cache. /// /// Logs have to be added with monotonically-increasing block numbers. NonConsecutive { log_index: u64, expected: usize }, /// The eth1 event log data was unable to be parsed. LogParse(String), /// There are insufficient deposits in the cache to fulfil the request. InsufficientDeposits { known_deposits: usize, requested: u64, }, /// A log with the given index is already present in the cache and it does not match the one /// provided. DuplicateDistinctLog(u64), /// Attempted to insert log with given index after the log had been finalized FinalizedLogInsert { log_index: u64, finalized_index: u64, }, /// The deposit count must always be large enough to account for the requested deposit range. /// /// E.g., you cannot request deposit 10 when the deposit count is 9. DepositCountInvalid { deposit_count: u64, range_end: u64 }, /// You can't request deposits on or before the finalized deposit DepositRangeInvalid { range_start: u64, finalized_count: u64, }, /// You can't finalize what's already been finalized and the cache must have the logs /// that you wish to finalize InvalidFinalizeIndex { requested_count: u64, currently_finalized: u64, deposit_count: u64, }, /// Error with the merkle tree for deposits. DepositTree(merkle_proof::MerkleTreeError), /// An unexpected condition was encountered. Internal(String), /// This is for errors that should never occur PleaseNotifyTheDevs, } pub type SszDepositCache = SszDepositCacheV13; #[superstruct( variants(V1, V13), variant_attributes(derive(Encode, Decode, Clone)), no_enum )] pub struct SszDepositCache { pub logs: Vec, pub leaves: Vec, pub deposit_contract_deploy_block: u64, #[superstruct(only(V13))] pub finalized_deposit_count: u64, #[superstruct(only(V13))] pub finalized_block_height: u64, #[superstruct(only(V13))] pub deposit_tree_snapshot: Option, pub deposit_roots: Vec, } impl SszDepositCache { pub fn from_deposit_cache(cache: &DepositCache) -> Self { Self { logs: cache.logs.clone(), leaves: cache.leaves.clone(), deposit_contract_deploy_block: cache.deposit_contract_deploy_block, finalized_deposit_count: cache.finalized_deposit_count, finalized_block_height: cache.finalized_block_height, deposit_tree_snapshot: cache.deposit_tree.get_snapshot(), deposit_roots: cache.deposit_roots.clone(), } } pub fn to_deposit_cache(&self) -> Result { let deposit_tree = self .deposit_tree_snapshot .as_ref() .map(|snapshot| { let mut tree = DepositDataTree::from_snapshot(snapshot, DEPOSIT_TREE_DEPTH) .map_err(|e| format!("Invalid SszDepositCache: {:?}", e))?; for leaf in &self.leaves { tree.push_leaf(*leaf).map_err(|e| { format!("Invalid SszDepositCache: unable to push leaf: {:?}", e) })?; } Ok::<_, String>(tree) }) .unwrap_or_else(|| { // deposit_tree_snapshot = None (tree was never finalized) // Create DepositDataTree from leaves Ok(DepositDataTree::create( &self.leaves, self.leaves.len(), DEPOSIT_TREE_DEPTH, )) })?; // Check for invalid SszDepositCache conditions if self.leaves.len() != self.logs.len() { return Err("Invalid SszDepositCache: logs and leaves should have equal length".into()); } // `deposit_roots` also includes the zero root if self.leaves.len() + 1 != self.deposit_roots.len() { return Err( "Invalid SszDepositCache: deposit_roots length must be only one more than leaves" .into(), ); } Ok(DepositCache { logs: self.logs.clone(), leaves: self.leaves.clone(), deposit_contract_deploy_block: self.deposit_contract_deploy_block, finalized_deposit_count: self.finalized_deposit_count, finalized_block_height: self.finalized_block_height, deposit_tree, deposit_roots: self.deposit_roots.clone(), }) } } /// Mirrors the merkle tree of deposits in the eth1 deposit contract. /// /// Provides `Deposit` objects with merkle proofs included. #[cfg_attr(test, derive(PartialEq))] pub struct DepositCache { logs: Vec, leaves: Vec, deposit_contract_deploy_block: u64, finalized_deposit_count: u64, finalized_block_height: u64, /// An incremental merkle tree which represents the current state of the /// deposit contract tree. deposit_tree: DepositDataTree, /// Vector of deposit roots. `deposit_roots[i]` denotes `deposit_root` at /// `deposit_index` `i`. deposit_roots: Vec, } impl Default for DepositCache { fn default() -> Self { let deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); let deposit_roots = vec![deposit_tree.root()]; DepositCache { logs: Vec::new(), leaves: Vec::new(), deposit_contract_deploy_block: 1, finalized_deposit_count: 0, finalized_block_height: 0, deposit_tree, deposit_roots, } } } #[derive(Debug, PartialEq)] pub enum DepositCacheInsertOutcome { Inserted, Duplicate, } impl DepositCache { /// Create new `DepositCache` given block number at which deposit /// contract was deployed. pub fn new(deposit_contract_deploy_block: u64) -> Self { DepositCache { deposit_contract_deploy_block, finalized_block_height: deposit_contract_deploy_block.saturating_sub(1), ..Self::default() } } pub fn from_deposit_snapshot( deposit_contract_deploy_block: u64, snapshot: &DepositTreeSnapshot, ) -> Result { let deposit_tree = DepositDataTree::from_snapshot(snapshot, DEPOSIT_TREE_DEPTH) .map_err(|e| format!("Invalid DepositSnapshot: {:?}", e))?; Ok(DepositCache { logs: Vec::new(), leaves: Vec::new(), deposit_contract_deploy_block, finalized_deposit_count: snapshot.deposit_count, finalized_block_height: snapshot.execution_block_height, deposit_tree, deposit_roots: vec![snapshot.deposit_root], }) } /// Returns the number of deposits the cache stores pub fn len(&self) -> usize { self.finalized_deposit_count as usize + self.logs.len() } /// True if the cache does not store any blocks. pub fn is_empty(&self) -> bool { self.finalized_deposit_count != 0 && self.logs.is_empty() } /// Returns the block number for the most recent deposit in the cache. pub fn latest_block_number(&self) -> u64 { self.logs .last() .map(|log| log.block_number) .unwrap_or(self.finalized_block_height) } /// Returns an iterator over all the logs in `self` that aren't finalized. pub fn iter(&self) -> impl Iterator { self.logs.iter() } /// Returns the deposit log with INDEX i. pub fn get_log(&self, i: usize) -> Option<&DepositLog> { let finalized_deposit_count = self.finalized_deposit_count as usize; if i < finalized_deposit_count { None } else { self.logs.get(i - finalized_deposit_count) } } /// Returns the deposit root with DEPOSIT COUNT (not index) i pub fn get_root(&self, i: usize) -> Option<&Hash256> { let finalized_deposit_count = self.finalized_deposit_count as usize; if i < finalized_deposit_count { None } else { self.deposit_roots.get(i - finalized_deposit_count) } } /// Returns the finalized deposit count pub fn finalized_deposit_count(&self) -> u64 { self.finalized_deposit_count } /// Finalizes the cache up to `eth1_block.deposit_count`. pub fn finalize(&mut self, eth1_block: Eth1Block) -> Result<(), Error> { let deposits_to_finalize = eth1_block.deposit_count.ok_or_else(|| { Error::Internal("Eth1Block did not contain deposit_count".to_string()) })?; let currently_finalized = self.finalized_deposit_count; if deposits_to_finalize > self.len() as u64 || deposits_to_finalize <= currently_finalized { Err(Error::InvalidFinalizeIndex { requested_count: deposits_to_finalize, currently_finalized, deposit_count: self.len() as u64, }) } else { let finalized_log = self .get_log((deposits_to_finalize - 1) as usize) .cloned() .ok_or(Error::PleaseNotifyTheDevs)?; let drop = (deposits_to_finalize - currently_finalized) as usize; self.deposit_tree .finalize(eth1_block.into()) .map_err(Error::DepositTree)?; self.logs.drain(0..drop); self.leaves.drain(0..drop); self.deposit_roots.drain(0..drop); self.finalized_deposit_count = deposits_to_finalize; self.finalized_block_height = finalized_log.block_number; Ok(()) } } /// Returns the deposit tree snapshot (if tree is finalized) pub fn get_deposit_snapshot(&self) -> Option { self.deposit_tree.get_snapshot() } /// Adds `log` to self. /// /// This function enforces that `logs` are imported one-by-one with no gaps between /// `log.index`, starting at `log.index == 0`. /// /// ## Errors /// /// - If a log with index `log.index - 1` is not already present in `self` (ignored when empty). /// - If a log with `log.index` is already known, but the given `log` is distinct to it. pub fn insert_log(&mut self, log: DepositLog) -> Result { match log.index.cmp(&(self.len() as u64)) { Ordering::Equal => { let deposit = log.deposit_data.tree_hash_root(); // should push to deposit_tree first because it's fallible self.deposit_tree .push_leaf(deposit) .map_err(Error::DepositTree)?; self.leaves.push(deposit); self.logs.push(log); self.deposit_roots.push(self.deposit_tree.root()); Ok(DepositCacheInsertOutcome::Inserted) } Ordering::Less => { let mut compare_index = log.index as usize; if log.index < self.finalized_deposit_count { return Err(Error::FinalizedLogInsert { log_index: log.index, finalized_index: self.finalized_deposit_count - 1, }); } else { compare_index -= self.finalized_deposit_count as usize; } if self.logs[compare_index] == log { Ok(DepositCacheInsertOutcome::Duplicate) } else { Err(Error::DuplicateDistinctLog(log.index)) } } Ordering::Greater => Err(Error::NonConsecutive { log_index: log.index, expected: self.logs.len(), }), } } /// Returns a list of `Deposit` objects, within the given deposit index `range`. /// /// The `deposit_count` is used to generate the proofs for the `Deposits`. For example, if we /// have 100 proofs, but the eth2 chain only acknowledges 50 of them, we must produce our /// proofs with respect to a tree size of 50. /// /// /// ## Errors /// /// - If `deposit_count` is less than `end`. /// - There are not sufficient deposits in the tree to generate the proof. pub fn get_deposits( &self, start: u64, end: u64, deposit_count: u64, ) -> Result<(Hash256, Vec), Error> { if deposit_count < end { // It's invalid to ask for more deposits than should exist. Err(Error::DepositCountInvalid { deposit_count, range_end: end, }) } else if end > self.len() as u64 { // The range of requested deposits exceeds the deposits stored locally. Err(Error::InsufficientDeposits { requested: end, known_deposits: self.logs.len(), }) } else if self.finalized_deposit_count > start { // Can't ask for deposits before or on the finalized deposit Err(Error::DepositRangeInvalid { range_start: start, finalized_count: self.finalized_deposit_count, }) } else { let (start, end, deposit_count) = ( start - self.finalized_deposit_count, end - self.finalized_deposit_count, deposit_count - self.finalized_deposit_count, ); let leaves = self .leaves .get(0..deposit_count as usize) .ok_or_else(|| Error::Internal("Unable to get known leaves".into()))?; let tree = self .deposit_tree .get_snapshot() .map(|snapshot| { // The tree has already been finalized. So we can just start from the snapshot // and replay the deposits up to `deposit_count` let mut tree = DepositDataTree::from_snapshot(&snapshot, DEPOSIT_TREE_DEPTH) .map_err(Error::DepositTree)?; for leaf in leaves { tree.push_leaf(*leaf).map_err(Error::DepositTree)?; } Ok(tree) }) .unwrap_or_else(|| { // Deposit tree hasn't been finalized yet, will have to re-create the whole tree Ok(DepositDataTree::create( leaves, leaves.len(), DEPOSIT_TREE_DEPTH, )) })?; let mut deposits = vec![]; self.logs .get(start as usize..end as usize) .ok_or_else(|| Error::Internal("Unable to get known log".into()))? .iter() .try_for_each(|deposit_log| { let (_leaf, proof) = tree .generate_proof(deposit_log.index as usize) .map_err(Error::DepositTree)?; deposits.push(Deposit { proof: proof.into(), data: deposit_log.deposit_data.clone(), }); Ok(()) })?; Ok((tree.root(), deposits)) } } /// Returns the number of deposits with valid signatures that have been observed up to and /// including the block at `block_number`. /// /// Returns `None` if the `block_number` is zero or prior to contract deployment. pub fn get_valid_signature_count(&self, block_number: u64) -> Option { if block_number == 0 || block_number < self.deposit_contract_deploy_block { None } else { Some( self.logs .iter() .take_while(|deposit| deposit.block_number <= block_number) .filter(|deposit| deposit.signature_is_valid) .count(), ) } } /// Returns the number of deposits that have been observed up to and /// including the block at `block_number`. /// /// Returns `None` if the `block_number` is zero or prior to contract deployment /// or prior to last finalized deposit. pub fn get_deposit_count_from_cache(&self, block_number: u64) -> Option { if block_number == 0 || block_number < self.deposit_contract_deploy_block || block_number < self.finalized_block_height { None } else if block_number == self.finalized_block_height { Some(self.finalized_deposit_count) } else { Some( self.finalized_deposit_count + self .logs .iter() .take_while(|deposit| deposit.block_number <= block_number) .count() as u64, ) } } /// Gets the deposit root at block height = block_number. /// /// Fetches the `deposit_count` on or just before the queried `block_number` /// and queries the `deposit_roots` map to get the corresponding `deposit_root`. pub fn get_deposit_root_from_cache(&self, block_number: u64) -> Option { let count = self.get_deposit_count_from_cache(block_number)?; self.get_root(count as usize).cloned() } } #[cfg(test)] pub mod tests { use super::*; use execution_layer::http::deposit_log::Log; use types::{EthSpec, MainnetEthSpec}; /// The data from a deposit event, using the v0.8.3 version of the deposit contract. pub const EXAMPLE_LOG: &[u8] = &[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 167, 108, 6, 69, 88, 17, 3, 51, 6, 4, 158, 232, 82, 248, 218, 2, 71, 219, 55, 102, 86, 125, 136, 203, 36, 77, 64, 213, 43, 52, 175, 154, 239, 50, 142, 52, 201, 77, 54, 239, 0, 229, 22, 46, 139, 120, 62, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 64, 89, 115, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 140, 74, 175, 158, 209, 20, 206, 30, 63, 215, 238, 113, 60, 132, 216, 211, 100, 186, 202, 71, 34, 200, 160, 225, 212, 213, 119, 88, 51, 80, 101, 74, 2, 45, 78, 153, 12, 192, 44, 51, 77, 40, 10, 72, 246, 34, 193, 187, 22, 95, 4, 211, 245, 224, 13, 162, 21, 163, 54, 225, 22, 124, 3, 56, 14, 81, 122, 189, 149, 250, 251, 159, 22, 77, 94, 157, 197, 196, 253, 110, 201, 88, 193, 246, 136, 226, 221, 18, 113, 232, 105, 100, 114, 103, 237, 189, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; fn example_log() -> DepositLog { let spec = MainnetEthSpec::default_spec(); let log = Log { block_number: 42, data: EXAMPLE_LOG.to_vec(), }; log.to_deposit_log(&spec).expect("should decode log") } fn get_cache_with_deposits(n: u64) -> DepositCache { let mut deposit_cache = DepositCache::default(); for i in 0..n { let mut log = example_log(); log.index = i; log.block_number = i; log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i); deposit_cache .insert_log(log) .expect("should add consecutive logs"); } assert_eq!(deposit_cache.len() as u64, n, "should have {} deposits", n); deposit_cache } #[test] fn insert_log_valid() { let mut deposit_cache = DepositCache::default(); for i in 0..16 { let mut log = example_log(); log.index = i; deposit_cache .insert_log(log) .expect("should add consecutive logs"); } } #[test] fn insert_log_invalid() { let mut deposit_cache = DepositCache::default(); for i in 0..4 { let mut log = example_log(); log.index = i; deposit_cache .insert_log(log) .expect("should add consecutive logs"); } // Add duplicate, when given is the same as the one known. let mut log = example_log(); log.index = 3; assert_eq!( deposit_cache.insert_log(log).unwrap(), DepositCacheInsertOutcome::Duplicate ); // Add duplicate, when given is different to the one known. let mut log = example_log(); log.index = 3; log.block_number = 99; assert!(deposit_cache.insert_log(log).is_err()); // Skip inserting a log. let mut log = example_log(); log.index = 5; assert!(deposit_cache.insert_log(log).is_err()); } #[test] fn get_deposit_valid() { let n = 1_024; let deposit_cache = get_cache_with_deposits(n); // Get 0 deposits, with max deposit count. let (_, deposits) = deposit_cache .get_deposits(0, 0, n) .expect("should get the full tree"); assert_eq!(deposits.len(), 0, "should return no deposits"); // Get 0 deposits, with 0 deposit count. let (_, deposits) = deposit_cache .get_deposits(0, 0, 0) .expect("should get the full tree"); assert_eq!(deposits.len(), 0, "should return no deposits"); // Get all deposits, with max deposit count. let (full_root, deposits) = deposit_cache .get_deposits(0, n, n) .expect("should get the full tree"); assert_eq!(deposits.len(), n as usize, "should return all deposits"); // Get 4 deposits, with max deposit count. let (root, deposits) = deposit_cache .get_deposits(0, 4, n) .expect("should get the four from the full tree"); assert_eq!( deposits.len(), 4_usize, "should get 4 deposits from full tree" ); assert_eq!( root, full_root, "should still return full root when getting deposit subset" ); // Get half of the deposits, with half deposit count. let half = n / 2; let (half_root, deposits) = deposit_cache .get_deposits(0, half, half) .expect("should get the half tree"); assert_eq!(deposits.len(), half as usize, "should return half deposits"); // Get 4 deposits, with half deposit count. let (root, deposits) = deposit_cache .get_deposits(0, 4, n / 2) .expect("should get the half tree"); assert_eq!( deposits.len(), 4_usize, "should get 4 deposits from half tree" ); assert_eq!( root, half_root, "should still return half root when getting deposit subset" ); assert_ne!( full_root, half_root, "should get different root when pinning deposit count" ); } #[test] fn get_deposit_invalid() { let n = 16; let mut tree = get_cache_with_deposits(n); // Range too high. assert!(tree.get_deposits(0, n + 1, n).is_err()); // Count too high. assert!(tree.get_deposits(0, n, n + 1).is_err()); // Range higher than count. assert!(tree.get_deposits(0, 4, 2).is_err()); let block7 = fake_eth1_block(&tree, 7).expect("should create fake eth1 block"); tree.finalize(block7).expect("should finalize"); // Range starts <= finalized deposit assert!(tree.get_deposits(6, 9, 11).is_err()); assert!(tree.get_deposits(7, 9, 11).is_err()); // Range start > finalized deposit should be OK assert!(tree.get_deposits(8, 9, 11).is_ok()); } // returns an eth1 block that can be used to finalize the cache at `deposit_index` // this will ensure the `deposit_root` on the `Eth1Block` is correct fn fake_eth1_block(deposit_cache: &DepositCache, deposit_index: usize) -> Option { let deposit_log = deposit_cache.get_log(deposit_index)?; Some(Eth1Block { hash: Hash256::from_low_u64_be(deposit_log.block_number), timestamp: 0, number: deposit_log.block_number, deposit_root: deposit_cache.get_root(deposit_index + 1).cloned(), deposit_count: Some(deposit_log.index + 1), }) } #[test] fn test_finalization_boundaries() { let n = 8; let half = (n / 2) as usize; let mut deposit_cache = get_cache_with_deposits(n as u64); let full_root_before_finalization = deposit_cache.deposit_tree.root(); let half_log_plus1_before_finalization = deposit_cache .get_log(half + 1) .expect("log should exist") .clone(); let half_root_plus1_before_finalization = *deposit_cache.get_root(half + 1).expect("root should exist"); let (root_before_finalization, proof_before_finalization) = deposit_cache .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) .expect("should return 1 deposit with proof"); // finalize on the tree at half let half_block = fake_eth1_block(&deposit_cache, half).expect("fake block should be created"); assert!( deposit_cache.get_deposit_snapshot().is_none(), "snapshot should not exist as tree has not been finalized" ); deposit_cache .finalize(half_block) .expect("tree should_finalize"); // check boundary conditions for get_log assert!( deposit_cache.get_log(half).is_none(), "log at finalized deposit should NOT exist" ); assert_eq!( *deposit_cache.get_log(half + 1).expect("log should exist"), half_log_plus1_before_finalization, "log after finalized deposit should match before finalization" ); // check boundary conditions for get_root assert!( deposit_cache.get_root(half).is_none(), "root at finalized deposit should NOT exist" ); assert_eq!( *deposit_cache.get_root(half + 1).expect("root should exist"), half_root_plus1_before_finalization, "root after finalized deposit should match before finalization" ); // full root should match before and after finalization assert_eq!( deposit_cache.deposit_tree.root(), full_root_before_finalization, "full root should match before and after finalization" ); // check boundary conditions for get_deposits (proof) assert!( deposit_cache .get_deposits(half as u64, (half + 1) as u64, (half + 1) as u64) .is_err(), "cannot prove the finalized deposit" ); let (root_after_finalization, proof_after_finalization) = deposit_cache .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) .expect("should return 1 deposit with proof"); assert_eq!( root_before_finalization, root_after_finalization, "roots before and after finalization should match" ); assert_eq!( proof_before_finalization, proof_after_finalization, "proof before and after finalization should match" ); // recover tree from snapshot by replaying deposits let snapshot = deposit_cache .get_deposit_snapshot() .expect("snapshot should exist"); let mut recovered = DepositCache::from_deposit_snapshot(1, &snapshot) .expect("should recover finalized tree"); for i in half + 1..n { let mut log = example_log(); log.index = i as u64; log.block_number = i as u64; log.deposit_data.withdrawal_credentials = Hash256::from_low_u64_be(i as u64); recovered .insert_log(log) .expect("should add consecutive logs"); } // check the same boundary conditions above for the recovered tree assert!( recovered.get_log(half).is_none(), "log at finalized deposit should NOT exist" ); assert_eq!( *recovered.get_log(half + 1).expect("log should exist"), half_log_plus1_before_finalization, "log after finalized deposit should match before finalization in recovered tree" ); // check boundary conditions for get_root assert!( recovered.get_root(half).is_none(), "root at finalized deposit should NOT exist" ); assert_eq!( *recovered.get_root(half + 1).expect("root should exist"), half_root_plus1_before_finalization, "root after finalized deposit should match before finalization in recovered tree" ); // full root should match before and after finalization assert_eq!( recovered.deposit_tree.root(), full_root_before_finalization, "full root should match before and after finalization" ); // check boundary conditions for get_deposits (proof) assert!( recovered .get_deposits(half as u64, (half + 1) as u64, (half + 1) as u64) .is_err(), "cannot prove the finalized deposit" ); let (recovered_root_after_finalization, recovered_proof_after_finalization) = recovered .get_deposits((half + 1) as u64, (half + 2) as u64, (half + 2) as u64) .expect("should return 1 deposit with proof"); assert_eq!( root_before_finalization, recovered_root_after_finalization, "recovered roots before and after finalization should match" ); assert_eq!( proof_before_finalization, recovered_proof_after_finalization, "recovered proof before and after finalization should match" ); } #[test] fn test_finalization() { let n = 1024; let half = n / 2; let quarter = half / 2; let mut deposit_cache = get_cache_with_deposits(n); let full_root_before_finalization = deposit_cache.deposit_tree.root(); let q3_root_before_finalization = deposit_cache .get_root((half + quarter) as usize) .cloned() .expect("root should exist"); let q3_log_before_finalization = deposit_cache .get_log((half + quarter) as usize) .cloned() .expect("log should exist"); // get_log(half+quarter) should return log with index `half+quarter` assert_eq!( q3_log_before_finalization.index, (half + quarter) as u64, "log index should be {}", (half + quarter), ); // get lower quarter of deposits with max deposit count let (lower_quarter_root_before_finalization, lower_quarter_deposits_before_finalization) = deposit_cache .get_deposits(quarter, half, n) .expect("should get lower quarter"); assert_eq!( lower_quarter_deposits_before_finalization.len(), quarter as usize, "should get {} deposits from lower quarter", quarter, ); // since the lower quarter was done with full deposits, root should be the same as full_root_before_finalization assert_eq!( lower_quarter_root_before_finalization, full_root_before_finalization, "should still get full root with deposit subset", ); // get upper quarter of deposits with slightly reduced deposit count let (upper_quarter_root_before_finalization, upper_quarter_deposits_before_finalization) = deposit_cache .get_deposits(half, half + quarter, n - 2) .expect("should get upper quarter"); assert_eq!( upper_quarter_deposits_before_finalization.len(), quarter as usize, "should get {} deposits from upper quarter", quarter, ); // since upper quarter was with subset of nodes, it should differ from full root assert_ne!( full_root_before_finalization, upper_quarter_root_before_finalization, "subtree root should differ from full root", ); let f0_log = deposit_cache .get_log((quarter - 1) as usize) .cloned() .expect("should return log"); let f0_block = fake_eth1_block(&deposit_cache, (quarter - 1) as usize) .expect("fake eth1 block should be created"); // finalize first quarter deposit_cache .finalize(f0_block) .expect("should finalize first quarter"); // finalized count and block number should match log assert_eq!( deposit_cache.finalized_deposit_count, f0_log.index + 1, "after calling finalize(eth1block) finalized_deposit_count should equal eth1_block.deposit_count", ); assert_eq!( deposit_cache.finalized_block_height, f0_log.block_number, "after calling finalize(eth1block) finalized_block_number should equal eth1block.block_number" ); // check get_log boundaries assert!( deposit_cache.get_log((quarter - 1) as usize).is_none(), "get_log() should return None for index <= finalized log index", ); assert!( deposit_cache.get_log(quarter as usize).is_some(), "get_log() should return Some(log) for index >= finalized_deposit_count", ); // full root should remain the same after finalization assert_eq!( full_root_before_finalization, deposit_cache.deposit_tree.root(), "root should be the same before and after finalization", ); // get_root should return the same root before and after finalization assert_eq!( q3_root_before_finalization, deposit_cache .get_root((half + quarter) as usize) .cloned() .expect("root should exist"), "get_root should return the same root before and after finalization", ); // get_log should return the same log before and after finalization assert_eq!( q3_log_before_finalization, deposit_cache .get_log((half + quarter) as usize) .cloned() .expect("log should exist"), "get_log should return the same log before and after finalization", ); // again get lower quarter of deposits with max deposit count after finalization let (f0_lower_quarter_root, f0_lower_quarter_deposits) = deposit_cache .get_deposits(quarter, half, n) .expect("should get lower quarter"); assert_eq!( f0_lower_quarter_deposits.len(), quarter as usize, "should get {} deposits from lower quarter", quarter, ); // again get upper quarter of deposits with slightly reduced deposit count after finalization let (f0_upper_quarter_root, f0_upper_quarter_deposits) = deposit_cache .get_deposits(half, half + quarter, n - 2) .expect("should get upper quarter"); assert_eq!( f0_upper_quarter_deposits.len(), quarter as usize, "should get {} deposits from upper quarter", quarter, ); // lower quarter root and deposits should be the same assert_eq!( lower_quarter_root_before_finalization, f0_lower_quarter_root, "root should be the same before and after finalization", ); for i in 0..lower_quarter_deposits_before_finalization.len() { assert_eq!( lower_quarter_deposits_before_finalization[i], f0_lower_quarter_deposits[i], "get_deposits() should be the same before and after finalization", ); } // upper quarter root and deposits should be the same assert_eq!( upper_quarter_root_before_finalization, f0_upper_quarter_root, "subtree root should be the same before and after finalization", ); for i in 0..upper_quarter_deposits_before_finalization.len() { assert_eq!( upper_quarter_deposits_before_finalization[i], f0_upper_quarter_deposits[i], "get_deposits() should be the same before and after finalization", ); } let f1_log = deposit_cache .get_log((half - 2) as usize) .cloned() .expect("should return log"); // finalize a little less than half to test multiple finalization let f1_block = fake_eth1_block(&deposit_cache, (half - 2) as usize) .expect("should create fake eth1 block"); deposit_cache .finalize(f1_block) .expect("should finalize a little less than half"); // finalized count and block number should match f1_log assert_eq!( deposit_cache.finalized_deposit_count, f1_log.index + 1, "after calling finalize(eth1block) finalized_deposit_count should equal eth1_block.deposit_count", ); assert_eq!( deposit_cache.finalized_block_height, f1_log.block_number, "after calling finalize(eth1block) finalized_block_number should equal eth1block.block_number" ); // check get_log boundaries assert!( deposit_cache.get_log((half - 2) as usize).is_none(), "get_log() should return None for index <= finalized log index", ); assert!( deposit_cache.get_log((half - 1) as usize).is_some(), "get_log() should return Some(log) for index >= finalized_deposit_count", ); // full root should still be unchanged assert_eq!( full_root_before_finalization, deposit_cache.deposit_tree.root(), "root should be the same before and after finalization", ); // again get upper quarter of deposits with slightly reduced deposit count after second finalization let (f1_upper_quarter_root, f1_upper_quarter_deposits) = deposit_cache .get_deposits(half, half + quarter, n - 2) .expect("should get upper quarter"); // upper quarter root and deposits should be the same after second finalization assert_eq!( f0_upper_quarter_root, f1_upper_quarter_root, "subtree root should be the same after multiple finalization", ); for i in 0..f0_upper_quarter_deposits.len() { assert_eq!( f0_upper_quarter_deposits[i], f1_upper_quarter_deposits[i], "get_deposits() should be the same before and after finalization", ); } } fn verify_equality(original: &DepositCache, copy: &DepositCache) { // verify each field individually so that if one field should // fail to recover, this test will point right to it assert_eq!(original.deposit_contract_deploy_block, copy.deposit_contract_deploy_block, "DepositCache: deposit_contract_deploy_block should remain the same after encoding and decoding from ssz" ); assert_eq!( original.leaves, copy.leaves, "DepositCache: leaves should remain the same after encoding and decoding from ssz" ); assert_eq!( original.logs, copy.logs, "DepositCache: logs should remain the same after encoding and decoding from ssz" ); assert_eq!(original.finalized_deposit_count, copy.finalized_deposit_count, "DepositCache: finalized_deposit_count should remain the same after encoding and decoding from ssz"); assert_eq!(original.finalized_block_height, copy.finalized_block_height, "DepositCache: finalized_block_height should remain the same after encoding and decoding from ssz"); assert_eq!(original.deposit_roots, copy.deposit_roots, "DepositCache: deposit_roots should remain the same before and after encoding and decoding from ssz"); assert!(original.deposit_tree == copy.deposit_tree, "DepositCache: deposit_tree should remain the same before and after encoding and decoding from ssz"); // verify all together for good measure assert!( original == copy, "Deposit cache should remain the same after encoding and decoding from ssz" ); } fn ssz_round_trip(original: &DepositCache) -> DepositCache { use ssz::{Decode, Encode}; let bytes = SszDepositCache::from_deposit_cache(original).as_ssz_bytes(); let ssz_cache = SszDepositCache::from_ssz_bytes(&bytes).expect("should decode from ssz bytes"); SszDepositCache::to_deposit_cache(&ssz_cache).expect("should recover cache") } #[test] fn ssz_encode_decode() { let deposit_cache = get_cache_with_deposits(512); let recovered_cache = ssz_round_trip(&deposit_cache); verify_equality(&deposit_cache, &recovered_cache); } #[test] fn ssz_encode_decode_with_finalization() { let mut deposit_cache = get_cache_with_deposits(512); let block383 = fake_eth1_block(&deposit_cache, 383).expect("should create fake eth1 block"); deposit_cache.finalize(block383).expect("should finalize"); let mut first_recovery = ssz_round_trip(&deposit_cache); verify_equality(&deposit_cache, &first_recovery); // finalize again to verify equality after multiple finalizations let block447 = fake_eth1_block(&deposit_cache, 447).expect("should create fake eth1 block"); first_recovery.finalize(block447).expect("should finalize"); let mut second_recovery = ssz_round_trip(&first_recovery); verify_equality(&first_recovery, &second_recovery); // verify equality of a tree that finalized block383, block447, block479 // with a tree that finalized block383, block479 let block479 = fake_eth1_block(&deposit_cache, 479).expect("should create fake eth1 block"); second_recovery .finalize(block479.clone()) .expect("should finalize"); let third_recovery = ssz_round_trip(&second_recovery); deposit_cache.finalize(block479).expect("should finalize"); verify_equality(&deposit_cache, &third_recovery); } }