7afbaa807e
## Issue Addressed - Related to #1691 ## Proposed Changes Adds the following API endpoints: - `GET lighthouse/eth1/syncing`: status about how synced we are with Eth1. - `GET lighthouse/eth1/block_cache`: all locally cached eth1 blocks. - `GET lighthouse/eth1/deposit_cache`: all locally cached eth1 deposits. Additionally: - Moves some types from the `beacon_node/eth1` to the `common/eth2` crate, so they can be used in the API without duplication. - Allow `update_deposit_cache` and `update_block_cache` to take an optional head block number to avoid duplicate requests. ## Additional Info TBC
275 lines
9.0 KiB
Rust
275 lines
9.0 KiB
Rust
use ssz_derive::{Decode, Encode};
|
|
use std::ops::RangeInclusive;
|
|
|
|
pub use eth2::lighthouse::Eth1Block;
|
|
|
|
#[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<Eth1Block>,
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
/// Returns the latest (highest timestamp) block, if any.
|
|
pub fn latest_block(&self) -> Option<&Eth1Block> {
|
|
self.blocks.last()
|
|
}
|
|
|
|
/// Returns the timestamp of the earliest block in the cache (if any).
|
|
pub fn earliest_block_timestamp(&self) -> Option<u64> {
|
|
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<u64> {
|
|
self.blocks.last().map(|block| block.timestamp)
|
|
}
|
|
|
|
/// Returns the lowest block number stored.
|
|
pub fn lowest_block_number(&self) -> Option<u64> {
|
|
self.blocks.first().map(|block| block.number)
|
|
}
|
|
|
|
/// Returns the highest block number stored.
|
|
pub fn highest_block_number(&self) -> Option<u64> {
|
|
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<Item = &Eth1Block> + Clone {
|
|
self.blocks.iter()
|
|
}
|
|
|
|
/// 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() {
|
|
self.blocks = self.blocks.split_off(self.blocks.len() - len);
|
|
}
|
|
}
|
|
|
|
/// 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<RangeInclusive<u64>> {
|
|
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()?,
|
|
)
|
|
}
|
|
|
|
/// 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
self.blocks.push(block);
|
|
|
|
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<Eth1Block> {
|
|
(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");
|
|
}
|
|
|
|
assert_eq!(cache.blocks, blocks, "should have added all blocks");
|
|
}
|
|
}
|