From 22b8a6003cf2c456655a2a57f8ee799c0d0f3c33 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 23 Feb 2022 10:43:41 +1000 Subject: [PATCH] 3. refactor(state): move database reads and writes to a new zebra_db module (#3579) * refactor(state): move disk_db reads to a new zebra_db module * refactor(state): make finalized value pool method names consistent * refactor(state): split database writes into the zebra_db module * refactor(state): move the block batch method to DiskWriteBatch * refactor(state): actually add the zebra_db module Unfortunately, I've lost the interim changes to this file, so this commit might be the only one that compiles. * refactor(state): add a newly created file to the cached state CI job --- .github/workflows/test.yml | 6 +- zebra-state/src/service/finalized_state.rs | 790 +++++------------- .../src/service/finalized_state/arbitrary.rs | 2 +- .../src/service/finalized_state/disk_db.rs | 4 +- .../service/finalized_state/disk_format.rs | 2 +- .../src/service/finalized_state/zebra_db.rs | 509 +++++++++++ .../src/service/non_finalized_state.rs | 2 +- .../service/non_finalized_state/tests/prop.rs | 2 +- .../non_finalized_state/tests/vectors.rs | 10 +- zebra-state/src/service/tests.rs | 6 +- 10 files changed, 720 insertions(+), 613 deletions(-) create mode 100644 zebra-state/src/service/finalized_state/zebra_db.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c987795c4..965d45312 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -158,15 +158,17 @@ jobs: persist-credentials: false fetch-depth: '2' + # only run this job if the database format might have changed - name: Get specific changed files id: changed-files-specific uses: tj-actions/changed-files@v14.4 with: files: | + /zebra-state/**/constants.rs + /zebra-state/**/finalized_state.rs /zebra-state/**/disk_format.rs /zebra-state/**/disk_db.rs - /zebra-state/**/finalized_state.rs - /zebra-state/**/constants.rs + /zebra-state/**/zebra_db.rs - name: Inject slug/short variables uses: rlespinasse/github-slug-action@v4 diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index eeda88403..184af047f 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -1,4 +1,13 @@ -//! The primary implementation of the `zebra_state::Service` built upon rocksdb +//! The primary implementation of the `zebra_state::Service` built upon rocksdb. +//! +//! 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... +//! - [`disk_format`]: converts types to raw database bytes. +//! +//! These layers allow us to split [`zebra_chain`] types for efficient database storage. +//! They reduce the risk of data corruption bugs, runtime inconsistencies, and panics. //! //! # Correctness //! @@ -6,40 +15,26 @@ //! be incremented each time the database format (column, serialization, etc) changes. use std::{ - borrow::Borrow, collections::HashMap, - convert::TryInto, io::{stderr, stdout, Write}, path::Path, sync::Arc, }; use zebra_chain::{ - amount::NonNegative, block::{self, Block}, - history_tree::{HistoryTree, NonEmptyHistoryTree}, - orchard, + history_tree::HistoryTree, parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, - sapling, sprout, - transaction::{self, Transaction}, - transparent, - value_balance::ValueBalance, }; use crate::{ - service::{ - check, - finalized_state::{ - disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, - disk_format::{FromDisk, TransactionLocation}, - }, - QueuedFinalized, - }, - BoxError, Config, FinalizedBlock, HashOrHeight, + service::{check, finalized_state::disk_db::DiskDb, QueuedFinalized}, + BoxError, Config, FinalizedBlock, }; mod disk_db; mod disk_format; +mod zebra_db; #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -118,19 +113,29 @@ impl FinalizedState { new_state } - /// 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 { - let debug_stop_at_height = match self.debug_stop_at_height { - Some(debug_stop_at_height) => debug_stop_at_height, - None => return false, - }; + /// Returns the `Path` where the files used by this database are located. + #[allow(dead_code)] + pub fn path(&self) -> &Path { + self.db.path() + } - if block_height < debug_stop_at_height { - return false; - } + /// Returns the hash of the current finalized tip block. + pub fn finalized_tip_hash(&self) -> block::Hash { + self.tip() + .map(|(_, hash)| hash) + // if the state is empty, return the genesis previous block hash + .unwrap_or(GENESIS_PREVIOUS_BLOCK_HASH) + } - true + /// Returns the height of the current finalized tip block. + pub fn finalized_tip_height(&self) -> Option { + self.tip().map(|(height, _)| height) + } + + /// Returns the tip block, if there is one. + pub fn tip_block(&self) -> Option> { + let (height, _hash) = self.tip()?; + self.block(height.into()) } /// Queue a finalized block to be committed to the state. @@ -180,319 +185,6 @@ impl FinalizedState { highest_queue_commit } - /// Returns the hash of the current finalized tip block. - pub fn finalized_tip_hash(&self) -> block::Hash { - self.tip() - .map(|(_, hash)| hash) - // if the state is empty, return the genesis previous block hash - .unwrap_or(GENESIS_PREVIOUS_BLOCK_HASH) - } - - /// Returns the height of the current finalized tip block. - pub fn finalized_tip_height(&self) -> Option { - self.tip().map(|(height, _)| height) - } - - /// Immediately commit `finalized` to the finalized state. - /// - /// This can be called either by the non-finalized state (when finalizing - /// a block) or by the checkpoint verifier. - /// - /// Use `source` as 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 - /// - If `hashFinalSaplingRoot` / `hashLightClientRoot` / `hashBlockCommitments` - /// does not match the expected value - pub fn commit_finalized_direct( - &mut self, - finalized: FinalizedBlock, - source: &str, - ) -> Result { - let finalized_tip_height = self.finalized_tip_height(); - - let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); - let height_by_hash = self.db.cf_handle("height_by_hash").unwrap(); - let block_by_height = self.db.cf_handle("block_by_height").unwrap(); - let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap(); - let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap(); - - let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap(); - let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap(); - let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap(); - - 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(); - - let sprout_note_commitment_tree_cf = - self.db.cf_handle("sprout_note_commitment_tree").unwrap(); - let sapling_note_commitment_tree_cf = - self.db.cf_handle("sapling_note_commitment_tree").unwrap(); - let orchard_note_commitment_tree_cf = - self.db.cf_handle("orchard_note_commitment_tree").unwrap(); - let history_tree_cf = self.db.cf_handle("history_tree").unwrap(); - - let tip_chain_value_pool = self.db.cf_handle("tip_chain_value_pool").unwrap(); - - // Assert that callers (including unit tests) get the chain order correct - if self.db.is_empty(hash_by_height) { - assert_eq!( - GENESIS_PREVIOUS_BLOCK_HASH, finalized.block.header.previous_block_hash, - "the first block added to an empty state must be a genesis block, source: {}", - source, - ); - assert_eq!( - block::Height(0), - finalized.height, - "cannot commit genesis: invalid height, source: {}", - source, - ); - } else { - assert_eq!( - finalized_tip_height.expect("state must have a genesis block committed") + 1, - Some(finalized.height), - "committed block height must be 1 more than the finalized tip height, source: {}", - source, - ); - - assert_eq!( - self.finalized_tip_hash(), - finalized.block.header.previous_block_hash, - "committed block must be a child of the finalized tip, source: {}", - source, - ); - } - - // Read the current note commitment trees. If there are no blocks in the - // state, these will contain the empty trees. - let mut sprout_note_commitment_tree = self.sprout_note_commitment_tree(); - let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree(); - let mut orchard_note_commitment_tree = self.orchard_note_commitment_tree(); - let mut history_tree = self.history_tree(); - - // Check the block commitment. For Nu5-onward, the block hash commits only - // to non-authorizing data (see ZIP-244). This checks the authorizing data - // commitment, making sure the entire block contents were committed to. - // The test is done here (and not during semantic validation) because it needs - // 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. - check::finalized_block_commitment_is_valid_for_chain_history( - &finalized, - self.network, - &history_tree, - )?; - - let FinalizedBlock { - block, - hash, - height, - new_outputs, - transaction_hashes, - } = finalized; - - // Prepare a batch of DB modifications and return it (without actually writing anything). - // We use a closure so we can use an early return for control flow in - // the genesis case. - // If the closure returns an error it will be propagated and the batch will not be written - // to the BD afterwards. - let prepare_commit = || -> Result { - let mut batch = DiskWriteBatch::new(); - - // Index the block - batch.zs_insert(hash_by_height, height, hash); - batch.zs_insert(height_by_hash, hash, height); - batch.zs_insert(block_by_height, height, &block); - - // # Consensus - // - // > A transaction MUST NOT spend an output of the genesis block coinbase transaction. - // > (There is one such zero-valued output, on each of Testnet and Mainnet.) - // - // https://zips.z.cash/protocol/protocol.pdf#txnconsensus - if block.header.previous_block_hash == GENESIS_PREVIOUS_BLOCK_HASH { - // Insert empty note commitment trees. Note that these can't be - // used too early (e.g. the Orchard tree before Nu5 activates) - // since the block validation will make sure only appropriate - // transactions are allowed in a block. - batch.zs_insert( - sprout_note_commitment_tree_cf, - height, - sprout_note_commitment_tree, - ); - batch.zs_insert( - sapling_note_commitment_tree_cf, - height, - sapling_note_commitment_tree, - ); - batch.zs_insert( - orchard_note_commitment_tree_cf, - height, - orchard_note_commitment_tree, - ); - return Ok(batch); - } - - // Index all new transparent outputs - for (outpoint, utxo) in new_outputs.borrow().iter() { - batch.zs_insert(utxo_by_outpoint, outpoint, utxo); - } - - // Create a map for all the utxos spent by the block - let mut all_utxos_spent_by_block = HashMap::new(); - - // Index each transaction, spent inputs, nullifiers - for (transaction_index, (transaction, transaction_hash)) in block - .transactions - .iter() - .zip(transaction_hashes.iter()) - .enumerate() - { - let transaction_location = TransactionLocation { - height, - index: transaction_index - .try_into() - .expect("no more than 4 billion transactions per block"), - }; - batch.zs_insert(tx_by_hash, transaction_hash, transaction_location); - - // Mark all transparent inputs as spent, collect them as well. - for input in transaction.inputs() { - match input { - transparent::Input::PrevOut { outpoint, .. } => { - if let Some(utxo) = self.utxo(outpoint) { - all_utxos_spent_by_block.insert(*outpoint, utxo); - } - batch.zs_delete(utxo_by_outpoint, outpoint); - } - // Coinbase inputs represent new coins, - // so there are no UTXOs to mark as spent. - transparent::Input::Coinbase { .. } => {} - } - } - - // Mark sprout, sapling and orchard nullifiers as spent - for sprout_nullifier in transaction.sprout_nullifiers() { - batch.zs_insert(sprout_nullifiers, sprout_nullifier, ()); - } - for sapling_nullifier in transaction.sapling_nullifiers() { - batch.zs_insert(sapling_nullifiers, sapling_nullifier, ()); - } - for orchard_nullifier in transaction.orchard_nullifiers() { - batch.zs_insert(orchard_nullifiers, orchard_nullifier, ()); - } - - for sprout_note_commitment in transaction.sprout_note_commitments() { - sprout_note_commitment_tree.append(*sprout_note_commitment)?; - } - for sapling_note_commitment in transaction.sapling_note_commitments() { - sapling_note_commitment_tree.append(*sapling_note_commitment)?; - } - for orchard_note_commitment in transaction.orchard_note_commitments() { - orchard_note_commitment_tree.append(*orchard_note_commitment)?; - } - } - - let sprout_root = sprout_note_commitment_tree.root(); - let sapling_root = sapling_note_commitment_tree.root(); - let orchard_root = orchard_note_commitment_tree.root(); - - history_tree.push(self.network, block.clone(), sapling_root, orchard_root)?; - - // Compute the new anchors and index them - // Note: if the root hasn't changed, we write the same value again. - batch.zs_insert(sprout_anchors, sprout_root, &sprout_note_commitment_tree); - batch.zs_insert(sapling_anchors, sapling_root, ()); - batch.zs_insert(orchard_anchors, orchard_root, ()); - - // Update the trees in state - if let Some(h) = finalized_tip_height { - batch.zs_delete(sprout_note_commitment_tree_cf, h); - batch.zs_delete(sapling_note_commitment_tree_cf, h); - batch.zs_delete(orchard_note_commitment_tree_cf, h); - batch.zs_delete(history_tree_cf, h); - } - - batch.zs_insert( - sprout_note_commitment_tree_cf, - height, - sprout_note_commitment_tree, - ); - - batch.zs_insert( - sapling_note_commitment_tree_cf, - height, - sapling_note_commitment_tree, - ); - - batch.zs_insert( - orchard_note_commitment_tree_cf, - height, - orchard_note_commitment_tree, - ); - - if let Some(history_tree) = history_tree.as_ref() { - batch.zs_insert(history_tree_cf, height, history_tree); - } - - // Some utxos are spent in the same block so they will be in `new_outputs`. - all_utxos_spent_by_block.extend(new_outputs); - - let current_pool = self.current_value_pool(); - let new_pool = current_pool.add_block(block.borrow(), &all_utxos_spent_by_block)?; - batch.zs_insert(tip_chain_value_pool, (), new_pool); - - Ok(batch) - }; - - // In case of errors, propagate and do not write the batch. - let batch = prepare_commit()?; - - // The block has passed contextual validation, so update the metrics - block_precommit_metrics(&block, hash, height); - - let result = self.db.write(batch).map(|()| hash); - - tracing::trace!(?source, "committed block from"); - - // TODO: move the stop height check to the syncer (#3442) - if result.is_ok() && self.is_at_stop_height(height) { - tracing::info!(?source, "committed block from"); - tracing::info!( - ?height, - ?hash, - "stopping at configured height, flushing database to disk" - ); - - self.db.shutdown(); - - Self::exit_process(); - } - - result.map_err(Into::into) - } - - /// Exit the host process. - /// - /// Designed for debugging and tests. - /// - /// TODO: move the stop height check to the syncer (#3442) - fn exit_process() -> ! { - tracing::info!("exiting Zebra"); - - // Some OSes require a flush to send all output to the terminal. - // Zebra's logging doesn't depend on `tokio`, so we flush the stdlib sync streams. - // - // TODO: if this doesn't work, send an empty line as well. - let _ = stdout().lock().flush(); - let _ = stderr().lock().flush(); - - std::process::exit(0); - } - /// Commit a finalized block to the state. /// /// It's the caller's responsibility to ensure that blocks are committed in @@ -532,267 +224,171 @@ impl FinalizedState { block_result } - /// Returns the tip height and hash if there is one. - pub fn tip(&self) -> Option<(block::Height, block::Hash)> { - let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); - self.db - .reverse_iterator(hash_by_height) - .next() - .map(|(height_bytes, hash_bytes)| { - let height = block::Height::from_bytes(height_bytes); - let hash = block::Hash::from_bytes(hash_bytes); - - (height, hash) - }) - } - - /// Returns the tip block, if there is one. - pub fn tip_block(&self) -> Option> { - let (height, _hash) = self.tip()?; - self.block(height.into()) - } - - /// Returns the height of the given block if it exists. - pub fn height(&self, hash: block::Hash) -> Option { - let height_by_hash = self.db.cf_handle("height_by_hash").unwrap(); - self.db.zs_get(height_by_hash, &hash) - } - - /// Returns the given block if it exists. - pub fn block(&self, hash_or_height: HashOrHeight) -> Option> { - let height_by_hash = self.db.cf_handle("height_by_hash").unwrap(); - let block_by_height = self.db.cf_handle("block_by_height").unwrap(); - let height = hash_or_height.height_or_else(|hash| self.db.zs_get(height_by_hash, &hash))?; - - self.db.zs_get(block_by_height, &height) - } - - /// Returns the `transparent::Output` pointed to by the given - /// `transparent::OutPoint` if it is present. - pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { - let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap(); - self.db.zs_get(utxo_by_outpoint, outpoint) - } - - /// Returns `true` if the finalized state contains `sprout_nullifier`. - pub fn contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool { - let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap(); - self.db.zs_contains(sprout_nullifiers, &sprout_nullifier) - } - - /// Returns `true` if the finalized state contains `sapling_nullifier`. - pub fn contains_sapling_nullifier(&self, sapling_nullifier: &sapling::Nullifier) -> bool { - let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap(); - self.db.zs_contains(sapling_nullifiers, &sapling_nullifier) - } - - /// Returns `true` if the finalized state contains `orchard_nullifier`. - pub fn contains_orchard_nullifier(&self, orchard_nullifier: &orchard::Nullifier) -> bool { - let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap(); - self.db.zs_contains(orchard_nullifiers, &orchard_nullifier) - } - - /// Returns `true` if the finalized state contains `sprout_anchor`. - #[allow(unused)] - pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool { - let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap(); - self.db.zs_contains(sprout_anchors, &sprout_anchor) - } - - /// Returns `true` if the finalized state contains `sapling_anchor`. - pub fn contains_sapling_anchor(&self, sapling_anchor: &sapling::tree::Root) -> bool { - let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap(); - self.db.zs_contains(sapling_anchors, &sapling_anchor) - } - - /// Returns `true` if the finalized state contains `orchard_anchor`. - pub fn contains_orchard_anchor(&self, orchard_anchor: &orchard::tree::Root) -> bool { - let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap(); - self.db.zs_contains(orchard_anchors, &orchard_anchor) - } - - /// Returns the finalized hash for a given `block::Height` if it is present. - pub fn hash(&self, height: block::Height) -> Option { - let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); - self.db.zs_get(hash_by_height, &height) - } - - /// Returns the given transaction if it exists. - pub fn transaction(&self, hash: transaction::Hash) -> Option> { - let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap(); - self.db - .zs_get(tx_by_hash, &hash) - .map(|TransactionLocation { index, height }| { - let block = self - .block(height.into()) - .expect("block will exist if TransactionLocation does"); - - block.transactions[index as usize].clone() - }) - } - - /// Returns the Sprout note commitment tree of the finalized tip - /// or the empty tree if the state is empty. - pub fn sprout_note_commitment_tree(&self) -> sprout::tree::NoteCommitmentTree { - let height = match self.finalized_tip_height() { - Some(h) => h, - None => return Default::default(), - }; - - let sprout_note_commitment_tree = self.db.cf_handle("sprout_note_commitment_tree").unwrap(); - - self.db - .zs_get(sprout_note_commitment_tree, &height) - .expect("Sprout note commitment tree must exist if there is a finalized tip") - } - - /// Returns the Sprout note commitment tree matching the given anchor. + /// Immediately commit a `finalized` block to the finalized state. /// - /// This is used for interstitial tree building, which is unique to Sprout. - pub fn sprout_note_commitment_tree_by_anchor( - &self, - sprout_anchor: &sprout::tree::Root, - ) -> Option { - let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap(); + /// This can be called either by the non-finalized state (when finalizing + /// a block) or by the checkpoint verifier. + /// + /// Use `source` as 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 + /// - If `hashFinalSaplingRoot` / `hashLightClientRoot` / `hashBlockCommitments` + /// does not match the expected value + pub fn commit_finalized_direct( + &mut self, + finalized: FinalizedBlock, + source: &str, + ) -> Result { + let committed_tip_hash = self.finalized_tip_hash(); + let committed_tip_height = self.finalized_tip_height(); - self.db.zs_get(sprout_anchors, sprout_anchor) - } + // Assert that callers (including unit tests) get the chain order correct + if self.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: {}", + source, + ); + assert_eq!( + block::Height(0), + finalized.height, + "cannot commit genesis: invalid height, source: {}", + source, + ); + } else { + assert_eq!( + committed_tip_height.expect("state must have a genesis block committed") + 1, + Some(finalized.height), + "committed block height must be 1 more than the finalized tip height, source: {}", + source, + ); - /// Returns the Sapling note commitment tree of the finalized tip - /// or the empty tree if the state is empty. - pub fn sapling_note_commitment_tree(&self) -> sapling::tree::NoteCommitmentTree { - let height = match self.finalized_tip_height() { - Some(h) => h, - None => return Default::default(), - }; - - let sapling_note_commitment_tree = - self.db.cf_handle("sapling_note_commitment_tree").unwrap(); - - self.db - .zs_get(sapling_note_commitment_tree, &height) - .expect("Sapling note commitment tree must exist if there is a finalized tip") - } - - /// Returns the Orchard note commitment tree of the finalized tip - /// or the empty tree if the state is empty. - pub fn orchard_note_commitment_tree(&self) -> orchard::tree::NoteCommitmentTree { - let height = match self.finalized_tip_height() { - Some(h) => h, - None => return Default::default(), - }; - - let orchard_note_commitment_tree = - self.db.cf_handle("orchard_note_commitment_tree").unwrap(); - - self.db - .zs_get(orchard_note_commitment_tree, &height) - .expect("Orchard note commitment tree must exist if there is a finalized tip") - } - - /// 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 { - match self.finalized_tip_height() { - Some(height) => { - let history_tree_cf = self.db.cf_handle("history_tree").unwrap(); - let history_tree: Option = - self.db.zs_get(history_tree_cf, &height); - if let Some(non_empty_tree) = history_tree { - HistoryTree::from(non_empty_tree) - } else { - Default::default() - } - } - None => Default::default(), + assert_eq!( + committed_tip_hash, finalized.block.header.previous_block_hash, + "committed block must be a child of the finalized tip, source: {}", + source, + ); } + + // Check the block commitment. For Nu5-onward, the block hash commits only + // to non-authorizing data (see ZIP-244). This checks the authorizing data + // commitment, making sure the entire block contents were committed to. + // The test is done here (and not during semantic validation) because it needs + // 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(); + check::finalized_block_commitment_is_valid_for_chain_history( + &finalized, + self.network, + &history_tree, + )?; + + let finalized_height = finalized.height; + let finalized_hash = finalized.hash; + + let result = self.write_block(finalized, history_tree, source); + + // TODO: move the stop height check to the syncer (#3442) + if result.is_ok() && self.is_at_stop_height(finalized_height) { + tracing::info!( + height = ?finalized_height, + hash = ?finalized_hash, + block_source = ?source, + "stopping at configured height, flushing database to disk" + ); + + self.db.shutdown(); + + Self::exit_process(); + } + + result } - /// Returns the `Path` where the files used by this database are located. - #[allow(dead_code)] - pub fn path(&self) -> &Path { - self.db.path() + /// 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; + + 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 batch = disk_db::DiskWriteBatch::new(); + + // In case of errors, propagate and do not write the batch. + let batch = batch.prepare_block_batch( + &self.db, + finalized, + self.network, + self.finalized_tip_height(), + all_utxos_spent_by_block, + self.sprout_note_commitment_tree(), + self.sapling_note_commitment_tree(), + self.orchard_note_commitment_tree(), + history_tree, + self.finalized_value_pool(), + )?; + + self.db.write(batch)?; + + tracing::trace!(?source, "committed block from"); + + Ok(finalized_hash) } - /// Returns the stored `ValueBalance` for the best chain at the finalized tip height. - pub fn current_value_pool(&self) -> ValueBalance { - let value_pool_cf = self.db.cf_handle("tip_chain_value_pool").unwrap(); - self.db - .zs_get(value_pool_cf, &()) - .unwrap_or_else(ValueBalance::zero) + /// 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 { + let debug_stop_at_height = match self.debug_stop_at_height { + Some(debug_stop_at_height) => debug_stop_at_height, + None => return false, + }; + + if block_height < debug_stop_at_height { + return false; + } + + true + } + + /// Exit the host process. + /// + /// Designed for debugging and tests. + /// + /// TODO: move the stop height check to the syncer (#3442) + fn exit_process() -> ! { + tracing::info!("exiting Zebra"); + + // Some OSes require a flush to send all output to the terminal. + // Zebra's logging doesn't depend on `tokio`, so we flush the stdlib sync streams. + // + // TODO: if this doesn't work, send an empty line as well. + let _ = stdout().lock().flush(); + let _ = stderr().lock().flush(); + + std::process::exit(0); } } - -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 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 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" - ); - - 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 - ); -} diff --git a/zebra-state/src/service/finalized_state/arbitrary.rs b/zebra-state/src/service/finalized_state/arbitrary.rs index 4adcf0395..8f9af08b4 100644 --- a/zebra-state/src/service/finalized_state/arbitrary.rs +++ b/zebra-state/src/service/finalized_state/arbitrary.rs @@ -96,7 +96,7 @@ where impl FinalizedState { /// Allow to set up a fake value pool in the database for testing purposes. - pub fn set_current_value_pool(&self, fake_value_pool: ValueBalance) { + 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(); diff --git a/zebra-state/src/service/finalized_state/disk_db.rs b/zebra-state/src/service/finalized_state/disk_db.rs index 9ac4052bf..a983cea1f 100644 --- a/zebra-state/src/service/finalized_state/disk_db.rs +++ b/zebra-state/src/service/finalized_state/disk_db.rs @@ -1,7 +1,7 @@ -//! Module defining access to RocksDB via accessor traits. +//! Provides low-level access to RocksDB using some database-specific types. //! //! This module makes sure that: -//! - all disk writes happen inside a RocksDB transaction, and +//! - all disk writes happen inside a RocksDB transaction ([`WriteBatch`]), and //! - format-specific invariants are maintained. //! //! # Correctness diff --git a/zebra-state/src/service/finalized_state/disk_format.rs b/zebra-state/src/service/finalized_state/disk_format.rs index 654fc3ba6..7aca5f013 100644 --- a/zebra-state/src/service/finalized_state/disk_format.rs +++ b/zebra-state/src/service/finalized_state/disk_format.rs @@ -1,4 +1,4 @@ -//! Module defining the serialization format for finalized data. +//! Serialization formats for finalized data. //! //! # Correctness //! diff --git a/zebra-state/src/service/finalized_state/zebra_db.rs b/zebra-state/src/service/finalized_state/zebra_db.rs new file mode 100644 index 000000000..b03ec0ea8 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db.rs @@ -0,0 +1,509 @@ +//! Provides high-level access to the database using [`zebra_chain`] types. +//! +//! This module makes sure that: +//! - all disk writes happen inside a RocksDB transaction, and +//! - format-specific invariants are maintained. +//! +//! # Correctness +//! +//! The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must +//! be incremented each time the database format (column, serialization, etc) changes. + +use std::{borrow::Borrow, collections::HashMap, sync::Arc}; + +use zebra_chain::{ + amount::NonNegative, + block::{self, Block, Height}, + history_tree::{HistoryTree, NonEmptyHistoryTree}, + orchard, + parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, + sapling, sprout, + transaction::{self, Transaction}, + transparent, + value_balance::ValueBalance, +}; + +use crate::{ + service::finalized_state::{ + disk_db::{DiskDb, ReadDisk, WriteDisk}, + disk_format::{FromDisk, TransactionLocation}, + FinalizedBlock, FinalizedState, + }, + BoxError, HashOrHeight, +}; + +use super::disk_db::DiskWriteBatch; + +impl FinalizedState { + // Read block methods + + /// Returns true if the database is empty. + pub fn is_empty(&self) -> bool { + let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); + self.db.is_empty(hash_by_height) + } + + /// Returns the tip height and hash, if there is one. + pub fn tip(&self) -> Option<(block::Height, block::Hash)> { + let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); + self.db + .reverse_iterator(hash_by_height) + .next() + .map(|(height_bytes, hash_bytes)| { + let height = block::Height::from_bytes(height_bytes); + let hash = block::Hash::from_bytes(hash_bytes); + + (height, hash) + }) + } + + /// Returns the finalized hash for a given `block::Height` if it is present. + pub fn hash(&self, height: block::Height) -> Option { + let hash_by_height = self.db.cf_handle("hash_by_height").unwrap(); + self.db.zs_get(hash_by_height, &height) + } + + /// Returns the height of the given block if it exists. + pub fn height(&self, hash: block::Hash) -> Option { + let height_by_hash = self.db.cf_handle("height_by_hash").unwrap(); + self.db.zs_get(height_by_hash, &hash) + } + + /// Returns the given block if it exists. + pub fn block(&self, hash_or_height: HashOrHeight) -> Option> { + let height_by_hash = self.db.cf_handle("height_by_hash").unwrap(); + let block_by_height = self.db.cf_handle("block_by_height").unwrap(); + let height = hash_or_height.height_or_else(|hash| self.db.zs_get(height_by_hash, &hash))?; + + self.db.zs_get(block_by_height, &height) + } + + // Read transaction methods + + /// Returns the given transaction if it exists. + pub fn transaction(&self, hash: transaction::Hash) -> Option> { + let tx_by_hash = self.db.cf_handle("tx_by_hash").unwrap(); + self.db + .zs_get(tx_by_hash, &hash) + .map(|TransactionLocation { index, height }| { + let block = self + .block(height.into()) + .expect("block will exist if TransactionLocation does"); + + block.transactions[index as usize].clone() + }) + } + + // Read transparent methods + + /// Returns the `transparent::Output` pointed to by the given + /// `transparent::OutPoint` if it is present. + pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option { + let utxo_by_outpoint = self.db.cf_handle("utxo_by_outpoint").unwrap(); + self.db.zs_get(utxo_by_outpoint, outpoint) + } + + // Read shielded methods + + /// Returns `true` if the finalized state contains `sprout_nullifier`. + pub fn contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool { + let sprout_nullifiers = self.db.cf_handle("sprout_nullifiers").unwrap(); + self.db.zs_contains(sprout_nullifiers, &sprout_nullifier) + } + + /// Returns `true` if the finalized state contains `sapling_nullifier`. + pub fn contains_sapling_nullifier(&self, sapling_nullifier: &sapling::Nullifier) -> bool { + let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap(); + self.db.zs_contains(sapling_nullifiers, &sapling_nullifier) + } + + /// Returns `true` if the finalized state contains `orchard_nullifier`. + pub fn contains_orchard_nullifier(&self, orchard_nullifier: &orchard::Nullifier) -> bool { + let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap(); + self.db.zs_contains(orchard_nullifiers, &orchard_nullifier) + } + + /// Returns `true` if the finalized state contains `sprout_anchor`. + #[allow(unused)] + pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool { + let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap(); + self.db.zs_contains(sprout_anchors, &sprout_anchor) + } + + /// Returns `true` if the finalized state contains `sapling_anchor`. + pub fn contains_sapling_anchor(&self, sapling_anchor: &sapling::tree::Root) -> bool { + let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap(); + self.db.zs_contains(sapling_anchors, &sapling_anchor) + } + + /// Returns `true` if the finalized state contains `orchard_anchor`. + pub fn contains_orchard_anchor(&self, orchard_anchor: &orchard::tree::Root) -> bool { + let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap(); + self.db.zs_contains(orchard_anchors, &orchard_anchor) + } + + /// Returns the Sprout note commitment tree of the finalized tip + /// or the empty tree if the state is empty. + pub fn sprout_note_commitment_tree(&self) -> sprout::tree::NoteCommitmentTree { + let height = match self.finalized_tip_height() { + Some(h) => h, + None => return Default::default(), + }; + + let sprout_note_commitment_tree = self.db.cf_handle("sprout_note_commitment_tree").unwrap(); + + self.db + .zs_get(sprout_note_commitment_tree, &height) + .expect("Sprout note commitment tree must exist if there is a finalized tip") + } + + /// Returns the Sprout note commitment tree matching the given anchor. + /// + /// This is used for interstitial tree building, which is unique to Sprout. + pub fn sprout_note_commitment_tree_by_anchor( + &self, + sprout_anchor: &sprout::tree::Root, + ) -> Option { + let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap(); + + self.db.zs_get(sprout_anchors, sprout_anchor) + } + + /// Returns the Sapling note commitment tree of the finalized tip + /// or the empty tree if the state is empty. + pub fn sapling_note_commitment_tree(&self) -> sapling::tree::NoteCommitmentTree { + let height = match self.finalized_tip_height() { + Some(h) => h, + None => return Default::default(), + }; + + let sapling_note_commitment_tree = + self.db.cf_handle("sapling_note_commitment_tree").unwrap(); + + self.db + .zs_get(sapling_note_commitment_tree, &height) + .expect("Sapling note commitment tree must exist if there is a finalized tip") + } + + /// Returns the Orchard note commitment tree of the finalized tip + /// or the empty tree if the state is empty. + pub fn orchard_note_commitment_tree(&self) -> orchard::tree::NoteCommitmentTree { + let height = match self.finalized_tip_height() { + Some(h) => h, + None => return Default::default(), + }; + + let orchard_note_commitment_tree = + self.db.cf_handle("orchard_note_commitment_tree").unwrap(); + + self.db + .zs_get(orchard_note_commitment_tree, &height) + .expect("Orchard note commitment tree must exist if there is a finalized tip") + } + + // Read chain methods + + /// 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 { + match self.finalized_tip_height() { + Some(height) => { + let history_tree_cf = self.db.cf_handle("history_tree").unwrap(); + let history_tree: Option = + self.db.zs_get(history_tree_cf, &height); + if let Some(non_empty_tree) = history_tree { + HistoryTree::from(non_empty_tree) + } else { + Default::default() + } + } + None => Default::default(), + } + } + + /// Returns the stored `ValueBalance` for the best chain at the finalized tip height. + pub fn finalized_value_pool(&self) -> ValueBalance { + let value_pool_cf = self.db.cf_handle("tip_chain_value_pool").unwrap(); + self.db + .zs_get(value_pool_cf, &()) + .unwrap_or_else(ValueBalance::zero) + } + + // Metrics methods + + /// Update metrics before committing a block. + 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 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 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" + ); + + 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 + ); + } +} + +// Write methods + +impl DiskWriteBatch { + /// Prepare a database batch containing a `finalized` block, + /// and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch will not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from writing to the DB + /// - Propagates any errors from updating history and note commitment trees + /// + /// TODO: split up this function in the next PR. + #[allow(clippy::too_many_arguments)] + pub fn prepare_block_batch( + mut self, + db: &DiskDb, + finalized: FinalizedBlock, + network: Network, + current_tip_height: Option, + mut all_utxos_spent_by_block: HashMap, + mut sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree, + mut sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, + mut orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + mut history_tree: HistoryTree, + current_value_pool: ValueBalance, + ) -> Result { + let hash_by_height = db.cf_handle("hash_by_height").unwrap(); + let height_by_hash = db.cf_handle("height_by_hash").unwrap(); + let block_by_height = db.cf_handle("block_by_height").unwrap(); + let tx_by_hash = db.cf_handle("tx_by_hash").unwrap(); + let utxo_by_outpoint = db.cf_handle("utxo_by_outpoint").unwrap(); + + let sprout_nullifiers = db.cf_handle("sprout_nullifiers").unwrap(); + let sapling_nullifiers = db.cf_handle("sapling_nullifiers").unwrap(); + let orchard_nullifiers = db.cf_handle("orchard_nullifiers").unwrap(); + + let sprout_anchors = db.cf_handle("sprout_anchors").unwrap(); + let sapling_anchors = db.cf_handle("sapling_anchors").unwrap(); + let orchard_anchors = db.cf_handle("orchard_anchors").unwrap(); + + let sprout_note_commitment_tree_cf = db.cf_handle("sprout_note_commitment_tree").unwrap(); + let sapling_note_commitment_tree_cf = db.cf_handle("sapling_note_commitment_tree").unwrap(); + let orchard_note_commitment_tree_cf = db.cf_handle("orchard_note_commitment_tree").unwrap(); + let history_tree_cf = db.cf_handle("history_tree").unwrap(); + + let tip_chain_value_pool = db.cf_handle("tip_chain_value_pool").unwrap(); + + let FinalizedBlock { + block, + hash, + height, + new_outputs, + transaction_hashes, + } = finalized; + + // The block has passed contextual validation, so update the metrics + FinalizedState::block_precommit_metrics(&block, hash, height); + + // Index the block + self.zs_insert(hash_by_height, height, hash); + self.zs_insert(height_by_hash, hash, height); + self.zs_insert(block_by_height, height, &block); + + // # Consensus + // + // > A transaction MUST NOT spend an output of the genesis block coinbase transaction. + // > (There is one such zero-valued output, on each of Testnet and Mainnet.) + // + // https://zips.z.cash/protocol/protocol.pdf#txnconsensus + if block.header.previous_block_hash == GENESIS_PREVIOUS_BLOCK_HASH { + // Insert empty note commitment trees. Note that these can't be + // used too early (e.g. the Orchard tree before Nu5 activates) + // since the block validation will make sure only appropriate + // transactions are allowed in a block. + self.zs_insert( + sprout_note_commitment_tree_cf, + height, + sprout_note_commitment_tree, + ); + self.zs_insert( + sapling_note_commitment_tree_cf, + height, + sapling_note_commitment_tree, + ); + self.zs_insert( + orchard_note_commitment_tree_cf, + height, + orchard_note_commitment_tree, + ); + return Ok(self); + } + + // Index all new transparent outputs + for (outpoint, utxo) in new_outputs.borrow().iter() { + self.zs_insert(utxo_by_outpoint, outpoint, utxo); + } + + // Index each transaction, spent inputs, nullifiers + for (transaction_index, (transaction, transaction_hash)) in block + .transactions + .iter() + .zip(transaction_hashes.iter()) + .enumerate() + { + let transaction_location = TransactionLocation { + height, + index: transaction_index + .try_into() + .expect("no more than 4 billion transactions per block"), + }; + self.zs_insert(tx_by_hash, transaction_hash, transaction_location); + + // Mark all transparent inputs as spent. + // + // Coinbase inputs represent new coins, + // so there are no UTXOs to mark as spent. + for outpoint in transaction + .inputs() + .iter() + .flat_map(|input| input.outpoint()) + { + self.zs_delete(utxo_by_outpoint, outpoint); + } + + // Mark sprout, sapling and orchard nullifiers as spent + for sprout_nullifier in transaction.sprout_nullifiers() { + self.zs_insert(sprout_nullifiers, sprout_nullifier, ()); + } + for sapling_nullifier in transaction.sapling_nullifiers() { + self.zs_insert(sapling_nullifiers, sapling_nullifier, ()); + } + for orchard_nullifier in transaction.orchard_nullifiers() { + self.zs_insert(orchard_nullifiers, orchard_nullifier, ()); + } + + for sprout_note_commitment in transaction.sprout_note_commitments() { + sprout_note_commitment_tree.append(*sprout_note_commitment)?; + } + for sapling_note_commitment in transaction.sapling_note_commitments() { + sapling_note_commitment_tree.append(*sapling_note_commitment)?; + } + for orchard_note_commitment in transaction.orchard_note_commitments() { + orchard_note_commitment_tree.append(*orchard_note_commitment)?; + } + } + + let sprout_root = sprout_note_commitment_tree.root(); + let sapling_root = sapling_note_commitment_tree.root(); + let orchard_root = orchard_note_commitment_tree.root(); + + history_tree.push(network, block.clone(), sapling_root, orchard_root)?; + + // Compute the new anchors and index them + // Note: if the root hasn't changed, we write the same value again. + self.zs_insert(sprout_anchors, sprout_root, &sprout_note_commitment_tree); + self.zs_insert(sapling_anchors, sapling_root, ()); + self.zs_insert(orchard_anchors, orchard_root, ()); + + // Update the trees in state + if let Some(h) = current_tip_height { + self.zs_delete(sprout_note_commitment_tree_cf, h); + self.zs_delete(sapling_note_commitment_tree_cf, h); + self.zs_delete(orchard_note_commitment_tree_cf, h); + self.zs_delete(history_tree_cf, h); + } + + self.zs_insert( + sprout_note_commitment_tree_cf, + height, + sprout_note_commitment_tree, + ); + + self.zs_insert( + sapling_note_commitment_tree_cf, + height, + sapling_note_commitment_tree, + ); + + self.zs_insert( + orchard_note_commitment_tree_cf, + height, + orchard_note_commitment_tree, + ); + + if let Some(history_tree) = history_tree.as_ref() { + self.zs_insert(history_tree_cf, height, history_tree); + } + + // Some utxos are spent in the same block so they will be in `new_outputs`. + all_utxos_spent_by_block.extend(new_outputs); + + let new_pool = current_value_pool.add_block(block.borrow(), &all_utxos_spent_by_block)?; + self.zs_insert(tip_chain_value_pool, (), new_pool); + + Ok(self) + } +} diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index b7252b2f2..b6ad1fad7 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -175,7 +175,7 @@ impl NonFinalizedState { finalized_state.sapling_note_commitment_tree(), finalized_state.orchard_note_commitment_tree(), finalized_state.history_tree(), - finalized_state.current_value_pool(), + finalized_state.finalized_value_pool(), ); let (height, hash) = (prepared.height, prepared.hash); diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index f8603ea2e..a185b464a 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -466,7 +466,7 @@ fn rejection_restores_internal_state_genesis() -> Result<()> { let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); // use `valid_count` as the number of valid blocks before an invalid block let valid_tip_height = chain[valid_count - 1].height; diff --git a/zebra-state/src/service/non_finalized_state/tests/vectors.rs b/zebra-state/src/service/non_finalized_state/tests/vectors.rs index 5d407a098..87a9952f0 100644 --- a/zebra-state/src/service/non_finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/non_finalized_state/tests/vectors.rs @@ -190,7 +190,7 @@ fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> { let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); state.commit_new_chain(block1.clone().prepare(), &finalized_state)?; state.commit_block(block2.clone().prepare(), &finalized_state)?; @@ -241,7 +241,7 @@ fn commit_block_extending_best_chain_doesnt_drop_worst_chains_for_network( let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); assert_eq!(0, state.chain_set.len()); state.commit_new_chain(block1.prepare(), &finalized_state)?; @@ -288,7 +288,7 @@ fn shorter_chain_can_be_best_chain_for_network(network: Network) -> Result<()> { let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); state.commit_new_chain(block1.prepare(), &finalized_state)?; state.commit_block(long_chain_block1.prepare(), &finalized_state)?; @@ -335,7 +335,7 @@ fn longer_chain_with_more_work_wins_for_network(network: Network) -> Result<()> let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); state.commit_new_chain(block1.prepare(), &finalized_state)?; state.commit_block(long_chain_block1.prepare(), &finalized_state)?; @@ -380,7 +380,7 @@ fn equal_length_goes_to_more_work_for_network(network: Network) -> Result<()> { let finalized_state = FinalizedState::new(&Config::ephemeral(), network); let fake_value_pool = ValueBalance::::fake_populated_pool(); - finalized_state.set_current_value_pool(fake_value_pool); + finalized_state.set_finalized_value_pool(fake_value_pool); state.commit_new_chain(block1.prepare(), &finalized_state)?; state.commit_block(less_work_child.prepare(), &finalized_state)?; diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs index 5d03e2b31..c73d0d827 100644 --- a/zebra-state/src/service/tests.rs +++ b/zebra-state/src/service/tests.rs @@ -471,7 +471,7 @@ proptest! { let (mut state_service, _, _) = StateService::new(Config::ephemeral(), network); - prop_assert_eq!(state_service.disk.current_value_pool(), ValueBalance::zero()); + prop_assert_eq!(state_service.disk.finalized_value_pool(), ValueBalance::zero()); prop_assert_eq!( state_service.mem.best_chain().map(|chain| chain.chain_value_pools).unwrap_or_else(ValueBalance::zero), ValueBalance::zero() @@ -495,7 +495,7 @@ proptest! { state_service.queue_and_commit_finalized(block.clone()); prop_assert_eq!( - state_service.disk.current_value_pool(), + state_service.disk.finalized_value_pool(), expected_finalized_value_pool.clone()?.constrain()? ); @@ -504,7 +504,7 @@ proptest! { let transparent_value = ValueBalance::from_transparent_amount(transparent_value); expected_transparent_pool = (expected_transparent_pool + transparent_value).unwrap(); prop_assert_eq!( - state_service.disk.current_value_pool(), + state_service.disk.finalized_value_pool(), expected_transparent_pool ); }