ZIP-221: Add Orchard support to history tree (#2531)
* Add Orchard support to HistoryTree * Handle network upgrades in HistoryTree * Add additional methods to save/load HistoryTree * Apply suggestions from code review Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> * Clarification of Entry documentation * Improvements from code review * Add HistoryTree tests * Improved test comments and variable names based on feedback from #2458 on similar test * Update zebra-chain/src/history_tree.rs Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> * Use type aliases for V1 and V2 history trees Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
7218aec19f
commit
fe989e0758
|
|
@ -14,7 +14,7 @@ pub mod arbitrary;
|
||||||
#[cfg(any(test, feature = "bench"))]
|
#[cfg(any(test, feature = "bench"))]
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
|
||||||
use std::{collections::HashMap, fmt};
|
use std::{collections::HashMap, convert::TryInto, fmt};
|
||||||
|
|
||||||
pub use commitment::{ChainHistoryMmrRootHash, Commitment, CommitmentError};
|
pub use commitment::{ChainHistoryMmrRootHash, Commitment, CommitmentError};
|
||||||
pub use hash::Hash;
|
pub use hash::Hash;
|
||||||
|
|
@ -146,6 +146,30 @@ impl Block {
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count how many Sapling transactions exist in a block,
|
||||||
|
/// i.e. transactions "where either of vSpendsSapling or vOutputsSapling is non-empty"
|
||||||
|
/// (https://zips.z.cash/zip-0221#tree-node-specification).
|
||||||
|
pub fn sapling_transactions_count(&self) -> u64 {
|
||||||
|
self.transactions
|
||||||
|
.iter()
|
||||||
|
.filter(|tx| tx.has_sapling_shielded_data())
|
||||||
|
.count()
|
||||||
|
.try_into()
|
||||||
|
.expect("number of transactions must fit u64")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count how many Orchard transactions exist in a block,
|
||||||
|
/// i.e. transactions "where vActionsOrchard is non-empty."
|
||||||
|
/// (https://zips.z.cash/zip-0221#tree-node-specification).
|
||||||
|
pub fn orchard_transactions_count(&self) -> u64 {
|
||||||
|
self.transactions
|
||||||
|
.iter()
|
||||||
|
.filter(|tx| tx.has_orchard_shielded_data())
|
||||||
|
.count()
|
||||||
|
.try_into()
|
||||||
|
.expect("number of transactions must fit u64")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all the value balances from this block by summing all the value balances
|
/// Get all the value balances from this block by summing all the value balances
|
||||||
/// in each transaction the block has.
|
/// in each transaction the block has.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
//! History tree (Merkle mountain range) structure that contains information about
|
//! History tree (Merkle mountain range) structure that contains information about
|
||||||
//! the block history as specified in ZIP-221.
|
//! the block history as specified in ZIP-221.
|
||||||
|
|
||||||
|
mod tests;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashSet},
|
||||||
io,
|
io,
|
||||||
|
|
@ -13,7 +15,7 @@ use crate::{
|
||||||
block::{Block, ChainHistoryMmrRootHash, Height},
|
block::{Block, ChainHistoryMmrRootHash, Height},
|
||||||
orchard,
|
orchard,
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
primitives::zcash_history::{Entry, Tree as InnerHistoryTree},
|
primitives::zcash_history::{Entry, Tree, V1 as PreOrchard, V2 as OrchardOnward},
|
||||||
sapling,
|
sapling,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -30,6 +32,14 @@ pub enum HistoryTreeError {
|
||||||
IOError(#[from] io::Error),
|
IOError(#[from] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The inner [Tree] in one of its supported versions.
|
||||||
|
enum InnerHistoryTree {
|
||||||
|
/// A pre-Orchard tree.
|
||||||
|
PreOrchard(Tree<PreOrchard>),
|
||||||
|
/// An Orchard-onward tree.
|
||||||
|
OrchardOnward(Tree<OrchardOnward>),
|
||||||
|
}
|
||||||
|
|
||||||
/// History tree (Merkle mountain range) structure that contains information about
|
/// History tree (Merkle mountain range) structure that contains information about
|
||||||
// the block history, as specified in [ZIP-221][https://zips.z.cash/zip-0221].
|
// the block history, as specified in [ZIP-221][https://zips.z.cash/zip-0221].
|
||||||
pub struct HistoryTree {
|
pub struct HistoryTree {
|
||||||
|
|
@ -49,19 +59,98 @@ pub struct HistoryTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryTree {
|
impl HistoryTree {
|
||||||
|
/// Recreate a [`HistoryTree`] from previously saved data.
|
||||||
|
///
|
||||||
|
/// The parameters must come from the values of [HistoryTree::size],
|
||||||
|
/// [HistoryTree::peaks] and [HistoryTree::current_height] of a HistoryTree.
|
||||||
|
pub fn from_cache(
|
||||||
|
network: Network,
|
||||||
|
size: u32,
|
||||||
|
peaks: BTreeMap<u32, Entry>,
|
||||||
|
current_height: Height,
|
||||||
|
) -> Result<Self, io::Error> {
|
||||||
|
let network_upgrade = NetworkUpgrade::current(network, current_height);
|
||||||
|
let inner = match network_upgrade {
|
||||||
|
NetworkUpgrade::Genesis
|
||||||
|
| NetworkUpgrade::BeforeOverwinter
|
||||||
|
| NetworkUpgrade::Overwinter
|
||||||
|
| NetworkUpgrade::Sapling
|
||||||
|
| NetworkUpgrade::Blossom => {
|
||||||
|
panic!("HistoryTree does not exist for pre-Heartwood upgrades")
|
||||||
|
}
|
||||||
|
NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
|
||||||
|
let tree = Tree::<PreOrchard>::new_from_cache(
|
||||||
|
network,
|
||||||
|
network_upgrade,
|
||||||
|
size,
|
||||||
|
&peaks,
|
||||||
|
&Default::default(),
|
||||||
|
)?;
|
||||||
|
InnerHistoryTree::PreOrchard(tree)
|
||||||
|
}
|
||||||
|
NetworkUpgrade::Nu5 => {
|
||||||
|
let tree = Tree::<OrchardOnward>::new_from_cache(
|
||||||
|
network,
|
||||||
|
network_upgrade,
|
||||||
|
size,
|
||||||
|
&peaks,
|
||||||
|
&Default::default(),
|
||||||
|
)?;
|
||||||
|
InnerHistoryTree::OrchardOnward(tree)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
network,
|
||||||
|
network_upgrade,
|
||||||
|
inner,
|
||||||
|
size,
|
||||||
|
peaks,
|
||||||
|
current_height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new history tree with a single block.
|
/// Create a new history tree with a single block.
|
||||||
|
///
|
||||||
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
|
||||||
|
/// (ignored for pre-Orchard blocks).
|
||||||
pub fn from_block(
|
pub fn from_block(
|
||||||
network: Network,
|
network: Network,
|
||||||
block: Arc<Block>,
|
block: Arc<Block>,
|
||||||
sapling_root: &sapling::tree::Root,
|
sapling_root: &sapling::tree::Root,
|
||||||
_orchard_root: Option<&orchard::tree::Root>,
|
orchard_root: &orchard::tree::Root,
|
||||||
) -> Result<Self, io::Error> {
|
) -> Result<Self, io::Error> {
|
||||||
let height = block
|
let height = block
|
||||||
.coinbase_height()
|
.coinbase_height()
|
||||||
.expect("block must have coinbase height during contextual verification");
|
.expect("block must have coinbase height during contextual verification");
|
||||||
let network_upgrade = NetworkUpgrade::current(network, height);
|
let network_upgrade = NetworkUpgrade::current(network, height);
|
||||||
// TODO: handle Orchard root, see https://github.com/ZcashFoundation/zebra/issues/2283
|
let (tree, entry) = match network_upgrade {
|
||||||
let (tree, entry) = InnerHistoryTree::new_from_block(network, block, sapling_root)?;
|
NetworkUpgrade::Genesis
|
||||||
|
| NetworkUpgrade::BeforeOverwinter
|
||||||
|
| NetworkUpgrade::Overwinter
|
||||||
|
| NetworkUpgrade::Sapling
|
||||||
|
| NetworkUpgrade::Blossom => {
|
||||||
|
panic!("HistoryTree does not exist for pre-Heartwood upgrades")
|
||||||
|
}
|
||||||
|
NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
|
||||||
|
let (tree, entry) = Tree::<PreOrchard>::new_from_block(
|
||||||
|
network,
|
||||||
|
block,
|
||||||
|
sapling_root,
|
||||||
|
&Default::default(),
|
||||||
|
)?;
|
||||||
|
(InnerHistoryTree::PreOrchard(tree), entry)
|
||||||
|
}
|
||||||
|
NetworkUpgrade::Nu5 => {
|
||||||
|
let (tree, entry) = Tree::<OrchardOnward>::new_from_block(
|
||||||
|
network,
|
||||||
|
block,
|
||||||
|
sapling_root,
|
||||||
|
orchard_root,
|
||||||
|
)?;
|
||||||
|
(InnerHistoryTree::OrchardOnward(tree), entry)
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut peaks = BTreeMap::new();
|
let mut peaks = BTreeMap::new();
|
||||||
peaks.insert(0u32, entry);
|
peaks.insert(0u32, entry);
|
||||||
Ok(HistoryTree {
|
Ok(HistoryTree {
|
||||||
|
|
@ -76,6 +165,10 @@ impl HistoryTree {
|
||||||
|
|
||||||
/// Add block data to the tree.
|
/// Add block data to the tree.
|
||||||
///
|
///
|
||||||
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
|
||||||
|
/// (ignored for pre-Orchard blocks).
|
||||||
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// If the block height is not one more than the previously pushed block.
|
/// If the block height is not one more than the previously pushed block.
|
||||||
|
|
@ -83,7 +176,7 @@ impl HistoryTree {
|
||||||
&mut self,
|
&mut self,
|
||||||
block: Arc<Block>,
|
block: Arc<Block>,
|
||||||
sapling_root: &sapling::tree::Root,
|
sapling_root: &sapling::tree::Root,
|
||||||
_orchard_root: Option<&orchard::tree::Root>,
|
orchard_root: &orchard::tree::Root,
|
||||||
) -> Result<(), HistoryTreeError> {
|
) -> Result<(), HistoryTreeError> {
|
||||||
// Check if the block has the expected height.
|
// Check if the block has the expected height.
|
||||||
// librustzcash assumes the heights are correct and corrupts the tree if they are wrong,
|
// librustzcash assumes the heights are correct and corrupts the tree if they are wrong,
|
||||||
|
|
@ -97,19 +190,31 @@ impl HistoryTree {
|
||||||
height, self.current_height
|
height, self.current_height
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let network_upgrade = NetworkUpgrade::current(self.network, height);
|
||||||
|
if network_upgrade != self.network_upgrade {
|
||||||
|
// This is the activation block of a network upgrade.
|
||||||
|
// Create a new tree.
|
||||||
|
let new_tree = Self::from_block(self.network, block, sapling_root, orchard_root)?;
|
||||||
|
// Replaces self with the new tree
|
||||||
|
*self = new_tree;
|
||||||
|
assert_eq!(self.network_upgrade, network_upgrade);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: handle orchard root
|
let new_entries = match &mut self.inner {
|
||||||
let new_entries = self
|
InnerHistoryTree::PreOrchard(tree) => tree
|
||||||
.inner
|
.append_leaf(block, sapling_root, orchard_root)
|
||||||
.append_leaf(block, sapling_root)
|
.map_err(|e| HistoryTreeError::InnerError { inner: e })?,
|
||||||
.map_err(|e| HistoryTreeError::InnerError { inner: e })?;
|
InnerHistoryTree::OrchardOnward(tree) => tree
|
||||||
|
.append_leaf(block, sapling_root, orchard_root)
|
||||||
|
.map_err(|e| HistoryTreeError::InnerError { inner: e })?,
|
||||||
|
};
|
||||||
for entry in new_entries {
|
for entry in new_entries {
|
||||||
// Not every entry is a peak; those will be trimmed later
|
// Not every entry is a peak; those will be trimmed later
|
||||||
self.peaks.insert(self.size, entry);
|
self.peaks.insert(self.size, entry);
|
||||||
self.size += 1;
|
self.size += 1;
|
||||||
}
|
}
|
||||||
self.prune()?;
|
self.prune()?;
|
||||||
// TODO: implement network upgrade logic: drop previous history, start new history
|
|
||||||
self.current_height = height;
|
self.current_height = height;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -117,13 +222,7 @@ impl HistoryTree {
|
||||||
/// Extend the history tree with the given blocks.
|
/// Extend the history tree with the given blocks.
|
||||||
pub fn try_extend<
|
pub fn try_extend<
|
||||||
'a,
|
'a,
|
||||||
T: IntoIterator<
|
T: IntoIterator<Item = (Arc<Block>, &'a sapling::tree::Root, &'a orchard::tree::Root)>,
|
||||||
Item = (
|
|
||||||
Arc<Block>,
|
|
||||||
&'a sapling::tree::Root,
|
|
||||||
Option<&'a orchard::tree::Root>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
>(
|
>(
|
||||||
&mut self,
|
&mut self,
|
||||||
iter: T,
|
iter: T,
|
||||||
|
|
@ -208,32 +307,77 @@ impl HistoryTree {
|
||||||
// Remove all non-peak entries
|
// Remove all non-peak entries
|
||||||
self.peaks.retain(|k, _| peak_pos_set.contains(k));
|
self.peaks.retain(|k, _| peak_pos_set.contains(k));
|
||||||
// Rebuild tree
|
// Rebuild tree
|
||||||
self.inner = InnerHistoryTree::new_from_cache(
|
self.inner = match self.inner {
|
||||||
self.network,
|
InnerHistoryTree::PreOrchard(_) => {
|
||||||
self.network_upgrade,
|
InnerHistoryTree::PreOrchard(Tree::<PreOrchard>::new_from_cache(
|
||||||
self.size,
|
self.network,
|
||||||
&self.peaks,
|
self.network_upgrade,
|
||||||
&Default::default(),
|
self.size,
|
||||||
)?;
|
&self.peaks,
|
||||||
|
&Default::default(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
InnerHistoryTree::OrchardOnward(_) => {
|
||||||
|
InnerHistoryTree::OrchardOnward(Tree::<OrchardOnward>::new_from_cache(
|
||||||
|
self.network,
|
||||||
|
self.network_upgrade,
|
||||||
|
self.size,
|
||||||
|
&self.peaks,
|
||||||
|
&Default::default(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the hash of the tree root.
|
/// Return the hash of the tree root.
|
||||||
pub fn hash(&self) -> ChainHistoryMmrRootHash {
|
pub fn hash(&self) -> ChainHistoryMmrRootHash {
|
||||||
self.inner.hash()
|
match &self.inner {
|
||||||
|
InnerHistoryTree::PreOrchard(tree) => tree.hash(),
|
||||||
|
InnerHistoryTree::OrchardOnward(tree) => tree.hash(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the peaks of the tree.
|
||||||
|
pub fn peaks(&self) -> &BTreeMap<u32, Entry> {
|
||||||
|
&self.peaks
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the (total) number of nodes in the tree.
|
||||||
|
pub fn size(&self) -> u32 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the height of the last added block.
|
||||||
|
pub fn current_height(&self) -> Height {
|
||||||
|
self.current_height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for HistoryTree {
|
impl Clone for HistoryTree {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let tree = InnerHistoryTree::new_from_cache(
|
let tree = match self.inner {
|
||||||
self.network,
|
InnerHistoryTree::PreOrchard(_) => InnerHistoryTree::PreOrchard(
|
||||||
self.network_upgrade,
|
Tree::<PreOrchard>::new_from_cache(
|
||||||
self.size,
|
self.network,
|
||||||
&self.peaks,
|
self.network_upgrade,
|
||||||
&Default::default(),
|
self.size,
|
||||||
)
|
&self.peaks,
|
||||||
.expect("rebuilding an existing tree should always work");
|
&Default::default(),
|
||||||
|
)
|
||||||
|
.expect("rebuilding an existing tree should always work"),
|
||||||
|
),
|
||||||
|
InnerHistoryTree::OrchardOnward(_) => InnerHistoryTree::OrchardOnward(
|
||||||
|
Tree::<OrchardOnward>::new_from_cache(
|
||||||
|
self.network,
|
||||||
|
self.network_upgrade,
|
||||||
|
self.size,
|
||||||
|
&self.peaks,
|
||||||
|
&Default::default(),
|
||||||
|
)
|
||||||
|
.expect("rebuilding an existing tree should always work"),
|
||||||
|
),
|
||||||
|
};
|
||||||
HistoryTree {
|
HistoryTree {
|
||||||
network: self.network,
|
network: self.network,
|
||||||
network_upgrade: self.network_upgrade,
|
network_upgrade: self.network_upgrade,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
//! Tests for history trees
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vectors;
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
block::{
|
||||||
|
Block,
|
||||||
|
Commitment::{self, ChainHistoryActivationReserved},
|
||||||
|
},
|
||||||
|
history_tree::HistoryTree,
|
||||||
|
parameters::{Network, NetworkUpgrade},
|
||||||
|
sapling,
|
||||||
|
serialization::ZcashDeserializeInto,
|
||||||
|
};
|
||||||
|
|
||||||
|
use color_eyre::eyre;
|
||||||
|
use eyre::Result;
|
||||||
|
use zebra_test::vectors::{
|
||||||
|
MAINNET_BLOCKS, MAINNET_FINAL_SAPLING_ROOTS, TESTNET_BLOCKS, TESTNET_FINAL_SAPLING_ROOTS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Test the history tree using the activation block of a network upgrade
|
||||||
|
/// and its next block.
|
||||||
|
///
|
||||||
|
/// This test is very similar to the zcash_history test in
|
||||||
|
/// zebra-chain/src/primitives/zcash_history/tests/vectors.rs, but with the
|
||||||
|
/// higher level API.
|
||||||
|
#[test]
|
||||||
|
fn push_and_prune() -> Result<()> {
|
||||||
|
push_and_prune_for_network_upgrade(Network::Mainnet, NetworkUpgrade::Heartwood)?;
|
||||||
|
push_and_prune_for_network_upgrade(Network::Testnet, NetworkUpgrade::Heartwood)?;
|
||||||
|
push_and_prune_for_network_upgrade(Network::Mainnet, NetworkUpgrade::Canopy)?;
|
||||||
|
push_and_prune_for_network_upgrade(Network::Testnet, NetworkUpgrade::Canopy)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_and_prune_for_network_upgrade(
|
||||||
|
network: Network,
|
||||||
|
network_upgrade: NetworkUpgrade,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (blocks, sapling_roots) = match network {
|
||||||
|
Network::Mainnet => (&*MAINNET_BLOCKS, &*MAINNET_FINAL_SAPLING_ROOTS),
|
||||||
|
Network::Testnet => (&*TESTNET_BLOCKS, &*TESTNET_FINAL_SAPLING_ROOTS),
|
||||||
|
};
|
||||||
|
let height = network_upgrade.activation_height(network).unwrap().0;
|
||||||
|
|
||||||
|
// Load first block (activation block of the given network upgrade)
|
||||||
|
let first_block = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&height)
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check its commitment
|
||||||
|
let first_commitment = first_block.commitment(network)?;
|
||||||
|
if network_upgrade == NetworkUpgrade::Heartwood {
|
||||||
|
// Heartwood is the only upgrade that has a reserved value.
|
||||||
|
// (For other upgrades we could compare with the expected commitment,
|
||||||
|
// but we haven't calculated them.)
|
||||||
|
assert_eq!(first_commitment, ChainHistoryActivationReserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build initial history tree tree with only the first block
|
||||||
|
let first_sapling_root =
|
||||||
|
sapling::tree::Root(**sapling_roots.get(&height).expect("test vector exists"));
|
||||||
|
let mut tree = HistoryTree::from_block(
|
||||||
|
network,
|
||||||
|
first_block,
|
||||||
|
&first_sapling_root,
|
||||||
|
&Default::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(tree.size(), 1);
|
||||||
|
assert_eq!(tree.peaks().len(), 1);
|
||||||
|
assert_eq!(tree.current_height().0, height);
|
||||||
|
|
||||||
|
// Compute root hash of the history tree, which will be included in the next block
|
||||||
|
let first_root = tree.hash();
|
||||||
|
|
||||||
|
// Load second block (activation + 1)
|
||||||
|
let second_block = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&(height + 1))
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check its commitment
|
||||||
|
let second_commitment = second_block.commitment(network)?;
|
||||||
|
assert_eq!(second_commitment, Commitment::ChainHistoryRoot(first_root));
|
||||||
|
|
||||||
|
// Append second block to history tree
|
||||||
|
let second_sapling_root = sapling::tree::Root(
|
||||||
|
**sapling_roots
|
||||||
|
.get(&(height + 1))
|
||||||
|
.expect("test vector exists"),
|
||||||
|
);
|
||||||
|
tree.push(second_block, &second_sapling_root, &Default::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Adding a second block will produce a 3-node tree (one parent and two leafs).
|
||||||
|
assert_eq!(tree.size(), 3);
|
||||||
|
// The tree must have been pruned, resulting in a single peak (the parent).
|
||||||
|
assert_eq!(tree.peaks().len(), 1);
|
||||||
|
assert_eq!(tree.current_height().0, height + 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the history tree works during a network upgrade using the block
|
||||||
|
/// of a network upgrade and the previous block from the previous upgrade.
|
||||||
|
#[test]
|
||||||
|
fn upgrade() -> Result<()> {
|
||||||
|
// The history tree only exists Hearwood-onward, and the only upgrade for which
|
||||||
|
// we have vectors since then is Canopy. Therefore, only test the Heartwood->Canopy upgrade.
|
||||||
|
upgrade_for_network_upgrade(Network::Mainnet, NetworkUpgrade::Canopy)?;
|
||||||
|
upgrade_for_network_upgrade(Network::Testnet, NetworkUpgrade::Canopy)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade) -> Result<()> {
|
||||||
|
let (blocks, sapling_roots) = match network {
|
||||||
|
Network::Mainnet => (&*MAINNET_BLOCKS, &*MAINNET_FINAL_SAPLING_ROOTS),
|
||||||
|
Network::Testnet => (&*TESTNET_BLOCKS, &*TESTNET_FINAL_SAPLING_ROOTS),
|
||||||
|
};
|
||||||
|
let height = network_upgrade.activation_height(network).unwrap().0;
|
||||||
|
|
||||||
|
// Load previous block (the block before the activation block of the given network upgrade)
|
||||||
|
let block_prev = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&(height - 1))
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build a history tree with only the previous block (activation height - 1)
|
||||||
|
// This tree will not match the actual tree (which has all the blocks since the previous
|
||||||
|
// network upgrade), so we won't be able to check if its root is correct.
|
||||||
|
let sapling_root_prev =
|
||||||
|
sapling::tree::Root(**sapling_roots.get(&height).expect("test vector exists"));
|
||||||
|
let mut tree =
|
||||||
|
HistoryTree::from_block(network, block_prev, &sapling_root_prev, &Default::default())?;
|
||||||
|
|
||||||
|
assert_eq!(tree.size(), 1);
|
||||||
|
assert_eq!(tree.peaks().len(), 1);
|
||||||
|
assert_eq!(tree.current_height().0, height - 1);
|
||||||
|
|
||||||
|
// Load block of the activation height
|
||||||
|
let activation_block = Arc::new(
|
||||||
|
blocks
|
||||||
|
.get(&height)
|
||||||
|
.expect("test vector exists")
|
||||||
|
.zcash_deserialize_into::<Block>()
|
||||||
|
.expect("block is structurally valid"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append block to history tree. This must trigger a upgrade of the tree,
|
||||||
|
// which should be recreated.
|
||||||
|
let activation_sapling_root = sapling::tree::Root(
|
||||||
|
**sapling_roots
|
||||||
|
.get(&(height + 1))
|
||||||
|
.expect("test vector exists"),
|
||||||
|
);
|
||||||
|
tree.push(
|
||||||
|
activation_block,
|
||||||
|
&activation_sapling_root,
|
||||||
|
&Default::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check if the tree has a single node, i.e. it has been recreated.
|
||||||
|
assert_eq!(tree.size(), 1);
|
||||||
|
assert_eq!(tree.peaks().len(), 1);
|
||||||
|
assert_eq!(tree.current_height().0, height);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -8,20 +8,34 @@ mod tests;
|
||||||
|
|
||||||
use std::{collections::BTreeMap, convert::TryInto, io, sync::Arc};
|
use std::{collections::BTreeMap, convert::TryInto, io, sync::Arc};
|
||||||
|
|
||||||
|
pub use zcash_history::{V1, V2};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
block::{Block, ChainHistoryMmrRootHash},
|
block::{Block, ChainHistoryMmrRootHash},
|
||||||
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
|
orchard,
|
||||||
|
parameters::{Network, NetworkUpgrade},
|
||||||
sapling,
|
sapling,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A trait to represent a version of `Tree`.
|
||||||
|
pub trait Version: zcash_history::Version {
|
||||||
|
/// Convert a Block into the NodeData for this version.
|
||||||
|
fn block_to_history_node(
|
||||||
|
block: Arc<Block>,
|
||||||
|
network: Network,
|
||||||
|
sapling_root: &sapling::tree::Root,
|
||||||
|
orchard_root: &orchard::tree::Root,
|
||||||
|
) -> Self::NodeData;
|
||||||
|
}
|
||||||
|
|
||||||
/// A MMR Tree using zcash_history::Tree.
|
/// A MMR Tree using zcash_history::Tree.
|
||||||
///
|
///
|
||||||
/// Currently it should not be used as a long-term data structure because it
|
/// Currently it should not be used as a long-term data structure because it
|
||||||
/// may grow without limits.
|
/// may grow without limits.
|
||||||
pub struct Tree {
|
pub struct Tree<V: zcash_history::Version> {
|
||||||
network: Network,
|
network: Network,
|
||||||
network_upgrade: NetworkUpgrade,
|
network_upgrade: NetworkUpgrade,
|
||||||
inner: zcash_history::Tree<zcash_history::V1>,
|
inner: zcash_history::Tree<V>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An encoded tree node data.
|
/// An encoded tree node data.
|
||||||
|
|
@ -50,9 +64,21 @@ pub struct Entry {
|
||||||
inner: [u8; zcash_history::MAX_ENTRY_SIZE],
|
inner: [u8; zcash_history::MAX_ENTRY_SIZE],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<zcash_history::Entry<zcash_history::V1>> for Entry {
|
impl Entry {
|
||||||
/// Convert from librustzcash.
|
/// Create a leaf Entry for the given block, its network, and the root of its
|
||||||
fn from(inner_entry: zcash_history::Entry<zcash_history::V1>) -> Self {
|
/// note commitment trees.
|
||||||
|
///
|
||||||
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
|
||||||
|
/// (ignored for V1 trees).
|
||||||
|
fn new_leaf<V: Version>(
|
||||||
|
block: Arc<Block>,
|
||||||
|
network: Network,
|
||||||
|
sapling_root: &sapling::tree::Root,
|
||||||
|
orchard_root: &orchard::tree::Root,
|
||||||
|
) -> Self {
|
||||||
|
let node_data = V::block_to_history_node(block, network, sapling_root, orchard_root);
|
||||||
|
let inner_entry = zcash_history::Entry::<V>::new_leaf(node_data);
|
||||||
let mut entry = Entry {
|
let mut entry = Entry {
|
||||||
inner: [0; zcash_history::MAX_ENTRY_SIZE],
|
inner: [0; zcash_history::MAX_ENTRY_SIZE],
|
||||||
};
|
};
|
||||||
|
|
@ -63,34 +89,7 @@ impl From<zcash_history::Entry<zcash_history::V1>> for Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl<V: Version> Tree<V> {
|
||||||
/// Create a leaf Entry for the given block, its network, and the root of its
|
|
||||||
/// Sapling note commitment tree.
|
|
||||||
fn new_leaf(block: Arc<Block>, network: Network, sapling_root: &sapling::tree::Root) -> Self {
|
|
||||||
let node_data = block_to_history_node(block, network, sapling_root);
|
|
||||||
let inner_entry = zcash_history::Entry::<zcash_history::V1>::new_leaf(node_data);
|
|
||||||
inner_entry.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a node (non-leaf) Entry from the encoded node data and the indices of
|
|
||||||
/// its children (in the array representation of the MMR tree).
|
|
||||||
fn new_node(
|
|
||||||
branch_id: ConsensusBranchId,
|
|
||||||
data: NodeData,
|
|
||||||
left_idx: u32,
|
|
||||||
right_idx: u32,
|
|
||||||
) -> Result<Self, io::Error> {
|
|
||||||
let node_data = zcash_history::NodeData::from_bytes(branch_id.into(), data.inner)?;
|
|
||||||
let inner_entry = zcash_history::Entry::new(
|
|
||||||
node_data,
|
|
||||||
zcash_history::EntryLink::Stored(left_idx),
|
|
||||||
zcash_history::EntryLink::Stored(right_idx),
|
|
||||||
);
|
|
||||||
Ok(inner_entry.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tree {
|
|
||||||
/// Create a MMR tree with the given length from the given cache of nodes.
|
/// Create a MMR tree with the given length from the given cache of nodes.
|
||||||
///
|
///
|
||||||
/// The `peaks` are the peaks of the MMR tree to build and their position in the
|
/// The `peaks` are the peaks of the MMR tree to build and their position in the
|
||||||
|
|
@ -134,16 +133,19 @@ impl Tree {
|
||||||
/// Create a single-node MMR tree for the given block.
|
/// Create a single-node MMR tree for the given block.
|
||||||
///
|
///
|
||||||
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
|
||||||
|
/// (ignored for V1 trees).
|
||||||
pub fn new_from_block(
|
pub fn new_from_block(
|
||||||
network: Network,
|
network: Network,
|
||||||
block: Arc<Block>,
|
block: Arc<Block>,
|
||||||
sapling_root: &sapling::tree::Root,
|
sapling_root: &sapling::tree::Root,
|
||||||
|
orchard_root: &orchard::tree::Root,
|
||||||
) -> Result<(Self, Entry), io::Error> {
|
) -> Result<(Self, Entry), io::Error> {
|
||||||
let height = block
|
let height = block
|
||||||
.coinbase_height()
|
.coinbase_height()
|
||||||
.expect("block must have coinbase height during contextual verification");
|
.expect("block must have coinbase height during contextual verification");
|
||||||
let network_upgrade = NetworkUpgrade::current(network, height);
|
let network_upgrade = NetworkUpgrade::current(network, height);
|
||||||
let entry0 = Entry::new_leaf(block, network, sapling_root);
|
let entry0 = Entry::new_leaf::<V>(block, network, sapling_root, orchard_root);
|
||||||
let mut peaks = BTreeMap::new();
|
let mut peaks = BTreeMap::new();
|
||||||
peaks.insert(0u32, entry0);
|
peaks.insert(0u32, entry0);
|
||||||
Ok((
|
Ok((
|
||||||
|
|
@ -157,6 +159,8 @@ impl Tree {
|
||||||
/// Append a new block to the tree, as a new leaf.
|
/// Append a new block to the tree, as a new leaf.
|
||||||
///
|
///
|
||||||
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block;
|
||||||
|
/// (ignored for V1 trees).
|
||||||
///
|
///
|
||||||
/// Returns a vector of nodes added to the tree (leaf + internal nodes).
|
/// Returns a vector of nodes added to the tree (leaf + internal nodes).
|
||||||
///
|
///
|
||||||
|
|
@ -168,6 +172,7 @@ impl Tree {
|
||||||
&mut self,
|
&mut self,
|
||||||
block: Arc<Block>,
|
block: Arc<Block>,
|
||||||
sapling_root: &sapling::tree::Root,
|
sapling_root: &sapling::tree::Root,
|
||||||
|
orchard_root: &orchard::tree::Root,
|
||||||
) -> Result<Vec<Entry>, zcash_history::Error> {
|
) -> Result<Vec<Entry>, zcash_history::Error> {
|
||||||
let height = block
|
let height = block
|
||||||
.coinbase_height()
|
.coinbase_height()
|
||||||
|
|
@ -180,7 +185,7 @@ impl Tree {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let node_data = block_to_history_node(block, self.network, sapling_root);
|
let node_data = V::block_to_history_node(block, self.network, sapling_root, orchard_root);
|
||||||
let appended = self.inner.append_leaf(node_data)?;
|
let appended = self.inner.append_leaf(node_data)?;
|
||||||
|
|
||||||
let mut new_nodes = Vec::new();
|
let mut new_nodes = Vec::new();
|
||||||
|
|
@ -202,11 +207,11 @@ impl Tree {
|
||||||
/// Append multiple blocks to the tree.
|
/// Append multiple blocks to the tree.
|
||||||
fn append_leaf_iter(
|
fn append_leaf_iter(
|
||||||
&mut self,
|
&mut self,
|
||||||
vals: impl Iterator<Item = (Arc<Block>, sapling::tree::Root)>,
|
vals: impl Iterator<Item = (Arc<Block>, sapling::tree::Root, orchard::tree::Root)>,
|
||||||
) -> Result<Vec<Entry>, zcash_history::Error> {
|
) -> Result<Vec<Entry>, zcash_history::Error> {
|
||||||
let mut new_nodes = Vec::new();
|
let mut new_nodes = Vec::new();
|
||||||
for (block, root) in vals {
|
for (block, sapling_root, orchard_root) in vals {
|
||||||
new_nodes.append(&mut self.append_leaf(block, &root)?);
|
new_nodes.append(&mut self.append_leaf(block, &sapling_root, &orchard_root)?);
|
||||||
}
|
}
|
||||||
Ok(new_nodes)
|
Ok(new_nodes)
|
||||||
}
|
}
|
||||||
|
|
@ -222,72 +227,96 @@ impl Tree {
|
||||||
pub fn hash(&self) -> ChainHistoryMmrRootHash {
|
pub fn hash(&self) -> ChainHistoryMmrRootHash {
|
||||||
// Both append_leaf() and truncate_leaf() leave a root node, so it should
|
// Both append_leaf() and truncate_leaf() leave a root node, so it should
|
||||||
// always exist.
|
// always exist.
|
||||||
self.inner
|
V::hash(self.inner.root_node().expect("must have root node").data()).into()
|
||||||
.root_node()
|
|
||||||
.expect("must have root node")
|
|
||||||
.data()
|
|
||||||
.hash()
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a Block into a zcash_history::NodeData used in the MMR tree.
|
impl Version for zcash_history::V1 {
|
||||||
///
|
/// Convert a Block into a V1::NodeData used in the MMR tree.
|
||||||
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
///
|
||||||
fn block_to_history_node(
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
block: Arc<Block>,
|
/// `orchard_root` is ignored.
|
||||||
network: Network,
|
fn block_to_history_node(
|
||||||
sapling_root: &sapling::tree::Root,
|
block: Arc<Block>,
|
||||||
) -> zcash_history::NodeData {
|
network: Network,
|
||||||
let height = block
|
sapling_root: &sapling::tree::Root,
|
||||||
.coinbase_height()
|
_orchard_root: &orchard::tree::Root,
|
||||||
.expect("block must have coinbase height during contextual verification");
|
) -> Self::NodeData {
|
||||||
let branch_id = ConsensusBranchId::current(network, height)
|
let height = block
|
||||||
.expect("must have branch ID for chain history network upgrades");
|
.coinbase_height()
|
||||||
let block_hash = block.hash().0;
|
.expect("block must have coinbase height during contextual verification");
|
||||||
let time: u32 = block
|
let network_upgrade = NetworkUpgrade::current(network, height);
|
||||||
.header
|
let branch_id = network_upgrade
|
||||||
.time
|
.branch_id()
|
||||||
.timestamp()
|
.expect("must have branch ID for chain history network upgrades");
|
||||||
.try_into()
|
let block_hash = block.hash().0;
|
||||||
.expect("deserialized and generated timestamps are u32 values");
|
let time: u32 = block
|
||||||
let target = block.header.difficulty_threshold.0;
|
.header
|
||||||
let sapling_root: [u8; 32] = sapling_root.into();
|
.time
|
||||||
let work = block
|
.timestamp()
|
||||||
.header
|
.try_into()
|
||||||
.difficulty_threshold
|
.expect("deserialized and generated timestamps are u32 values");
|
||||||
.to_work()
|
let target = block.header.difficulty_threshold.0;
|
||||||
.expect("work must be valid during contextual verification");
|
let sapling_root: [u8; 32] = sapling_root.into();
|
||||||
// There is no direct `std::primitive::u128` to `bigint::U256` conversion
|
let work = block
|
||||||
let work = bigint::U256::from_big_endian(&work.as_u128().to_be_bytes());
|
.header
|
||||||
|
.difficulty_threshold
|
||||||
|
.to_work()
|
||||||
|
.expect("work must be valid during contextual verification");
|
||||||
|
// There is no direct `std::primitive::u128` to `bigint::U256` conversion
|
||||||
|
let work = bigint::U256::from_big_endian(&work.as_u128().to_be_bytes());
|
||||||
|
|
||||||
let sapling_tx_count = count_sapling_transactions(block);
|
let sapling_tx_count = block.sapling_transactions_count();
|
||||||
|
|
||||||
zcash_history::NodeData {
|
match network_upgrade {
|
||||||
consensus_branch_id: branch_id.into(),
|
NetworkUpgrade::Genesis
|
||||||
subtree_commitment: block_hash,
|
| NetworkUpgrade::BeforeOverwinter
|
||||||
start_time: time,
|
| NetworkUpgrade::Overwinter
|
||||||
end_time: time,
|
| NetworkUpgrade::Sapling
|
||||||
start_target: target,
|
| NetworkUpgrade::Blossom => {
|
||||||
end_target: target,
|
panic!("HistoryTree does not exist for pre-Heartwood upgrades")
|
||||||
start_sapling_root: sapling_root,
|
}
|
||||||
end_sapling_root: sapling_root,
|
// Nu5 is included because this function is called by the V2 implementation
|
||||||
subtree_total_work: work,
|
// since the V1::NodeData is included inside the V2::NodeData.
|
||||||
start_height: height.0 as u64,
|
NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy | NetworkUpgrade::Nu5 => {
|
||||||
end_height: height.0 as u64,
|
zcash_history::NodeData {
|
||||||
sapling_tx: sapling_tx_count,
|
consensus_branch_id: branch_id.into(),
|
||||||
|
subtree_commitment: block_hash,
|
||||||
|
start_time: time,
|
||||||
|
end_time: time,
|
||||||
|
start_target: target,
|
||||||
|
end_target: target,
|
||||||
|
start_sapling_root: sapling_root,
|
||||||
|
end_sapling_root: sapling_root,
|
||||||
|
subtree_total_work: work,
|
||||||
|
start_height: height.0 as u64,
|
||||||
|
end_height: height.0 as u64,
|
||||||
|
sapling_tx: sapling_tx_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count how many Sapling transactions exist in a block,
|
impl Version for V2 {
|
||||||
/// i.e. transactions "where either of vSpendsSapling or vOutputsSapling is non-empty"
|
/// Convert a Block into a V1::NodeData used in the MMR tree.
|
||||||
/// (https://zips.z.cash/zip-0221#tree-node-specification).
|
///
|
||||||
fn count_sapling_transactions(block: Arc<Block>) -> u64 {
|
/// `sapling_root` is the root of the Sapling note commitment tree of the block.
|
||||||
block
|
/// `orchard_root` is the root of the Orchard note commitment tree of the block.
|
||||||
.transactions
|
fn block_to_history_node(
|
||||||
.iter()
|
block: Arc<Block>,
|
||||||
.filter(|tx| tx.has_sapling_shielded_data())
|
network: Network,
|
||||||
.count()
|
sapling_root: &sapling::tree::Root,
|
||||||
.try_into()
|
orchard_root: &orchard::tree::Root,
|
||||||
.expect("number of transactions must fit u64")
|
) -> Self::NodeData {
|
||||||
|
let orchard_tx_count = block.orchard_transactions_count();
|
||||||
|
let node_data_v1 = V1::block_to_history_node(block, network, sapling_root, orchard_root);
|
||||||
|
let orchard_root: [u8; 32] = orchard_root.into();
|
||||||
|
Self::NodeData {
|
||||||
|
v1: node_data_v1,
|
||||||
|
start_orchard_root: orchard_root,
|
||||||
|
end_orchard_root: orchard_root,
|
||||||
|
orchard_tx: orchard_tx_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ fn tree_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade) -
|
||||||
// Build initial MMR tree with only Block 0
|
// Build initial MMR tree with only Block 0
|
||||||
let sapling_root0 =
|
let sapling_root0 =
|
||||||
sapling::tree::Root(**sapling_roots.get(&height).expect("test vector exists"));
|
sapling::tree::Root(**sapling_roots.get(&height).expect("test vector exists"));
|
||||||
let (mut tree, _) = Tree::new_from_block(network, block0, &sapling_root0)?;
|
let (mut tree, _) =
|
||||||
|
Tree::<V1>::new_from_block(network, block0, &sapling_root0, &Default::default())?;
|
||||||
|
|
||||||
// Compute root hash of the MMR tree, which will be included in the next block
|
// Compute root hash of the MMR tree, which will be included in the next block
|
||||||
let hash0 = tree.hash();
|
let hash0 = tree.hash();
|
||||||
|
|
@ -73,7 +74,9 @@ fn tree_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade) -
|
||||||
.get(&(height + 1))
|
.get(&(height + 1))
|
||||||
.expect("test vector exists"),
|
.expect("test vector exists"),
|
||||||
);
|
);
|
||||||
let append = tree.append_leaf(block1, &sapling_root1).unwrap();
|
let append = tree
|
||||||
|
.append_leaf(block1, &sapling_root1, &Default::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Tree how has 3 nodes: two leafs for each block, and one parent node
|
// Tree how has 3 nodes: two leafs for each block, and one parent node
|
||||||
// which is the new root
|
// which is the new root
|
||||||
|
|
|
||||||
|
|
@ -690,6 +690,11 @@ impl Transaction {
|
||||||
.map(|orchard_shielded_data| orchard_shielded_data.flags)
|
.map(|orchard_shielded_data| orchard_shielded_data.flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return if the transaction has any Orchard shielded data.
|
||||||
|
pub fn has_orchard_shielded_data(&self) -> bool {
|
||||||
|
self.orchard_shielded_data().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
// value balances
|
// value balances
|
||||||
|
|
||||||
/// Return the transparent value balance.
|
/// Return the transparent value balance.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue