diff --git a/Cargo.lock b/Cargo.lock index f1e6b59b..6e6231e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-epoch" version = "0.8.2" @@ -354,7 +363,7 @@ checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" dependencies = [ "byteorder", "digest", - "rand_core", + "rand_core 0.5.1", "subtle", "zeroize", ] @@ -416,7 +425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb6195ea92e78e243aef73daa954f7afa0f018cd5bad78c39b7f141244eb78b3" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.5.1", "serde", "sha2", "thiserror", @@ -443,6 +452,22 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -554,6 +579,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generational-arena" version = "0.2.7" @@ -791,9 +825,9 @@ checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" [[package]] name = "lock_api" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" dependencies = [ "scopeguard", ] @@ -932,7 +966,7 @@ dependencies = [ "metrics-observer-prometheus", "metrics-observer-yaml", "metrics-util", - "parking_lot", + "parking_lot 0.9.0", "quanta", ] @@ -1075,10 +1109,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.6.2", "rustc_version", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core 0.7.2", +] + [[package]] name = "parking_lot_core" version = "0.6.2" @@ -1094,6 +1138,20 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec 1.4.0", + "winapi 0.3.8", +] + [[package]] name = "pin-project" version = "0.4.22" @@ -1174,7 +1232,7 @@ dependencies = [ "lazy_static", "num-traits", "quick-error", - "rand", + "rand 0.7.3", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -1227,6 +1285,19 @@ dependencies = [ "proc-macro2 1.0.9", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.8", +] + [[package]] name = "rand" version = "0.7.3" @@ -1236,7 +1307,7 @@ dependencies = [ "getrandom", "libc", "rand_chacha", - "rand_core", + "rand_core 0.5.1", "rand_hc", ] @@ -1247,9 +1318,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -1265,7 +1351,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", ] [[package]] @@ -1274,7 +1360,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -1285,7 +1380,7 @@ checksum = "05af26cd84133b56adf15179b564320ace234fa926b7a8c5fa8e391ffb65359f" dependencies = [ "blake2b_simd", "jubjub", - "rand_core", + "rand_core 0.5.1", "serde", "thiserror", ] @@ -1536,6 +1631,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "sled" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb6824dde66ad33bf20c6e8476f5b82b871bc8bc3c129a10ea2f7dae5060fa3" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils 0.7.2", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.10.2", +] + [[package]] name = "smallvec" version = "0.6.13" @@ -1622,6 +1733,16 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -1630,7 +1751,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ "cfg-if", "libc", - "rand", + "rand 0.7.3", "redox_syscall", "remove_dir_all", "winapi 0.3.8", @@ -2076,7 +2197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.5.1", "zeroize", ] @@ -2106,7 +2227,7 @@ dependencies = [ "lazy_static", "proptest", "proptest-derive", - "rand_core", + "rand_core 0.5.1", "redjubjub", "ripemd160", "secp256k1", @@ -2154,7 +2275,7 @@ dependencies = [ "pin-project", "proptest", "proptest-derive", - "rand", + "rand 0.7.3", "serde", "thiserror", "tokio", @@ -2183,7 +2304,10 @@ dependencies = [ "futures", "hex", "lazy_static", + "serde", + "sled", "spandoc", + "tempdir", "tokio", "tower", "tracing", @@ -2216,7 +2340,7 @@ dependencies = [ "metrics", "metrics-runtime", "once_cell", - "rand", + "rand 0.7.3", "serde", "thiserror", "tokio", diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 51527e3e..aada2385 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -14,6 +14,8 @@ eyre = "0.4.2" futures = "0.3.5" lazy_static = "1.4.0" hex = "0.4.2" +sled = "0.31.0" +serde = { version = "1", features = ["serde_derive"] } [dev-dependencies] color-eyre = "0.3.4" @@ -25,3 +27,4 @@ tracing = "0.1.15" tracing-futures = "0.2.4" tracing-error = "0.1.2" tracing-subscriber = "0.2.5" +tempdir = "0.3.7" diff --git a/zebra-state/src/in_memory.rs b/zebra-state/src/in_memory.rs index 81ae72c2..7f991c05 100644 --- a/zebra-state/src/in_memory.rs +++ b/zebra-state/src/in_memory.rs @@ -1,3 +1,8 @@ +//! A basic implementation of the zebra-state service entirely in memory +//! +//! This service is provided as an independent implementation of the +//! zebra-state service to use in verifying the correctness of `on_disk`'s +//! `Service` implementation. use super::{Request, Response}; use futures::prelude::*; use std::{ @@ -11,13 +16,13 @@ use tower::{buffer::Buffer, Service}; mod block_index; #[derive(Default)] -struct ZebraState { +struct InMemoryState { index: block_index::BlockIndex, } type Error = Box; -impl Service for ZebraState { +impl Service for InMemoryState { type Response = Response; type Error = Error; type Future = @@ -59,6 +64,8 @@ impl Service for ZebraState { } } +/// Return's a type that implement's the `zebra_state::Service` entirely in +/// memory using `HashMaps` pub fn init() -> impl Service< Request, Response = Response, @@ -67,5 +74,5 @@ pub fn init() -> impl Service< > + Send + Clone + 'static { - Buffer::new(ZebraState::default(), 1) + Buffer::new(InMemoryState::default(), 1) } diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 6dd2723d..2d341446 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -1,24 +1,84 @@ +//! State storage code for Zebra. 🦓 +//! +//! ## Organizational Structure +//! +//! zebra-state tracks `Blocks` using two key-value trees +//! +//! * BlockHeaderHash -> Block +//! * BlockHeight -> Block +//! +//! Inserting a block into the service will create a mapping in each tree for that block. #![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")] #![doc(html_root_url = "https://doc.zebra.zfnd.org/zebra_state")] +#![warn(missing_docs)] #![allow(clippy::try_err)] +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use std::sync::Arc; use zebra_chain::block::{Block, BlockHeaderHash}; pub mod in_memory; +pub mod on_disk; + +/// Configuration for networking code. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + /// The root directory for the state storage + pub path: PathBuf, +} + +impl Config { + pub(crate) fn sled_config(&self) -> sled::Config { + sled::Config::default().path(&self.path) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + path: PathBuf::from("./.zebra-state"), + } + } +} #[derive(Debug, PartialEq)] +/// A state request, used to manipulate the zebra-state on disk or in memory pub enum Request { // TODO(jlusby): deprecate in the future based on our validation story - AddBlock { block: Arc }, - GetBlock { hash: BlockHeaderHash }, + /// Add a block to the zebra-state + AddBlock { + /// The block to be added to the state + block: Arc, + }, + /// Get a block from the zebra-state + GetBlock { + /// The hash used to identify the block + hash: BlockHeaderHash, + }, + /// Get the block that is the tip of the current chain GetTip, } #[derive(Debug, PartialEq)] +/// A state response pub enum Response { - Added { hash: BlockHeaderHash }, - Block { block: Arc }, - Tip { hash: BlockHeaderHash }, + /// A response to a `AddBlock` request indicating a block was successfully + /// added to the state + Added { + /// The hash of the block that was added + hash: BlockHeaderHash, + }, + /// The response to a `GetBlock` request by hash + Block { + /// The block that was requested + block: Arc, + }, + /// The response to a `GetTip` request + Tip { + /// The hash of the block at the tip of the current chain + hash: BlockHeaderHash, + }, } #[cfg(test)] @@ -26,34 +86,58 @@ mod tests { use super::*; use color_eyre::Report; use eyre::{bail, ensure, eyre}; + use std::sync::Once; use tower::Service; + use tracing_error::ErrorLayer; + use tracing_subscriber::prelude::*; + use tracing_subscriber::{fmt, EnvFilter}; use zebra_chain::serialization::ZcashDeserialize; + static LOGGER_INIT: Once = Once::new(); + fn install_tracing() { - use tracing_error::ErrorLayer; - use tracing_subscriber::prelude::*; - use tracing_subscriber::{fmt, EnvFilter}; + LOGGER_INIT.call_once(|| { + let fmt_layer = fmt::layer().with_target(false); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); - 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(); + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .with(ErrorLayer::default()) + .init(); + }) } #[tokio::test] - async fn round_trip() -> Result<(), Report> { + async fn test_round_trip() -> Result<(), Report> { + install_tracing(); + + let service = in_memory::init(); + round_trip(service).await?; + + let mut config = crate::Config::default(); + let tmp_dir = tempdir::TempDir::new("round_trip")?; + config.path = tmp_dir.path().to_owned(); + let service = on_disk::init(config); + get_tip(service).await?; + + Ok(()) + } + + async fn round_trip(mut service: S) -> Result<(), Report> + where + S: Service< + Request, + Error = Box, + Response = Response, + >, + { let block: Arc<_> = Block::zcash_deserialize(&zebra_test_vectors::BLOCK_MAINNET_415000_BYTES[..])?.into(); let hash = block.as_ref().into(); - let mut service = in_memory::init(); - let response = service .call(Request::AddBlock { block: block.clone(), @@ -83,8 +167,30 @@ mod tests { } #[tokio::test] + async fn test_get_tip() -> Result<(), Report> { + install_tracing(); + + let service = in_memory::init(); + get_tip(service).await?; + + let mut config = crate::Config::default(); + let tmp_dir = tempdir::TempDir::new("get_tip")?; + config.path = tmp_dir.path().to_owned(); + let service = on_disk::init(config); + get_tip(service).await?; + + Ok(()) + } + #[spandoc::spandoc] - async fn get_tip() -> Result<(), Report> { + async fn get_tip(mut service: S) -> Result<(), Report> + where + S: Service< + Request, + Error = Box, + Response = Response, + >, + { install_tracing(); let block0: Arc<_> = @@ -96,8 +202,6 @@ mod tests { let block1_hash: BlockHeaderHash = block1.as_ref().into(); let expected_hash: BlockHeaderHash = block1_hash; - let mut service = in_memory::init(); - /// insert the higher block first let response = service .call(Request::AddBlock { block: block1 }) diff --git a/zebra-state/src/on_disk.rs b/zebra-state/src/on_disk.rs new file mode 100644 index 00000000..1a534944 --- /dev/null +++ b/zebra-state/src/on_disk.rs @@ -0,0 +1,172 @@ +//! The primary implementation of the `zebra_state::Service` built upon sled +use super::{Request, Response}; +use crate::Config; +use futures::prelude::*; +use std::sync::Arc; +use std::{ + error, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use tower::{buffer::Buffer, Service}; +use zebra_chain::serialization::{ZcashDeserialize, ZcashSerialize}; +use zebra_chain::{ + block::{Block, BlockHeaderHash}, + types::BlockHeight, +}; + +#[derive(Clone)] +struct SledState { + storage: sled::Db, +} + +impl SledState { + pub(crate) fn new(config: &Config) -> Self { + let config = config.sled_config(); + + Self { + storage: config.open().unwrap(), + } + } + + pub(super) fn insert( + &mut self, + block: impl Into>, + ) -> Result { + let block = block.into(); + let hash: BlockHeaderHash = block.as_ref().into(); + let height = block.coinbase_height().unwrap(); + + let by_height = self.storage.open_tree(b"by_height")?; + let by_hash = self.storage.open_tree(b"by_hash")?; + + let mut bytes = Vec::new(); + block.zcash_serialize(&mut bytes)?; + + // TODO(jlusby): make this transactional + by_height.insert(&height.0.to_be_bytes(), bytes.as_slice())?; + by_hash.insert(&hash.0, bytes)?; + + Ok(hash) + } + + pub(super) fn get(&self, query: impl Into) -> Result>, Error> { + let query = query.into(); + let value = match query { + BlockQuery::ByHash(hash) => { + let by_hash = self.storage.open_tree(b"by_hash")?; + let key = &hash.0; + by_hash.get(key)? + } + BlockQuery::ByHeight(height) => { + let by_height = self.storage.open_tree(b"by_height")?; + let key = height.0.to_be_bytes(); + by_height.get(key)? + } + }; + + if let Some(bytes) = value { + let bytes = bytes.as_ref(); + let block = ZcashDeserialize::zcash_deserialize(bytes)?; + Ok(Some(block)) + } else { + Ok(None) + } + } + + pub(super) fn get_tip(&self) -> Result, Error> { + let tree = self.storage.open_tree(b"by_height")?; + let last_entry = tree.iter().values().next_back(); + + match last_entry { + Some(Ok(bytes)) => { + let block = Arc::::zcash_deserialize(bytes.as_ref())?; + Ok(Some(block.as_ref().into())) + } + Some(Err(e)) => Err(e)?, + None => Ok(None), + } + } +} + +impl Default for SledState { + fn default() -> Self { + let config = crate::Config::default(); + Self::new(&config) + } +} + +impl Service for SledState { + type Response = Response; + type Error = Error; + 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 mut storage = self.clone(); + + async move { storage.insert(block).map(|hash| Response::Added { hash }) }.boxed() + } + Request::GetBlock { hash } => { + let storage = self.clone(); + async move { + storage + .get(hash)? + .map(|block| Response::Block { block }) + .ok_or_else(|| "block could not be found".into()) + } + .boxed() + } + Request::GetTip => { + let storage = self.clone(); + async move { + storage + .get_tip()? + .map(|hash| Response::Tip { hash }) + .ok_or_else(|| "zebra-state contains no blocks".into()) + } + .boxed() + } + } + } +} + +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) + } +} + +/// Return's a type that implement's the `zebra_state::Service` using `sled` +pub fn init( + config: Config, +) -> impl Service< + Request, + Response = Response, + Error = Error, + Future = impl Future>, +> + Send + + Clone + + 'static { + Buffer::new(SledState::new(&config), 1) +} + +type Error = Box;