diff --git a/zebra-state/src/config.rs b/zebra-state/src/config.rs new file mode 100644 index 00000000..584063f7 --- /dev/null +++ b/zebra-state/src/config.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use zebra_chain::parameters::Network; + +/// Configuration for the state service. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + /// The root directory for storing cached data. + /// + /// Cached data includes any state that can be replicated from the network + /// (e.g., the chain state, the blocks, the UTXO set, etc.). It does *not* + /// include private data that cannot be replicated from the network, such as + /// wallet data. That data is not handled by `zebra-state`. + /// + /// Each network has a separate state, which is stored in "mainnet/state" + /// and "testnet/state" subdirectories. + /// + /// The default directory is platform dependent, based on + /// [`dirs::cache_dir()`](https://docs.rs/dirs/3.0.1/dirs/fn.cache_dir.html): + /// + /// |Platform | Value | Example | + /// | ------- | ----------------------------------------------- | ---------------------------------- | + /// | Linux | `$XDG_CACHE_HOME/zebra` or `$HOME/.cache/zebra` | /home/alice/.cache/zebra | + /// | macOS | `$HOME/Library/Caches/zebra` | /Users/Alice/Library/Caches/zebra | + /// | Windows | `{FOLDERID_LocalAppData}\zebra` | C:\Users\Alice\AppData\Local\zebra | + /// | Other | `std::env::current_dir()/cache` | | + pub cache_dir: PathBuf, + + /// The maximum number of bytes to use caching data in memory. + pub memory_cache_bytes: u64, + + /// Whether to use an ephemeral database. + /// + /// Ephemeral databases are stored in memory on Linux, and in a temporary directory on other OSes. + /// + /// Set to `false` by default. If this is set to `true`, [`cache_dir`] is ignored. + /// + /// [`cache_dir`]: struct.Config.html#structfield.cache_dir + pub ephemeral: bool, +} + +impl Config { + /// Generate the appropriate `sled::Config` for `network`, based on the + /// provided `zebra_state::Config`. + pub(crate) fn sled_config(&self, network: Network) -> sled::Config { + let net_dir = match network { + Network::Mainnet => "mainnet", + Network::Testnet => "testnet", + }; + + let config = sled::Config::default() + .cache_capacity(self.memory_cache_bytes) + .mode(sled::Mode::LowSpace); + + if self.ephemeral { + config.temporary(self.ephemeral) + } else { + let path = self.cache_dir.join(net_dir).join("state"); + config.path(path) + } + } + + /// Construct a config for an ephemeral in memory database + pub fn ephemeral() -> Self { + let mut config = Self::default(); + config.ephemeral = true; + config + } +} + +impl Default for Config { + fn default() -> Self { + let cache_dir = dirs::cache_dir() + .unwrap_or_else(|| std::env::current_dir().unwrap().join("cache")) + .join("zebra"); + + Self { + cache_dir, + memory_cache_bytes: 512 * 1024 * 1024, + ephemeral: false, + } + } +} diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs new file mode 100644 index 00000000..9143abfe --- /dev/null +++ b/zebra-state/src/constants.rs @@ -0,0 +1,15 @@ +use zebra_chain::block; + +/// The maturity threshold for transparent coinbase outputs. +/// +/// A transaction MUST NOT spend a transparent output of a coinbase transaction +/// from a block less than 100 blocks prior to the spend. Note that transparent +/// outputs of coinbase transactions include Founders' Reward outputs. +pub const MIN_TRASPARENT_COINBASE_MATURITY: block::Height = block::Height(100); + +/// The maximum chain reorganisation height. +/// +/// Allowing reorganisations past this height could allow double-spends of +/// coinbase transactions. +pub const MAX_BLOCK_REORG_HEIGHT: block::Height = + block::Height(MIN_TRASPARENT_COINBASE_MATURITY.0 - 1); diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 2fddca39..033aff7f 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -1,13 +1,4 @@ //! State storage code for Zebra. 🦓 -//! -//! ## Organizational Structure -//! -//! zebra-state tracks `Blocks` using two key-value trees -//! -//! * block::Hash -> Block -//! * Height -> Block -//! -//! Inserting a block into the service will create a mapping in each tree for that block. #![doc(html_favicon_url = "https://www.zfnd.org/images/zebra-favicon-128.png")] #![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")] @@ -15,327 +6,18 @@ #![warn(missing_docs)] #![allow(clippy::try_err)] -use color_eyre::eyre::{eyre, Report}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::{error, iter, sync::Arc}; -use tower::{Service, ServiceExt}; -use zebra_chain::{ - block::{self, Block}, - parameters::Network, -}; - -pub use on_disk::init; - -mod on_disk; - -/// The maturity threshold for transparent coinbase outputs. -/// -/// A transaction MUST NOT spend a transparent output of a coinbase transaction -/// from a block less than 100 blocks prior to the spend. Note that transparent -/// outputs of coinbase transactions include Founders' Reward outputs. -const MIN_TRASPARENT_COINBASE_MATURITY: block::Height = block::Height(100); - -/// The maximum chain reorganisation height. -/// -/// Allowing reorganisations past this height could allow double-spends of -/// coinbase transactions. -const MAX_BLOCK_REORG_HEIGHT: block::Height = block::Height(MIN_TRASPARENT_COINBASE_MATURITY.0 - 1); - -/// Configuration for the state service. -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(deny_unknown_fields, default)] -pub struct Config { - /// The root directory for storing cached data. - /// - /// Cached data includes any state that can be replicated from the network - /// (e.g., the chain state, the blocks, the UTXO set, etc.). It does *not* - /// include private data that cannot be replicated from the network, such as - /// wallet data. That data is not handled by `zebra-state`. - /// - /// Each network has a separate state, which is stored in "mainnet/state" - /// and "testnet/state" subdirectories. - /// - /// The default directory is platform dependent, based on - /// [`dirs::cache_dir()`](https://docs.rs/dirs/3.0.1/dirs/fn.cache_dir.html): - /// - /// |Platform | Value | Example | - /// | ------- | ----------------------------------------------- | ---------------------------------- | - /// | Linux | `$XDG_CACHE_HOME/zebra` or `$HOME/.cache/zebra` | /home/alice/.cache/zebra | - /// | macOS | `$HOME/Library/Caches/zebra` | /Users/Alice/Library/Caches/zebra | - /// | Windows | `{FOLDERID_LocalAppData}\zebra` | C:\Users\Alice\AppData\Local\zebra | - /// | Other | `std::env::current_dir()/cache` | | - pub cache_dir: PathBuf, - - /// The maximum number of bytes to use caching data in memory. - pub memory_cache_bytes: u64, - - /// Whether to use an ephemeral database. - /// - /// Ephemeral databases are stored in memory on Linux, and in a temporary directory on other OSes. - /// - /// Set to `false` by default. If this is set to `true`, [`cache_dir`] is ignored. - /// - /// [`cache_dir`]: struct.Config.html#structfield.cache_dir - pub ephemeral: bool, -} - -impl Config { - /// Generate the appropriate `sled::Config` for `network`, based on the - /// provided `zebra_state::Config`. - pub(crate) fn sled_config(&self, network: Network) -> sled::Config { - let net_dir = match network { - Network::Mainnet => "mainnet", - Network::Testnet => "testnet", - }; - - let config = sled::Config::default() - .cache_capacity(self.memory_cache_bytes) - .mode(sled::Mode::LowSpace); - - if self.ephemeral { - config.temporary(self.ephemeral) - } else { - let path = self.cache_dir.join(net_dir).join("state"); - config.path(path) - } - } - - /// Construct a config for an ephemeral in memory database - pub fn ephemeral() -> Self { - let mut config = Self::default(); - config.ephemeral = true; - config - } -} - -impl Default for Config { - fn default() -> Self { - let cache_dir = dirs::cache_dir() - .unwrap_or_else(|| std::env::current_dir().unwrap().join("cache")) - .join("zebra"); - - Self { - cache_dir, - memory_cache_bytes: 512 * 1024 * 1024, - ephemeral: false, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -/// 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 - /// 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: block::Hash, - }, - /// Get a block locator list for the current best chain - GetBlockLocator { - /// The genesis block of the current best chain - genesis: block::Hash, - }, - /// Get the block that is the tip of the current chain - GetTip, - /// Ask the state if the given hash is part of the current best chain - GetDepth { - /// The hash to check against the current chain - hash: block::Hash, - }, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -/// A state response -pub enum Response { - /// The response to a `AddBlock` request indicating a block was successfully - /// added to the state - Added { - /// The hash of the block that was added - hash: block::Hash, - }, - /// The response to a `GetBlock` request by hash - Block { - /// The block that was requested - block: Arc, - }, - /// The response to a `GetBlockLocator` request - BlockLocator { - /// The set of blocks that make up the block locator - block_locator: Vec, - }, - /// The response to a `GetTip` request - Tip { - /// The hash of the block at the tip of the current chain - hash: block::Hash, - }, - /// The response to a `Contains` request indicating that the given has is in - /// the current best chain - Depth( - /// The number of blocks above the given block in the current best chain - Option, - ), -} - -/// Get the heights of the blocks for constructing a block_locator list -fn block_locator_heights(tip_height: block::Height) -> impl Iterator { - // Stop at the reorg limit, or the genesis block. - let min_locator_height = tip_height.0.saturating_sub(MAX_BLOCK_REORG_HEIGHT.0); - let locators = iter::successors(Some(1u32), |h| h.checked_mul(2)) - .flat_map(move |step| tip_height.0.checked_sub(step)); - let locators = iter::once(tip_height.0) - .chain(locators) - .take_while(move |&height| height > min_locator_height) - .chain(iter::once(min_locator_height)) - .map(block::Height); - - let locators: Vec<_> = locators.collect(); - tracing::info!( - ?tip_height, - ?min_locator_height, - ?locators, - "created block locator" - ); - - locators.into_iter() -} - -/// The error type for the State Service. -// TODO(jlusby): Error = Report ? -type Error = Box; - -/// Get the tip block, using `state`. -/// -/// If there is no tip, returns `Ok(None)`. -/// Returns an error if `state.poll_ready` errors. -pub async fn current_tip(state: S) -> Result>, Report> -where - S: Service + Send + Clone + 'static, - S::Future: Send + 'static, -{ - let current_tip_hash = state - .clone() - .ready_and() - .await - .map_err(|e| eyre!(e))? - .call(Request::GetTip) - .await - .map(|response| match response { - Response::Tip { hash } => hash, - _ => unreachable!("GetTip request can only result in Response::Tip"), - }) - .ok(); - - let current_tip_block = match current_tip_hash { - Some(hash) => state - .clone() - .ready_and() - .await - .map_err(|e| eyre!(e))? - .call(Request::GetBlock { hash }) - .await - .map(|response| match response { - Response::Block { block } => block, - _ => unreachable!("GetBlock request can only result in Response::Block"), - }) - .ok(), - None => None, - }; - - Ok(current_tip_block) -} +mod config; +mod constants; +mod request; +mod response; +mod sled_state; +mod util; +// TODO: move these to integration tests. #[cfg(test)] -mod tests { - use super::*; +mod tests; - use std::ffi::OsStr; - - #[test] - fn test_path_mainnet() { - test_path(Network::Mainnet); - } - - #[test] - fn test_path_testnet() { - test_path(Network::Testnet); - } - - /// Check the sled path for `network`. - fn test_path(network: Network) { - zebra_test::init(); - - let config = Config::default(); - // we can't do many useful tests on this value, because it depends on the - // local environment and OS. - let sled_config = config.sled_config(network); - let mut path = sled_config.get_path(); - assert_eq!(path.file_name(), Some(OsStr::new("state"))); - assert!(path.pop()); - match network { - Network::Mainnet => assert_eq!(path.file_name(), Some(OsStr::new("mainnet"))), - Network::Testnet => assert_eq!(path.file_name(), Some(OsStr::new("testnet"))), - } - } - - /// Block heights, and the expected minimum block locator height - static BLOCK_LOCATOR_CASES: &[(u32, u32)] = &[ - (0, 0), - (1, 0), - (10, 0), - (98, 0), - (99, 0), - (100, 1), - (101, 2), - (1000, 901), - (10000, 9901), - ]; - - /// Check that the block locator heights are sensible. - #[test] - fn test_block_locator_heights() { - for (height, min_height) in BLOCK_LOCATOR_CASES.iter().cloned() { - let locator = block_locator_heights(block::Height(height)).collect::>(); - - assert!(!locator.is_empty(), "locators must not be empty"); - if (height - min_height) > 1 { - assert!( - locator.len() > 2, - "non-trivial locators must have some intermediate heights" - ); - } - - assert_eq!( - locator[0], - block::Height(height), - "locators must start with the tip height" - ); - - // Check that the locator is sorted, and that it has no duplicates - // TODO: replace with dedup() and is_sorted_by() when sorting stabilises. - assert!(locator.windows(2).all(|v| match v { - [a, b] => a.0 > b.0, - _ => unreachable!("windows returns exact sized slices"), - })); - - let final_height = locator[locator.len() - 1]; - assert_eq!( - final_height, - block::Height(min_height), - "locators must end with the specified final height" - ); - assert!(height - final_height.0 <= MAX_BLOCK_REORG_HEIGHT.0, - format!("locator for {} must not be more than the maximum reorg height {} below the tip, but {} is {} blocks below the tip", - height, - MAX_BLOCK_REORG_HEIGHT.0, - final_height.0, - height - final_height.0)); - } - } -} +pub use config::Config; +pub use request::Request; +pub use response::Response; +pub use sled_state::init; diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs new file mode 100644 index 00000000..80ef33b1 --- /dev/null +++ b/zebra-state/src/request.rs @@ -0,0 +1,32 @@ +use std::sync::Arc; +use zebra_chain::block::{self, Block}; + +#[derive(Clone, Debug, PartialEq, Eq)] +/// A query about or modification to the chain state. +/// +/// TODO: replace these variants with the ones in RFC5. +pub enum Request { + // TODO(jlusby): deprecate in the future based on our validation story + /// 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: block::Hash, + }, + /// Get a block locator list for the current best chain + GetBlockLocator { + /// The genesis block of the current best chain + genesis: block::Hash, + }, + /// Get the block that is the tip of the current chain + GetTip, + /// Ask the state if the given hash is part of the current best chain + GetDepth { + /// The hash to check against the current chain + hash: block::Hash, + }, +} diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs new file mode 100644 index 00000000..339e8bb7 --- /dev/null +++ b/zebra-state/src/response.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use zebra_chain::block::{self, Block}; + +#[derive(Clone, Debug, PartialEq, Eq)] +/// A response to a state [`Request`](super::Request). +pub enum Response { + /// The response to a `AddBlock` request indicating a block was successfully + /// added to the state + Added { + /// The hash of the block that was added + hash: block::Hash, + }, + /// The response to a `GetBlock` request by hash + Block { + /// The block that was requested + block: Arc, + }, + /// The response to a `GetBlockLocator` request + BlockLocator { + /// The set of blocks that make up the block locator + block_locator: Vec, + }, + /// The response to a `GetTip` request + Tip { + /// The hash of the block at the tip of the current chain + hash: block::Hash, + }, + /// The response to a `Contains` request indicating that the given has is in + /// the current best chain + Depth( + /// The number of blocks above the given block in the current best chain + Option, + ), +} diff --git a/zebra-state/src/on_disk.rs b/zebra-state/src/sled_state.rs similarity index 99% rename from zebra-state/src/on_disk.rs rename to zebra-state/src/sled_state.rs index 6191a318..fa97eda2 100644 --- a/zebra-state/src/on_disk.rs +++ b/zebra-state/src/sled_state.rs @@ -241,7 +241,7 @@ impl Service for SledState { .coinbase_height() .expect("tip of the current chain will have a coinbase height"); - let heights = crate::block_locator_heights(tip_height); + let heights = crate::util::block_locator_heights(tip_height); let block_locator = heights .map(|height| { diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs new file mode 100644 index 00000000..0ef842f9 --- /dev/null +++ b/zebra-state/src/tests.rs @@ -0,0 +1,87 @@ +use std::ffi::OsStr; + +use zebra_chain::{block, parameters::Network}; + +use super::*; + +#[test] +fn test_path_mainnet() { + test_path(Network::Mainnet); +} + +#[test] +fn test_path_testnet() { + test_path(Network::Testnet); +} + +/// Check the sled path for `network`. +fn test_path(network: Network) { + zebra_test::init(); + + let config = Config::default(); + // we can't do many useful tests on this value, because it depends on the + // local environment and OS. + let sled_config = config.sled_config(network); + let mut path = sled_config.get_path(); + assert_eq!(path.file_name(), Some(OsStr::new("state"))); + assert!(path.pop()); + match network { + Network::Mainnet => assert_eq!(path.file_name(), Some(OsStr::new("mainnet"))), + Network::Testnet => assert_eq!(path.file_name(), Some(OsStr::new("testnet"))), + } +} + +/// Block heights, and the expected minimum block locator height +static BLOCK_LOCATOR_CASES: &[(u32, u32)] = &[ + (0, 0), + (1, 0), + (10, 0), + (98, 0), + (99, 0), + (100, 1), + (101, 2), + (1000, 901), + (10000, 9901), +]; + +/// Check that the block locator heights are sensible. +#[test] +fn test_block_locator_heights() { + for (height, min_height) in BLOCK_LOCATOR_CASES.iter().cloned() { + let locator = util::block_locator_heights(block::Height(height)).collect::>(); + + assert!(!locator.is_empty(), "locators must not be empty"); + if (height - min_height) > 1 { + assert!( + locator.len() > 2, + "non-trivial locators must have some intermediate heights" + ); + } + + assert_eq!( + locator[0], + block::Height(height), + "locators must start with the tip height" + ); + + // Check that the locator is sorted, and that it has no duplicates + // TODO: replace with dedup() and is_sorted_by() when sorting stabilises. + assert!(locator.windows(2).all(|v| match v { + [a, b] => a.0 > b.0, + _ => unreachable!("windows returns exact sized slices"), + })); + + let final_height = locator[locator.len() - 1]; + assert_eq!( + final_height, + block::Height(min_height), + "locators must end with the specified final height" + ); + assert!(height - final_height.0 <= constants::MAX_BLOCK_REORG_HEIGHT.0, + format!("locator for {} must not be more than the maximum reorg height {} below the tip, but {} is {} blocks below the tip", + height, + constants::MAX_BLOCK_REORG_HEIGHT.0, + final_height.0, + height - final_height.0)); + } +} diff --git a/zebra-state/src/util.rs b/zebra-state/src/util.rs new file mode 100644 index 00000000..5cd4d22e --- /dev/null +++ b/zebra-state/src/util.rs @@ -0,0 +1,29 @@ +use std::iter; +use zebra_chain::block; + +use crate::constants; + +/// Get the heights of the blocks for constructing a block_locator list +pub fn block_locator_heights(tip_height: block::Height) -> impl Iterator { + // Stop at the reorg limit, or the genesis block. + let min_locator_height = tip_height + .0 + .saturating_sub(constants::MAX_BLOCK_REORG_HEIGHT.0); + let locators = iter::successors(Some(1u32), |h| h.checked_mul(2)) + .flat_map(move |step| tip_height.0.checked_sub(step)); + let locators = iter::once(tip_height.0) + .chain(locators) + .take_while(move |&height| height > min_locator_height) + .chain(iter::once(min_locator_height)) + .map(block::Height); + + let locators: Vec<_> = locators.collect(); + tracing::info!( + ?tip_height, + ?min_locator_height, + ?locators, + "created block locator" + ); + + locators.into_iter() +}