Add Sprout anchors to `zebra-state` (#3100)

* Add Sprout anchors to the state

* Update zebra-state/src/service/non_finalized_state/chain.rs

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

* Return new types of note commitments from Sprout transactions

* Refactor the tests

* Refactor some comments

Co-authored-by: teor <teor@riseup.net>

* Increment `DATABASE_FORMAT_VERSION`

* Update `test.yml` with the new image name

* Refactor the `version = 5` transaction description

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

* Update comment

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>
Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Marek 2021-11-30 11:05:58 +01:00 committed by GitHub
parent 2f46d698dd
commit 3c9ad89018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 251 additions and 71 deletions

View File

@ -54,7 +54,7 @@ jobs:
--container-image rust:buster \
--container-mount-disk mount-path='/mainnet',name="zebrad-cache-$SHORT_SHA-mainnet-canopy" \
--container-restart-policy never \
--create-disk name="zebrad-cache-$SHORT_SHA-mainnet-canopy",image=zebrad-cache-0fafd6af-mainnet-canopy \
--create-disk name="zebrad-cache-$SHORT_SHA-mainnet-canopy",image=zebrad-cache-13c6a826-mainnet-canopy \
--machine-type n2-standard-8 \
--service-account cos-vm@zealous-zebra.iam.gserviceaccount.com \
--scopes cloud-platform \

View File

@ -109,7 +109,7 @@ pub enum Transaction {
/// The sapling shielded data for this transaction, if any.
sapling_shielded_data: Option<sapling::ShieldedData<sapling::PerSpendAnchor>>,
},
/// A `version = 5` transaction, which supports `Sapling` and `Orchard`.
/// A `version = 5` transaction , which supports Orchard, Sapling, and transparent, but not Sprout.
V5 {
/// The Network Upgrade for this transaction.
///
@ -692,18 +692,36 @@ impl Transaction {
&self,
) -> Box<dyn Iterator<Item = &sprout::commitment::NoteCommitment> + '_> {
match self {
// Return [`NoteCommitment`]s with [`Bctv14Proof`]s.
Transaction::V2 {
joinsplit_data: Some(joinsplit_data),
..
}
| Transaction::V3 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
Transaction::V1 { .. }
| Transaction::V2 {
// Return [`NoteCommitment`]s with [`Groth16Proof`]s.
Transaction::V4 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
// Return an empty iterator.
Transaction::V2 {
joinsplit_data: None,
..
}
| Transaction::V3 { .. }
| Transaction::V4 { .. }
| Transaction::V3 {
joinsplit_data: None,
..
}
| Transaction::V4 {
joinsplit_data: None,
..
}
| Transaction::V1 { .. }
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
}
}

View File

@ -18,7 +18,7 @@ pub use zebra_chain::transparent::MIN_TRANSPARENT_COINBASE_MATURITY;
pub const MAX_BLOCK_REORG_HEIGHT: u32 = MIN_TRANSPARENT_COINBASE_MATURITY - 1;
/// The database format version, incremented each time the database format changes.
pub const DATABASE_FORMAT_VERSION: u32 = 10;
pub const DATABASE_FORMAT_VERSION: u32 = 11;
/// The maximum number of blocks to check for NU5 transactions,
/// before we assume we are on a pre-NU5 legacy chain.

View File

@ -214,6 +214,9 @@ pub enum ValidateContextError {
height: Option<block::Height>,
},
#[error("error in Sprout note commitment tree")]
SproutNoteCommitmentTreeError(#[from] zebra_chain::sprout::tree::NoteCommitmentTreeError),
#[error("error in Sapling note commitment tree")]
SaplingNoteCommitmentTreeError(#[from] zebra_chain::sapling::tree::NoteCommitmentTreeError),

View File

@ -53,6 +53,8 @@ pub struct FinalizedState {
impl FinalizedState {
pub fn new(config: &Config, network: Network) -> Self {
let (path, db_options) = config.db_config(network);
// Note: The [`crate::constants::DATABASE_FORMAT_VERSION`] constant must
// be incremented each time the database format (column, serialization, etc) changes.
let column_families = vec![
rocksdb::ColumnFamilyDescriptor::new("hash_by_height", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("height_by_hash", db_options.clone()),
@ -62,8 +64,10 @@ impl FinalizedState {
rocksdb::ColumnFamilyDescriptor::new("sprout_nullifiers", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("sapling_nullifiers", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("orchard_nullifiers", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("sprout_anchors", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("sapling_anchors", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("orchard_anchors", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new("sprout_note_commitment_tree", db_options.clone()),
rocksdb::ColumnFamilyDescriptor::new(
"sapling_note_commitment_tree",
db_options.clone(),
@ -256,9 +260,12 @@ impl FinalizedState {
let sapling_nullifiers = self.db.cf_handle("sapling_nullifiers").unwrap();
let orchard_nullifiers = self.db.cf_handle("orchard_nullifiers").unwrap();
let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap();
let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap();
let sprout_note_commitment_tree_cf =
self.db.cf_handle("sprout_note_commitment_tree").unwrap();
let sapling_note_commitment_tree_cf =
self.db.cf_handle("sapling_note_commitment_tree").unwrap();
let orchard_note_commitment_tree_cf =
@ -298,6 +305,7 @@ impl FinalizedState {
// Read the current note commitment trees. If there are no blocks in the
// state, these will contain the empty trees.
let mut sprout_note_commitment_tree = self.sprout_note_commitment_tree();
let mut sapling_note_commitment_tree = self.sapling_note_commitment_tree();
let mut orchard_note_commitment_tree = self.orchard_note_commitment_tree();
let mut history_tree = self.history_tree();
@ -344,6 +352,11 @@ impl FinalizedState {
// used too early (e.g. the Orchard tree before Nu5 activates)
// since the block validation will make sure only appropriate
// transactions are allowed in a block.
batch.zs_insert(
sprout_note_commitment_tree_cf,
height,
sprout_note_commitment_tree,
);
batch.zs_insert(
sapling_note_commitment_tree_cf,
height,
@ -406,6 +419,9 @@ impl FinalizedState {
batch.zs_insert(orchard_nullifiers, orchard_nullifier, ());
}
for sprout_note_commitment in transaction.sprout_note_commitments() {
sprout_note_commitment_tree.append(*sprout_note_commitment)?;
}
for sapling_note_commitment in transaction.sapling_note_commitments() {
sapling_note_commitment_tree.append(*sapling_note_commitment)?;
}
@ -414,6 +430,7 @@ impl FinalizedState {
}
}
let sprout_root = sprout_note_commitment_tree.root();
let sapling_root = sapling_note_commitment_tree.root();
let orchard_root = orchard_note_commitment_tree.root();
@ -421,25 +438,36 @@ impl FinalizedState {
// Compute the new anchors and index them
// Note: if the root hasn't changed, we write the same value again.
batch.zs_insert(sprout_anchors, sprout_root, ());
batch.zs_insert(sapling_anchors, sapling_root, ());
batch.zs_insert(orchard_anchors, orchard_root, ());
// Update the trees in state
if let Some(h) = finalized_tip_height {
batch.zs_delete(sprout_note_commitment_tree_cf, h);
batch.zs_delete(sapling_note_commitment_tree_cf, h);
batch.zs_delete(orchard_note_commitment_tree_cf, h);
batch.zs_delete(history_tree_cf, h);
}
batch.zs_insert(
sprout_note_commitment_tree_cf,
height,
sprout_note_commitment_tree,
);
batch.zs_insert(
sapling_note_commitment_tree_cf,
height,
sapling_note_commitment_tree,
);
batch.zs_insert(
orchard_note_commitment_tree_cf,
height,
orchard_note_commitment_tree,
);
if let Some(history_tree) = history_tree.as_ref() {
batch.zs_insert(history_tree_cf, height, history_tree);
}
@ -624,6 +652,21 @@ impl FinalizedState {
})
}
/// 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 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 {
@ -631,11 +674,13 @@ impl FinalizedState {
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("note commitment tree must exist if there is a finalized tip")
.expect("Sapling note commitment tree must exist if there is a finalized tip")
}
/// Returns the Orchard note commitment tree of the finalized tip
@ -645,11 +690,13 @@ impl FinalizedState {
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("note commitment tree must exist if there is a finalized tip")
.expect("Orchard note commitment tree must exist if there is a finalized tip")
}
/// Returns the ZIP-221 history tree of the finalized tip or `None`

View File

@ -239,6 +239,14 @@ impl IntoDisk for transparent::OutPoint {
}
}
impl IntoDisk for sprout::tree::Root {
type Bytes = [u8; 32];
fn as_bytes(&self) -> Self::Bytes {
self.into()
}
}
impl IntoDisk for sapling::tree::Root {
type Bytes = [u8; 32];
@ -277,6 +285,23 @@ impl FromDisk for ValueBalance<NonNegative> {
// in particular to disallow trailing bytes; see
// https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions
impl IntoDisk for sprout::tree::NoteCommitmentTree {
type Bytes = Vec<u8>;
fn as_bytes(&self) -> Self::Bytes {
bincode::DefaultOptions::new()
.serialize(self)
.expect("serialization to vec doesn't fail")
}
}
impl FromDisk for sprout::tree::NoteCommitmentTree {
fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
bincode::DefaultOptions::new()
.deserialize(bytes.as_ref())
.expect("deserialization format should match the serialization format used by IntoDisk")
}
}
impl IntoDisk for sapling::tree::NoteCommitmentTree {
type Bytes = Vec<u8>;

View File

@ -17,14 +17,11 @@ use zebra_chain::{
history_tree::HistoryTree,
orchard,
parameters::Network,
sapling,
sapling, sprout,
transaction::{self, Transaction},
transparent,
};
#[cfg(test)]
use zebra_chain::sprout;
use crate::{
request::ContextuallyValidBlock, FinalizedBlock, HashOrHeight, PreparedBlock,
ValidateContextError,
@ -135,6 +132,7 @@ impl NonFinalizedState {
let parent_chain = self.parent_chain(
parent_hash,
finalized_state.sprout_note_commitment_tree(),
finalized_state.sapling_note_commitment_tree(),
finalized_state.orchard_note_commitment_tree(),
finalized_state.history_tree(),
@ -171,6 +169,7 @@ impl NonFinalizedState {
) -> Result<(), ValidateContextError> {
let chain = Chain::new(
self.network,
finalized_state.sprout_note_commitment_tree(),
finalized_state.sapling_note_commitment_tree(),
finalized_state.orchard_note_commitment_tree(),
finalized_state.history_tree(),
@ -397,6 +396,7 @@ impl NonFinalizedState {
fn parent_chain(
&mut self,
parent_hash: block::Hash,
sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree,
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
history_tree: HistoryTree,
@ -412,6 +412,7 @@ impl NonFinalizedState {
chain
.fork(
parent_hash,
sprout_note_commitment_tree.clone(),
sapling_note_commitment_tree.clone(),
orchard_note_commitment_tree.clone(),
history_tree.clone(),

View File

@ -1,3 +1,5 @@
//! Chain that is a part of the non-finalized state.
use std::{
cmp::Ordering,
collections::{BTreeMap, HashMap, HashSet},
@ -25,6 +27,7 @@ use crate::{service::check, ContextuallyValidBlock, ValidateContextError};
#[derive(Debug, Clone)]
pub struct Chain {
// The function `eq_internal_state` must be updated every time a field is added to `Chain`.
network: Network,
/// The contextually valid blocks which form this non-finalized partial chain, in height order.
pub(crate) blocks: BTreeMap<block::Height, ContextuallyValidBlock>,
@ -43,6 +46,9 @@ pub struct Chain {
/// including those created by earlier transactions or blocks in the chain.
pub(crate) spent_utxos: HashSet<transparent::OutPoint>,
/// The Sprout note commitment tree of the tip of this `Chain`,
/// including all finalized notes, and the non-finalized notes in this chain.
pub(super) sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree,
/// The Sapling note commitment tree of the tip of this `Chain`,
/// including all finalized notes, and the non-finalized notes in this chain.
pub(super) sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
@ -53,6 +59,10 @@ pub struct Chain {
/// including all finalized blocks, and the non-finalized `blocks` in this chain.
pub(crate) history_tree: HistoryTree,
/// The Sprout anchors created by `blocks`.
pub(super) sprout_anchors: HashMultiSet<sprout::tree::Root>,
/// The Sprout anchors created by each block in `blocks`.
pub(super) sprout_anchors_by_height: BTreeMap<block::Height, sprout::tree::Root>,
/// The Sapling anchors created by `blocks`.
pub(super) sapling_anchors: HashMultiSet<sapling::tree::Root>,
/// The Sapling anchors created by each block in `blocks`.
@ -89,6 +99,7 @@ impl Chain {
/// Create a new Chain with the given trees and network.
pub(crate) fn new(
network: Network,
sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree,
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
history_tree: HistoryTree,
@ -100,9 +111,12 @@ impl Chain {
height_by_hash: Default::default(),
tx_by_hash: Default::default(),
created_utxos: Default::default(),
sprout_note_commitment_tree,
sapling_note_commitment_tree,
orchard_note_commitment_tree,
spent_utxos: Default::default(),
sprout_anchors: HashMultiSet::new(),
sprout_anchors_by_height: Default::default(),
sapling_anchors: HashMultiSet::new(),
sapling_anchors_by_height: Default::default(),
orchard_anchors: HashMultiSet::new(),
@ -130,8 +144,6 @@ impl Chain {
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
self.blocks == other.blocks &&
self.height_by_hash == other.height_by_hash &&
@ -142,6 +154,7 @@ impl Chain {
self.spent_utxos == other.spent_utxos &&
// note commitment trees
self.sprout_note_commitment_tree.root() == other.sprout_note_commitment_tree.root() &&
self.sapling_note_commitment_tree.root() == other.sapling_note_commitment_tree.root() &&
self.orchard_note_commitment_tree.root() == other.orchard_note_commitment_tree.root() &&
@ -149,6 +162,8 @@ impl Chain {
self.history_tree.as_ref().map(NonEmptyHistoryTree::hash) == other.history_tree.as_ref().map(NonEmptyHistoryTree::hash) &&
// anchors
self.sprout_anchors == other.sprout_anchors &&
self.sprout_anchors_by_height == other.sprout_anchors_by_height &&
self.sapling_anchors == other.sapling_anchors &&
self.sapling_anchors_by_height == other.sapling_anchors_by_height &&
self.orchard_anchors == other.orchard_anchors &&
@ -216,6 +231,7 @@ impl Chain {
pub fn fork(
&self,
fork_tip: block::Hash,
sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree,
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
history_tree: HistoryTree,
@ -225,6 +241,7 @@ impl Chain {
}
let mut forked = self.with_trees(
sprout_note_commitment_tree,
sapling_note_commitment_tree,
orchard_note_commitment_tree,
history_tree,
@ -240,12 +257,20 @@ impl Chain {
// See https://github.com/ZcashFoundation/zebra/issues/2378
for block in forked.blocks.values() {
for transaction in block.block.transactions.iter() {
for sprout_note_commitment in transaction.sprout_note_commitments() {
forked
.sprout_note_commitment_tree
.append(*sprout_note_commitment)
.expect("must work since it was already appended before the fork");
}
for sapling_note_commitment in transaction.sapling_note_commitments() {
forked
.sapling_note_commitment_tree
.append(*sapling_note_commitment)
.expect("must work since it was already appended before the fork");
}
for orchard_note_commitment in transaction.orchard_note_commitments() {
forked
.orchard_note_commitment_tree
@ -256,15 +281,16 @@ impl Chain {
// 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(),
@ -339,6 +365,7 @@ impl Chain {
/// Useful when forking, where the trees are rebuilt anyway.
fn with_trees(
&self,
sprout_note_commitment_tree: sprout::tree::NoteCommitmentTree,
sapling_note_commitment_tree: sapling::tree::NoteCommitmentTree,
orchard_note_commitment_tree: orchard::tree::NoteCommitmentTree,
history_tree: HistoryTree,
@ -350,10 +377,13 @@ impl Chain {
tx_by_hash: self.tx_by_hash.clone(),
created_utxos: self.created_utxos.clone(),
spent_utxos: self.spent_utxos.clone(),
sprout_note_commitment_tree,
sapling_note_commitment_tree,
orchard_note_commitment_tree,
sprout_anchors: self.sprout_anchors.clone(),
sapling_anchors: self.sapling_anchors.clone(),
orchard_anchors: self.orchard_anchors.clone(),
sprout_anchors_by_height: self.sprout_anchors_by_height.clone(),
sapling_anchors_by_height: self.sapling_anchors_by_height.clone(),
orchard_anchors_by_height: self.orchard_anchors_by_height.clone(),
sprout_nullifiers: self.sprout_nullifiers.clone(),
@ -487,6 +517,9 @@ impl UpdateWith<ContextuallyValidBlock> 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 sprout_root = self.sprout_note_commitment_tree.root();
self.sprout_anchors.insert(sprout_root);
self.sprout_anchors_by_height.insert(height, sprout_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);
@ -593,6 +626,15 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
self.revert_chain_with(orchard_shielded_data, position);
}
let anchor = self
.sprout_anchors_by_height
.remove(&height)
.expect("Sprout anchor must be present if block was added to chain");
assert!(
self.sprout_anchors.remove(&anchor),
"Sprout anchor must be present if block was added to chain"
);
let anchor = self
.sapling_anchors_by_height
.remove(&height)
@ -601,6 +643,7 @@ impl UpdateWith<ContextuallyValidBlock> for Chain {
self.sapling_anchors.remove(&anchor),
"Sapling anchor must be present if block was added to chain"
);
let anchor = self
.orchard_anchors_by_height
.remove(&height)
@ -673,6 +716,10 @@ impl UpdateWith<Option<transaction::JoinSplitData<Groth16Proof>>> for Chain {
joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
) -> Result<(), ValidateContextError> {
if let Some(joinsplit_data) = joinsplit_data {
for cm in joinsplit_data.note_commitments() {
self.sprout_note_commitment_tree.append(*cm)?;
}
check::nullifier::add_to_non_finalized_chain_unique(
&mut self.sprout_nullifiers,
joinsplit_data.nullifiers(),
@ -711,6 +758,9 @@ where
sapling_shielded_data: &Option<sapling::ShieldedData<AnchorV>>,
) -> Result<(), ValidateContextError> {
if let Some(sapling_shielded_data) = sapling_shielded_data {
// The `_u` here indicates that the Sapling note commitment is
// specified only by the `u`-coordinate of the Jubjub curve
// point `(u, v)`.
for cm_u in sapling_shielded_data.note_commitments() {
self.sapling_note_commitment_tree.append(*cm_u)?;
}

View File

@ -45,7 +45,7 @@ fn push_genesis_chain() -> Result<()> {
|((chain, count, network, empty_tree) in PreparedChain::default())| {
prop_assert!(empty_tree.is_none());
let mut only_chain = Chain::new(network, Default::default(), Default::default(), empty_tree, ValueBalance::zero());
let mut only_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), empty_tree, ValueBalance::zero());
// contains the block value pool changes and chain value pool balances for each height
let mut chain_values = BTreeMap::new();
@ -97,7 +97,7 @@ fn push_history_tree_chain() -> Result<()> {
let count = std::cmp::min(count, chain.len() - 1);
let chain = &chain[1..];
let mut only_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree, ValueBalance::zero());
let mut only_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), finalized_tree, ValueBalance::zero());
for block in chain
.iter()
@ -112,13 +112,19 @@ fn push_history_tree_chain() -> Result<()> {
Ok(())
}
/// Check that a forked genesis chain is the same as a chain that had the same blocks appended.
/// Checks that a forked genesis chain is the same as a chain that had the same
/// blocks appended.
///
/// Also check that:
/// - there are no transparent spends in the chain from the genesis block,
/// because genesis transparent outputs are ignored
/// - transactions only spend transparent outputs from earlier in the block or chain
/// - chain value balances are non-negative
/// In other words, this test checks that we get the same chain if we:
/// - fork the original chain, then push some blocks, or
/// - push the same blocks to the original chain.
///
/// Also checks that:
/// - There are no transparent spends in the chain from the genesis block,
/// because genesis transparent outputs are ignored.
/// - Transactions only spend transparent outputs from earlier in the block or
/// chain.
/// - Chain value balances are non-negative.
#[test]
fn forked_equals_pushed_genesis() -> Result<()> {
zebra_test::init();
@ -129,73 +135,90 @@ fn forked_equals_pushed_genesis() -> Result<()> {
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_PARTIAL_CHAIN_PROPTEST_CASES)),
|((chain, fork_at_count, network, empty_tree) in PreparedChain::default())| {
prop_assert!(empty_tree.is_none());
// use `fork_at_count` as the fork tip
let fork_tip_hash = chain[fork_at_count - 1].hash;
let mut full_chain = Chain::new(network, Default::default(), Default::default(), empty_tree.clone(), ValueBalance::zero());
let mut partial_chain = Chain::new(network, Default::default(), Default::default(), empty_tree.clone(), ValueBalance::zero());
// This chain will be used to check if the blocks in the forked chain
// correspond to the blocks in the original chain before the fork.
let mut partial_chain = Chain::new(
network,
Default::default(),
Default::default(),
Default::default(),
empty_tree.clone(),
ValueBalance::zero(),
);
for block in chain.iter().take(fork_at_count).cloned() {
let block =
ContextuallyValidBlock::with_block_and_spent_utxos(
block,
partial_chain.unspent_utxos(),
)?;
partial_chain = partial_chain.push(block).expect("partial chain push is valid");
let block = ContextuallyValidBlock::with_block_and_spent_utxos(
block,
partial_chain.unspent_utxos(),
)?;
partial_chain = partial_chain
.push(block)
.expect("partial chain push is valid");
}
// This chain will be forked.
let mut full_chain = Chain::new(
network,
Default::default(),
Default::default(),
Default::default(),
empty_tree.clone(),
ValueBalance::zero(),
);
for block in chain.iter().cloned() {
let block =
ContextuallyValidBlock::with_block_and_spent_utxos(
block,
full_chain.unspent_utxos(),
)?;
full_chain = full_chain.push(block.clone()).expect("full chain push is valid");
ContextuallyValidBlock::with_block_and_spent_utxos(block, full_chain.unspent_utxos())?;
full_chain = full_chain
.push(block.clone())
.expect("full chain push is valid");
// check some other properties of generated chains
if block.height == block::Height(0) {
prop_assert_eq!(
block
.block
.transactions
.iter()
.flat_map(|t| t.inputs())
.filter_map(|i| i.outpoint())
.count(),
0,
"unexpected transparent prevout input at height {:?}: \
genesis transparent outputs must be ignored, \
so there can not be any spends in the genesis block",
block.height,
);
}
// Check some other properties of generated chains.
if block.height == block::Height(0) {
prop_assert_eq!(
block
.block
.transactions
.iter()
.flat_map(|t| t.inputs())
.filter_map(|i| i.outpoint())
.count(),
0,
"unexpected transparent prevout input at height {:?}: \
genesis transparent outputs must be ignored, \
so there can not be any spends in the genesis block",
block.height,
);
}
}
// Use [`fork_at_count`] as the fork tip.
let fork_tip_hash = chain[fork_at_count - 1].hash;
// Fork the chain.
let mut forked = full_chain
.fork(
fork_tip_hash,
Default::default(),
Default::default(),
Default::default(),
empty_tree,
)
.expect("fork works")
.expect("hash is present");
// the first check is redundant, but it's useful for debugging
// This check is redundant, but it's useful for debugging.
prop_assert_eq!(forked.blocks.len(), partial_chain.blocks.len());
// Check that the entire internal state of the forked chain corresponds to the state of
// the original chain.
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
// same original full chain.
for block in chain.iter().skip(fork_at_count).cloned() {
let block =
ContextuallyValidBlock::with_block_and_spent_utxos(
block,
forked.unspent_utxos(),
)?;
ContextuallyValidBlock::with_block_and_spent_utxos(block, forked.unspent_utxos())?;
forked = forked.push(block).expect("forked chain push is valid");
}
@ -229,8 +252,8 @@ fn forked_equals_pushed_history_tree() -> Result<()> {
// use `fork_at_count` as the fork tip
let fork_tip_hash = chain[fork_at_count - 1].hash;
let mut full_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero());
let mut partial_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero());
let mut full_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero());
let mut partial_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero());
for block in chain
.iter()
@ -250,6 +273,7 @@ fn forked_equals_pushed_history_tree() -> Result<()> {
fork_tip_hash,
Default::default(),
Default::default(),
Default::default(),
finalized_tree,
)
.expect("fork works")
@ -297,7 +321,7 @@ fn finalized_equals_pushed_genesis() -> Result<()> {
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
let mut full_chain = Chain::new(network, Default::default(), Default::default(), empty_tree, fake_value_pool);
let mut full_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), empty_tree, fake_value_pool);
for block in chain
.iter()
.take(finalized_count)
@ -307,6 +331,7 @@ fn finalized_equals_pushed_genesis() -> Result<()> {
let mut partial_chain = Chain::new(
network,
full_chain.sprout_note_commitment_tree.clone(),
full_chain.sapling_note_commitment_tree.clone(),
full_chain.orchard_note_commitment_tree.clone(),
full_chain.history_tree.clone(),
@ -366,7 +391,7 @@ fn finalized_equals_pushed_history_tree() -> Result<()> {
let fake_value_pool = ValueBalance::<NonNegative>::fake_populated_pool();
let mut full_chain = Chain::new(network, Default::default(), Default::default(), finalized_tree, fake_value_pool);
let mut full_chain = Chain::new(network, Default::default(), Default::default(), Default::default(), finalized_tree, fake_value_pool);
for block in chain
.iter()
.take(finalized_count)
@ -376,11 +401,13 @@ fn finalized_equals_pushed_history_tree() -> Result<()> {
let mut partial_chain = Chain::new(
network,
full_chain.sprout_note_commitment_tree.clone(),
full_chain.sapling_note_commitment_tree.clone(),
full_chain.orchard_note_commitment_tree.clone(),
full_chain.history_tree.clone(),
full_chain.chain_value_pools,
);
for block in chain
.iter()
.skip(finalized_count)
@ -528,8 +555,8 @@ fn different_blocks_different_chains() -> Result<()> {
} else {
Default::default()
};
let chain1 = Chain::new(Network::Mainnet, Default::default(), Default::default(), finalized_tree1, ValueBalance::fake_populated_pool());
let chain2 = Chain::new(Network::Mainnet, Default::default(), Default::default(), finalized_tree2, ValueBalance::fake_populated_pool());
let chain1 = Chain::new(Network::Mainnet, Default::default(), Default::default(), Default::default(), finalized_tree1, ValueBalance::fake_populated_pool());
let chain2 = Chain::new(Network::Mainnet, Default::default(), Default::default(), Default::default(), finalized_tree2, ValueBalance::fake_populated_pool());
let block1 = vec1[1].clone().prepare().test_with_zero_spent_utxos();
let block2 = vec2[1].clone().prepare().test_with_zero_spent_utxos();
@ -564,6 +591,7 @@ fn different_blocks_different_chains() -> Result<()> {
chain1.spent_utxos = chain2.spent_utxos.clone();
// note commitment trees
chain1.sprout_note_commitment_tree = chain2.sprout_note_commitment_tree.clone();
chain1.sapling_note_commitment_tree = chain2.sapling_note_commitment_tree.clone();
chain1.orchard_note_commitment_tree = chain2.orchard_note_commitment_tree.clone();
@ -571,6 +599,8 @@ fn different_blocks_different_chains() -> Result<()> {
chain1.history_tree = chain2.history_tree.clone();
// anchors
chain1.sprout_anchors = chain2.sprout_anchors.clone();
chain1.sprout_anchors_by_height = chain2.sprout_anchors_by_height.clone();
chain1.sapling_anchors = chain2.sapling_anchors.clone();
chain1.sapling_anchors_by_height = chain2.sapling_anchors_by_height.clone();
chain1.orchard_anchors = chain2.orchard_anchors.clone();

View File

@ -28,6 +28,7 @@ fn construct_empty() {
Default::default(),
Default::default(),
Default::default(),
Default::default(),
ValueBalance::zero(),
);
}
@ -43,8 +44,10 @@ fn construct_single() -> Result<()> {
Default::default(),
Default::default(),
Default::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
chain = chain.push(block.prepare().test_with_zero_spent_utxos())?;
assert_eq!(1, chain.blocks.len());
@ -71,6 +74,7 @@ fn construct_many() -> Result<()> {
Default::default(),
Default::default(),
Default::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
@ -96,6 +100,7 @@ fn ord_matches_work() -> Result<()> {
Default::default(),
Default::default(),
Default::default(),
Default::default(),
ValueBalance::fake_populated_pool(),
);
lesser_chain = lesser_chain.push(less_block.prepare().test_with_zero_spent_utxos())?;
@ -105,6 +110,7 @@ fn ord_matches_work() -> Result<()> {
Default::default(),
Default::default(),
Default::default(),
Default::default(),
ValueBalance::zero(),
);
bigger_chain = bigger_chain.push(more_block.prepare().test_with_zero_spent_utxos())?;