//! 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 /// 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 { /// 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)] mod tests { use super::*; use color_eyre::eyre::Report; use color_eyre::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() { 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(); tracing_subscriber::registry() .with(filter_layer) .with(fmt_layer) .with(ErrorLayer::default()) .init(); }) } #[tokio::test] 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 response = service .call(Request::AddBlock { block: block.clone(), }) .await .map_err(|e| eyre!(e))?; ensure!( response == Response::Added { hash }, "unexpected response kind: {:?}", response ); let block_response = service .call(Request::GetBlock { hash }) .await .map_err(|e| eyre!(e))?; match block_response { Response::Block { block: returned_block, } => assert_eq!(block, returned_block), _ => bail!("unexpected response kind: {:?}", block_response), } Ok(()) } #[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(mut service: S) -> Result<(), Report> where S: Service< Request, Error = Box, Response = Response, >, { 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 block0_hash: BlockHeaderHash = block0.as_ref().into(); let block1_hash: BlockHeaderHash = block1.as_ref().into(); let expected_hash: BlockHeaderHash = block1_hash; /// insert the higher block first let response = service .call(Request::AddBlock { block: block1 }) .await .map_err(|e| eyre!(e))?; ensure!( response == Response::Added { hash: block1_hash }, "unexpected response kind: {:?}", response ); /// genesis block second let response = service .call(Request::AddBlock { block: block0.clone(), }) .await .map_err(|e| eyre!(e))?; ensure!( response == Response::Added { hash: block0_hash }, "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(()) } }