diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index d472e930c..8b1e17ba3 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -2,6 +2,8 @@ use std::{collections::HashMap, fmt, ops::Neg, sync::Arc}; +use halo2::pasta::pallas; + use crate::{ amount::NegativeAllowed, block::merkle::AuthDataRoot, @@ -152,16 +154,30 @@ impl Block { /// Access the [`orchard::Nullifier`]s from all transactions in this block. pub fn orchard_nullifiers(&self) -> impl Iterator { - // Work around a compiler panic (ICE) with flat_map(): - // https://github.com/rust-lang/rust/issues/105044 - #[allow(clippy::needless_collect)] - let nullifiers: Vec<_> = self - .transactions + self.transactions .iter() .flat_map(|transaction| transaction.orchard_nullifiers()) - .collect(); + } - nullifiers.into_iter() + /// Access the [`sprout::NoteCommitment`]s from all transactions in this block. + pub fn sprout_note_commitments(&self) -> impl Iterator { + self.transactions + .iter() + .flat_map(|transaction| transaction.sprout_note_commitments()) + } + + /// Access the [sapling note commitments](jubjub::Fq) from all transactions in this block. + pub fn sapling_note_commitments(&self) -> impl Iterator { + self.transactions + .iter() + .flat_map(|transaction| transaction.sapling_note_commitments()) + } + + /// Access the [orchard note commitments](pallas::Base) from all transactions in this block. + pub fn orchard_note_commitments(&self) -> impl Iterator { + self.transactions + .iter() + .flat_map(|transaction| transaction.orchard_note_commitments()) } /// Count how many Sapling transactions exist in a block, diff --git a/zebra-chain/src/orchard/tree.rs b/zebra-chain/src/orchard/tree.rs index 205493ca6..c2adee9ba 100644 --- a/zebra-chain/src/orchard/tree.rs +++ b/zebra-chain/src/orchard/tree.rs @@ -32,7 +32,7 @@ use crate::{ serialization::{ serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize, }, - subtree::TRACKED_SUBTREE_HEIGHT, + subtree::{NoteCommitmentSubtreeIndex, TRACKED_SUBTREE_HEIGHT}, }; pub mod legacy; @@ -389,28 +389,48 @@ impl NoteCommitmentTree { } } + /// Returns frontier of non-empty tree, or `None` if the tree is empty. + fn frontier(&self) -> Option<&NonEmptyFrontier> { + self.inner.value() + } + /// Returns true if the most recently appended leaf completes the subtree - pub fn is_complete_subtree(tree: &NonEmptyFrontier) -> bool { + pub fn is_complete_subtree(&self) -> bool { + let Some(tree) = self.frontier() else { + // An empty tree can't be a complete subtree. + return false; + }; + tree.position() .is_complete_subtree(TRACKED_SUBTREE_HEIGHT.into()) } - /// Returns subtree address at [`TRACKED_SUBTREE_HEIGHT`] - pub fn subtree_address(tree: &NonEmptyFrontier) -> incrementalmerkletree::Address { - incrementalmerkletree::Address::above_position( + /// Returns the subtree index at [`TRACKED_SUBTREE_HEIGHT`]. + /// This is the number of complete or incomplete subtrees that are currently in the tree. + /// Returns `None` if the tree is empty. + #[allow(clippy::unwrap_in_result)] + pub fn subtree_index(&self) -> Option { + let tree = self.frontier()?; + + let index = incrementalmerkletree::Address::above_position( TRACKED_SUBTREE_HEIGHT.into(), tree.position(), ) + .index() + .try_into() + .expect("fits in u16"); + + Some(index) } /// Returns subtree index and root if the most recently appended leaf completes the subtree - #[allow(clippy::unwrap_in_result)] - pub fn completed_subtree_index_and_root(&self) -> Option<(u16, Node)> { - let value = self.inner.value()?; - Self::is_complete_subtree(value).then_some(())?; - let address = Self::subtree_address(value); - let index = address.index().try_into().expect("should fit in u16"); - let root = value.root(Some(TRACKED_SUBTREE_HEIGHT.into())); + pub fn completed_subtree_index_and_root(&self) -> Option<(NoteCommitmentSubtreeIndex, Node)> { + if !self.is_complete_subtree() { + return None; + } + + let index = self.subtree_index()?; + let root = self.frontier()?.root(Some(TRACKED_SUBTREE_HEIGHT.into())); Some((index, root)) } diff --git a/zebra-chain/src/parallel/tree.rs b/zebra-chain/src/parallel/tree.rs index 46fc7c080..361b35d10 100644 --- a/zebra-chain/src/parallel/tree.rs +++ b/zebra-chain/src/parallel/tree.rs @@ -4,7 +4,11 @@ use std::sync::Arc; use thiserror::Error; -use crate::{block::Block, orchard, sapling, sprout, subtree::NoteCommitmentSubtree}; +use crate::{ + block::Block, + orchard, sapling, sprout, + subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeIndex}, +}; /// An argument wrapper struct for note commitment trees. #[derive(Clone, Debug)] @@ -65,24 +69,9 @@ impl NoteCommitmentTrees { .. } = self.clone(); - let sprout_note_commitments: Vec<_> = block - .transactions - .iter() - .flat_map(|tx| tx.sprout_note_commitments()) - .cloned() - .collect(); - let sapling_note_commitments: Vec<_> = block - .transactions - .iter() - .flat_map(|tx| tx.sapling_note_commitments()) - .cloned() - .collect(); - let orchard_note_commitments: Vec<_> = block - .transactions - .iter() - .flat_map(|tx| tx.orchard_note_commitments()) - .cloned() - .collect(); + let sprout_note_commitments: Vec<_> = block.sprout_note_commitments().cloned().collect(); + let sapling_note_commitments: Vec<_> = block.sapling_note_commitments().cloned().collect(); + let orchard_note_commitments: Vec<_> = block.orchard_note_commitments().cloned().collect(); let mut sprout_result = None; let mut sapling_result = None; @@ -163,7 +152,7 @@ impl NoteCommitmentTrees { ) -> Result< ( Arc, - Option<(u16, sapling::tree::Node)>, + Option<(NoteCommitmentSubtreeIndex, sapling::tree::Node)>, ), NoteCommitmentTreeError, > { @@ -202,7 +191,7 @@ impl NoteCommitmentTrees { ) -> Result< ( Arc, - Option<(u16, orchard::tree::Node)>, + Option<(NoteCommitmentSubtreeIndex, orchard::tree::Node)>, ), NoteCommitmentTreeError, > { diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index 698596216..6112aa0cc 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -33,7 +33,7 @@ use crate::{ serialization::{ serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize, }, - subtree::TRACKED_SUBTREE_HEIGHT, + subtree::{NoteCommitmentSubtreeIndex, TRACKED_SUBTREE_HEIGHT}, }; pub mod legacy; @@ -370,28 +370,48 @@ impl NoteCommitmentTree { } } + /// Returns frontier of non-empty tree, or None. + fn frontier(&self) -> Option<&NonEmptyFrontier> { + self.inner.value() + } + /// Returns true if the most recently appended leaf completes the subtree - pub fn is_complete_subtree(tree: &NonEmptyFrontier) -> bool { + pub fn is_complete_subtree(&self) -> bool { + let Some(tree) = self.frontier() else { + // An empty tree can't be a complete subtree. + return false; + }; + tree.position() .is_complete_subtree(TRACKED_SUBTREE_HEIGHT.into()) } - /// Returns subtree address at [`TRACKED_SUBTREE_HEIGHT`] - pub fn subtree_address(tree: &NonEmptyFrontier) -> incrementalmerkletree::Address { - incrementalmerkletree::Address::above_position( + /// Returns the subtree index at [`TRACKED_SUBTREE_HEIGHT`]. + /// This is the number of complete or incomplete subtrees that are currently in the tree. + /// Returns `None` if the tree is empty. + #[allow(clippy::unwrap_in_result)] + pub fn subtree_index(&self) -> Option { + let tree = self.frontier()?; + + let index = incrementalmerkletree::Address::above_position( TRACKED_SUBTREE_HEIGHT.into(), tree.position(), ) + .index() + .try_into() + .expect("fits in u16"); + + Some(index) } /// Returns subtree index and root if the most recently appended leaf completes the subtree - #[allow(clippy::unwrap_in_result)] - pub fn completed_subtree_index_and_root(&self) -> Option<(u16, Node)> { - let value = self.inner.value()?; - Self::is_complete_subtree(value).then_some(())?; - let address = Self::subtree_address(value); - let index = address.index().try_into().expect("should fit in u16"); - let root = value.root(Some(TRACKED_SUBTREE_HEIGHT.into())); + pub fn completed_subtree_index_and_root(&self) -> Option<(NoteCommitmentSubtreeIndex, Node)> { + if !self.is_complete_subtree() { + return None; + } + + let index = self.subtree_index()?; + let root = self.frontier()?.root(Some(TRACKED_SUBTREE_HEIGHT.into())); Some((index, root)) } diff --git a/zebra-chain/src/subtree.rs b/zebra-chain/src/subtree.rs index 3a59f125f..4a9ef8768 100644 --- a/zebra-chain/src/subtree.rs +++ b/zebra-chain/src/subtree.rs @@ -1,5 +1,7 @@ //! Struct representing Sapling/Orchard note commitment subtrees +use std::num::TryFromIntError; + use serde::{Deserialize, Serialize}; use crate::block::Height; @@ -23,6 +25,22 @@ impl From for NoteCommitmentSubtreeIndex { } } +impl TryFrom for NoteCommitmentSubtreeIndex { + type Error = TryFromIntError; + + fn try_from(value: u64) -> Result { + u16::try_from(value).map(Self) + } +} + +// If we want to automatically convert NoteCommitmentSubtreeIndex to the generic integer literal +// type, we can only implement conversion into u64. (Or u16, but not both.) +impl From for u64 { + fn from(value: NoteCommitmentSubtreeIndex) -> Self { + value.0.into() + } +} + // TODO: // - consider defining sapling::SubtreeRoot and orchard::SubtreeRoot types or type wrappers, // to avoid type confusion between the leaf Node and subtree root types. diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs index 6c454b6d8..8abf3750d 100644 --- a/zebra-state/src/constants.rs +++ b/zebra-state/src/constants.rs @@ -48,11 +48,11 @@ pub(crate) const DATABASE_FORMAT_VERSION: u64 = 25; /// - adding new column families, /// - changing the format of a column family in a compatible way, or /// - breaking changes with compatibility code in all supported Zebra versions. -pub(crate) const DATABASE_FORMAT_MINOR_VERSION: u64 = 1; +pub(crate) const DATABASE_FORMAT_MINOR_VERSION: u64 = 2; /// The database format patch version, incremented each time the on-disk database format has a /// significant format compatibility fix. -pub(crate) const DATABASE_FORMAT_PATCH_VERSION: u64 = 1; +pub(crate) const DATABASE_FORMAT_PATCH_VERSION: u64 = 0; /// The name of the file containing the minor and patch database versions. /// diff --git a/zebra-state/src/service/finalized_state.rs b/zebra-state/src/service/finalized_state.rs index 95d118362..61c8f3e08 100644 --- a/zebra-state/src/service/finalized_state.rs +++ b/zebra-state/src/service/finalized_state.rs @@ -98,7 +98,7 @@ impl FinalizedState { network: Network, #[cfg(feature = "elasticsearch")] elastic_db: Option, ) -> Self { - let db = ZebraDb::new(config, network); + let db = ZebraDb::new(config, network, false); #[cfg(feature = "elasticsearch")] let new_state = Self { diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs index 3f1daff41..b4755751e 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs @@ -25,6 +25,8 @@ use crate::{ Config, }; +pub(crate) mod add_subtrees; + /// The kind of database format change we're performing. #[derive(Clone, Debug, Eq, PartialEq)] pub enum DbFormatChange { @@ -195,7 +197,7 @@ impl DbFormatChange { network, initial_tip_height, upgrade_db.clone(), - cancel_receiver, + &cancel_receiver, )?, NewlyCreated { .. } => { @@ -218,12 +220,14 @@ impl DbFormatChange { } } - // This check should pass for all format changes: - // - upgrades should de-duplicate trees if needed (and they already do this check) - // - an empty state doesn't have any trees, so it can't have duplicate trees - // - since this Zebra code knows how to de-duplicate trees, downgrades using this code - // still know how to make sure trees are unique - Self::check_for_duplicate_trees(upgrade_db); + // These checks should pass for all format changes: + // - upgrades should produce a valid format (and they already do that check) + // - an empty state should pass all the format checks + // - since the running Zebra code knows how to upgrade the database to this format, + // downgrades using this running code still know how to create a valid database + // (unless a future upgrade breaks these format checks) + Self::check_for_duplicate_trees(upgrade_db.clone()); + add_subtrees::check(&upgrade_db); Ok(()) } @@ -245,7 +249,7 @@ impl DbFormatChange { network: Network, initial_tip_height: Option, db: ZebraDb, - cancel_receiver: mpsc::Receiver, + cancel_receiver: &mpsc::Receiver, ) -> Result<(), CancelFormatChange> { let Upgrade { newer_running_version, @@ -277,7 +281,7 @@ impl DbFormatChange { return Ok(()); }; - // Start of a database upgrade task. + // Note commitment tree de-duplication database upgrade task. let version_for_pruning_trees = Version::parse("25.1.1").expect("Hardcoded version string should be valid."); @@ -339,13 +343,30 @@ impl DbFormatChange { } // Before marking the state as upgraded, check that the upgrade completed successfully. - Self::check_for_duplicate_trees(db); + Self::check_for_duplicate_trees(db.clone()); // Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the // database is marked, so the upgrade MUST be complete at this point. Self::mark_as_upgraded_to(&version_for_pruning_trees, &config, network); } + // Note commitment subtree creation database upgrade task. + + let version_for_adding_subtrees = + Version::parse("25.2.0").expect("Hardcoded version string should be valid."); + + // Check if we need to add note commitment subtrees to the database. + if older_disk_version < version_for_adding_subtrees { + add_subtrees::run(initial_tip_height, &db, cancel_receiver)?; + + // Before marking the state as upgraded, check that the upgrade completed successfully. + add_subtrees::check(&db); + + // Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the + // database is marked, so the upgrade MUST be complete at this point. + Self::mark_as_upgraded_to(&version_for_adding_subtrees, &config, network); + } + // # New Upgrades Usually Go Here // // New code goes above this comment! @@ -353,6 +374,7 @@ impl DbFormatChange { // Run the latest format upgrade code after the other upgrades are complete, // then mark the format as upgraded. The code should check `cancel_receiver` // every time it runs its inner update loop. + info!( ?newer_running_version, "Zebra automatically upgraded the database format to:" diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs new file mode 100644 index 000000000..42565167e --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade/add_subtrees.rs @@ -0,0 +1,466 @@ +//! Fully populate the Sapling and Orchard note commitment subtrees for existing blocks in the database. + +use std::sync::{mpsc, Arc}; + +use zebra_chain::{ + block::Height, + orchard, sapling, + subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeIndex}, +}; + +use crate::service::finalized_state::{ + disk_format::upgrade::CancelFormatChange, DiskWriteBatch, ZebraDb, +}; + +/// Runs disk format upgrade for adding Sapling and Orchard note commitment subtrees to database. +/// +/// Returns `Ok` if the upgrade completed, and `Err` if it was cancelled. +#[allow(clippy::unwrap_in_result)] +pub fn run( + initial_tip_height: Height, + upgrade_db: &ZebraDb, + cancel_receiver: &mpsc::Receiver, +) -> Result<(), CancelFormatChange> { + let mut subtree_count = 0; + let mut prev_tree: Option<_> = None; + for (height, tree) in upgrade_db.sapling_tree_by_height_range(..=initial_tip_height) { + // Return early if there is a cancel signal. + if !matches!(cancel_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)) { + return Err(CancelFormatChange); + } + + // Empty note commitment trees can't contain subtrees. + let Some(end_of_block_subtree_index) = tree.subtree_index() else { + prev_tree = Some(tree); + continue; + }; + + // Blocks cannot complete multiple level 16 subtrees, + // so the subtree index can increase by a maximum of 1 every ~20 blocks. + // If this block does complete a subtree, the subtree is either completed by a note before + // the final note (so the final note is in the next subtree), or by the final note + // (so the final note is the end of this subtree). + + if let Some((index, node)) = tree.completed_subtree_index_and_root() { + // If the leaf at the end of the block is the final leaf in a subtree, + // we already have that subtree root available in the tree. + assert_eq!( + index.0, subtree_count, + "trees are inserted in order with no gaps" + ); + write_sapling_subtree(upgrade_db, index, height, node); + subtree_count += 1; + } else if end_of_block_subtree_index.0 > subtree_count { + // If the leaf at the end of the block is in the next subtree, + // we need to calculate that subtree root based on the tree from the previous block. + let mut prev_tree = prev_tree + .take() + .expect("should have some previous sapling frontier"); + let sapling_nct = Arc::make_mut(&mut prev_tree); + + let block = upgrade_db + .block(height.into()) + .expect("height with note commitment tree should have block"); + + for sapling_note_commitment in block.sapling_note_commitments() { + // Return early if there is a cancel signal. + if !matches!(cancel_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)) { + return Err(CancelFormatChange); + } + + sapling_nct + .append(*sapling_note_commitment) + .expect("finalized notes should append successfully"); + + // The loop always breaks on this condition, + // because we checked the block has enough commitments, + // and that the final commitment in the block doesn't complete a subtree. + if sapling_nct.is_complete_subtree() { + break; + } + } + + let (index, node) = sapling_nct.completed_subtree_index_and_root().expect( + "block should have completed a subtree before its final note commitment: \ + already checked is_complete_subtree(), and that the block must complete a subtree", + ); + + assert_eq!( + index.0, subtree_count, + "trees are inserted in order with no gaps" + ); + write_sapling_subtree(upgrade_db, index, height, node); + subtree_count += 1; + } + + prev_tree = Some(tree); + } + + let mut subtree_count = 0; + let mut prev_tree: Option<_> = None; + for (height, tree) in upgrade_db.orchard_tree_by_height_range(..=initial_tip_height) { + // Return early if there is a cancel signal. + if !matches!(cancel_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)) { + return Err(CancelFormatChange); + } + + // Empty note commitment trees can't contain subtrees. + let Some(end_of_block_subtree_index) = tree.subtree_index() else { + prev_tree = Some(tree); + continue; + }; + + // Blocks cannot complete multiple level 16 subtrees, + // so the subtree index can increase by a maximum of 1 every ~20 blocks. + // If this block does complete a subtree, the subtree is either completed by a note before + // the final note (so the final note is in the next subtree), or by the final note + // (so the final note is the end of this subtree). + + if let Some((index, node)) = tree.completed_subtree_index_and_root() { + // If the leaf at the end of the block is the final leaf in a subtree, + // we already have that subtree root available in the tree. + assert_eq!( + index.0, subtree_count, + "trees are inserted in order with no gaps" + ); + write_orchard_subtree(upgrade_db, index, height, node); + subtree_count += 1; + } else if end_of_block_subtree_index.0 > subtree_count { + // If the leaf at the end of the block is in the next subtree, + // we need to calculate that subtree root based on the tree from the previous block. + let mut prev_tree = prev_tree + .take() + .expect("should have some previous orchard frontier"); + let orchard_nct = Arc::make_mut(&mut prev_tree); + + let block = upgrade_db + .block(height.into()) + .expect("height with note commitment tree should have block"); + + for orchard_note_commitment in block.orchard_note_commitments() { + // Return early if there is a cancel signal. + if !matches!(cancel_receiver.try_recv(), Err(mpsc::TryRecvError::Empty)) { + return Err(CancelFormatChange); + } + + orchard_nct + .append(*orchard_note_commitment) + .expect("finalized notes should append successfully"); + + // The loop always breaks on this condition, + // because we checked the block has enough commitments, + // and that the final commitment in the block doesn't complete a subtree. + if orchard_nct.is_complete_subtree() { + break; + } + } + + let (index, node) = orchard_nct.completed_subtree_index_and_root().expect( + "block should have completed a subtree before its final note commitment: \ + already checked is_complete_subtree(), and that the block must complete a subtree", + ); + + assert_eq!( + index.0, subtree_count, + "trees are inserted in order with no gaps" + ); + write_orchard_subtree(upgrade_db, index, height, node); + subtree_count += 1; + } + + prev_tree = Some(tree); + } + + Ok(()) +} + +/// Check that note commitment subtrees were correctly added. +/// +/// # Panics +/// +/// If a note commitment subtree is missing or incorrect. +pub fn check(db: &ZebraDb) { + let check_sapling_subtrees = check_sapling_subtrees(db); + let check_orchard_subtrees = check_orchard_subtrees(db); + if !check_sapling_subtrees || !check_orchard_subtrees { + panic!("missing or bad subtree(s)"); + } +} + +/// Check that Sapling note commitment subtrees were correctly added. +/// +/// # Panics +/// +/// If a note commitment subtree is missing or incorrect. +fn check_sapling_subtrees(db: &ZebraDb) -> bool { + let Some(NoteCommitmentSubtreeIndex(mut first_incomplete_subtree_index)) = + db.sapling_tree().subtree_index() + else { + return true; + }; + + // If there are no incomplete subtrees in the tree, also expect a subtree for the final index. + if db.sapling_tree().is_complete_subtree() { + first_incomplete_subtree_index += 1; + } + + let mut is_valid = true; + for index in 0..first_incomplete_subtree_index { + // Check that there's a continuous range of subtrees from index [0, first_incomplete_subtree_index) + let Some(subtree) = db.sapling_subtree_by_index(index) else { + error!(index, "missing subtree"); + is_valid = false; + continue; + }; + + // Check that there was a sapling note at the subtree's end height. + let Some(tree) = db.sapling_tree_by_height(&subtree.end) else { + error!(?subtree.end, "missing note commitment tree at subtree completion height"); + is_valid = false; + continue; + }; + + // Check the index and root if the sapling note commitment tree at this height is a complete subtree. + if let Some((index, node)) = tree.completed_subtree_index_and_root() { + if subtree.index != index { + error!("completed subtree indexes should match"); + is_valid = false; + } + + if subtree.node != node { + error!("completed subtree roots should match"); + is_valid = false; + } + } + // Check that the final note has a greater subtree index if it didn't complete a subtree. + else { + let Some(prev_tree) = db.sapling_tree_by_height(&subtree.end.previous()) else { + error!(?subtree.end, "missing note commitment tree at subtree completion height"); + is_valid = false; + continue; + }; + + let prev_subtree_index = prev_tree.subtree_index(); + let subtree_index = tree.subtree_index(); + if subtree_index <= prev_subtree_index { + error!( + ?subtree_index, + ?prev_subtree_index, + "note commitment tree at end height should have incremented subtree index" + ); + is_valid = false; + } + } + } + + let mut subtree_count = 0; + for (index, height, tree) in db + .sapling_tree_by_height_range(..) + .filter_map(|(height, tree)| Some((tree.subtree_index()?, height, tree))) + .filter_map(|(subtree_index, height, tree)| { + if tree.is_complete_subtree() || subtree_index.0 > subtree_count { + let subtree_index = subtree_count; + subtree_count += 1; + Some((subtree_index, height, tree)) + } else { + None + } + }) + { + let Some(subtree) = db.sapling_subtree_by_index(index) else { + error!(?index, "missing subtree"); + is_valid = false; + continue; + }; + + if subtree.index.0 != index { + error!("completed subtree indexes should match"); + is_valid = false; + } + + if subtree.end != height { + let is_complete = tree.is_complete_subtree(); + error!(?subtree.end, ?height, ?index, ?is_complete, "bad sapling subtree end height"); + is_valid = false; + } + + if let Some((_index, node)) = tree.completed_subtree_index_and_root() { + if subtree.node != node { + error!("completed subtree roots should match"); + is_valid = false; + } + } + } + + if !is_valid { + error!( + ?subtree_count, + first_incomplete_subtree_index, "missing or bad sapling subtrees" + ); + } + + is_valid +} + +/// Check that Orchard note commitment subtrees were correctly added. +/// +/// # Panics +/// +/// If a note commitment subtree is missing or incorrect. +fn check_orchard_subtrees(db: &ZebraDb) -> bool { + let Some(NoteCommitmentSubtreeIndex(mut first_incomplete_subtree_index)) = + db.orchard_tree().subtree_index() + else { + return true; + }; + + // If there are no incomplete subtrees in the tree, also expect a subtree for the final index. + if db.orchard_tree().is_complete_subtree() { + first_incomplete_subtree_index += 1; + } + + let mut is_valid = true; + for index in 0..first_incomplete_subtree_index { + // Check that there's a continuous range of subtrees from index [0, first_incomplete_subtree_index) + let Some(subtree) = db.orchard_subtree_by_index(index) else { + error!(index, "missing subtree"); + is_valid = false; + continue; + }; + + // Check that there was a orchard note at the subtree's end height. + let Some(tree) = db.orchard_tree_by_height(&subtree.end) else { + error!(?subtree.end, "missing note commitment tree at subtree completion height"); + is_valid = false; + continue; + }; + + // Check the index and root if the orchard note commitment tree at this height is a complete subtree. + if let Some((index, node)) = tree.completed_subtree_index_and_root() { + if subtree.index != index { + error!("completed subtree indexes should match"); + is_valid = false; + } + + if subtree.node != node { + error!("completed subtree roots should match"); + is_valid = false; + } + } + // Check that the final note has a greater subtree index if it didn't complete a subtree. + else { + let Some(prev_tree) = db.orchard_tree_by_height(&subtree.end.previous()) else { + error!(?subtree.end, "missing note commitment tree at subtree completion height"); + is_valid = false; + continue; + }; + + let prev_subtree_index = prev_tree.subtree_index(); + let subtree_index = tree.subtree_index(); + if subtree_index <= prev_subtree_index { + error!( + ?subtree_index, + ?prev_subtree_index, + "note commitment tree at end height should have incremented subtree index" + ); + is_valid = false; + } + } + } + + let mut subtree_count = 0; + for (index, height, tree) in db + .orchard_tree_by_height_range(..) + .filter_map(|(height, tree)| Some((tree.subtree_index()?, height, tree))) + .filter_map(|(subtree_index, height, tree)| { + if tree.is_complete_subtree() || subtree_index.0 > subtree_count { + let subtree_index = subtree_count; + subtree_count += 1; + Some((subtree_index, height, tree)) + } else { + None + } + }) + { + let Some(subtree) = db.orchard_subtree_by_index(index) else { + error!(?index, "missing subtree"); + is_valid = false; + continue; + }; + + if subtree.index.0 != index { + error!("completed subtree indexes should match"); + is_valid = false; + } + + if subtree.end != height { + let is_complete = tree.is_complete_subtree(); + error!(?subtree.end, ?height, ?index, ?is_complete, "bad orchard subtree end height"); + is_valid = false; + } + + if let Some((_index, node)) = tree.completed_subtree_index_and_root() { + if subtree.node != node { + error!("completed subtree roots should match"); + is_valid = false; + } + } + } + + if !is_valid { + error!( + ?subtree_count, + first_incomplete_subtree_index, "missing or bad orchard subtrees" + ); + } + + is_valid +} + +/// Writes a Sapling note commitment subtree to `upgrade_db`. +fn write_sapling_subtree( + upgrade_db: &ZebraDb, + index: NoteCommitmentSubtreeIndex, + height: Height, + node: sapling::tree::Node, +) { + let subtree = NoteCommitmentSubtree::new(index, height, node); + + let mut batch = DiskWriteBatch::new(); + + batch.insert_sapling_subtree(upgrade_db, &subtree); + + upgrade_db + .write_batch(batch) + .expect("writing sapling note commitment subtrees should always succeed."); + + if index.0 % 100 == 0 { + info!(?height, index = ?index.0, "calculated and added sapling subtree"); + } + // This log happens about once per second on recent machines with SSD disks. + debug!(?height, index = ?index.0, ?node, "calculated and added sapling subtree"); +} + +/// Writes a Orchard note commitment subtree to `upgrade_db`. +fn write_orchard_subtree( + upgrade_db: &ZebraDb, + index: NoteCommitmentSubtreeIndex, + height: Height, + node: orchard::tree::Node, +) { + let subtree = NoteCommitmentSubtree::new(index, height, node); + + let mut batch = DiskWriteBatch::new(); + + batch.insert_orchard_subtree(upgrade_db, &subtree); + + upgrade_db + .write_batch(batch) + .expect("writing orchard note commitment subtrees should always succeed."); + + if index.0 % 300 == 0 { + info!(?height, index = ?index.0, "calculated and added orchard subtree"); + } + // This log happens about 3 times per second on recent machines with SSD disks. + debug!(?height, index = ?index.0, ?node, "calculated and added orchard subtree"); +} diff --git a/zebra-state/src/service/finalized_state/zebra_db.rs b/zebra-state/src/service/finalized_state/zebra_db.rs index 479f337b0..109e7e883 100644 --- a/zebra-state/src/service/finalized_state/zebra_db.rs +++ b/zebra-state/src/service/finalized_state/zebra_db.rs @@ -19,7 +19,7 @@ use crate::{ disk_db::DiskDb, disk_format::{ block::MAX_ON_DISK_HEIGHT, - upgrade::{DbFormatChange, DbFormatChangeThreadHandle}, + upgrade::{self, DbFormatChange, DbFormatChangeThreadHandle}, }, }, Config, @@ -60,7 +60,10 @@ pub struct ZebraDb { impl ZebraDb { /// Opens or creates the database at `config.path` for `network`, /// and returns a shared high-level typed database wrapper. - pub fn new(config: &Config, network: Network) -> ZebraDb { + /// + /// If `debug_skip_format_upgrades` is true, don't do any format upgrades or format checks. + /// This argument is only used when running tests, it is ignored in production code. + pub fn new(config: &Config, network: Network, debug_skip_format_upgrades: bool) -> ZebraDb { let running_version = database_format_version_in_code(); let disk_version = database_format_version_on_disk(config, network) .expect("unable to read database format version file"); @@ -84,7 +87,12 @@ impl ZebraDb { // a while to start, and new blocks can be committed as soon as we return from this method. let initial_tip_height = db.finalized_tip_height(); - // Start any required format changes. + // Always do format upgrades & checks in production code. + if cfg!(test) && debug_skip_format_upgrades { + return db; + } + + // Start any required format changes, and do format checks. // // TODO: should debug_stop_at_height wait for these upgrades, or not? if let Some(format_change) = format_change { @@ -108,9 +116,10 @@ impl ZebraDb { db.format_change_handle = Some(format_change_handle); } else { // If we're re-opening a previously upgraded or newly created database, - // the trees should already be de-duplicated. + // the database format should be valid. // (There's no format change here, so the format change checks won't run.) DbFormatChange::check_for_duplicate_trees(db.clone()); + upgrade::add_subtrees::check(&db.clone()); } db diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 1f99b24ea..93db70f19 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -26,7 +26,7 @@ use zebra_chain::{ use zebra_test::vectors::{MAINNET_BLOCKS, TESTNET_BLOCKS}; use crate::{ - service::finalized_state::{disk_db::DiskWriteBatch, FinalizedState}, + service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb}, CheckpointVerifiedBlock, Config, }; @@ -77,11 +77,11 @@ fn test_block_db_round_trip_with( ) { let _init_guard = zebra_test::init(); - let state = FinalizedState::new( + let state = ZebraDb::new( &Config::ephemeral(), network, - #[cfg(feature = "elasticsearch")] - None, + // The raw database accesses in this test create invalid database formats. + true, ); // Check that each block round-trips to the database diff --git a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs index a703630bf..0db9c5be9 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/shielded.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/shielded.rs @@ -180,17 +180,41 @@ impl ZebraDb { self.db.zs_range_iter(&sapling_trees, range) } + /// Returns the Sapling note commitment subtree at this `index`. + /// + /// # Correctness + /// + /// This method should not be used to get subtrees for RPC responses, + /// because those subtree lists require that the start subtree is present in the list. + /// Instead, use `sapling_subtree_list_by_index_for_rpc()`. + #[allow(clippy::unwrap_in_result)] + pub(in super::super) fn sapling_subtree_by_index( + &self, + index: impl Into + Copy, + ) -> Option> { + let sapling_subtrees = self + .db + .cf_handle("sapling_note_commitment_subtree") + .unwrap(); + + let subtree_data: NoteCommitmentSubtreeData = + self.db.zs_get(&sapling_subtrees, &index.into())?; + + Some(subtree_data.with_index(index)) + } + /// Returns a list of Sapling [`NoteCommitmentSubtree`]s starting at `start_index`. /// If `limit` is provided, the list is limited to `limit` entries. /// /// If there is no subtree at `start_index`, the returned list is empty. /// Otherwise, subtrees are continuous up to the finalized tip. /// - /// There is no API for retrieving single subtrees by index, because it can accidentally be used - /// to create an inconsistent list of subtrees after concurrent non-finalized and finalized - /// updates. + /// # Correctness + /// + /// This method is specifically designed for the `z_getsubtreesbyindex` state request. + /// It might not work for other RPCs or state checks. #[allow(clippy::unwrap_in_result)] - pub fn sapling_subtrees_by_index( + pub fn sapling_subtree_list_by_index_for_rpc( &self, start_index: NoteCommitmentSubtreeIndex, limit: Option, @@ -289,17 +313,41 @@ impl ZebraDb { self.db.zs_range_iter(&orchard_trees, range) } + /// Returns the Orchard note commitment subtree at this `index`. + /// + /// # Correctness + /// + /// This method should not be used to get subtrees for RPC responses, + /// because those subtree lists require that the start subtree is present in the list. + /// Instead, use `orchard_subtree_list_by_index_for_rpc()`. + #[allow(clippy::unwrap_in_result)] + pub(in super::super) fn orchard_subtree_by_index( + &self, + index: impl Into + Copy, + ) -> Option> { + let orchard_subtrees = self + .db + .cf_handle("orchard_note_commitment_subtree") + .unwrap(); + + let subtree_data: NoteCommitmentSubtreeData = + self.db.zs_get(&orchard_subtrees, &index.into())?; + + Some(subtree_data.with_index(index)) + } + /// Returns a list of Orchard [`NoteCommitmentSubtree`]s starting at `start_index`. /// If `limit` is provided, the list is limited to `limit` entries. /// /// If there is no subtree at `start_index`, the returned list is empty. /// Otherwise, subtrees are continuous up to the finalized tip. /// - /// There is no API for retrieving single subtrees by index, because it can accidentally be used - /// to create an inconsistent list of subtrees after concurrent non-finalized and finalized - /// updates. + /// # Correctness + /// + /// This method is specifically designed for the `z_getsubtreesbyindex` state request. + /// It might not work for other RPCs or state checks. #[allow(clippy::unwrap_in_result)] - pub fn orchard_subtrees_by_index( + pub fn orchard_subtree_list_by_index_for_rpc( &self, start_index: NoteCommitmentSubtreeIndex, limit: Option, @@ -437,9 +485,6 @@ impl DiskWriteBatch { let sapling_tree_cf = db.cf_handle("sapling_note_commitment_tree").unwrap(); let orchard_tree_cf = db.cf_handle("orchard_note_commitment_tree").unwrap(); - let _sapling_subtree_cf = db.cf_handle("sapling_note_commitment_subtree").unwrap(); - let _orchard_subtree_cf = db.cf_handle("orchard_note_commitment_subtree").unwrap(); - let height = finalized.verified.height; let trees = finalized.treestate.note_commitment_trees.clone(); @@ -485,19 +530,32 @@ impl DiskWriteBatch { self.zs_insert(&orchard_tree_cf, height, trees.orchard); } - // TODO: Increment DATABASE_FORMAT_MINOR_VERSION and uncomment these insertions + if let Some(subtree) = trees.sapling_subtree { + self.insert_sapling_subtree(zebra_db, &subtree); + } - // if let Some(subtree) = trees.sapling_subtree { - // self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data()); - // } - - // if let Some(subtree) = trees.orchard_subtree { - // self.zs_insert(&orchard_subtree_cf, subtree.index, subtree.into_data()); - // } + if let Some(subtree) = trees.orchard_subtree { + self.insert_orchard_subtree(zebra_db, &subtree); + } self.prepare_history_batch(db, finalized) } + // Sapling tree methods + + /// Inserts the Sapling note commitment subtree. + pub fn insert_sapling_subtree( + &mut self, + zebra_db: &ZebraDb, + subtree: &NoteCommitmentSubtree, + ) { + let sapling_subtree_cf = zebra_db + .db + .cf_handle("sapling_note_commitment_subtree") + .unwrap(); + self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data()); + } + /// Deletes the Sapling note commitment tree at the given [`Height`]. pub fn delete_sapling_tree(&mut self, zebra_db: &ZebraDb, height: &Height) { let sapling_tree_cf = zebra_db @@ -519,6 +577,21 @@ impl DiskWriteBatch { self.zs_delete_range(&sapling_tree_cf, from, to); } + // Orchard tree methods + + /// Inserts the Orchard note commitment subtree. + pub fn insert_orchard_subtree( + &mut self, + zebra_db: &ZebraDb, + subtree: &NoteCommitmentSubtree, + ) { + let orchard_subtree_cf = zebra_db + .db + .cf_handle("orchard_note_commitment_subtree") + .unwrap(); + self.zs_insert(&orchard_subtree_cf, subtree.index, subtree.into_data()); + } + /// Deletes the Orchard note commitment tree at the given [`Height`]. pub fn delete_orchard_tree(&mut self, zebra_db: &ZebraDb, height: &Height) { let orchard_tree_cf = zebra_db diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 9fc68c288..fb3865cce 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -682,6 +682,11 @@ impl Chain { /// Returns the Sapling [`NoteCommitmentSubtree`] that was completed at a block with /// [`HashOrHeight`], if it exists in the non-finalized [`Chain`]. + /// + /// # Concurrency + /// + /// This method should not be used to get subtrees in concurrent code by height, + /// because the same heights in different chain forks can have different subtrees. pub fn sapling_subtree( &self, hash_or_height: HashOrHeight, @@ -872,6 +877,11 @@ impl Chain { /// Returns the Orchard [`NoteCommitmentSubtree`] that was completed at a block with /// [`HashOrHeight`], if it exists in the non-finalized [`Chain`]. + /// + /// # Concurrency + /// + /// This method should not be used to get subtrees in concurrent code by height, + /// because the same heights in different chain forks can have different subtrees. pub fn orchard_subtree( &self, hash_or_height: HashOrHeight, diff --git a/zebra-state/src/service/read/tree.rs b/zebra-state/src/service/read/tree.rs index 05bd2c4a1..759952496 100644 --- a/zebra-state/src/service/read/tree.rs +++ b/zebra-state/src/service/read/tree.rs @@ -74,7 +74,7 @@ where // In that case, we ignore all the trees in `chain` after the first inconsistent tree, // because we know they will be inconsistent as well. (It is cryptographically impossible // for tree roots to be equal once the leaves have diverged.) - let mut db_list = db.sapling_subtrees_by_index(start_index, limit); + let mut db_list = db.sapling_subtree_list_by_index_for_rpc(start_index, limit); // If there's no chain, then we have the complete list. let Some(chain) = chain else { @@ -162,7 +162,7 @@ where // In that case, we ignore all the trees in `chain` after the first inconsistent tree, // because we know they will be inconsistent as well. (It is cryptographically impossible // for tree roots to be equal once the leaves have diverged.) - let mut db_list = db.orchard_subtrees_by_index(start_index, limit); + let mut db_list = db.orchard_subtree_list_by_index_for_rpc(start_index, limit); // If there's no chain, then we have the complete list. let Some(chain) = chain else {