//! 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_favicon_url = "https://www.zfnd.org/images/zebra-favicon-128.png")] #![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 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::{Block, BlockHeaderHash}, types::BlockHeight, Network, Network::*, }; pub mod in_memory; pub mod on_disk; /// Configuration for the state service. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields)] 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, } 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 { Mainnet => "mainnet", Testnet => "testnet", }; let path = self.cache_dir.join(net_dir).join("state"); sled::Config::default().path(path) } } 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 } } } #[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: BlockHeaderHash, }, /// Get a block locator list for the current best chain GetBlockLocator { /// The genesis block of the current best chain genesis: BlockHeaderHash, }, /// 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: BlockHeaderHash, }, } #[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: BlockHeaderHash, }, /// 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: BlockHeaderHash, }, /// 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: BlockHeight) -> impl Iterator { iter::successors(Some(1u32), |h| h.checked_mul(2)) .flat_map(move |step| tip_height.0.checked_sub(step)) .map(BlockHeight) .chain(iter::once(BlockHeight(0))) } /// 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) } #[cfg(test)] mod tests { use super::*; use std::ffi::OsStr; #[test] fn test_path_mainnet() { test_path(Mainnet); } #[test] fn test_path_testnet() { test_path(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 { Mainnet => assert_eq!(path.file_name(), Some(OsStr::new("mainnet"))), Testnet => assert_eq!(path.file_name(), Some(OsStr::new("testnet"))), } } }