use std::{
default::Default,
fmt,
hash::{Hash, Hasher},
io,
};
use bitvec::prelude::*;
use halo2::pasta::{group::ff::PrimeField, pallas};
use hex::ToHex;
use incrementalmerkletree::{frontier::NonEmptyFrontier, Hashable};
use lazy_static::lazy_static;
use thiserror::Error;
use zcash_primitives::merkle_tree::HashSer;
use sinsemilla::HashDomain;
use crate::{
serialization::{
serde_helpers, ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize,
},
subtree::{NoteCommitmentSubtreeIndex, TRACKED_SUBTREE_HEIGHT},
};
pub mod legacy;
use legacy::LegacyNoteCommitmentTree;
pub type NoteCommitmentUpdate = pallas::Base;
pub(super) const MERKLE_DEPTH: u8 = 32;
lazy_static! {
static ref ORCHARD_MERKLE_CRH_DOMAIN: HashDomain =
HashDomain::new("z.cash:Orchard-MerkleCRH");
}
fn merkle_crh_orchard(layer: u8, left: pallas::Base, right: pallas::Base) -> pallas::Base {
let mut s = bitvec![u8, Lsb0;];
let l = MERKLE_DEPTH - 1 - layer;
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from([l, 0])[0..10]);
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from(left.to_repr())[0..255]);
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from(right.to_repr())[0..255]);
let hash: Option<pallas::Base> = ORCHARD_MERKLE_CRH_DOMAIN
.hash(s.iter().map(|b| *b.as_ref()))
.into();
hash.unwrap_or_else(pallas::Base::zero)
}
lazy_static! {
pub(super) static ref EMPTY_ROOTS: Vec<pallas::Base> = {
let mut v = vec![NoteCommitmentTree::uncommitted()];
for layer in (0..MERKLE_DEPTH).rev()
{
let next = merkle_crh_orchard(layer, v[0], v[0]);
v.insert(0, next);
}
v
};
}
#[derive(Clone, Copy, Default, Eq, Serialize, Deserialize)]
pub struct Root(#[serde(with = "serde_helpers::Base")] pub(crate) pallas::Base);
impl Root {
pub fn bytes_in_display_order(&self) -> [u8; 32] {
self.into()
}
}
impl fmt::Debug for Root {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("Root")
.field(&hex::encode(self.0.to_repr()))
.finish()
}
}
impl From<Root> for [u8; 32] {
fn from(root: Root) -> Self {
root.0.into()
}
}
impl From<&Root> for [u8; 32] {
fn from(root: &Root) -> Self {
(*root).into()
}
}
impl Hash for Root {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_repr().hash(state)
}
}
impl PartialEq for Root {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl TryFrom<[u8; 32]> for Root {
type Error = SerializationError;
fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
let possible_point = pallas::Base::from_repr(bytes);
if possible_point.is_some().into() {
Ok(Self(possible_point.unwrap()))
} else {
Err(SerializationError::Parse(
"Invalid pallas::Base value for Orchard note commitment tree root",
))
}
}
}
impl ZcashSerialize for Root {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&<[u8; 32]>::from(*self)[..])?;
Ok(())
}
}
impl ZcashDeserialize for Root {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
Self::try_from(reader.read_32_bytes()?)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Default)]
pub struct Node(pallas::Base);
impl Node {
pub fn to_repr(&self) -> [u8; 32] {
self.0.to_repr()
}
pub fn bytes_in_display_order(&self) -> [u8; 32] {
self.to_repr()
}
}
impl TryFrom<&[u8]> for Node {
type Error = &'static str;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
<[u8; 32]>::try_from(bytes)
.map_err(|_| "wrong byte slice len")?
.try_into()
}
}
impl TryFrom<[u8; 32]> for Node {
type Error = &'static str;
fn try_from(bytes: [u8; 32]) -> Result<Self, Self::Error> {
Option::<pallas::Base>::from(pallas::Base::from_repr(bytes))
.map(Node)
.ok_or("invalid Pallas field element")
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.encode_hex::<String>())
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("orchard::Node")
.field(&self.encode_hex::<String>())
.finish()
}
}
impl ToHex for &Node {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
self.bytes_in_display_order().encode_hex_upper()
}
}
impl ToHex for Node {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}
fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}
impl HashSer for Node {
fn read<R: io::Read>(mut reader: R) -> io::Result<Self> {
let mut repr = [0u8; 32];
reader.read_exact(&mut repr)?;
let maybe_node = pallas::Base::from_repr(repr).map(Self);
<Option<_>>::from(maybe_node).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Non-canonical encoding of Pallas base field value.",
)
})
}
fn write<W: io::Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.0.to_repr())
}
}
impl Hashable for Node {
fn empty_leaf() -> Self {
Self(NoteCommitmentTree::uncommitted())
}
fn combine(level: incrementalmerkletree::Level, a: &Self, b: &Self) -> Self {
let layer = MERKLE_DEPTH - 1 - u8::from(level);
Self(merkle_crh_orchard(layer, a.0, b.0))
}
fn empty_root(level: incrementalmerkletree::Level) -> Self {
let layer_below = usize::from(MERKLE_DEPTH) - usize::from(level);
Self(EMPTY_ROOTS[layer_below])
}
}
impl From<pallas::Base> for Node {
fn from(x: pallas::Base) -> Self {
Node(x)
}
}
impl serde::Serialize for Node {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.to_repr().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for Node {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = <[u8; 32]>::deserialize(deserializer)?;
Option::<pallas::Base>::from(pallas::Base::from_repr(bytes))
.map(Node)
.ok_or_else(|| serde::de::Error::custom("invalid Pallas field element"))
}
}
#[derive(Error, Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[allow(missing_docs)]
pub enum NoteCommitmentTreeError {
#[error("The note commitment tree is full")]
FullTree,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(into = "LegacyNoteCommitmentTree")]
#[serde(from = "LegacyNoteCommitmentTree")]
pub struct NoteCommitmentTree {
inner: incrementalmerkletree::frontier::Frontier<Node, MERKLE_DEPTH>,
cached_root: std::sync::RwLock<Option<Root>>,
}
impl NoteCommitmentTree {
#[allow(clippy::unwrap_in_result)]
pub fn append(&mut self, cm_x: NoteCommitmentUpdate) -> Result<(), NoteCommitmentTreeError> {
if self.inner.append(cm_x.into()) {
let cached_root = self
.cached_root
.get_mut()
.expect("a thread that previously held exclusive lock access panicked");
*cached_root = None;
Ok(())
} else {
Err(NoteCommitmentTreeError::FullTree)
}
}
#[allow(clippy::unwrap_in_result)]
pub fn append_batch(
&mut self,
note_commitments: &[NoteCommitmentUpdate],
) -> Result<Option<(NoteCommitmentSubtreeIndex, Node)>, NoteCommitmentTreeError> {
use crate::parallel::batch_frontier::append_batch_with_subtree;
if note_commitments.is_empty() {
return Ok(None);
}
let nodes: Vec<Node> = note_commitments
.iter()
.map(|commitment_x| (*commitment_x).into())
.collect();
let (frontier, completed) = append_batch_with_subtree(self.inner.clone(), nodes)
.map_err(|_| NoteCommitmentTreeError::FullTree)?;
self.inner = frontier;
*self
.cached_root
.get_mut()
.expect("a thread that previously held exclusive lock access panicked") = None;
Ok(completed.map(|(index_value, root)| {
let index = NoteCommitmentSubtreeIndex(
index_value.try_into().expect("subtree index fits in u16"),
);
(index, root)
}))
}
fn frontier(&self) -> Option<&NonEmptyFrontier<Node>> {
self.inner.value()
}
pub fn position(&self) -> Option<u64> {
let Some(tree) = self.frontier() else {
return None;
};
Some(tree.position().into())
}
pub fn contains_new_subtree(&self, prev_tree: &Self) -> bool {
let index = self.subtree_index().map_or(-1, |index| i32::from(index.0));
let prev_index = prev_tree
.subtree_index()
.map_or(-1, |index| i32::from(index.0));
let index_difference = index - prev_index;
if index < prev_index {
return false;
}
if index_difference > 1 {
return true;
}
if index == prev_index {
return self.is_complete_subtree();
}
if self.is_complete_subtree() {
return true;
}
if prev_tree.is_complete_subtree() || prev_index == -1 {
return false;
}
true
}
pub fn is_complete_subtree(&self) -> bool {
let Some(tree) = self.frontier() else {
return false;
};
tree.position()
.is_complete_subtree(TRACKED_SUBTREE_HEIGHT.into())
}
#[allow(clippy::unwrap_in_result)]
pub fn subtree_index(&self) -> Option<NoteCommitmentSubtreeIndex> {
let tree = self.frontier()?;
let index = incrementalmerkletree::Address::above_position(
TRACKED_SUBTREE_HEIGHT.into(),
tree.position(),
)
.index()
.try_into()
.expect("fits in u16");
Some(index)
}
#[allow(clippy::unwrap_in_result)]
pub fn remaining_subtree_leaf_nodes(&self) -> usize {
let remaining = match self.frontier() {
Some(tree) => {
let max_position = incrementalmerkletree::Address::above_position(
TRACKED_SUBTREE_HEIGHT.into(),
tree.position(),
)
.max_position();
max_position - tree.position().into()
}
None => {
let subtree_address = incrementalmerkletree::Address::above_position(
TRACKED_SUBTREE_HEIGHT.into(),
0.into(),
);
assert_eq!(
subtree_address.position_range_start(),
0.into(),
"address is not in the first subtree"
);
subtree_address.position_range_end()
}
};
u64::from(remaining).try_into().expect("fits in usize")
}
pub fn completed_subtree_index_and_root(&self) -> Option<(NoteCommitmentSubtreeIndex, Node)> {
if !self.is_complete_subtree() {
return None;
}
let index = self.subtree_index()?;
let root = self.frontier()?.root(Some(TRACKED_SUBTREE_HEIGHT.into()));
Some((index, root))
}
pub fn root(&self) -> Root {
if let Some(root) = self.cached_root() {
return root;
}
let mut write_root = self
.cached_root
.write()
.expect("a thread that previously held exclusive lock access panicked");
let read_root = write_root.as_ref().cloned();
match read_root {
Some(root) => root,
None => {
let root = self.recalculate_root();
*write_root = Some(root);
root
}
}
}
#[allow(clippy::unwrap_in_result)]
pub fn cached_root(&self) -> Option<Root> {
*self
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked")
}
pub fn recalculate_root(&self) -> Root {
Root(self.inner.root().0)
}
pub fn hash(&self) -> [u8; 32] {
self.root().into()
}
pub fn uncommitted() -> pallas::Base {
pallas::Base::one().double()
}
pub fn count(&self) -> u64 {
self.inner
.value()
.map_or(0, |x| u64::from(x.position()) + 1)
}
#[cfg(any(test, feature = "proptest-impl"))]
pub fn assert_frontier_eq(&self, other: &Self) {
assert_eq!(self.cached_root(), other.cached_root());
assert_eq!(self.inner, other.inner);
assert_eq!(self.to_rpc_bytes(), other.to_rpc_bytes());
}
pub fn to_rpc_bytes(&self) -> Vec<u8> {
let tree = incrementalmerkletree::frontier::CommitmentTree::from_frontier(&self.inner);
let mut rpc_bytes = vec![];
zcash_primitives::merkle_tree::write_commitment_tree(&tree, &mut rpc_bytes)
.expect("serializable tree");
rpc_bytes
}
}
impl Clone for NoteCommitmentTree {
fn clone(&self) -> Self {
let cached_root = self.cached_root();
Self {
inner: self.inner.clone(),
cached_root: std::sync::RwLock::new(cached_root),
}
}
}
impl Default for NoteCommitmentTree {
fn default() -> Self {
Self {
inner: incrementalmerkletree::frontier::Frontier::empty(),
cached_root: Default::default(),
}
}
}
impl Eq for NoteCommitmentTree {}
impl PartialEq for NoteCommitmentTree {
fn eq(&self, other: &Self) -> bool {
if let (Some(root), Some(other_root)) = (self.cached_root(), other.cached_root()) {
root == other_root
} else {
self.inner == other.inner
}
}
}
impl From<Vec<pallas::Base>> for NoteCommitmentTree {
fn from(values: Vec<pallas::Base>) -> Self {
let mut tree = Self::default();
if values.is_empty() {
return tree;
}
for cm_x in values {
let _ = tree.append(cm_x);
}
tree
}
}
#[cfg(test)]
mod tests {
use incrementalmerkletree::{frontier::Frontier, Position};
use super::*;
fn node(value: u64) -> Node {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&value.to_le_bytes());
Node(
Option::<pallas::Base>::from(pallas::Base::from_repr(bytes))
.expect("small little-endian integers are canonical field elements"),
)
}
fn note_commitment(value: u64) -> NoteCommitmentUpdate {
let mut bytes = [0; 32];
bytes[..8].copy_from_slice(&value.to_le_bytes());
Option::<pallas::Base>::from(pallas::Base::from_repr(bytes))
.expect("small little-endian integers are canonical field elements")
}
fn merkle_crh_orchard_uncached(
layer: u8,
left: pallas::Base,
right: pallas::Base,
) -> pallas::Base {
let mut s = bitvec![u8, Lsb0;];
let l = MERKLE_DEPTH - 1 - layer;
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from([l, 0])[0..10]);
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from(left.to_repr())[0..255]);
s.extend_from_bitslice(&BitArray::<_, Lsb0>::from(right.to_repr())[0..255]);
match crate::orchard::sinsemilla::sinsemilla_hash(b"z.cash:Orchard-MerkleCRH", &s) {
Some(h) => h,
None => pallas::Base::zero(),
}
}
fn full_width_field_elements() -> Vec<pallas::Base> {
vec![
pallas::Base::zero() - pallas::Base::one(),
pallas::Base::zero() - pallas::Base::from(2),
pallas::Base::from_raw([u64::MAX, u64::MAX, u64::MAX, u64::MAX]),
pallas::Base::from_raw([0, 0, 0, u64::MAX]),
pallas::Base::from_raw([
0x0123_4567_89ab_cdef,
0xfedc_ba98_7654_3210,
0xdead_beef_cafe_babe,
0x0f1e_2d3c_4b5a_6978,
]),
]
}
#[test]
fn cached_domain_merkle_crh_matches_fresh_domain() {
let mut values: Vec<pallas::Base> = [0u64, 1, 2, 7, 65_535, u64::MAX]
.iter()
.map(|&v| node(v).0)
.collect();
values.extend(full_width_field_elements());
for layer in 0..MERKLE_DEPTH {
for &left in &values {
for &right in &values {
assert_eq!(
merkle_crh_orchard(layer, left, right).to_repr(),
merkle_crh_orchard_uncached(layer, left, right).to_repr(),
"cached domain must match fresh domain at layer {layer}",
);
}
}
}
}
proptest::proptest! {
#[test]
fn cached_domain_merkle_crh_matches_fresh_domain_random(
layer in 0u8..MERKLE_DEPTH,
left_limbs in proptest::prelude::any::<[u64; 4]>(),
right_limbs in proptest::prelude::any::<[u64; 4]>(),
) {
let left = pallas::Base::from_raw(left_limbs);
let right = pallas::Base::from_raw(right_limbs);
proptest::prop_assert_eq!(
merkle_crh_orchard(layer, left, right).to_repr(),
merkle_crh_orchard_uncached(layer, left, right).to_repr(),
"cached domain must match fresh domain at layer {}", layer
);
}
}
fn build_tree(prefix_len: u64) -> NoteCommitmentTree {
let mut tree = NoteCommitmentTree::default();
for value in 0..prefix_len {
tree.append(note_commitment(value))
.expect("small test tree is not full");
}
tree
}
fn pre_subtree_boundary_tree() -> NoteCommitmentTree {
let subtree_size = 1u64 << TRACKED_SUBTREE_HEIGHT;
let pre_boundary_pos = subtree_size - 2;
let leaf = node(1);
let ommers: Vec<Node> = (2..=16).map(node).collect();
let inner = Frontier::from_parts(Position::from(pre_boundary_pos), leaf, ommers)
.expect("frontier with 15 ommers at position 65534 is valid");
NoteCommitmentTree {
inner,
cached_root: Default::default(),
}
}
fn sequential_append_batch(
tree: &mut NoteCommitmentTree,
note_commitments: &[NoteCommitmentUpdate],
) -> Result<Option<(NoteCommitmentSubtreeIndex, Node)>, NoteCommitmentTreeError> {
let mut completed_subtree = None;
for note_commitment in note_commitments {
tree.append(*note_commitment)?;
if let Some(subtree) = tree.completed_subtree_index_and_root() {
assert!(
completed_subtree.is_none(),
"test batches must cross at most one subtree boundary"
);
completed_subtree = Some(subtree);
}
}
Ok(completed_subtree)
}
#[test]
fn append_batch_matches_sequential_for_table_cases() {
let cases = [
("empty tree, empty batch", 0, 0),
("empty tree, one leaf", 0, 1),
("empty tree, small batch", 0, 5),
("one-leaf tree, empty batch", 1, 0),
("one-leaf tree, one leaf", 1, 1),
("odd tree, small batch", 3, 4),
("power-of-two tree, small batch", 8, 7),
("after power-of-two tree, empty batch", 9, 0),
("after power-of-two tree, small batch", 9, 6),
];
for (name, prefix_len, batch_len) in cases {
let start = build_tree(prefix_len);
let mut seq_tree = start.clone();
let mut batch_tree = start;
let note_commitments: Vec<_> = (0..batch_len)
.map(|value| note_commitment(1_000 + prefix_len + value))
.collect();
let _ = seq_tree.root();
let _ = batch_tree.root();
let seq_result = sequential_append_batch(&mut seq_tree, ¬e_commitments)
.expect("sequential append succeeds");
let batch_result = batch_tree
.append_batch(¬e_commitments)
.expect("batch append succeeds");
assert_eq!(batch_result, seq_result, "{name}: subtree result mismatch");
batch_tree.assert_frontier_eq(&seq_tree);
assert_eq!(batch_tree.root(), seq_tree.root(), "{name}: root mismatch");
}
}
#[test]
fn append_batch_matches_sequential_near_subtree_boundary() {
let cases = [
("before subtree boundary, empty batch", 0),
("complete subtree boundary", 1),
("complete and start next subtree", 2),
("complete and keep appending", 3),
];
for (name, batch_len) in cases {
let start = pre_subtree_boundary_tree();
let mut seq_tree = start.clone();
let mut batch_tree = start;
let note_commitments: Vec<_> = (0..batch_len)
.map(|value| note_commitment(10_000 + value))
.collect();
let _ = seq_tree.root();
let _ = batch_tree.root();
let seq_result = sequential_append_batch(&mut seq_tree, ¬e_commitments)
.expect("sequential append succeeds");
let batch_result = batch_tree
.append_batch(¬e_commitments)
.expect("batch append succeeds");
assert_eq!(batch_result, seq_result, "{name}: subtree result mismatch");
batch_tree.assert_frontier_eq(&seq_tree);
assert_eq!(batch_tree.root(), seq_tree.root(), "{name}: root mismatch");
}
}
#[test]
fn append_batch_crosses_subtree_boundary() {
let subtree_size = 1u64 << TRACKED_SUBTREE_HEIGHT;
let pre_boundary_pos = subtree_size - 2; let leaf = node(1);
let ommers: Vec<Node> = (2..=16).map(node).collect();
let inner = Frontier::from_parts(Position::from(pre_boundary_pos), leaf, ommers)
.expect("frontier with 15 ommers at position 65534 is valid");
let tree = NoteCommitmentTree {
inner,
cached_root: Default::default(),
};
let note_commitments = [note_commitment(100), note_commitment(200)];
let mut seq_tree = tree.clone();
seq_tree
.append(note_commitments[0])
.expect("sequential first append");
let expected_subtree = seq_tree.completed_subtree_index_and_root();
seq_tree
.append(note_commitments[1])
.expect("sequential second append");
let mut batch_tree = tree;
let batch_result = batch_tree
.append_batch(¬e_commitments)
.expect("batch append succeeds");
assert!(
batch_result.is_some(),
"batch crossing boundary must return a subtree"
);
assert_eq!(
batch_result.unwrap().0,
NoteCommitmentSubtreeIndex(0),
"first subtree index"
);
assert_eq!(
batch_result, expected_subtree,
"subtree result matches sequential"
);
batch_tree.assert_frontier_eq(&seq_tree);
assert_eq!(batch_tree.root(), seq_tree.root());
}
#[test]
fn append_batch_overflow_preserves_tree_and_cached_root() {
let max_position = (1u64 << MERKLE_DEPTH) - 1;
let leaf = Node(note_commitment(1));
let ommers = vec![Node(note_commitment(2)); usize::from(MERKLE_DEPTH)];
let inner = Frontier::from_parts(Position::from(max_position), leaf, ommers)
.expect("max-depth frontier is valid");
let mut tree = NoteCommitmentTree {
inner,
cached_root: Default::default(),
};
let _ = tree.root();
let original = tree.clone();
let result = tree.append_batch(&[note_commitment(3)]);
assert_eq!(result, Err(NoteCommitmentTreeError::FullTree));
tree.assert_frontier_eq(&original);
assert_eq!(tree.root(), original.root());
}
#[test]
fn append_batch_matches_sequential_at_tree_capacity() {
let max_position = (1u64 << MERKLE_DEPTH) - 1;
let start_position = max_position - 2;
let leaf = node(1);
let ommers: Vec<Node> = (2..=32).map(node).collect();
let inner = Frontier::from_parts(Position::from(start_position), leaf, ommers)
.expect("frontier two leaves below capacity is valid");
let start = NoteCommitmentTree {
inner,
cached_root: Default::default(),
};
let note_commitments = [note_commitment(100), note_commitment(200)];
let mut seq_tree = start.clone();
let seq_result = sequential_append_batch(&mut seq_tree, ¬e_commitments)
.expect("two sequential appends reach exact capacity");
let mut batch_tree = start;
let batch_result = batch_tree
.append_batch(¬e_commitments)
.expect("batch append reaches exact capacity");
assert_eq!(batch_result, seq_result);
assert_eq!(
batch_result.map(|(index, _)| index),
Some(NoteCommitmentSubtreeIndex(u16::MAX)),
);
batch_tree.assert_frontier_eq(&seq_tree);
assert_eq!(batch_tree.root(), seq_tree.root());
}
#[test]
fn append_batch_multiple_subtrees_preserves_tree_and_cached_root() {
let mut tree = NoteCommitmentTree::default();
let _ = tree.root();
let original = tree.clone();
let subtree_size = 1usize << TRACKED_SUBTREE_HEIGHT;
let note_commitments: Vec<_> = (0..subtree_size * 2)
.map(|value| note_commitment(value as u64))
.collect();
let result = tree.append_batch(¬e_commitments);
assert_eq!(result, Err(NoteCommitmentTreeError::FullTree));
tree.assert_frontier_eq(&original);
assert_eq!(tree.root(), original.root());
}
}