zebra/zebra-state/src/service/finalized_state/zebra_db/chain.rs

130 lines
4.4 KiB
Rust

//! 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, sapling, transparent,
value_balance::ValueBalance,
};
use crate::{
service::finalized_state::{
disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk},
zebra_db::ZebraDb,
FinalizedBlock,
},
BoxError,
};
impl ZebraDb {
/// Returns the ZIP-221 history tree of the finalized tip or `None`
/// if it does not exist yet in the state (pre-Heartwood).
pub fn history_tree(&self) -> HistoryTree {
match self.finalized_tip_height() {
Some(height) => {
let history_tree_cf = self.db.cf_handle("history_tree").unwrap();
let history_tree: Option<NonEmptyHistoryTree> =
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<NonNegative> {
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
#[allow(clippy::unwrap_in_result)]
pub fn prepare_history_batch(
&mut self,
db: &DiskDb,
finalized: &FinalizedBlock,
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(self.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: if we ever need concurrent read-only access to the history tree,
// store it by `()`, not height.
// Otherwise, the ReadStateService could access a height
// that was just deleted by a concurrent StateService write.
// 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
#[allow(clippy::unwrap_in_result)]
pub fn prepare_chain_value_pools_batch(
&mut self,
db: &DiskDb,
finalized: &FinalizedBlock,
utxos_spent_by_block: HashMap<transparent::OutPoint, transparent::Utxo>,
value_pool: ValueBalance<NonNegative>,
) -> Result<(), BoxError> {
let tip_chain_value_pool = db.cf_handle("tip_chain_value_pool").unwrap();
let FinalizedBlock { block, .. } = finalized;
let new_pool = value_pool.add_block(block.borrow(), &utxos_spent_by_block)?;
self.zs_insert(&tip_chain_value_pool, (), new_pool);
Ok(())
}
}