diff --git a/Cargo.lock b/Cargo.lock index 31e9ec63..722a4774 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,6 +829,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -1106,7 +1112,7 @@ dependencies = [ "libc", "redox_syscall", "rustc_version", - "smallvec", + "smallvec 0.6.13", "winapi 0.3.8", ] @@ -1552,6 +1558,12 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "smallvec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" + [[package]] name = "socket2" version = "0.3.12" @@ -1564,6 +1576,19 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "spandoc" +version = "0.1.3" +source = "git+https://github.com/yaahc/spandoc.git#554358be632b156a6f0af963b0b244e2665b4767" +dependencies = [ + "matches", + "proc-macro2 1.0.9", + "quote 1.0.3", + "syn 1.0.17", + "tracing", + "tracing-futures", +] + [[package]] name = "stable_deref_trait" version = "1.1.1" @@ -1931,6 +1956,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ccba2f8f16e0ed268fc765d9b7ff22e965e7185d32f8f1ec8294fe17d86e79" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.1.6" @@ -1943,7 +1978,7 @@ dependencies = [ "matchers", "owning_ref", "regex", - "smallvec", + "smallvec 0.6.13", "tracing-core", "tracing-log", ] @@ -1954,8 +1989,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d53c40489aa69c9aed21ff483f26886ca8403df33bdc2d2f87c60c1617826d2" dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", "sharded-slab", + "smallvec 1.4.0", "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -2160,8 +2205,13 @@ dependencies = [ "futures", "hex", "lazy_static", + "spandoc", "tokio", "tower", + "tracing", + "tracing-error", + "tracing-futures", + "tracing-subscriber 0.2.5", "zebra-chain", "zebra-test-vectors", ] diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index fc7b3fbe..51527e3e 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -20,3 +20,8 @@ color-eyre = "0.3.4" eyre = "0.4.2" tokio = { version = "0.2.21", features = ["full"] } zebra-test-vectors = { path = "../zebra-test-vectors/" } +spandoc = { git = "https://github.com/yaahc/spandoc.git" } +tracing = "0.1.15" +tracing-futures = "0.2.4" +tracing-error = "0.1.2" +tracing-subscriber = "0.2.5" diff --git a/zebra-state/src/in_memory.rs b/zebra-state/src/in_memory.rs new file mode 100644 index 00000000..0309d783 --- /dev/null +++ b/zebra-state/src/in_memory.rs @@ -0,0 +1,67 @@ +use super::{Request, Response}; +use futures::prelude::*; +use std::{ + error::Error, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use tower::{buffer::Buffer, Service}; + +mod block_index; + +#[derive(Default)] +struct ZebraState { + index: block_index::BlockIndex, +} + +impl Service for ZebraState { + type Response = Response; + type Error = Box; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + match req { + Request::AddBlock { block } => { + let result = self.index.insert(block).map(|_| Response::Added); + + async { result }.boxed() + } + Request::GetBlock { hash } => { + let result = self + .index + .get(hash) + .map(|block| Response::Block { block }) + .ok_or_else(|| "block could not be found".into()); + + async move { result }.boxed() + } + Request::GetTip => { + let result = self + .index + .get_tip() + .map(|block| block.as_ref().into()) + .map(|hash| Response::Tip { hash }) + .ok_or_else(|| "zebra-state contains no blocks".into()); + + async move { result }.boxed() + } + } + } +} + +pub fn init() -> impl Service< + Request, + Response = Response, + Error = Box, + Future = impl Future>>, +> + Send + + Clone + + 'static { + Buffer::new(ZebraState::default(), 1) +} diff --git a/zebra-state/src/in_memory/block_index.rs b/zebra-state/src/in_memory/block_index.rs new file mode 100644 index 00000000..87a0c1de --- /dev/null +++ b/zebra-state/src/in_memory/block_index.rs @@ -0,0 +1,67 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap, HashMap}, + error::Error, + sync::Arc, +}; +use zebra_chain::{ + block::{Block, BlockHeaderHash}, + types::BlockHeight, +}; +#[derive(Default)] +pub(super) struct BlockIndex { + by_hash: HashMap>, + by_height: BTreeMap>, +} + +impl BlockIndex { + pub(super) fn insert( + &mut self, + block: impl Into>, + ) -> Result<(), Box> { + let block = block.into(); + let hash = block.as_ref().into(); + let height = block.coinbase_height().unwrap(); + + match self.by_height.entry(height) { + Entry::Vacant(entry) => { + let _ = entry.insert(block.clone()); + let _ = self.by_hash.insert(hash, block); + Ok(()) + } + Entry::Occupied(_) => Err("forks in the chain aren't supported yet")?, + } + } + + pub(super) fn get(&mut self, query: impl Into) -> Option> { + match query.into() { + BlockQuery::ByHash(hash) => self.by_hash.get(&hash), + BlockQuery::ByHeight(height) => self.by_height.get(&height), + } + .cloned() + } + + pub(super) fn get_tip(&self) -> Option> { + self.by_height + .iter() + .next_back() + .map(|(_key, value)| value) + .cloned() + } +} + +pub(super) enum BlockQuery { + ByHash(BlockHeaderHash), + ByHeight(BlockHeight), +} + +impl From for BlockQuery { + fn from(hash: BlockHeaderHash) -> Self { + Self::ByHash(hash) + } +} + +impl From for BlockQuery { + fn from(height: BlockHeight) -> Self { + Self::ByHeight(height) + } +} diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 6c7e58f0..1e7f8f8f 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -1,81 +1,24 @@ #![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")] #![doc(html_root_url = "https://doc.zebra.zfnd.org/zebra_state")] -use futures::prelude::*; -use std::{ - collections::HashMap, - error::Error, - future::Future, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; -use tower::{buffer::Buffer, Service}; +#![allow(clippy::try_err)] +use std::sync::Arc; use zebra_chain::block::{Block, BlockHeaderHash}; +pub mod in_memory; + #[derive(Debug)] pub enum Request { // TODO(jlusby): deprecate in the future based on our validation story AddBlock { block: Arc }, GetBlock { hash: BlockHeaderHash }, + GetTip, } #[derive(Debug)] pub enum Response { Added, Block { block: Arc }, -} - -pub mod in_memory { - use super::*; - use std::error::Error; - - pub fn init() -> impl Service< - Request, - Response = Response, - Error = Box, - Future = impl Future>>, - > + Send - + Clone - + 'static { - Buffer::new(ZebraState::default(), 1) - } -} - -#[derive(Default)] -struct ZebraState { - blocks: HashMap>, -} - -impl Service for ZebraState { - type Response = Response; - type Error = Box; - type Future = - Pin> + Send + 'static>>; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - match req { - Request::AddBlock { block } => { - let hash = block.as_ref().into(); - self.blocks.insert(hash, block); - - async { Ok(Response::Added) }.boxed() - } - Request::GetBlock { hash } => { - let result = self - .blocks - .get(&hash) - .cloned() - .map(|block| Response::Block { block }) - .ok_or_else(|| "block could not be found".into()); - - async move { result }.boxed() - } - } - } + Tip { hash: BlockHeaderHash }, } #[cfg(test)] @@ -83,8 +26,26 @@ mod tests { use super::*; use color_eyre::Report; use eyre::{bail, ensure, eyre}; + use tower::Service; use zebra_chain::serialization::ZcashDeserialize; + fn install_tracing() { + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; + + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); + } + #[tokio::test] async fn round_trip() -> Result<(), Report> { let block: Arc<_> = @@ -120,4 +81,55 @@ mod tests { Ok(()) } + + #[tokio::test] + #[spandoc::spandoc] + async fn get_tip() -> Result<(), Report> { + install_tracing(); + + let block0: Arc<_> = + Block::zcash_deserialize(&zebra_test_vectors::BLOCK_MAINNET_GENESIS_BYTES[..])?.into(); + let block1: Arc<_> = + Block::zcash_deserialize(&zebra_test_vectors::BLOCK_MAINNET_1_BYTES[..])?.into(); + + let expected_hash: BlockHeaderHash = block1.as_ref().into(); + + let mut service = in_memory::init(); + + /// insert the higher block first + let response = service + .call(Request::AddBlock { block: block1 }) + .await + .map_err(|e| eyre!(e))?; + + ensure!( + matches!(response, Response::Added), + "unexpected response kind: {:?}", + response + ); + + /// genesis block second + let response = service + .call(Request::AddBlock { + block: block0.clone(), + }) + .await + .map_err(|e| eyre!(e))?; + + ensure!( + matches!(response, Response::Added), + "unexpected response kind: {:?}", + response + ); + + let block_response = service.call(Request::GetTip).await.map_err(|e| eyre!(e))?; + + /// assert that the higher block is returned as the tip even tho it was least recently inserted + match block_response { + Response::Tip { hash } => assert_eq!(expected_hash, hash), + _ => bail!("unexpected response kind: {:?}", block_response), + } + + Ok(()) + } }