From f8a4021c077d4f0b3204dd7cb8dc729522209244 Mon Sep 17 00:00:00 2001 From: teor Date: Tue, 1 Mar 2022 08:21:03 +1000 Subject: [PATCH] refactor(state): split database access into modules by Zebra types (#3617) Also split the genesis block check from the genesis note commitment trees. --- .../src/service/finalized_state/zebra_db.rs | 723 +----------------- .../service/finalized_state/zebra_db/block.rs | 238 ++++++ .../service/finalized_state/zebra_db/chain.rs | 132 ++++ .../finalized_state/zebra_db/metrics.rs | 85 ++ .../finalized_state/zebra_db/shielded.rs | 317 ++++++++ .../finalized_state/zebra_db/transparent.rs | 75 ++ 6 files changed, 852 insertions(+), 718 deletions(-) create mode 100644 zebra-state/src/service/finalized_state/zebra_db/block.rs create mode 100644 zebra-state/src/service/finalized_state/zebra_db/chain.rs create mode 100644 zebra-state/src/service/finalized_state/zebra_db/metrics.rs create mode 100644 zebra-state/src/service/finalized_state/zebra_db/shielded.rs create mode 100644 zebra-state/src/service/finalized_state/zebra_db/transparent.rs diff --git a/zebra-state/src/service/finalized_state/zebra_db.rs b/zebra-state/src/service/finalized_state/zebra_db.rs index 7df4552c..2ea74457 100644 --- a/zebra-state/src/service/finalized_state/zebra_db.rs +++ b/zebra-state/src/service/finalized_state/zebra_db.rs @@ -9,721 +9,8 @@ //! 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}, - 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, DiskWriteBatch, ReadDisk, WriteDisk}, - disk_format::{FromDisk, TransactionLocation}, - FinalizedBlock, FinalizedState, - }, - BoxError, HashOrHeight, -}; - -/// An argument wrapper struct for note commitment trees. -#[derive(Clone, Debug)] -pub struct NoteCommitmentTrees { - sprout: sprout::tree::NoteCommitmentTree, - sapling: sapling::tree::NoteCommitmentTree, - orchard: orchard::tree::NoteCommitmentTree, -} - -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) - } - - // Read chain history methods - - /// 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") - } - - /// Returns the shielded note commitment trees of the finalized tip - /// or the empty trees if the state is empty. - pub fn note_commitment_trees(&self) -> NoteCommitmentTrees { - NoteCommitmentTrees { - sprout: self.sprout_note_commitment_tree(), - sapling: self.sapling_note_commitment_tree(), - orchard: self.orchard_note_commitment_tree(), - } - } - - /// 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) - } - - // Update metrics methods - used when writing - - /// 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 - ); - } -} - -impl DiskWriteBatch { - /// Prepare a database batch containing `finalized.block`, - /// and return it (without actually writing anything). - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Propagates any errors from updating history tree, note commitment trees, or value pools - #[allow(clippy::too_many_arguments)] - pub fn prepare_block_batch( - &mut self, - db: &DiskDb, - finalized: FinalizedBlock, - network: Network, - all_utxos_spent_by_block: HashMap, - // TODO: make an argument struct for all the current note commitment trees & history - mut note_commitment_trees: NoteCommitmentTrees, - history_tree: HistoryTree, - value_pool: ValueBalance, - ) -> Result<(), BoxError> { - 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 FinalizedBlock { - block, - hash, - height, - .. - } = &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 - // - // By returning early, Zebra commits the genesis block and transaction data, - // but it ignores the genesis UTXO and value pool updates. - // - // TODO: commit transaction data but not UTXOs in the next PR. - if self.prepare_genesis_batch(db, &finalized) { - return Ok(()); - } - - self.prepare_transaction_index_batch(db, &finalized, &mut note_commitment_trees)?; - - self.prepare_note_commitment_batch( - db, - &finalized, - network, - note_commitment_trees, - history_tree, - )?; - - self.prepare_chain_value_pools_batch(db, finalized, all_utxos_spent_by_block, value_pool) - } - - /// If `finalized.block` is a genesis block, - /// prepare a database batch that finishes intializing the database, - /// and return `true` (without actually writing anything). - /// - /// Since the genesis block's transactions are skipped, - /// the returned genesis batch should be written to the database immediately. - /// - /// If `finalized.block` is not a genesis block, does nothing. - /// - /// This method never returns an error. - pub fn prepare_genesis_batch(&mut self, db: &DiskDb, finalized: &FinalizedBlock) -> bool { - 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 FinalizedBlock { block, height, .. } = finalized; - - 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::tree::NoteCommitmentTree::default(), - ); - self.zs_insert( - sapling_note_commitment_tree_cf, - height, - sapling::tree::NoteCommitmentTree::default(), - ); - self.zs_insert( - orchard_note_commitment_tree_cf, - height, - orchard::tree::NoteCommitmentTree::default(), - ); - - return true; - } - - false - } - - /// Prepare a database batch containing `finalized.block`'s transaction indexes, - /// and return it (without actually writing anything). - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Propagates any errors from updating note commitment trees - pub fn prepare_transaction_index_batch( - &mut self, - db: &DiskDb, - finalized: &FinalizedBlock, - note_commitment_trees: &mut NoteCommitmentTrees, - ) -> Result<(), BoxError> { - let tx_by_hash = db.cf_handle("tx_by_hash").unwrap(); - - let FinalizedBlock { - block, - height, - transaction_hashes, - .. - } = finalized; - - // Index each transaction hash - for (transaction_index, (transaction, transaction_hash)) in block - .transactions - .iter() - .zip(transaction_hashes.iter()) - .enumerate() - { - let transaction_location = TransactionLocation { - height: *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); - - self.prepare_nullifier_batch(db, transaction)?; - - DiskWriteBatch::update_note_commitment_trees(transaction, note_commitment_trees)?; - } - - self.prepare_transparent_outputs_batch(db, finalized) - } - - /// Prepare a database batch containing `finalized.block`'s UTXO changes, - /// and return it (without actually writing anything). - /// - /// # Errors - /// - /// - This method doesn't currently return any errors, but it might in future - pub fn prepare_transparent_outputs_batch( - &mut self, - db: &DiskDb, - finalized: &FinalizedBlock, - ) -> Result<(), BoxError> { - let utxo_by_outpoint = db.cf_handle("utxo_by_outpoint").unwrap(); - - let FinalizedBlock { - block, new_outputs, .. - } = finalized; - - // Index all new transparent outputs, before deleting any we've spent - for (outpoint, utxo) in new_outputs.borrow().iter() { - self.zs_insert(utxo_by_outpoint, outpoint, utxo); - } - - // Mark all transparent inputs as spent. - // - // Coinbase inputs represent new coins, - // so there are no UTXOs to mark as spent. - for outpoint in block - .transactions - .iter() - .flat_map(|tx| tx.inputs()) - .flat_map(|input| input.outpoint()) - { - self.zs_delete(utxo_by_outpoint, outpoint); - } - - Ok(()) - } - - /// Prepare a database batch containing `finalized.block`'s nullifiers, - /// and return it (without actually writing anything). - /// - /// # Errors - /// - /// - This method doesn't currently return any errors, but it might in future - pub fn prepare_nullifier_batch( - &mut self, - db: &DiskDb, - transaction: &Transaction, - ) -> Result<(), BoxError> { - 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(); - - // 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, ()); - } - - Ok(()) - } - - /// Updates the supplied note commitment trees. - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Propagates any errors from updating note commitment trees - pub fn update_note_commitment_trees( - transaction: &Transaction, - note_commitment_trees: &mut NoteCommitmentTrees, - ) -> Result<(), BoxError> { - // Update the note commitment trees - for sprout_note_commitment in transaction.sprout_note_commitments() { - note_commitment_trees - .sprout - .append(*sprout_note_commitment)?; - } - for sapling_note_commitment in transaction.sapling_note_commitments() { - note_commitment_trees - .sapling - .append(*sapling_note_commitment)?; - } - for orchard_note_commitment in transaction.orchard_note_commitments() { - note_commitment_trees - .orchard - .append(*orchard_note_commitment)?; - } - - Ok(()) - } - - /// Prepare a database batch containing the note commitment and history tree updates - /// from `finalized.block`, and return it (without actually writing anything). - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Propagates any errors from updating the history tree - pub fn prepare_note_commitment_batch( - &mut self, - db: &DiskDb, - finalized: &FinalizedBlock, - network: Network, - // TODO: make an argument struct for all the note commitment trees & history - note_commitment_trees: NoteCommitmentTrees, - history_tree: HistoryTree, - ) -> Result<(), BoxError> { - 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 FinalizedBlock { height, .. } = finalized; - - let sprout_root = note_commitment_trees.sprout.root(); - let sapling_root = note_commitment_trees.sapling.root(); - let orchard_root = note_commitment_trees.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, ¬e_commitment_trees.sprout); - self.zs_insert(sapling_anchors, sapling_root, ()); - self.zs_insert(orchard_anchors, orchard_root, ()); - - // Update the trees in state - let current_tip_height = *height - 1; - 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_insert( - sprout_note_commitment_tree_cf, - height, - note_commitment_trees.sprout, - ); - - self.zs_insert( - sapling_note_commitment_tree_cf, - height, - note_commitment_trees.sapling, - ); - - self.zs_insert( - orchard_note_commitment_tree_cf, - height, - note_commitment_trees.orchard, - ); - - self.prepare_history_batch( - db, - finalized, - network, - sapling_root, - orchard_root, - history_tree, - ) - } - - /// Prepare a database batch containing the history tree updates - /// from `finalized.block`, and return it (without actually writing anything). - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Returns any errors from updating the history tree - pub fn prepare_history_batch( - &mut self, - db: &DiskDb, - finalized: &FinalizedBlock, - network: Network, - sapling_root: sapling::tree::Root, - orchard_root: orchard::tree::Root, - mut history_tree: HistoryTree, - ) -> Result<(), BoxError> { - let history_tree_cf = db.cf_handle("history_tree").unwrap(); - - let FinalizedBlock { block, height, .. } = finalized; - - history_tree.push(network, block.clone(), sapling_root, orchard_root)?; - - // Update the tree in state - let current_tip_height = *height - 1; - if let Some(h) = current_tip_height { - self.zs_delete(history_tree_cf, h); - } - - // TODO: just store a single history tree, using `()` as the key, - // and remove the delete (like the chain value pool balances). - // This requires a database version update. - if let Some(history_tree) = history_tree.as_ref() { - self.zs_insert(history_tree_cf, height, history_tree); - } - - Ok(()) - } - - /// Prepare a database batch containing the chain value pool update from `finalized.block`, - /// and return it (without actually writing anything). - /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. - /// - /// # Errors - /// - /// - Propagates any errors from updating value pools - pub fn prepare_chain_value_pools_batch( - &mut self, - db: &DiskDb, - finalized: FinalizedBlock, - mut all_utxos_spent_by_block: HashMap, - value_pool: ValueBalance, - ) -> Result<(), BoxError> { - let tip_chain_value_pool = db.cf_handle("tip_chain_value_pool").unwrap(); - - let FinalizedBlock { - block, new_outputs, .. - } = finalized; - - // 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 = value_pool.add_block(block.borrow(), &all_utxos_spent_by_block)?; - self.zs_insert(tip_chain_value_pool, (), new_pool); - - Ok(()) - } -} +pub mod block; +pub mod chain; +pub mod metrics; +pub mod shielded; +pub mod transparent; diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs new file mode 100644 index 00000000..45de0c4b --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -0,0 +1,238 @@ +//! Provides high-level access to database [`Block`]s and [`Transaction`]s. +//! +//! 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::{collections::HashMap, sync::Arc}; + +use zebra_chain::{ + amount::NonNegative, + block::{self, Block}, + history_tree::HistoryTree, + parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, + transaction::{self, Transaction}, + transparent, + value_balance::ValueBalance, +}; + +use crate::{ + service::finalized_state::{ + disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, + disk_format::{FromDisk, TransactionLocation}, + zebra_db::shielded::NoteCommitmentTrees, + FinalizedBlock, FinalizedState, + }, + BoxError, HashOrHeight, +}; + +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() + }) + } +} + +impl DiskWriteBatch { + // Write block methods + + /// Prepare a database batch containing `finalized.block`, + /// and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from updating history tree, note commitment trees, or value pools + #[allow(clippy::too_many_arguments)] + pub fn prepare_block_batch( + &mut self, + db: &DiskDb, + finalized: FinalizedBlock, + network: Network, + all_utxos_spent_by_block: HashMap, + // TODO: make an argument struct for all the current note commitment trees & history + mut note_commitment_trees: NoteCommitmentTrees, + history_tree: HistoryTree, + value_pool: ValueBalance, + ) -> Result<(), BoxError> { + 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 FinalizedBlock { + block, + hash, + height, + .. + } = &finalized; + + // Index the block + self.zs_insert(hash_by_height, height, hash); + self.zs_insert(height_by_hash, hash, height); + + // TODO: as part of ticket #3151, commit transaction data, but not UTXOs or address indexes + 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 + // + // By returning early, Zebra commits the genesis block and transaction data, + // but it ignores the genesis UTXO and value pool updates. + if self.prepare_genesis_batch(db, &finalized) { + return Ok(()); + } + + self.prepare_transaction_index_batch(db, &finalized, &mut note_commitment_trees)?; + + self.prepare_note_commitment_batch( + db, + &finalized, + network, + note_commitment_trees, + history_tree, + )?; + + 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); + + Ok(()) + } + + /// If `finalized.block` is a genesis block, + /// prepare a database batch that finishes intializing the database, + /// and return `true` (without actually writing anything). + /// + /// Since the genesis block's transactions are skipped, + /// the returned genesis batch should be written to the database immediately. + /// + /// If `finalized.block` is not a genesis block, does nothing. + /// + /// This method never returns an error. + pub fn prepare_genesis_batch(&mut self, db: &DiskDb, finalized: &FinalizedBlock) -> bool { + let FinalizedBlock { block, .. } = finalized; + + if block.header.previous_block_hash == GENESIS_PREVIOUS_BLOCK_HASH { + self.prepare_genesis_note_commitment_tree_batch(db, finalized); + + return true; + } + + false + } + + // Write transaction methods + + /// Prepare a database batch containing `finalized.block`'s transaction indexes, + /// and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from updating note commitment trees + pub fn prepare_transaction_index_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + note_commitment_trees: &mut NoteCommitmentTrees, + ) -> Result<(), BoxError> { + let tx_by_hash = db.cf_handle("tx_by_hash").unwrap(); + + let FinalizedBlock { + block, + height, + transaction_hashes, + .. + } = finalized; + + // Index each transaction hash + for (transaction_index, (transaction, transaction_hash)) in block + .transactions + .iter() + .zip(transaction_hashes.iter()) + .enumerate() + { + let transaction_location = TransactionLocation { + height: *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); + + self.prepare_nullifier_batch(db, transaction)?; + + DiskWriteBatch::update_note_commitment_trees(transaction, note_commitment_trees)?; + } + + self.prepare_transparent_outputs_batch(db, finalized) + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/chain.rs b/zebra-state/src/service/finalized_state/zebra_db/chain.rs new file mode 100644 index 00000000..80723953 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/chain.rs @@ -0,0 +1,132 @@ +//! Provides high-level access to database whole-chain: +//! - history trees +//! - chain value pools +//! +//! 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}; + +use zebra_chain::{ + amount::NonNegative, + history_tree::{HistoryTree, NonEmptyHistoryTree}, + orchard, + parameters::Network, + sapling, transparent, + value_balance::ValueBalance, +}; + +use crate::{ + service::finalized_state::{ + disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, + FinalizedBlock, FinalizedState, + }, + BoxError, +}; + +impl FinalizedState { + /// 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) + } +} + +impl DiskWriteBatch { + /// Prepare a database batch containing the history tree updates + /// from `finalized.block`, and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Returns any errors from updating the history tree + pub fn prepare_history_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + network: Network, + sapling_root: sapling::tree::Root, + orchard_root: orchard::tree::Root, + mut history_tree: HistoryTree, + ) -> Result<(), BoxError> { + let history_tree_cf = db.cf_handle("history_tree").unwrap(); + + let FinalizedBlock { block, height, .. } = finalized; + + history_tree.push(network, block.clone(), sapling_root, orchard_root)?; + + // Update the tree in state + let current_tip_height = *height - 1; + if let Some(h) = current_tip_height { + self.zs_delete(history_tree_cf, h); + } + + // TODO: just store a single history tree, using `()` as the key, + // and remove the delete (like the chain value pool balances). + // This requires a database version update. + if let Some(history_tree) = history_tree.as_ref() { + self.zs_insert(history_tree_cf, height, history_tree); + } + + Ok(()) + } + + /// Prepare a database batch containing the chain value pool update from `finalized.block`, + /// and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from updating value pools + pub fn prepare_chain_value_pools_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + mut all_utxos_spent_by_block: HashMap, + value_pool: ValueBalance, + ) -> Result<(), BoxError> { + let tip_chain_value_pool = db.cf_handle("tip_chain_value_pool").unwrap(); + + let FinalizedBlock { + block, new_outputs, .. + } = finalized; + + // Some utxos are spent in the same block, so they will be in `new_outputs`. + all_utxos_spent_by_block.extend(new_outputs.clone()); + + let new_pool = value_pool.add_block(block.borrow(), &all_utxos_spent_by_block)?; + self.zs_insert(tip_chain_value_pool, (), new_pool); + + Ok(()) + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/metrics.rs b/zebra-state/src/service/finalized_state/zebra_db/metrics.rs new file mode 100644 index 00000000..15519029 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/metrics.rs @@ -0,0 +1,85 @@ +//! Provides high-level database metrics. + +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 + .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/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs new file mode 100644 index 00000000..0eae0978 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -0,0 +1,317 @@ +//! Provides high-level access to database shielded: +//! - nullifiers +//! - note commitment trees +//! - anchors +//! +//! 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 zebra_chain::{ + history_tree::HistoryTree, orchard, parameters::Network, sapling, sprout, + transaction::Transaction, +}; + +use crate::{ + service::finalized_state::{ + disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, + FinalizedBlock, FinalizedState, + }, + BoxError, +}; + +/// An argument wrapper struct for note commitment trees. +#[derive(Clone, Debug)] +pub struct NoteCommitmentTrees { + sprout: sprout::tree::NoteCommitmentTree, + sapling: sapling::tree::NoteCommitmentTree, + orchard: orchard::tree::NoteCommitmentTree, +} + +impl FinalizedState { + // 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") + } + + /// Returns the shielded note commitment trees of the finalized tip + /// or the empty trees if the state is empty. + pub fn note_commitment_trees(&self) -> NoteCommitmentTrees { + NoteCommitmentTrees { + sprout: self.sprout_note_commitment_tree(), + sapling: self.sapling_note_commitment_tree(), + orchard: self.orchard_note_commitment_tree(), + } + } +} + +impl DiskWriteBatch { + /// Prepare a database batch containing `finalized.block`'s nullifiers, + /// and return it (without actually writing anything). + /// + /// # Errors + /// + /// - This method doesn't currently return any errors, but it might in future + pub fn prepare_nullifier_batch( + &mut self, + db: &DiskDb, + transaction: &Transaction, + ) -> Result<(), BoxError> { + 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(); + + // 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, ()); + } + + Ok(()) + } + + /// Updates the supplied note commitment trees. + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from updating note commitment trees + pub fn update_note_commitment_trees( + transaction: &Transaction, + note_commitment_trees: &mut NoteCommitmentTrees, + ) -> Result<(), BoxError> { + // Update the note commitment trees + for sprout_note_commitment in transaction.sprout_note_commitments() { + note_commitment_trees + .sprout + .append(*sprout_note_commitment)?; + } + for sapling_note_commitment in transaction.sapling_note_commitments() { + note_commitment_trees + .sapling + .append(*sapling_note_commitment)?; + } + for orchard_note_commitment in transaction.orchard_note_commitments() { + note_commitment_trees + .orchard + .append(*orchard_note_commitment)?; + } + + Ok(()) + } + + /// Prepare a database batch containing the note commitment and history tree updates + /// from `finalized.block`, and return it (without actually writing anything). + /// + /// If this method returns an error, it will be propagated, + /// and the batch should not be written to the database. + /// + /// # Errors + /// + /// - Propagates any errors from updating the history tree + pub fn prepare_note_commitment_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + network: Network, + // TODO: make an argument struct for all the note commitment trees & history + note_commitment_trees: NoteCommitmentTrees, + history_tree: HistoryTree, + ) -> Result<(), BoxError> { + 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 FinalizedBlock { height, .. } = finalized; + + let sprout_root = note_commitment_trees.sprout.root(); + let sapling_root = note_commitment_trees.sapling.root(); + let orchard_root = note_commitment_trees.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, ¬e_commitment_trees.sprout); + self.zs_insert(sapling_anchors, sapling_root, ()); + self.zs_insert(orchard_anchors, orchard_root, ()); + + // Update the trees in state + let current_tip_height = *height - 1; + 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_insert( + sprout_note_commitment_tree_cf, + height, + note_commitment_trees.sprout, + ); + + self.zs_insert( + sapling_note_commitment_tree_cf, + height, + note_commitment_trees.sapling, + ); + + self.zs_insert( + orchard_note_commitment_tree_cf, + height, + note_commitment_trees.orchard, + ); + + self.prepare_history_batch( + db, + finalized, + network, + sapling_root, + orchard_root, + history_tree, + ) + } + + /// Prepare a database batch containing the initial note commitment trees, + /// and return it (without actually writing anything). + /// + /// This method never returns an error. + pub fn prepare_genesis_note_commitment_tree_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + ) { + 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 FinalizedBlock { height, .. } = finalized; + + // 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::tree::NoteCommitmentTree::default(), + ); + self.zs_insert( + sapling_note_commitment_tree_cf, + height, + sapling::tree::NoteCommitmentTree::default(), + ); + self.zs_insert( + orchard_note_commitment_tree_cf, + height, + orchard::tree::NoteCommitmentTree::default(), + ); + } +} diff --git a/zebra-state/src/service/finalized_state/zebra_db/transparent.rs b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs new file mode 100644 index 00000000..22073fe6 --- /dev/null +++ b/zebra-state/src/service/finalized_state/zebra_db/transparent.rs @@ -0,0 +1,75 @@ +//! Provides high-level access to database: +//! - unspent [`transparent::Outputs`]s +//! - transparent address indexes +//! +//! 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; + +use zebra_chain::transparent; + +use crate::{ + service::finalized_state::{ + disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk}, + FinalizedBlock, FinalizedState, + }, + BoxError, +}; + +impl FinalizedState { + // 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) + } +} + +impl DiskWriteBatch { + /// Prepare a database batch containing `finalized.block`'s UTXO changes, + /// and return it (without actually writing anything). + /// + /// # Errors + /// + /// - This method doesn't currently return any errors, but it might in future + pub fn prepare_transparent_outputs_batch( + &mut self, + db: &DiskDb, + finalized: &FinalizedBlock, + ) -> Result<(), BoxError> { + let utxo_by_outpoint = db.cf_handle("utxo_by_outpoint").unwrap(); + + let FinalizedBlock { + block, new_outputs, .. + } = finalized; + + // Index all new transparent outputs, before deleting any we've spent + for (outpoint, utxo) in new_outputs.borrow().iter() { + self.zs_insert(utxo_by_outpoint, outpoint, utxo); + } + + // Mark all transparent inputs as spent. + // + // Coinbase inputs represent new coins, + // so there are no UTXOs to mark as spent. + for outpoint in block + .transactions + .iter() + .flat_map(|tx| tx.inputs()) + .flat_map(|input| input.outpoint()) + { + self.zs_delete(utxo_by_outpoint, outpoint); + } + + Ok(()) + } +}