From 294277f417d48c003fed1ed2f78e295ff101e208 Mon Sep 17 00:00:00 2001 From: Conrado Gouvea Date: Tue, 6 Jul 2021 21:29:08 -0300 Subject: [PATCH] ZIP-221: Validate chain history commitments in the non-finalized state (#2301) * sketch of implementation * refined implementation; still incomplete * update librustzcash, change zcash_history to work with it * simplified code per review; renamed MMR to HistoryTree * expand HistoryTree implementation * handle and propagate errors * simplify check.rs tracing * add suggested TODO * add HistoryTree::prune * fix bug in pruning * fix compilation of tests; still need to make them pass * Apply suggestions from code review Co-authored-by: teor * Apply suggestions from code review Co-authored-by: teor * improvements from code review * improve check.rs comments and variable names * fix HistoryTree which should use BTreeMap and not HashMap; fix non_finalized_state prop tests * fix finalized_state proptest * fix non_finalized_state tests by setting the correct commitments * renamed mmr.rs to history_tree.rs * Add HistoryTree struct * expand non_finalized_state protest * fix typo * Add HistoryTree struct * Update zebra-chain/src/primitives/zcash_history.rs Co-authored-by: Deirdre Connolly * fix formatting * Apply suggestions from code review Co-authored-by: Deirdre Connolly * history_tree.rs: fixes from code review * fixes to work with updated HistoryTree * Improvements from code review * Add Debug implementations to allow comparing Chains with proptest_assert_eq Co-authored-by: teor Co-authored-by: Deirdre Connolly --- zebra-chain/src/block/commitment.rs | 8 +- zebra-chain/src/history_tree.rs | 10 ++ zebra-state/src/error.rs | 11 +- zebra-state/src/service/arbitrary.rs | 37 ++++- .../src/service/non_finalized_state.rs | 8 +- .../src/service/non_finalized_state/chain.rs | 75 +++++++-- .../service/non_finalized_state/tests/prop.rs | 69 ++++++-- .../non_finalized_state/tests/vectors.rs | 153 +++++++++++++++--- 8 files changed, 317 insertions(+), 54 deletions(-) diff --git a/zebra-chain/src/block/commitment.rs b/zebra-chain/src/block/commitment.rs index 9b5876f0c9b..9b55adb37ad 100644 --- a/zebra-chain/src/block/commitment.rs +++ b/zebra-chain/src/block/commitment.rs @@ -149,6 +149,12 @@ impl From<[u8; 32]> for ChainHistoryMmrRootHash { } } +impl From for [u8; 32] { + fn from(hash: ChainHistoryMmrRootHash) -> Self { + hash.0 + } +} + /// A block commitment to chain history and transaction auth. /// - the chain history tree for all ancestors in the current network upgrade, /// and @@ -170,7 +176,7 @@ pub struct ChainHistoryBlockTxAuthCommitmentHash([u8; 32]); /// implement, and ensures that we don't reject blocks or transactions /// for a non-enumerated reason. #[allow(dead_code, missing_docs)] -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum CommitmentError { #[error("invalid final sapling root: expected {expected:?}, actual: {actual:?}")] InvalidFinalSaplingRoot { diff --git a/zebra-chain/src/history_tree.rs b/zebra-chain/src/history_tree.rs index 3abbc99f302..e0a8f062daf 100644 --- a/zebra-chain/src/history_tree.rs +++ b/zebra-chain/src/history_tree.rs @@ -33,6 +33,16 @@ pub enum HistoryTreeError { IOError(#[from] io::Error), } +impl PartialEq for HistoryTreeError { + fn eq(&self, other: &Self) -> bool { + // Workaround since subtypes do not implement Eq. + // This is only used for tests anyway. + format!("{:?}", self) == format!("{:?}", other) + } +} + +impl Eq for HistoryTreeError {} + /// The inner [Tree] in one of its supported versions. #[derive(Debug)] enum InnerHistoryTree { diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index b58afb29998..7f57dcc7208 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -4,8 +4,8 @@ use chrono::{DateTime, Utc}; use thiserror::Error; use zebra_chain::{ - amount, block, orchard, sapling, sprout, transparent, value_balance::ValueBalanceError, - work::difficulty::CompactDifficulty, + amount, block, history_tree::HistoryTreeError, orchard, sapling, sprout, transparent, + value_balance::ValueBalanceError, work::difficulty::CompactDifficulty, }; use crate::constants::MIN_TRANSPARENT_COINBASE_MATURITY; @@ -36,12 +36,12 @@ impl From for CloneError { pub type BoxError = Box; /// An error describing the reason a block could not be committed to the state. -#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[derive(Debug, Error, PartialEq, Eq)] #[error("block is not contextually valid")] pub struct CommitBlockError(#[from] ValidateContextError); /// An error describing why a block failed contextual validation. -#[derive(Debug, Error, Clone, PartialEq, Eq)] +#[derive(Debug, Error, PartialEq, Eq)] #[non_exhaustive] #[allow(missing_docs)] pub enum ValidateContextError { @@ -185,6 +185,9 @@ pub enum ValidateContextError { #[error("error in Orchard note commitment tree")] OrchardNoteCommitmentTreeError(#[from] zebra_chain::orchard::tree::NoteCommitmentTreeError), + + #[error("error building the history tree")] + HistoryTreeError(#[from] HistoryTreeError), } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/service/arbitrary.rs b/zebra-state/src/service/arbitrary.rs index 4168df13672..3bef15b524e 100644 --- a/zebra-state/src/service/arbitrary.rs +++ b/zebra-state/src/service/arbitrary.rs @@ -8,7 +8,7 @@ use proptest::{ }; use zebra_chain::{ - block::{self, Block}, + block::{self, Block, Height}, fmt::SummaryDebug, parameters::NetworkUpgrade, LedgerState, @@ -60,6 +60,30 @@ impl ValueTree for PreparedChainTree { pub struct PreparedChain { // the proptests are threaded (not async), so we want to use a threaded mutex here chain: std::sync::Mutex>>)>>, + // the height from which to start the chain. If None, starts at the genesis block + start_height: Option, +} + +impl PreparedChain { + /// Create a PreparedChain strategy with Heartwood-onward blocks. + #[cfg(test)] + pub(crate) fn new_heartwood() -> Self { + // The history tree only works with Heartwood onward. + // Since the network will be chosen later, we pick the larger + // between the mainnet and testnet Heartwood activation heights. + let main_height = NetworkUpgrade::Heartwood + .activation_height(Network::Mainnet) + .expect("must have height"); + let test_height = NetworkUpgrade::Heartwood + .activation_height(Network::Testnet) + .expect("must have height"); + let height = (std::cmp::max(main_height, test_height) + 1).expect("must be valid"); + + PreparedChain { + start_height: Some(height), + ..Default::default() + } + } } impl Strategy for PreparedChain { @@ -70,7 +94,12 @@ impl Strategy for PreparedChain { let mut chain = self.chain.lock().unwrap(); if chain.is_none() { // TODO: use the latest network upgrade (#1974) - let ledger_strategy = LedgerState::genesis_strategy(NetworkUpgrade::Nu5, None, false); + let ledger_strategy = match self.start_height { + Some(start_height) => { + LedgerState::height_strategy(start_height, NetworkUpgrade::Nu5, None, false) + } + None => LedgerState::genesis_strategy(NetworkUpgrade::Nu5, None, false), + }; let (network, blocks) = ledger_strategy .prop_flat_map(|ledger| { @@ -97,7 +126,9 @@ impl Strategy for PreparedChain { } let chain = chain.clone().expect("should be generated"); - let count = (1..chain.1.len()).new_tree(runner)?; + // `count` must be 1 less since the first block is used to build the + // history tree. + let count = (1..chain.1.len() - 1).new_tree(runner)?; Ok(PreparedChainTree { chain: chain.1, count, diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index b1bcf18ef86..491f208abde 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -14,6 +14,7 @@ use std::{collections::BTreeSet, mem, ops::Deref, sync::Arc}; use zebra_chain::{ block::{self, Block}, + history_tree::HistoryTree, orchard, parameters::Network, sapling, @@ -129,6 +130,7 @@ impl NonFinalizedState { parent_hash, finalized_state.sapling_note_commitment_tree(), finalized_state.orchard_note_commitment_tree(), + finalized_state.history_tree(), )?; // We might have taken a chain, so all validation must happen within @@ -161,8 +163,10 @@ impl NonFinalizedState { finalized_state: &FinalizedState, ) -> Result<(), ValidateContextError> { let chain = Chain::new( + self.network, finalized_state.sapling_note_commitment_tree(), finalized_state.orchard_note_commitment_tree(), + finalized_state.history_tree(), ); let (height, hash) = (prepared.height, prepared.hash); @@ -355,13 +359,14 @@ impl NonFinalizedState { /// The chain can be an existing chain in the non-finalized state or a freshly /// created fork, if needed. /// - /// The note commitment trees must be the trees of the finalized tip. + /// The trees must be the trees of the finalized tip. /// They are used to recreate the trees if a fork is needed. fn parent_chain( &mut self, parent_hash: block::Hash, sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + history_tree: HistoryTree, ) -> Result, ValidateContextError> { match self.take_chain_if(|chain| chain.non_finalized_tip_hash() == parent_hash) { // An existing chain in the non-finalized state @@ -376,6 +381,7 @@ impl NonFinalizedState { parent_hash, sapling_note_commitment_tree.clone(), orchard_note_commitment_tree.clone(), + history_tree.clone(), ) .transpose() }) diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 0cf3c165ca9..c72f74c8dae 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -8,14 +8,16 @@ use multiset::HashMultiSet; use tracing::instrument; use zebra_chain::{ - block, orchard, primitives::Groth16Proof, sapling, sprout, transaction, - transaction::Transaction::*, transparent, work::difficulty::PartialCumulativeWork, + block, history_tree::HistoryTree, orchard, parameters::Network, primitives::Groth16Proof, + sapling, sprout, transaction, transaction::Transaction::*, transparent, + work::difficulty::PartialCumulativeWork, }; use crate::{service::check, ContextuallyValidBlock, PreparedBlock, ValidateContextError}; #[derive(Debug, Clone)] pub struct Chain { + network: Network, /// The contextually valid blocks which form this non-finalized partial chain, in height order. pub(crate) blocks: BTreeMap, @@ -37,6 +39,8 @@ pub struct Chain { pub(super) sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, /// The Orchard note commitment tree of the tip of this Chain. pub(super) orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + /// The ZIP-221 history tree of the tip of this Chain. + pub(crate) history_tree: HistoryTree, /// The Sapling anchors created by `blocks`. pub(super) sapling_anchors: HashMultiSet, @@ -59,12 +63,15 @@ pub struct Chain { } impl Chain { - // Create a new Chain with the given note commitment trees. + /// Create a new Chain with the given note commitment trees. pub(crate) fn new( + network: Network, sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + history_tree: HistoryTree, ) -> Self { Self { + network, blocks: Default::default(), height_by_hash: Default::default(), tx_by_hash: Default::default(), @@ -80,6 +87,7 @@ impl Chain { sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), partial_cumulative_work: Default::default(), + history_tree, } } @@ -95,6 +103,8 @@ impl Chain { /// even if the blocks in the two chains are equal. #[cfg(test)] pub(crate) fn eq_internal_state(&self, other: &Chain) -> bool { + use zebra_chain::history_tree::NonEmptyHistoryTree; + // this method must be updated every time a field is added to Chain // blocks, heights, hashes @@ -110,6 +120,9 @@ impl Chain { self.sapling_note_commitment_tree.root() == other.sapling_note_commitment_tree.root() && self.orchard_note_commitment_tree.root() == other.orchard_note_commitment_tree.root() && + // history tree + self.history_tree.as_ref().map(NonEmptyHistoryTree::hash) == other.history_tree.as_ref().map(NonEmptyHistoryTree::hash) && + // anchors self.sapling_anchors == other.sapling_anchors && self.sapling_anchors_by_height == other.sapling_anchors_by_height && @@ -169,19 +182,24 @@ impl Chain { /// Fork a chain at the block with the given hash, if it is part of this /// chain. /// - /// The note commitment trees must be the trees of the finalized tip. + /// The trees must match the trees of the finalized tip and are used + /// to rebuild them after the fork. pub fn fork( &self, fork_tip: block::Hash, sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + history_tree: HistoryTree, ) -> Result, ValidateContextError> { if !self.height_by_hash.contains_key(&fork_tip) { return Ok(None); } - let mut forked = - self.with_trees(sapling_note_commitment_tree, orchard_note_commitment_tree); + let mut forked = self.with_trees( + sapling_note_commitment_tree, + orchard_note_commitment_tree, + history_tree, + ); while forked.non_finalized_tip_hash() != fork_tip { forked.pop_tip(); @@ -206,6 +224,24 @@ impl Chain { .expect("must work since it was already appended before the fork"); } } + + // Note that anchors don't need to be recreated since they are already + // handled in revert_chain_state_with. + + let sapling_root = forked + .sapling_anchors_by_height + .get(&block.height) + .expect("Sapling anchors must exist for pre-fork blocks"); + let orchard_root = forked + .orchard_anchors_by_height + .get(&block.height) + .expect("Orchard anchors must exist for pre-fork blocks"); + forked.history_tree.push( + self.network, + block.block.clone(), + *sapling_root, + *orchard_root, + )?; } Ok(Some(forked)) @@ -267,8 +303,10 @@ impl Chain { &self, sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree, orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree, + history_tree: HistoryTree, ) -> Self { Chain { + network: self.network, blocks: self.blocks.clone(), height_by_hash: self.height_by_hash.clone(), tx_by_hash: self.tx_by_hash.clone(), @@ -284,6 +322,7 @@ impl Chain { sapling_nullifiers: self.sapling_nullifiers.clone(), orchard_nullifiers: self.orchard_nullifiers.clone(), partial_cumulative_work: self.partial_cumulative_work, + history_tree, } } } @@ -395,12 +434,19 @@ impl UpdateWith for Chain { // Having updated all the note commitment trees and nullifier sets in // this block, the roots of the note commitment trees as of the last // transaction are the treestates of this block. - let root = self.sapling_note_commitment_tree.root(); - self.sapling_anchors.insert(root); - self.sapling_anchors_by_height.insert(height, root); - let root = self.orchard_note_commitment_tree.root(); - self.orchard_anchors.insert(root); - self.orchard_anchors_by_height.insert(height, root); + let sapling_root = self.sapling_note_commitment_tree.root(); + self.sapling_anchors.insert(sapling_root); + self.sapling_anchors_by_height.insert(height, sapling_root); + let orchard_root = self.orchard_note_commitment_tree.root(); + self.orchard_anchors.insert(orchard_root); + self.orchard_anchors_by_height.insert(height, orchard_root); + + self.history_tree.push( + self.network, + contextually_valid.block.clone(), + sapling_root, + orchard_root, + )?; Ok(()) } @@ -429,6 +475,11 @@ impl UpdateWith for Chain { .expect("work has already been validated"); self.partial_cumulative_work -= block_work; + // Note: the history tree is not modified in this method. + // This method is called on two scenarios: + // - When popping the root: the history tree does not change. + // - When popping the tip: the history tree is rebuilt in fork(). + // for each transaction in block for (transaction, transaction_hash) in block.transactions.iter().zip(transaction_hashes.iter()) diff --git a/zebra-state/src/service/non_finalized_state/tests/prop.rs b/zebra-state/src/service/non_finalized_state/tests/prop.rs index 4f3d4a8a10f..efec821eb83 100644 --- a/zebra-state/src/service/non_finalized_state/tests/prop.rs +++ b/zebra-state/src/service/non_finalized_state/tests/prop.rs @@ -3,9 +3,11 @@ use std::{env, sync::Arc}; use zebra_test::prelude::*; use zebra_chain::{ - block::{self, Block}, + block::{self, arbitrary::allow_all_transparent_coinbase_spends, Block}, fmt::DisplayToDebug, + history_tree::{HistoryTree, NonEmptyHistoryTree}, parameters::NetworkUpgrade::*, + parameters::{Network, *}, LedgerState, }; @@ -33,12 +35,16 @@ fn forked_equals_pushed() -> Result<()> { .ok() .and_then(|v| v.parse().ok()) .unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)), - |((chain, fork_at_count, _network) in PreparedChain::default())| { + |((chain, fork_at_count, network) in PreparedChain::new_heartwood())| { + // Build a history tree with the first block to simulate the tree of + // the finalized state. + let finalized_tree: HistoryTree = NonEmptyHistoryTree::from_block(network, chain[0].block.clone(), &Default::default(), &Default::default()).unwrap().into(); + let chain = &chain[1..]; // use `fork_at_count` as the fork tip let fork_tip_hash = chain[fork_at_count - 1].hash; - let mut full_chain = Chain::new(Default::default(), Default::default()); - let mut partial_chain = Chain::new(Default::default(), Default::default()); + let mut full_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree.clone()); + let mut partial_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree.clone()); for block in chain.iter().take(fork_at_count) { partial_chain = partial_chain.push(block.clone())?; @@ -65,11 +71,12 @@ fn forked_equals_pushed() -> Result<()> { } } - let forked = full_chain + let mut forked = full_chain .fork( fork_tip_hash, Default::default(), Default::default(), + finalized_tree, ) .expect("fork works") .expect("hash is present"); @@ -77,6 +84,15 @@ fn forked_equals_pushed() -> Result<()> { // the first check is redundant, but it's useful for debugging prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len()); prop_assert!(forked.eq_internal_state(&partial_chain)); + + // Re-add blocks to the fork and check if we arrive at the + // same original full chain + for block in chain.iter().skip(fork_at_count) { + forked = forked.push(block.clone())?; + } + + prop_assert_eq!(forked.blocks.len(), full_chain.blocks.len()); + prop_assert!(forked.eq_internal_state(&full_chain)); }); Ok(()) @@ -92,17 +108,24 @@ fn finalized_equals_pushed() -> Result<()> { .ok() .and_then(|v| v.parse().ok()) .unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)), - |((chain, end_count, _network) in PreparedChain::default())| { + |((chain, end_count, network) in PreparedChain::new_heartwood())| { + // Build a history tree with the first block to simulate the tree of + // the finalized state. + let finalized_tree: HistoryTree = NonEmptyHistoryTree::from_block(network, chain[0].block.clone(), &Default::default(), &Default::default()).unwrap().into(); + let chain = &chain[1..]; + // use `end_count` as the number of non-finalized blocks at the end of the chain let finalized_count = chain.len() - end_count; - let mut full_chain = Chain::new(Default::default(), Default::default()); + let mut full_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree); for block in chain.iter().take(finalized_count) { full_chain = full_chain.push(block.clone())?; } let mut partial_chain = Chain::new( + network, full_chain.sapling_note_commitment_tree.clone(), full_chain.orchard_note_commitment_tree.clone(), + full_chain.history_tree.clone(), ); for block in chain.iter().skip(finalized_count) { partial_chain = partial_chain.push(block.clone())?; @@ -205,7 +228,7 @@ fn different_blocks_different_chains() -> Result<()> { .ok() .and_then(|v| v.parse().ok()) .unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)), - |((block1, block2) in (any::(), any::()) + |((vec1, vec2) in (any::(), any::()) .prop_flat_map(|(is_nu5, is_v5)| { // generate a Canopy or NU5 block with v4 or v5 transactions LedgerState::coinbase_strategy( @@ -213,15 +236,28 @@ fn different_blocks_different_chains() -> Result<()> { if is_nu5 && is_v5 { 5 } else { 4 }, true, )}) - .prop_map(Block::arbitrary_with) + .prop_map(|ledger_state| Block::partial_chain_strategy(ledger_state, 2, allow_all_transparent_coinbase_spends)) .prop_flat_map(|block_strategy| (block_strategy.clone(), block_strategy)) - .prop_map(|(block1, block2)| (DisplayToDebug(block1), DisplayToDebug(block2))) )| { - let chain1 = Chain::new(Default::default(), Default::default()); - let chain2 = Chain::new(Default::default(), Default::default()); - - let block1 = Arc::new(block1.0).prepare(); - let block2 = Arc::new(block2.0).prepare(); + let prev_block1 = vec1[0].clone(); + let prev_block2 = vec2[0].clone(); + let height1 = prev_block1.coinbase_height().unwrap(); + let height2 = prev_block1.coinbase_height().unwrap(); + let finalized_tree1: HistoryTree = if height1 >= Heartwood.activation_height(Network::Mainnet).unwrap() { + NonEmptyHistoryTree::from_block(Network::Mainnet, prev_block1, &Default::default(), &Default::default()).unwrap().into() + } else { + Default::default() + }; + let finalized_tree2 = if height2 >= NetworkUpgrade::Heartwood.activation_height(Network::Mainnet).unwrap() { + NonEmptyHistoryTree::from_block(Network::Mainnet, prev_block2, &Default::default(), &Default::default()).unwrap().into() + } else { + Default::default() + }; + let chain1 = Chain::new(Network::Mainnet, Default::default(), Default::default(), finalized_tree1); + let chain2 = Chain::new(Network::Mainnet, Default::default(), Default::default(), finalized_tree2); + + let block1 = vec1[1].clone().prepare(); + let block2 = vec2[1].clone().prepare(); let result1 = chain1.push(block1.clone()); let result2 = chain2.push(block2.clone()); @@ -256,6 +292,9 @@ fn different_blocks_different_chains() -> Result<()> { chain1.sapling_note_commitment_tree = chain2.sapling_note_commitment_tree.clone(); chain1.orchard_note_commitment_tree = chain2.orchard_note_commitment_tree.clone(); + // history tree + chain1.history_tree = chain2.history_tree.clone(); + // anchors chain1.sapling_anchors = chain2.sapling_anchors.clone(); chain1.sapling_anchors_by_height = chain2.sapling_anchors_by_height.clone(); diff --git a/zebra-state/src/service/non_finalized_state/tests/vectors.rs b/zebra-state/src/service/non_finalized_state/tests/vectors.rs index 4be78298c6a..712cf828c3d 100644 --- a/zebra-state/src/service/non_finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/non_finalized_state/tests/vectors.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use zebra_chain::{block::Block, parameters::Network, serialization::ZcashDeserializeInto}; +use zebra_chain::{ + block::Block, + parameters::{Network, NetworkUpgrade}, + serialization::ZcashDeserializeInto, +}; use zebra_test::prelude::*; use crate::{ @@ -18,7 +22,12 @@ use self::assert_eq; #[test] fn construct_empty() { zebra_test::init(); - let _chain = Chain::new(Default::default(), Default::default()); + let _chain = Chain::new( + Network::Mainnet, + Default::default(), + Default::default(), + Default::default(), + ); } #[test] @@ -27,7 +36,12 @@ fn construct_single() -> Result<()> { let block: Arc = zebra_test::vectors::BLOCK_MAINNET_434873_BYTES.zcash_deserialize_into()?; - let mut chain = Chain::new(Default::default(), Default::default()); + let mut chain = Chain::new( + Network::Mainnet, + Default::default(), + Default::default(), + Default::default(), + ); chain = chain.push(block.prepare())?; assert_eq!(1, chain.blocks.len()); @@ -49,7 +63,12 @@ fn construct_many() -> Result<()> { block = next_block; } - let mut chain = Chain::new(Default::default(), Default::default()); + let mut chain = Chain::new( + Network::Mainnet, + Default::default(), + Default::default(), + Default::default(), + ); for block in blocks { chain = chain.push(block.prepare())?; @@ -68,10 +87,20 @@ fn ord_matches_work() -> Result<()> { .set_work(1); let more_block = less_block.clone().set_work(10); - let mut lesser_chain = Chain::new(Default::default(), Default::default()); + let mut lesser_chain = Chain::new( + Network::Mainnet, + Default::default(), + Default::default(), + Default::default(), + ); lesser_chain = lesser_chain.push(less_block.prepare())?; - let mut bigger_chain = Chain::new(Default::default(), Default::default()); + let mut bigger_chain = Chain::new( + Network::Mainnet, + Default::default(), + Default::default(), + Default::default(), + ); bigger_chain = bigger_chain.push(more_block.prepare())?; assert!(bigger_chain > lesser_chain); @@ -91,11 +120,14 @@ fn best_chain_wins() -> Result<()> { fn best_chain_wins_for_network(network: Network) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -128,11 +160,14 @@ fn finalize_pops_from_best_chain() -> Result<()> { fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -172,11 +207,14 @@ fn commit_block_extending_best_chain_doesnt_drop_worst_chains_for_network( network: Network, ) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -212,11 +250,14 @@ fn shorter_chain_can_be_best_chain() -> Result<()> { fn shorter_chain_can_be_best_chain_for_network(network: Network) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -251,11 +292,14 @@ fn longer_chain_with_more_work_wins() -> Result<()> { fn longer_chain_with_more_work_wins_for_network(network: Network) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -293,11 +337,14 @@ fn equal_length_goes_to_more_work() -> Result<()> { } fn equal_length_goes_to_more_work_for_network(network: Network) -> Result<()> { let block1: Arc = match network { + // Since the brand new FinalizedState below will pass a None history tree + // to the NonFinalizedState, we must use pre-Heartwood blocks since + // they won't trigger the history tree update in the NonFinalizedState. Network::Mainnet => { - zebra_test::vectors::BLOCK_MAINNET_1180900_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_MAINNET_653599_BYTES.zcash_deserialize_into()? } Network::Testnet => { - zebra_test::vectors::BLOCK_TESTNET_1326100_BYTES.zcash_deserialize_into()? + zebra_test::vectors::BLOCK_TESTNET_583999_BYTES.zcash_deserialize_into()? } }; @@ -318,3 +365,73 @@ fn equal_length_goes_to_more_work_for_network(network: Network) -> Result<()> { Ok(()) } + +#[test] +fn history_tree_is_updated() -> Result<()> { + history_tree_is_updated_for_network_upgrade(Network::Mainnet, NetworkUpgrade::Heartwood)?; + history_tree_is_updated_for_network_upgrade(Network::Testnet, NetworkUpgrade::Heartwood)?; + // TODO: we can't test other upgrades until we have a method for creating a FinalizedState + // with a HistoryTree. + Ok(()) +} + +fn history_tree_is_updated_for_network_upgrade( + network: Network, + network_upgrade: NetworkUpgrade, +) -> Result<()> { + let blocks = match network { + Network::Mainnet => &*zebra_test::vectors::MAINNET_BLOCKS, + Network::Testnet => &*zebra_test::vectors::TESTNET_BLOCKS, + }; + let height = network_upgrade.activation_height(network).unwrap().0; + + let prev_block = Arc::new( + blocks + .get(&(height - 1)) + .expect("test vector exists") + .zcash_deserialize_into::() + .expect("block is structurally valid"), + ); + let activation_block = prev_block.make_fake_child(); + let next_block = activation_block.make_fake_child(); + + let mut state = NonFinalizedState::new(network); + let finalized_state = FinalizedState::new(&Config::ephemeral(), network); + + state.commit_new_chain(prev_block.prepare(), &finalized_state)?; + + let chain = state.best_chain().unwrap(); + if network_upgrade == NetworkUpgrade::Heartwood { + assert!( + chain.history_tree.as_ref().is_none(), + "history tree must not exist yet" + ); + } else { + assert!( + chain.history_tree.as_ref().is_some(), + "history tree must already exist" + ); + } + + state.commit_block(activation_block.prepare(), &finalized_state)?; + + let chain = state.best_chain().unwrap(); + assert!( + chain.history_tree.as_ref().is_some(), + "history tree must have been (re)created" + ); + assert_eq!( + chain.history_tree.as_ref().as_ref().unwrap().size(), + 1, + "history tree must have a single node" + ); + + state.commit_block(next_block.prepare(), &finalized_state)?; + + assert!( + state.best_chain().unwrap().history_tree.as_ref().is_some(), + "history tree must still exist" + ); + + Ok(()) +}