use ssz_derive::{Decode, Encode}; use std::collections::HashMap; use std::ops::RangeInclusive; pub use eth2::lighthouse::Eth1Block; use eth2::types::Hash256; use std::sync::Arc; #[derive(Debug, PartialEq, Clone)] pub enum Error { /// The timestamp of each block equal to or later than the block prior to it. InconsistentTimestamp { parent: u64, child: u64 }, /// Some `Eth1Block` was provided with the same block number but different data. The source /// of eth1 data is inconsistent. Conflicting(u64), /// The given block was not one block number higher than the higest known block number. NonConsecutive { given: u64, expected: u64 }, /// Some invariant was violated, there is a likely bug in the code. Internal(String), } /// Stores block and deposit contract information and provides queries based upon the block /// timestamp. #[derive(Debug, PartialEq, Clone, Default, Encode, Decode)] pub struct BlockCache { blocks: Vec>, #[ssz(skip_serializing, skip_deserializing)] by_hash: HashMap>, } impl BlockCache { /// Returns the number of blocks stored in `self`. pub fn len(&self) -> usize { self.blocks.len() } /// True if the cache does not store any blocks. pub fn is_empty(&self) -> bool { self.blocks.is_empty() } /// Returns the earliest (lowest timestamp) block, if any. pub fn earliest_block(&self) -> Option<&Eth1Block> { self.blocks.first().map(|ptr| ptr.as_ref()) } /// Returns the latest (highest timestamp) block, if any. pub fn latest_block(&self) -> Option<&Eth1Block> { self.blocks.last().map(|ptr| ptr.as_ref()) } /// Returns the timestamp of the earliest block in the cache (if any). pub fn earliest_block_timestamp(&self) -> Option { self.blocks.first().map(|block| block.timestamp) } /// Returns the timestamp of the latest block in the cache (if any). pub fn latest_block_timestamp(&self) -> Option { self.blocks.last().map(|block| block.timestamp) } /// Returns the lowest block number stored. pub fn lowest_block_number(&self) -> Option { self.blocks.first().map(|block| block.number) } /// Returns the highest block number stored. pub fn highest_block_number(&self) -> Option { self.blocks.last().map(|block| block.number) } /// Returns an iterator over all blocks. /// /// Blocks a guaranteed to be returned with; /// /// - Monotonically increasing block numbers. /// - Non-uniformly increasing block timestamps. pub fn iter(&self) -> impl DoubleEndedIterator + Clone { self.blocks.iter().map(|ptr| ptr.as_ref()) } /// Shortens the cache, keeping the latest (by block number) `len` blocks while dropping the /// rest. /// /// If `len` is greater than the vector's current length, this has no effect. pub fn truncate(&mut self, len: usize) { if len < self.blocks.len() { let remaining = self.blocks.split_off(self.blocks.len() - len); for block in &self.blocks { self.by_hash.remove(&block.hash); } self.blocks = remaining; } } /// Returns the range of block numbers stored in the block cache. All blocks in this range can /// be accessed. fn available_block_numbers(&self) -> Option> { Some(self.blocks.first()?.number..=self.blocks.last()?.number) } /// Returns a block with the corresponding number, if any. pub fn block_by_number(&self, block_number: u64) -> Option<&Eth1Block> { self.blocks .get( self.blocks .as_slice() .binary_search_by(|block| block.number.cmp(&block_number)) .ok()?, ) .map(|ptr| ptr.as_ref()) } /// Returns a block with the corresponding hash, if any. pub fn block_by_hash(&self, block_hash: &Hash256) -> Option<&Eth1Block> { self.by_hash.get(block_hash).map(|ptr| ptr.as_ref()) } /// Rebuilds the by_hash map pub fn rebuild_by_hash_map(&mut self) { self.by_hash.clear(); for block in self.blocks.iter() { self.by_hash.insert(block.hash, block.clone()); } } /// Insert an `Eth1Snapshot` into `self`, allowing future queries. /// /// Allows inserting either: /// /// - The root block (i.e., any block if there are no existing blocks), or, /// - An immediate child of the most recent (highest block number) block. /// /// ## Errors /// /// - If the cache is not empty and `item.block.block_number - 1` is not already in `self`. /// - If `item.block.block_number` is in `self`, but is not identical to the supplied /// `Eth1Snapshot`. /// - If `item.block.timestamp` is prior to the parent. pub fn insert_root_or_child(&mut self, block: Eth1Block) -> Result<(), Error> { let expected_block_number = self .highest_block_number() .map(|n| n + 1) .unwrap_or_else(|| block.number); // If there are already some cached blocks, check to see if the new block number is one of // them. // // If the block is already known, check to see the given block is identical to it. If not, // raise an inconsistency error. This is mostly likely caused by some fork on the eth1 // chain. if let Some(local) = self.available_block_numbers() { if local.contains(&block.number) { let known_block = self.block_by_number(block.number).ok_or_else(|| { Error::Internal("An expected block was not present".to_string()) })?; if known_block == &block { return Ok(()); } else { return Err(Error::Conflicting(block.number)); }; } } // Only permit blocks when it's either: // // - The first block inserted. // - Exactly one block number higher than the highest known block number. if block.number != expected_block_number { return Err(Error::NonConsecutive { given: block.number, expected: expected_block_number, }); } // If the block is not the first block inserted, ensure that its timestamp is not higher // than its parents. if let Some(previous_block) = self.blocks.last() { if previous_block.timestamp > block.timestamp { return Err(Error::InconsistentTimestamp { parent: previous_block.timestamp, child: block.timestamp, }); } } let ptr = Arc::new(block); self.by_hash.insert(ptr.hash, ptr.clone()); self.blocks.push(ptr); Ok(()) } } #[cfg(test)] mod tests { use super::*; use types::Hash256; fn get_block(i: u64, interval_secs: u64) -> Eth1Block { Eth1Block { hash: Hash256::from_low_u64_be(i), timestamp: i * interval_secs, number: i, deposit_root: Some(Hash256::from_low_u64_be(i << 32)), deposit_count: Some(i), } } fn get_blocks(n: usize, interval_secs: u64) -> Vec { (0..n as u64).map(|i| get_block(i, interval_secs)).collect() } fn insert(cache: &mut BlockCache, s: Eth1Block) -> Result<(), Error> { cache.insert_root_or_child(s) } #[test] fn truncate() { let n = 16; let blocks = get_blocks(n, 10); let mut cache = BlockCache::default(); for block in blocks { insert(&mut cache, block.clone()).expect("should add consecutive blocks"); } for len in &[0, 1, 2, 3, 4, 8, 15, 16] { let mut cache = cache.clone(); cache.truncate(*len); assert_eq!( cache.blocks.len(), *len, "should truncate to length: {}", *len ); } let mut cache_2 = cache; cache_2.truncate(17); assert_eq!( cache_2.blocks.len(), n, "truncate to larger than n should be a no-op" ); } #[test] fn inserts() { let n = 16; let blocks = get_blocks(n, 10); let mut cache = BlockCache::default(); for block in blocks { insert(&mut cache, block.clone()).expect("should add consecutive blocks"); } // No error for re-adding a block identical to one that exists. assert!(insert(&mut cache, get_block(n as u64 - 1, 10)).is_ok()); // Error for re-adding a block that is different to the one that exists. assert!(insert(&mut cache, get_block(n as u64 - 1, 11)).is_err()); // Error for adding non-consecutive blocks. assert!(insert(&mut cache, get_block(n as u64 + 1, 10)).is_err()); assert!(insert(&mut cache, get_block(n as u64 + 2, 10)).is_err()); // Error for adding timestamp prior to previous. assert!(insert(&mut cache, get_block(n as u64, 1)).is_err()); // Double check to make sure previous test was only affected by timestamp. assert!(insert(&mut cache, get_block(n as u64, 10)).is_ok()); } #[test] fn duplicate_timestamp() { let mut blocks = get_blocks(7, 10); blocks[0].timestamp = 0; blocks[1].timestamp = 10; blocks[2].timestamp = 10; blocks[3].timestamp = 20; blocks[4].timestamp = 30; blocks[5].timestamp = 40; blocks[6].timestamp = 40; let mut cache = BlockCache::default(); for block in &blocks { insert(&mut cache, block.clone()) .expect("should add consecutive blocks with duplicate timestamps"); } let blocks = blocks.into_iter().map(Arc::new).collect::>(); assert_eq!(cache.blocks, blocks, "should have added all blocks"); } }