change(tests): Do round-trip tests for note commitment tree data structure and RPC serialisation (#7147)

* Add an assert_frontier_eq() method to note commitment trees for tests

* Check round-trip serialization for note commitment trees

* fix typos

---------

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
This commit is contained in:
teor 2023-07-06 11:04:28 +10:00 committed by GitHub
parent 00dd110265
commit 9df78ffdba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 45 deletions

View File

@ -14,7 +14,6 @@ use std::{
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io, io,
ops::Deref,
sync::Arc, sync::Arc,
}; };
@ -330,14 +329,9 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree, used as an anchor in Orchard /// Returns the current root of the tree, used as an anchor in Orchard
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
if let Some(root) = self if let Some(root) = self.cached_root() {
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root. // Return cached root.
return *root; return root;
} }
// Get exclusive access, compute the root, and cache it. // Get exclusive access, compute the root, and cache it.
@ -358,6 +352,15 @@ impl NoteCommitmentTree {
} }
} }
/// Returns the current root of the tree, if it has already been cached.
#[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")
}
/// Get the Pallas-based Sinsemilla hash / root node of this merkle tree of /// Get the Pallas-based Sinsemilla hash / root node of this merkle tree of
/// note commitments. /// note commitments.
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
@ -379,15 +382,33 @@ impl NoteCommitmentTree {
pub fn count(&self) -> u64 { pub fn count(&self) -> u64 {
self.inner.position().map_or(0, |pos| u64::from(pos) + 1) self.inner.position().map_or(0, |pos| u64::from(pos) + 1)
} }
/// Checks if the tree roots and inner data structures of `self` and `other` are equal.
///
/// # Panics
///
/// If they aren't equal, with a message explaining the differences.
///
/// Only for use in tests.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn assert_frontier_eq(&self, other: &Self) {
// It's technically ok for the cached root not to be preserved,
// but it can result in expensive cryptographic operations,
// so we fail the tests if it happens.
assert_eq!(self.cached_root(), other.cached_root());
// Check the data in the internal data structure
assert_eq!(self.inner, other.inner);
// Check the RPC serialization format (not the same as the Zebra database format)
assert_eq!(SerializedTree::from(self), SerializedTree::from(other));
}
} }
impl Clone for NoteCommitmentTree { impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new `RwLock` with the cloned root data. /// Clones the inner tree, and creates a new `RwLock` with the cloned root data.
fn clone(&self) -> Self { fn clone(&self) -> Self {
let cached_root = *self let cached_root = self.cached_root();
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self { Self {
inner: self.inner.clone(), inner: self.inner.clone(),

View File

@ -14,7 +14,6 @@ use std::{
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io, io,
ops::Deref,
sync::Arc, sync::Arc,
}; };
@ -333,14 +332,9 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree, used as an anchor in Sapling /// Returns the current root of the tree, used as an anchor in Sapling
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
if let Some(root) = self if let Some(root) = self.cached_root() {
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root. // Return cached root.
return *root; return root;
} }
// Get exclusive access, compute the root, and cache it. // Get exclusive access, compute the root, and cache it.
@ -361,6 +355,15 @@ impl NoteCommitmentTree {
} }
} }
/// Returns the current root of the tree, if it has already been cached.
#[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")
}
/// Gets the Jubjub-based Pedersen hash of root node of this merkle tree of /// Gets the Jubjub-based Pedersen hash of root node of this merkle tree of
/// note commitments. /// note commitments.
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
@ -382,16 +385,34 @@ impl NoteCommitmentTree {
pub fn count(&self) -> u64 { pub fn count(&self) -> u64 {
self.inner.position().map_or(0, |pos| u64::from(pos) + 1) self.inner.position().map_or(0, |pos| u64::from(pos) + 1)
} }
/// Checks if the tree roots and inner data structures of `self` and `other` are equal.
///
/// # Panics
///
/// If they aren't equal, with a message explaining the differences.
///
/// Only for use in tests.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn assert_frontier_eq(&self, other: &Self) {
// It's technically ok for the cached root not to be preserved,
// but it can result in expensive cryptographic operations,
// so we fail the tests if it happens.
assert_eq!(self.cached_root(), other.cached_root());
// Check the data in the internal data structure
assert_eq!(self.inner, other.inner);
// Check the RPC serialization format (not the same as the Zebra database format)
assert_eq!(SerializedTree::from(self), SerializedTree::from(other));
}
} }
impl Clone for NoteCommitmentTree { impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new [`RwLock`](std::sync::RwLock) /// Clones the inner tree, and creates a new [`RwLock`](std::sync::RwLock)
/// with the cloned root data. /// with the cloned root data.
fn clone(&self) -> Self { fn clone(&self) -> Self {
let cached_root = *self let cached_root = self.cached_root();
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self { Self {
inner: self.inner.clone(), inner: self.inner.clone(),

View File

@ -10,7 +10,7 @@
//! //!
//! A root of a note commitment tree is associated with each treestate. //! A root of a note commitment tree is associated with each treestate.
use std::{fmt, ops::Deref}; use std::fmt;
use byteorder::{BigEndian, ByteOrder}; use byteorder::{BigEndian, ByteOrder};
use incrementalmerkletree::{bridgetree, Frontier}; use incrementalmerkletree::{bridgetree, Frontier};
@ -266,14 +266,9 @@ impl NoteCommitmentTree {
/// Returns the current root of the tree; used as an anchor in Sprout /// Returns the current root of the tree; used as an anchor in Sprout
/// shielded transactions. /// shielded transactions.
pub fn root(&self) -> Root { pub fn root(&self) -> Root {
if let Some(root) = self if let Some(root) = self.cached_root() {
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked")
.deref()
{
// Return cached root. // Return cached root.
return *root; return root;
} }
// Get exclusive access, compute the root, and cache it. // Get exclusive access, compute the root, and cache it.
@ -294,6 +289,15 @@ impl NoteCommitmentTree {
} }
} }
/// Returns the current root of the tree, if it has already been cached.
#[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")
}
/// Returns a hash of the Sprout note commitment tree root. /// Returns a hash of the Sprout note commitment tree root.
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
self.root().into() self.root().into()
@ -316,15 +320,30 @@ impl NoteCommitmentTree {
pub fn count(&self) -> u64 { pub fn count(&self) -> u64 {
self.inner.position().map_or(0, |pos| u64::from(pos) + 1) self.inner.position().map_or(0, |pos| u64::from(pos) + 1)
} }
/// Checks if the tree roots and inner data structures of `self` and `other` are equal.
///
/// # Panics
///
/// If they aren't equal, with a message explaining the differences.
///
/// Only for use in tests.
#[cfg(any(test, feature = "proptest-impl"))]
pub fn assert_frontier_eq(&self, other: &Self) {
// It's technically ok for the cached root not to be preserved,
// but it can result in expensive cryptographic operations,
// so we fail the tests if it happens.
assert_eq!(self.cached_root(), other.cached_root());
// Check the data in the internal data structure
assert_eq!(self.inner, other.inner);
}
} }
impl Clone for NoteCommitmentTree { impl Clone for NoteCommitmentTree {
/// Clones the inner tree, and creates a new `RwLock` with the cloned root data. /// Clones the inner tree, and creates a new `RwLock` with the cloned root data.
fn clone(&self) -> Self { fn clone(&self) -> Self {
let cached_root = *self let cached_root = self.cached_root();
.cached_root
.read()
.expect("a thread that previously held exclusive lock access panicked");
Self { Self {
inner: self.inner.clone(), inner: self.inner.clone(),

View File

@ -48,9 +48,18 @@ fn sprout_note_commitment_tree_serialization() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the sprout tree database serialization format has not changed for one commitment. /// Check that the sprout tree database serialization format has not changed for one commitment.
@ -85,9 +94,18 @@ fn sprout_note_commitment_tree_serialization_one() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the sprout tree database serialization format has not changed when the number of /// Check that the sprout tree database serialization format has not changed when the number of
@ -131,9 +149,18 @@ fn sprout_note_commitment_tree_serialization_pow2() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sprout::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the sapling tree database serialization format has not changed. /// Check that the sapling tree database serialization format has not changed.
@ -172,9 +199,18 @@ fn sapling_note_commitment_tree_serialization() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the sapling tree database serialization format has not changed for one commitment. /// Check that the sapling tree database serialization format has not changed for one commitment.
@ -209,9 +245,18 @@ fn sapling_note_commitment_tree_serialization_one() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the sapling tree database serialization format has not changed when the number of /// Check that the sapling tree database serialization format has not changed when the number of
@ -259,9 +304,18 @@ fn sapling_note_commitment_tree_serialization_pow2() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = sapling::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the orchard tree database serialization format has not changed. /// Check that the orchard tree database serialization format has not changed.
@ -310,9 +364,18 @@ fn orchard_note_commitment_tree_serialization() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the orchard tree database serialization format has not changed for one commitment. /// Check that the orchard tree database serialization format has not changed for one commitment.
@ -349,9 +412,18 @@ fn orchard_note_commitment_tree_serialization_one() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }
/// Check that the orchard tree database serialization format has not changed when the number of /// Check that the orchard tree database serialization format has not changed when the number of
@ -399,7 +471,16 @@ fn orchard_note_commitment_tree_serialization_pow2() {
let serialized_tree = incremental_tree.as_bytes(); let serialized_tree = incremental_tree.as_bytes();
assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex); assert_eq!(hex::encode(&serialized_tree), expected_serialized_tree_hex);
let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(serialized_tree); let deserialized_tree = orchard::tree::NoteCommitmentTree::from_bytes(&serialized_tree);
// This check isn't enough to show that the entire struct is the same, because it just compares
// the cached serialized/deserialized roots. (NoteCommitmentTree::eq() also just compares
// roots.)
assert_eq!(incremental_tree.root(), deserialized_tree.root()); assert_eq!(incremental_tree.root(), deserialized_tree.root());
incremental_tree.assert_frontier_eq(&deserialized_tree);
// Double-check that the internal format is the same by re-serializing the tree.
let re_serialized_tree = deserialized_tree.as_bytes();
assert_eq!(serialized_tree, re_serialized_tree);
} }