From 6fb426ef936f1e51d699e19b22f0cb2b478c434f Mon Sep 17 00:00:00 2001 From: teor Date: Sat, 12 Mar 2022 06:23:32 +1000 Subject: [PATCH] 8. refactor(state): allow shared read access to the finalized state database (#3846) * Move database read methods to a new ZebraDb wrapper type * Rename struct fields --- zebra-state/src/service.rs | 48 ++++--- zebra-state/src/service/block_iter.rs | 2 +- zebra-state/src/service/check/anchors.rs | 4 +- zebra-state/src/service/check/nullifier.rs | 4 +- zebra-state/src/service/check/utxo.rs | 6 +- zebra-state/src/service/finalized_state.rs | 88 +++--------- .../src/service/finalized_state/arbitrary.rs | 60 ++------ .../src/service/finalized_state/disk_db.rs | 2 + .../service/finalized_state/disk_db/tests.rs | 2 +- .../src/service/finalized_state/zebra_db.rs | 43 ++++++ .../finalized_state/zebra_db/arbitrary.rs | 73 ++++++++++ .../service/finalized_state/zebra_db/block.rs | 60 +++++++- .../service/finalized_state/zebra_db/chain.rs | 5 +- .../finalized_state/zebra_db/metrics.rs | 132 +++++++++--------- .../finalized_state/zebra_db/shielded.rs | 5 +- .../finalized_state/zebra_db/transparent.rs | 5 +- .../src/service/non_finalized_state.rs | 29 ++-- 17 files changed, 328 insertions(+), 240 deletions(-) create mode 100644 zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 0ce48001..aa47fce6 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -39,7 +39,7 @@ use crate::{ request::HashOrHeight, service::{ chain_tip::{ChainTipBlock, ChainTipChange, ChainTipSender, LatestChainTip}, - finalized_state::{DiskDb, FinalizedState}, + finalized_state::{FinalizedState, ZebraDb}, non_finalized_state::{Chain, NonFinalizedState, QueuedBlocks}, pending_utxos::PendingUtxos, }, @@ -132,18 +132,18 @@ pub(crate) struct StateService { pub struct ReadStateService { /// The shared inner on-disk database for the finalized state. /// - /// RocksDB allows reads and writes via a shared reference. - /// TODO: prevent write access via this type. + /// RocksDB allows reads and writes via a shared reference, + /// but [`ZebraDb`] doesn't expose any write methods or types. /// /// This chain is updated concurrently with requests, /// so it might include some block data that is also in `best_mem`. - disk: DiskDb, + db: ZebraDb, /// A watch channel for the current best in-memory chain. /// /// This chain is only updated between requests, /// so it might include some block data that is also on `disk`. - best_mem: watch::Receiver>>, + best_chain_receiver: watch::Receiver>>, /// The configured Zcash network. network: Network, @@ -161,6 +161,7 @@ impl StateService { ) -> (Self, ReadStateService, LatestChainTip, ChainTipChange) { let disk = FinalizedState::new(&config, network); let initial_tip = disk + .db() .tip_block() .map(FinalizedBlock::from) .map(ChainTipBlock::from); @@ -243,7 +244,8 @@ impl StateService { tracing::debug!(block = %prepared.block, "queueing block for contextual verification"); let parent_hash = prepared.block.header.previous_block_hash; - if self.mem.any_chain_contains(&prepared.hash) || self.disk.hash(prepared.height).is_some() + if self.mem.any_chain_contains(&prepared.hash) + || self.disk.db().hash(prepared.height).is_some() { let (rsp_tx, rsp_rx) = oneshot::channel(); let _ = rsp_tx.send(Err("block is already committed to the state".into())); @@ -282,7 +284,7 @@ impl StateService { ); } - let finalized_tip_height = self.disk.finalized_tip_height().expect( + let finalized_tip_height = self.disk.db().finalized_tip_height().expect( "Finalized state must have at least one block before committing non-finalized state", ); self.queued_blocks.prune_by_height(finalized_tip_height); @@ -326,10 +328,10 @@ impl StateService { self.check_contextual_validity(&prepared)?; let parent_hash = prepared.block.header.previous_block_hash; - if self.disk.finalized_tip_hash() == parent_hash { - self.mem.commit_new_chain(prepared, &self.disk)?; + if self.disk.db().finalized_tip_hash() == parent_hash { + self.mem.commit_new_chain(prepared, self.disk.db())?; } else { - self.mem.commit_block(prepared, &self.disk)?; + self.mem.commit_block(prepared, self.disk.db())?; } Ok(()) @@ -337,7 +339,7 @@ impl StateService { /// Returns `true` if `hash` is a valid previous block hash for new non-finalized blocks. fn can_fork_chain_at(&self, hash: &block::Hash) -> bool { - self.mem.any_chain_contains(hash) || &self.disk.finalized_tip_hash() == hash + self.mem.any_chain_contains(hash) || &self.disk.db().finalized_tip_hash() == hash } /// Attempt to validate and commit all queued blocks whose parents have @@ -400,11 +402,11 @@ impl StateService { check::block_is_valid_for_recent_chain( prepared, self.network, - self.disk.finalized_tip_height(), + self.disk.db().finalized_tip_height(), relevant_chain, )?; - check::nullifier::no_duplicates_in_finalized_chain(prepared, &self.disk)?; + check::nullifier::no_duplicates_in_finalized_chain(prepared, self.disk.db())?; Ok(()) } @@ -427,7 +429,7 @@ impl StateService { /// Return the tip of the current best chain. pub fn best_tip(&self) -> Option<(block::Height, block::Hash)> { - self.mem.best_tip().or_else(|| self.disk.tip()) + self.mem.best_tip().or_else(|| self.disk.db().tip()) } /// Return the depth of block `hash` in the current best chain. @@ -436,7 +438,7 @@ impl StateService { let height = self .mem .best_height_by_hash(hash) - .or_else(|| self.disk.height(hash))?; + .or_else(|| self.disk.db().height(hash))?; Some(tip.0 - height.0) } @@ -447,7 +449,7 @@ impl StateService { self.mem .best_block(hash_or_height) .map(|contextual| contextual.block) - .or_else(|| self.disk.block(hash_or_height)) + .or_else(|| self.disk.db().block(hash_or_height)) } /// Return the transaction identified by `hash` if it exists in the current @@ -455,14 +457,14 @@ impl StateService { pub fn best_transaction(&self, hash: transaction::Hash) -> Option> { self.mem .best_transaction(hash) - .or_else(|| self.disk.transaction(hash)) + .or_else(|| self.disk.db().transaction(hash)) } /// Return the hash for the block at `height` in the current best chain. pub fn best_hash(&self, height: block::Height) -> Option { self.mem .best_hash(height) - .or_else(|| self.disk.hash(height)) + .or_else(|| self.disk.db().hash(height)) } /// Return true if `hash` is in the current best chain. @@ -474,14 +476,14 @@ impl StateService { pub fn best_height_by_hash(&self, hash: block::Hash) -> Option { self.mem .best_height_by_hash(hash) - .or_else(|| self.disk.height(hash)) + .or_else(|| self.disk.db().height(hash)) } /// Return the height for the block at `hash` in any chain. pub fn any_height_by_hash(&self, hash: block::Hash) -> Option { self.mem .any_height_by_hash(hash) - .or_else(|| self.disk.height(hash)) + .or_else(|| self.disk.db().height(hash)) } /// Return the [`Utxo`] pointed to by `outpoint` if it exists in any chain. @@ -489,7 +491,7 @@ impl StateService { self.mem .any_utxo(outpoint) .or_else(|| self.queued_blocks.utxo(outpoint)) - .or_else(|| self.disk.utxo(outpoint)) + .or_else(|| self.disk.db().utxo(outpoint)) } /// Return an iterator over the relevant chain of the block identified by @@ -674,8 +676,8 @@ impl ReadStateService { let (best_chain_sender, best_chain_receiver) = watch::channel(None); let read_only_service = Self { - disk: disk.db().clone(), - best_mem: best_chain_receiver, + db: disk.db().clone(), + best_chain_receiver, network: disk.network(), }; diff --git a/zebra-state/src/service/block_iter.rs b/zebra-state/src/service/block_iter.rs index 0cf9c7e3..8e430566 100644 --- a/zebra-state/src/service/block_iter.rs +++ b/zebra-state/src/service/block_iter.rs @@ -48,7 +48,7 @@ impl Iter<'_> { IterState::Finished => unreachable!(), }; - if let Some(block) = service.disk.block(hash_or_height) { + if let Some(block) = service.disk.db().block(hash_or_height) { let height = block .coinbase_height() .expect("valid blocks have a coinbase height"); diff --git a/zebra-state/src/service/check/anchors.rs b/zebra-state/src/service/check/anchors.rs index ebba8888..4413fc76 100644 --- a/zebra-state/src/service/check/anchors.rs +++ b/zebra-state/src/service/check/anchors.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use zebra_chain::sprout; use crate::{ - service::{finalized_state::FinalizedState, non_finalized_state::Chain}, + service::{finalized_state::ZebraDb, non_finalized_state::Chain}, PreparedBlock, ValidateContextError, }; @@ -20,7 +20,7 @@ use crate::{ /// treestate of any prior `JoinSplit` _within the same transaction_. #[tracing::instrument(skip(finalized_state, parent_chain, prepared))] pub(crate) fn anchors_refer_to_earlier_treestates( - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, parent_chain: &Chain, prepared: &PreparedBlock, ) -> Result<(), ValidateContextError> { diff --git a/zebra-state/src/service/check/nullifier.rs b/zebra-state/src/service/check/nullifier.rs index b7fe34b9..b7cde265 100644 --- a/zebra-state/src/service/check/nullifier.rs +++ b/zebra-state/src/service/check/nullifier.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use tracing::trace; use crate::{ - error::DuplicateNullifierError, service::finalized_state::FinalizedState, PreparedBlock, + error::DuplicateNullifierError, service::finalized_state::ZebraDb, PreparedBlock, ValidateContextError, }; @@ -26,7 +26,7 @@ use crate::{ #[tracing::instrument(skip(prepared, finalized_state))] pub(crate) fn no_duplicates_in_finalized_chain( prepared: &PreparedBlock, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result<(), ValidateContextError> { for nullifier in prepared.block.sprout_nullifiers() { if finalized_state.contains_sprout_nullifier(nullifier) { diff --git a/zebra-state/src/service/check/utxo.rs b/zebra-state/src/service/check/utxo.rs index d9e9e230..33fa1346 100644 --- a/zebra-state/src/service/check/utxo.rs +++ b/zebra-state/src/service/check/utxo.rs @@ -9,7 +9,7 @@ use zebra_chain::{ use crate::{ constants::MIN_TRANSPARENT_COINBASE_MATURITY, - service::finalized_state::FinalizedState, + service::finalized_state::ZebraDb, PreparedBlock, ValidateContextError::{ self, DuplicateTransparentSpend, EarlyTransparentSpend, ImmatureTransparentCoinbaseSpend, @@ -39,7 +39,7 @@ pub fn transparent_spend( prepared: &PreparedBlock, non_finalized_chain_unspent_utxos: &HashMap, non_finalized_chain_spent_utxos: &HashSet, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result, ValidateContextError> { let mut block_spends = HashMap::new(); @@ -117,7 +117,7 @@ fn transparent_spend_chain_order( block_new_outputs: &HashMap, non_finalized_chain_unspent_utxos: &HashMap, non_finalized_chain_spent_utxos: &HashSet, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result { if let Some(output) = block_new_outputs.get(&spend) { // reject the spend if it uses an output from this block, diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 7fc3cba7..077ed9cd 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -2,8 +2,8 @@ //! //! Zebra's database is implemented in 4 layers: //! - [`FinalizedState`]: queues, validates, and commits blocks, using... -//! - [`zebra_db`]: reads and writes [`zebra_chain`] types to the database, using... -//! - [`disk_db`]: reads and writes format-specific types to the database, using... +//! - [`ZebraDb`]: reads and writes [`zebra_chain`] types to the database, using... +//! - [`DiskDb`]: reads and writes format-specific types to the database, using... //! - [`disk_format`]: converts types to raw database bytes. //! //! These layers allow us to split [`zebra_chain`] types for efficient database storage. @@ -20,7 +20,7 @@ use std::{ path::Path, }; -use zebra_chain::{block, history_tree::HistoryTree, parameters::Network}; +use zebra_chain::{block, parameters::Network}; use crate::{ service::{check, QueuedFinalized}, @@ -37,15 +37,13 @@ mod arbitrary; #[cfg(test)] mod tests; -// TODO: replace with a ZebraDb struct -pub(super) use disk_db::DiskDb; +pub(super) use zebra_db::ZebraDb; /// The finalized part of the chain state, stored in the db. #[derive(Debug)] pub struct FinalizedState { /// The underlying database. - // TODO: replace with a ZebraDb struct - db: DiskDb, + db: ZebraDb, /// Queued blocks that arrived out of order, indexed by their parent block hash. queued_by_prev_hash: HashMap, @@ -67,7 +65,7 @@ pub struct FinalizedState { impl FinalizedState { pub fn new(config: &Config, network: Network) -> Self { - let db = DiskDb::new(config, network); + let db = ZebraDb::new(config, network); let new_state = Self { queued_by_prev_hash: HashMap::new(), @@ -77,12 +75,13 @@ impl FinalizedState { network, }; - if let Some(tip_height) = new_state.finalized_tip_height() { + // TODO: move debug_stop_at_height into a task in the start command (#3442) + if let Some(tip_height) = new_state.db.finalized_tip_height() { if new_state.is_at_stop_height(tip_height) { let debug_stop_at_height = new_state .debug_stop_at_height .expect("true from `is_at_stop_height` implies `debug_stop_at_height` is Some"); - let tip_hash = new_state.finalized_tip_hash(); + let tip_hash = new_state.db.finalized_tip_hash(); if tip_height > debug_stop_at_height { tracing::error!( @@ -108,7 +107,7 @@ impl FinalizedState { } } - tracing::info!(tip = ?new_state.tip(), "loaded Zebra state cache"); + tracing::info!(tip = ?new_state.db.tip(), "loaded Zebra state cache"); new_state } @@ -124,7 +123,7 @@ impl FinalizedState { } /// Returns a reference to the inner database instance. - pub(crate) fn db(&self) -> &DiskDb { + pub(crate) fn db(&self) -> &ZebraDb { &self.db } @@ -146,7 +145,10 @@ impl FinalizedState { let height = queued.0.height; self.queued_by_prev_hash.insert(prev_hash, queued); - while let Some(queued_block) = self.queued_by_prev_hash.remove(&self.finalized_tip_hash()) { + while let Some(queued_block) = self + .queued_by_prev_hash + .remove(&self.db.finalized_tip_hash()) + { if let Ok(finalized) = self.commit_finalized(queued_block) { highest_queue_commit = Some(finalized); } else { @@ -232,11 +234,11 @@ impl FinalizedState { finalized: FinalizedBlock, source: &str, ) -> Result { - let committed_tip_hash = self.finalized_tip_hash(); - let committed_tip_height = self.finalized_tip_height(); + let committed_tip_hash = self.db.finalized_tip_hash(); + let committed_tip_height = self.db.finalized_tip_height(); // Assert that callers (including unit tests) get the chain order correct - if self.is_empty() { + if self.db.is_empty() { assert_eq!( committed_tip_hash, finalized.block.header.previous_block_hash, "the first block added to an empty state must be a genesis block, source: {}", @@ -270,7 +272,7 @@ impl FinalizedState { // the history tree root. While it _is_ checked during contextual validation, // that is not called by the checkpoint verifier, and keeping a history tree there // would be harder to implement. - let history_tree = self.history_tree(); + let history_tree = self.db.history_tree(); check::finalized_block_commitment_is_valid_for_chain_history( &finalized, self.network, @@ -280,7 +282,9 @@ impl FinalizedState { let finalized_height = finalized.height; let finalized_hash = finalized.hash; - let result = self.write_block(finalized, history_tree, source); + let result = self + .db + .write_block(finalized, history_tree, self.network, source); // TODO: move the stop height check to the syncer (#3442) if result.is_ok() && self.is_at_stop_height(finalized_height) { @@ -300,54 +304,6 @@ impl FinalizedState { result } - /// Write `finalized` to the finalized state. - /// - /// Uses: - /// - `history_tree`: the current tip's history tree - /// - `source`: the source of the block in log messages - /// - /// # Errors - /// - /// - Propagates any errors from writing to the DB - /// - Propagates any errors from updating history and note commitment trees - fn write_block( - &mut self, - finalized: FinalizedBlock, - history_tree: HistoryTree, - source: &str, - ) -> Result { - let finalized_hash = finalized.hash; - - // Get a list of the spent UTXOs, before we delete any from the database - let all_utxos_spent_by_block = finalized - .block - .transactions - .iter() - .flat_map(|tx| tx.inputs().iter()) - .flat_map(|input| input.outpoint()) - .flat_map(|outpoint| self.utxo(&outpoint).map(|utxo| (outpoint, utxo))) - .collect(); - - let mut batch = disk_db::DiskWriteBatch::new(); - - // In case of errors, propagate and do not write the batch. - batch.prepare_block_batch( - &self.db, - finalized, - self.network, - all_utxos_spent_by_block, - self.note_commitment_trees(), - history_tree, - self.finalized_value_pool(), - )?; - - self.db.write(batch)?; - - tracing::trace!(?source, "committed block from"); - - Ok(finalized_hash) - } - /// Stop the process if `block_height` is greater than or equal to the /// configured stop height. fn is_at_stop_height(&self, block_height: block::Height) -> bool { diff --git a/zebra-state/src/service/finalized_state/arbitrary.rs b/zebra-state/src/service/finalized_state/arbitrary.rs index cb76bd10..9ff59c75 100644 --- a/zebra-state/src/service/finalized_state/arbitrary.rs +++ b/zebra-state/src/service/finalized_state/arbitrary.rs @@ -2,16 +2,22 @@ #![allow(dead_code)] -use std::sync::Arc; - -use zebra_chain::{amount::NonNegative, block::Block, sprout, value_balance::ValueBalance}; +use std::{ops::Deref, sync::Arc}; use crate::service::finalized_state::{ - disk_db::{DiskWriteBatch, WriteDisk}, disk_format::{FromDisk, IntoDisk}, - FinalizedState, + FinalizedState, ZebraDb, }; +// Enable older test code to automatically access the inner database via Deref coercion. +impl Deref for FinalizedState { + type Target = ZebraDb; + + fn deref(&self) -> &Self::Target { + self.db() + } +} + pub fn round_trip(input: T) -> T where T: IntoDisk + FromDisk, @@ -74,47 +80,3 @@ where assert_round_trip_arc(Arc::new(input.clone())); assert_round_trip(input); } - -impl FinalizedState { - /// Allow to set up a fake value pool in the database for testing purposes. - pub fn set_finalized_value_pool(&self, fake_value_pool: ValueBalance) { - let mut batch = DiskWriteBatch::new(); - let value_pool_cf = self.db.cf_handle("tip_chain_value_pool").unwrap(); - - batch.zs_insert(value_pool_cf, (), fake_value_pool); - self.db.write(batch).unwrap(); - } - - /// Artificially prime the note commitment tree anchor sets with anchors - /// referenced in a block, for testing purposes _only_. - pub fn populate_with_anchors(&self, block: &Block) { - let mut batch = DiskWriteBatch::new(); - - let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap(); - let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap(); - let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap(); - - for transaction in block.transactions.iter() { - // Sprout - for joinsplit in transaction.sprout_groth16_joinsplits() { - batch.zs_insert( - sprout_anchors, - joinsplit.anchor, - sprout::tree::NoteCommitmentTree::default(), - ); - } - - // Sapling - for anchor in transaction.sapling_anchors() { - batch.zs_insert(sapling_anchors, anchor, ()); - } - - // Orchard - if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { - batch.zs_insert(orchard_anchors, orchard_shielded_data.shared_anchor, ()); - } - } - - self.db.write(batch).unwrap(); - } -} diff --git a/zebra-state/src/service/finalized_state/disk_db.rs b/zebra-state/src/service/finalized_state/disk_db.rs index 9796315f..4a035196 100644 --- a/zebra-state/src/service/finalized_state/disk_db.rs +++ b/zebra-state/src/service/finalized_state/disk_db.rs @@ -182,6 +182,8 @@ impl DiskDb { /// stdio (3), and other OS facilities (2+). const RESERVED_FILE_COUNT: u64 = 48; + /// Opens or creates the database at `config.path` for `network`, + /// and returns a shared low-level database wrapper. pub fn new(config: &Config, network: Network) -> DiskDb { let path = config.db_path(network); let db_options = DiskDb::options(); diff --git a/zebra-state/src/service/finalized_state/disk_db/tests.rs b/zebra-state/src/service/finalized_state/disk_db/tests.rs index 1b540fbe..c11edb89 100644 --- a/zebra-state/src/service/finalized_state/disk_db/tests.rs +++ b/zebra-state/src/service/finalized_state/disk_db/tests.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] -use crate::service::finalized_state::DiskDb; +use crate::service::finalized_state::disk_db::DiskDb; impl DiskDb { /// Returns a list of column family names in this database. diff --git a/zebra-state/src/service/finalized_state/zebra_db.rs b/zebra-state/src/service/finalized_state/zebra_db.rs index 2ea74457..cdc83393 100644 --- a/zebra-state/src/service/finalized_state/zebra_db.rs +++ b/zebra-state/src/service/finalized_state/zebra_db.rs @@ -9,8 +9,51 @@ //! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must //! be incremented each time the database format (column, serialization, etc) changes. +use std::path::Path; + +use zebra_chain::parameters::Network; + +use crate::{service::finalized_state::disk_db::DiskDb, Config}; + pub mod block; pub mod chain; pub mod metrics; pub mod shielded; pub mod transparent; + +#[cfg(any(test, feature = "proptest-impl"))] +pub mod arbitrary; + +/// Wrapper struct to ensure high-level typed database access goes through the correct API. +#[derive(Clone, Debug)] +pub struct ZebraDb { + /// The inner low-level database wrapper for the RocksDB database. + /// This wrapper can be cloned and shared. + db: DiskDb, +} + +impl ZebraDb { + /// Opens or creates the database at `config.path` for `network`, + /// and returns a shared high-level typed database wrapper. + pub fn new(config: &Config, network: Network) -> ZebraDb { + ZebraDb { + db: DiskDb::new(config, network), + } + } + + /// Returns the `Path` where the files used by this database are located. + pub fn path(&self) -> &Path { + self.db.path() + } + + /// Shut down the database, cleaning up background tasks and ephemeral data. + /// + /// If `force` is true, clean up regardless of any shared references. + /// `force` can cause errors accessing the database from other shared references. + /// It should only be used in debugging or test code, immediately before a manual shutdown. + /// + /// See [`DiskDb::shutdown`] for details. + pub(crate) fn shutdown(&mut self, force: bool) { + self.db.shutdown(force); + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs new file mode 100644 index 00000000..c9792a53 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/arbitrary.rs @@ -0,0 +1,73 @@ +//! Arbitrary value generation and test harnesses for high-level typed database access. + +#![allow(dead_code)] + +use std::ops::Deref; + +use zebra_chain::{amount::NonNegative, block::Block, sprout, value_balance::ValueBalance}; + +use crate::service::finalized_state::{ + disk_db::{DiskDb, DiskWriteBatch, WriteDisk}, + ZebraDb, +}; + +// Enable older test code to automatically access the inner database via Deref coercion. +impl Deref for ZebraDb { + type Target = DiskDb; + + fn deref(&self) -> &Self::Target { + self.db() + } +} + +impl ZebraDb { + /// Returns the inner database. + /// + /// This is a test-only method, because it allows write access + /// and raw read access to the RocksDB instance. + pub fn db(&self) -> &DiskDb { + &self.db + } + + /// Allow to set up a fake value pool in the database for testing purposes. + pub fn set_finalized_value_pool(&self, fake_value_pool: ValueBalance) { + let mut batch = DiskWriteBatch::new(); + let value_pool_cf = self.db().cf_handle("tip_chain_value_pool").unwrap(); + + batch.zs_insert(value_pool_cf, (), fake_value_pool); + self.db().write(batch).unwrap(); + } + + /// Artificially prime the note commitment tree anchor sets with anchors + /// referenced in a block, for testing purposes _only_. + pub fn populate_with_anchors(&self, block: &Block) { + let mut batch = DiskWriteBatch::new(); + + let sprout_anchors = self.db().cf_handle("sprout_anchors").unwrap(); + let sapling_anchors = self.db().cf_handle("sapling_anchors").unwrap(); + let orchard_anchors = self.db().cf_handle("orchard_anchors").unwrap(); + + for transaction in block.transactions.iter() { + // Sprout + for joinsplit in transaction.sprout_groth16_joinsplits() { + batch.zs_insert( + sprout_anchors, + joinsplit.anchor, + sprout::tree::NoteCommitmentTree::default(), + ); + } + + // Sapling + for anchor in transaction.sapling_anchors() { + batch.zs_insert(sapling_anchors, anchor, ()); + } + + // Orchard + if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() { + batch.zs_insert(orchard_anchors, orchard_shielded_data.shared_anchor, ()); + } + } + + self.db().write(batch).unwrap(); + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index a076592e..4a50957a 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -25,8 +25,8 @@ use crate::{ service::finalized_state::{ disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, disk_format::{FromDisk, TransactionLocation}, - zebra_db::shielded::NoteCommitmentTrees, - FinalizedBlock, FinalizedState, + zebra_db::{metrics::block_precommit_metrics, shielded::NoteCommitmentTrees, ZebraDb}, + FinalizedBlock, }, BoxError, HashOrHeight, }; @@ -34,7 +34,7 @@ use crate::{ #[cfg(test)] mod tests; -impl FinalizedState { +impl ZebraDb { // Read block methods /// Returns true if the database is empty. @@ -115,6 +115,58 @@ impl FinalizedState { block.transactions[index.as_usize()].clone() }) } + + // Write block methods + + /// Write `finalized` to the finalized state. + /// + /// Uses: + /// - `history_tree`: the current tip's history tree + /// - `network`: the configured network + /// - `source`: the source of the block in log messages + /// + /// # Errors + /// + /// - Propagates any errors from writing to the DB + /// - Propagates any errors from updating history and note commitment trees + pub(in super::super) fn write_block( + &mut self, + finalized: FinalizedBlock, + history_tree: HistoryTree, + network: Network, + source: &str, + ) -> Result { + let finalized_hash = finalized.hash; + + // Get a list of the spent UTXOs, before we delete any from the database + let all_utxos_spent_by_block = finalized + .block + .transactions + .iter() + .flat_map(|tx| tx.inputs().iter()) + .flat_map(|input| input.outpoint()) + .flat_map(|outpoint| self.utxo(&outpoint).map(|utxo| (outpoint, utxo))) + .collect(); + + let mut batch = DiskWriteBatch::new(); + + // In case of errors, propagate and do not write the batch. + batch.prepare_block_batch( + &self.db, + finalized, + network, + all_utxos_spent_by_block, + self.note_commitment_trees(), + history_tree, + self.finalized_value_pool(), + )?; + + self.db.write(batch)?; + + tracing::trace!(?source, "committed block from"); + + Ok(finalized_hash) + } } impl DiskWriteBatch { @@ -180,7 +232,7 @@ impl DiskWriteBatch { self.prepare_chain_value_pools_batch(db, &finalized, all_utxos_spent_by_block, value_pool)?; // The block has passed contextual validation, so update the metrics - FinalizedState::block_precommit_metrics(block, *hash, *height); + block_precommit_metrics(block, *hash, *height); Ok(()) } diff --git a/zebra-state/src/service/finalized_state/zebra_db/chain.rs b/zebra-state/src/service/finalized_state/zebra_db/chain.rs index 80723953..c026a97d 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/chain.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/chain.rs @@ -25,12 +25,13 @@ use zebra_chain::{ use crate::{ service::finalized_state::{ disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, - FinalizedBlock, FinalizedState, + zebra_db::ZebraDb, + FinalizedBlock, }, BoxError, }; -impl FinalizedState { +impl ZebraDb { /// Returns the ZIP-221 history tree of the finalized tip or `None` /// if it does not exist yet in the state (pre-Heartwood). pub fn history_tree(&self) -> HistoryTree { diff --git a/zebra-state/src/service/finalized_state/zebra_db/metrics.rs b/zebra-state/src/service/finalized_state/zebra_db/metrics.rs index 15519029..77cbede9 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/metrics.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/metrics.rs @@ -2,84 +2,80 @@ use zebra_chain::block::{self, Block}; -use crate::service::finalized_state::FinalizedState; - -impl FinalizedState { - /// Update metrics before committing a block. - /// - /// The metrics are updated after contextually validating a block, - /// but before writing its batch to the state. - pub(crate) fn block_precommit_metrics(block: &Block, hash: block::Hash, height: block::Height) { - let transaction_count = block.transactions.len(); - let transparent_prevout_count = block +/// Update metrics before committing a block. +/// +/// The metrics are updated after contextually validating a block, +/// but before writing its batch to the state. +pub(crate) fn block_precommit_metrics(block: &Block, hash: block::Hash, height: block::Height) { + let transaction_count = block.transactions.len(); + let transparent_prevout_count = block .transactions .iter() .flat_map(|t| t.inputs().iter()) .count() // Each block has a single coinbase input which is not a previous output. - 1; - let transparent_newout_count = block - .transactions - .iter() - .flat_map(|t| t.outputs().iter()) - .count(); + let transparent_newout_count = block + .transactions + .iter() + .flat_map(|t| t.outputs().iter()) + .count(); - let sprout_nullifier_count = block - .transactions - .iter() - .flat_map(|t| t.sprout_nullifiers()) - .count(); + let sprout_nullifier_count = block + .transactions + .iter() + .flat_map(|t| t.sprout_nullifiers()) + .count(); - let sapling_nullifier_count = block - .transactions - .iter() - .flat_map(|t| t.sapling_nullifiers()) - .count(); + let sapling_nullifier_count = block + .transactions + .iter() + .flat_map(|t| t.sapling_nullifiers()) + .count(); - let orchard_nullifier_count = block - .transactions - .iter() - .flat_map(|t| t.orchard_nullifiers()) - .count(); + let orchard_nullifier_count = block + .transactions + .iter() + .flat_map(|t| t.orchard_nullifiers()) + .count(); - tracing::debug!( - ?hash, - ?height, - transaction_count, - transparent_prevout_count, - transparent_newout_count, - sprout_nullifier_count, - sapling_nullifier_count, - orchard_nullifier_count, - "preparing to commit finalized block" - ); + tracing::debug!( + ?hash, + ?height, + transaction_count, + transparent_prevout_count, + transparent_newout_count, + sprout_nullifier_count, + sapling_nullifier_count, + orchard_nullifier_count, + "preparing to commit finalized block" + ); - metrics::counter!("state.finalized.block.count", 1); - metrics::gauge!("state.finalized.block.height", height.0 as _); + metrics::counter!("state.finalized.block.count", 1); + metrics::gauge!("state.finalized.block.height", height.0 as _); - metrics::counter!( - "state.finalized.cumulative.transactions", - transaction_count as u64 - ); - metrics::counter!( - "state.finalized.cumulative.transparent_prevouts", - transparent_prevout_count as u64 - ); - metrics::counter!( - "state.finalized.cumulative.transparent_newouts", - transparent_newout_count as u64 - ); - metrics::counter!( - "state.finalized.cumulative.sprout_nullifiers", - sprout_nullifier_count as u64 - ); - metrics::counter!( - "state.finalized.cumulative.sapling_nullifiers", - sapling_nullifier_count as u64 - ); - metrics::counter!( - "state.finalized.cumulative.orchard_nullifiers", - orchard_nullifier_count as u64 - ); - } + metrics::counter!( + "state.finalized.cumulative.transactions", + transaction_count as u64 + ); + metrics::counter!( + "state.finalized.cumulative.transparent_prevouts", + transparent_prevout_count as u64 + ); + metrics::counter!( + "state.finalized.cumulative.transparent_newouts", + transparent_newout_count as u64 + ); + metrics::counter!( + "state.finalized.cumulative.sprout_nullifiers", + sprout_nullifier_count as u64 + ); + metrics::counter!( + "state.finalized.cumulative.sapling_nullifiers", + sapling_nullifier_count as u64 + ); + metrics::counter!( + "state.finalized.cumulative.orchard_nullifiers", + orchard_nullifier_count as u64 + ); } diff --git a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs index 0eae0978..ffb80163 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -20,7 +20,8 @@ use zebra_chain::{ use crate::{ service::finalized_state::{ disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, - FinalizedBlock, FinalizedState, + zebra_db::ZebraDb, + FinalizedBlock, }, BoxError, }; @@ -33,7 +34,7 @@ pub struct NoteCommitmentTrees { orchard: orchard::tree::NoteCommitmentTree, } -impl FinalizedState { +impl ZebraDb { // Read shielded methods /// Returns `true` if the finalized state contains `sprout_nullifier`. diff --git a/zebra-state/src/service/finalized_state/zebra_db/transparent.rs b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs index 9a1b5791..ac2e14ae 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/transparent.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs @@ -19,12 +19,13 @@ use crate::{ service::finalized_state::{ disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, disk_format::transparent::OutputLocation, - FinalizedBlock, FinalizedState, + zebra_db::ZebraDb, + FinalizedBlock, }, BoxError, }; -impl FinalizedState { +impl ZebraDb { // Read transparent methods /// Returns the `transparent::Output` pointed to by the given diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index c648d4e7..2ac84a0b 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -2,14 +2,6 @@ //! //! [RFC0005]: https://zebra.zfnd.org/dev/rfcs/0005-state-updates.html -mod chain; -mod queued_blocks; - -#[cfg(test)] -mod tests; - -pub use queued_blocks::QueuedBlocks; - use std::{collections::BTreeSet, mem, sync::Arc}; use zebra_chain::{ @@ -23,13 +15,20 @@ use zebra_chain::{ }; use crate::{ - request::ContextuallyValidBlock, FinalizedBlock, HashOrHeight, PreparedBlock, - ValidateContextError, + request::ContextuallyValidBlock, + service::{check, finalized_state::ZebraDb}, + FinalizedBlock, HashOrHeight, PreparedBlock, ValidateContextError, }; -pub(crate) use self::chain::Chain; +mod chain; +mod queued_blocks; -use super::{check, finalized_state::FinalizedState}; +#[cfg(test)] +mod tests; + +pub use queued_blocks::QueuedBlocks; + +pub(crate) use chain::Chain; /// The state of the chains in memory, including queued blocks. #[derive(Debug, Clone)] @@ -138,7 +137,7 @@ impl NonFinalizedState { pub fn commit_block( &mut self, prepared: PreparedBlock, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result<(), ValidateContextError> { let parent_hash = prepared.block.header.previous_block_hash; let (height, hash) = (prepared.height, prepared.hash); @@ -174,7 +173,7 @@ impl NonFinalizedState { pub fn commit_new_chain( &mut self, prepared: PreparedBlock, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result<(), ValidateContextError> { let chain = Chain::new( self.network, @@ -206,7 +205,7 @@ impl NonFinalizedState { &self, new_chain: Arc, prepared: PreparedBlock, - finalized_state: &FinalizedState, + finalized_state: &ZebraDb, ) -> Result, ValidateContextError> { let spent_utxos = check::utxo::transparent_spend( &prepared,