* merkle: add parent() helper function on NodeIndex * smt: add pairs_to_leaf() to trait * smt: add sorted_pairs_to_leaves() and test for it * smt: implement single subtree-8 hashing, w/ benchmarks & tests This will be composed into depth-8-subtree-based computation of entire sparse Merkle trees. * merkle: add a benchmark for constructing 256-balanced trees This is intended for comparison with the benchmarks from the previous commit. This benchmark represents the theoretical perfect-efficiency performance we could possibly (but impractically) get for computing depth-8 sparse Merkle subtrees. * smt: test that SparseMerkleTree::build_subtree() is composable * smt: test that subtree logic can correctly construct an entire tree This commit ensures that `SparseMerkleTree::build_subtree()` can correctly compose into building an entire sparse Merkle tree, without yet getting into potential complications concurrency introduces. * smt: implement test for basic parallelized subtree computation w/ rayon Building on the previous commit, this commit implements a test proving that `SparseMerkleTree::build_subtree()` can be composed into itself not just concurrently, but in parallel, without issue. * smt: add from_raw_parts() to trait interface This commit adds a new required method to the SparseMerkleTree trait, to allow generic construction from pre-computed parts. This will be used to add a generic version of `with_entries()` in a later commit. * smt: add parallel constructors to Smt and SimpleSmt What the previous few commits have been leading up to: SparseMerkleTree now has a function to construct the tree from existing data in parallel. This is significantly faster than the singlethreaded equivalent. Benchmarks incoming! --------- Co-authored-by: krushimir <krushimir@reilabs.co> Co-authored-by: krushimir <kresimir.grofelnik@reilabs.io>rpo-dsa
@ -0,0 +1,66 @@ |
|||
//! Benchmark for building a [`miden_crypto::merkle::MerkleTree`]. This is intended to be compared
|
|||
//! with the results from `benches/smt-subtree.rs`, as building a fully balanced Merkle tree with
|
|||
//! 256 leaves should indicate the *absolute best* performance we could *possibly* get for building
|
|||
//! a depth-8 sparse Merkle subtree, though practically speaking building a fully balanced Merkle
|
|||
//! tree will perform better than the sparse version. At the time of this writing (2024/11/24), this
|
|||
//! benchmark is about four times more efficient than the equivalent benchmark in
|
|||
//! `benches/smt-subtree.rs`.
|
|||
use std::{hint, mem, time::Duration};
|
|||
|
|||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
|||
use miden_crypto::{merkle::MerkleTree, Felt, Word, ONE};
|
|||
use rand_utils::prng_array;
|
|||
|
|||
fn balanced_merkle_even(c: &mut Criterion) {
|
|||
c.bench_function("balanced-merkle-even", |b| {
|
|||
b.iter_batched(
|
|||
|| {
|
|||
let entries: Vec<Word> =
|
|||
(0..256).map(|i| [Felt::new(i), ONE, ONE, Felt::new(i)]).collect();
|
|||
assert_eq!(entries.len(), 256);
|
|||
entries
|
|||
},
|
|||
|leaves| {
|
|||
let tree = MerkleTree::new(hint::black_box(leaves)).unwrap();
|
|||
assert_eq!(tree.depth(), 8);
|
|||
},
|
|||
BatchSize::SmallInput,
|
|||
);
|
|||
});
|
|||
}
|
|||
|
|||
fn balanced_merkle_rand(c: &mut Criterion) {
|
|||
let mut seed = [0u8; 32];
|
|||
c.bench_function("balanced-merkle-rand", |b| {
|
|||
b.iter_batched(
|
|||
|| {
|
|||
let entries: Vec<Word> = (0..256).map(|_| generate_word(&mut seed)).collect();
|
|||
assert_eq!(entries.len(), 256);
|
|||
entries
|
|||
},
|
|||
|leaves| {
|
|||
let tree = MerkleTree::new(hint::black_box(leaves)).unwrap();
|
|||
assert_eq!(tree.depth(), 8);
|
|||
},
|
|||
BatchSize::SmallInput,
|
|||
);
|
|||
});
|
|||
}
|
|||
|
|||
criterion_group! {
|
|||
name = smt_subtree_group;
|
|||
config = Criterion::default()
|
|||
.measurement_time(Duration::from_secs(20))
|
|||
.configure_from_args();
|
|||
targets = balanced_merkle_even, balanced_merkle_rand
|
|||
}
|
|||
criterion_main!(smt_subtree_group);
|
|||
|
|||
// HELPER FUNCTIONS
|
|||
// --------------------------------------------------------------------------------------------
|
|||
|
|||
fn generate_word(seed: &mut [u8; 32]) -> Word {
|
|||
mem::swap(seed, &mut prng_array(*seed));
|
|||
let nums: [u64; 4] = prng_array(*seed);
|
|||
[Felt::new(nums[0]), Felt::new(nums[1]), Felt::new(nums[2]), Felt::new(nums[3])]
|
|||
}
|
@ -0,0 +1,142 @@ |
|||
use std::{fmt::Debug, hint, mem, time::Duration};
|
|||
|
|||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
|||
use miden_crypto::{
|
|||
hash::rpo::RpoDigest,
|
|||
merkle::{build_subtree_for_bench, NodeIndex, SmtLeaf, SubtreeLeaf, SMT_DEPTH},
|
|||
Felt, Word, ONE,
|
|||
};
|
|||
use rand_utils::prng_array;
|
|||
use winter_utils::Randomizable;
|
|||
|
|||
const PAIR_COUNTS: [u64; 5] = [1, 64, 128, 192, 256];
|
|||
|
|||
fn smt_subtree_even(c: &mut Criterion) {
|
|||
let mut seed = [0u8; 32];
|
|||
|
|||
let mut group = c.benchmark_group("subtree8-even");
|
|||
|
|||
for pair_count in PAIR_COUNTS {
|
|||
let bench_id = BenchmarkId::from_parameter(pair_count);
|
|||
group.bench_with_input(bench_id, &pair_count, |b, &pair_count| {
|
|||
b.iter_batched(
|
|||
|| {
|
|||
// Setup.
|
|||
let entries: Vec<(RpoDigest, Word)> = (0..pair_count)
|
|||
.map(|n| {
|
|||
// A single depth-8 subtree can have a maximum of 255 leaves.
|
|||
let leaf_index = ((n as f64 / pair_count as f64) * 255.0) as u64;
|
|||
let key = RpoDigest::new([
|
|||
generate_value(&mut seed),
|
|||
ONE,
|
|||
Felt::new(n),
|
|||
Felt::new(leaf_index),
|
|||
]);
|
|||
let value = generate_word(&mut seed);
|
|||
(key, value)
|
|||
})
|
|||
.collect();
|
|||
|
|||
let mut leaves: Vec<_> = entries
|
|||
.iter()
|
|||
.map(|(key, value)| {
|
|||
let leaf = SmtLeaf::new_single(*key, *value);
|
|||
let col = NodeIndex::from(leaf.index()).value();
|
|||
let hash = leaf.hash();
|
|||
SubtreeLeaf { col, hash }
|
|||
})
|
|||
.collect();
|
|||
leaves.sort();
|
|||
leaves.dedup_by_key(|leaf| leaf.col);
|
|||
leaves
|
|||
},
|
|||
|leaves| {
|
|||
// Benchmarked function.
|
|||
let (subtree, _) = build_subtree_for_bench(
|
|||
hint::black_box(leaves),
|
|||
hint::black_box(SMT_DEPTH),
|
|||
hint::black_box(SMT_DEPTH),
|
|||
);
|
|||
assert!(!subtree.is_empty());
|
|||
},
|
|||
BatchSize::SmallInput,
|
|||
);
|
|||
});
|
|||
}
|
|||
}
|
|||
|
|||
fn smt_subtree_random(c: &mut Criterion) {
|
|||
let mut seed = [0u8; 32];
|
|||
|
|||
let mut group = c.benchmark_group("subtree8-rand");
|
|||
|
|||
for pair_count in PAIR_COUNTS {
|
|||
let bench_id = BenchmarkId::from_parameter(pair_count);
|
|||
group.bench_with_input(bench_id, &pair_count, |b, &pair_count| {
|
|||
b.iter_batched(
|
|||
|| {
|
|||
// Setup.
|
|||
let entries: Vec<(RpoDigest, Word)> = (0..pair_count)
|
|||
.map(|i| {
|
|||
let leaf_index: u8 = generate_value(&mut seed);
|
|||
let key = RpoDigest::new([
|
|||
ONE,
|
|||
ONE,
|
|||
Felt::new(i),
|
|||
Felt::new(leaf_index as u64),
|
|||
]);
|
|||
let value = generate_word(&mut seed);
|
|||
(key, value)
|
|||
})
|
|||
.collect();
|
|||
|
|||
let mut leaves: Vec<_> = entries
|
|||
.iter()
|
|||
.map(|(key, value)| {
|
|||
let leaf = SmtLeaf::new_single(*key, *value);
|
|||
let col = NodeIndex::from(leaf.index()).value();
|
|||
let hash = leaf.hash();
|
|||
SubtreeLeaf { col, hash }
|
|||
})
|
|||
.collect();
|
|||
leaves.sort();
|
|||
leaves
|
|||
},
|
|||
|leaves| {
|
|||
let (subtree, _) = build_subtree_for_bench(
|
|||
hint::black_box(leaves),
|
|||
hint::black_box(SMT_DEPTH),
|
|||
hint::black_box(SMT_DEPTH),
|
|||
);
|
|||
assert!(!subtree.is_empty());
|
|||
},
|
|||
BatchSize::SmallInput,
|
|||
);
|
|||
});
|
|||
}
|
|||
}
|
|||
|
|||
criterion_group! {
|
|||
name = smt_subtree_group;
|
|||
config = Criterion::default()
|
|||
.measurement_time(Duration::from_secs(40))
|
|||
.sample_size(60)
|
|||
.configure_from_args();
|
|||
targets = smt_subtree_even, smt_subtree_random
|
|||
}
|
|||
criterion_main!(smt_subtree_group);
|
|||
|
|||
// HELPER FUNCTIONS
|
|||
// --------------------------------------------------------------------------------------------
|
|||
|
|||
fn generate_value<T: Copy + Debug + Randomizable>(seed: &mut [u8; 32]) -> T {
|
|||
mem::swap(seed, &mut prng_array(*seed));
|
|||
let value: [T; 1] = rand_utils::prng_array(*seed);
|
|||
value[0]
|
|||
}
|
|||
|
|||
fn generate_word(seed: &mut [u8; 32]) -> Word {
|
|||
mem::swap(seed, &mut prng_array(*seed));
|
|||
let nums: [u64; 4] = prng_array(*seed);
|
|||
[Felt::new(nums[0]), Felt::new(nums[1]), Felt::new(nums[2]), Felt::new(nums[3])]
|
|||
}
|
@ -0,0 +1,71 @@ |
|||
use std::{fmt::Debug, hint, mem, time::Duration};
|
|||
|
|||
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
|||
use miden_crypto::{hash::rpo::RpoDigest, merkle::Smt, Felt, Word, ONE};
|
|||
use rand_utils::prng_array;
|
|||
use winter_utils::Randomizable;
|
|||
|
|||
// 2^0, 2^4, 2^8, 2^12, 2^16
|
|||
const PAIR_COUNTS: [u64; 6] = [1, 16, 256, 4096, 65536, 1_048_576];
|
|||
|
|||
fn smt_with_entries(c: &mut Criterion) {
|
|||
let mut seed = [0u8; 32];
|
|||
|
|||
let mut group = c.benchmark_group("smt-with-entries");
|
|||
|
|||
for pair_count in PAIR_COUNTS {
|
|||
let bench_id = BenchmarkId::from_parameter(pair_count);
|
|||
group.bench_with_input(bench_id, &pair_count, |b, &pair_count| {
|
|||
b.iter_batched(
|
|||
|| {
|
|||
// Setup.
|
|||
prepare_entries(pair_count, &mut seed)
|
|||
},
|
|||
|entries| {
|
|||
// Benchmarked function.
|
|||
Smt::with_entries(hint::black_box(entries)).unwrap();
|
|||
},
|
|||
BatchSize::SmallInput,
|
|||
);
|
|||
});
|
|||
}
|
|||
}
|
|||
|
|||
criterion_group! {
|
|||
name = smt_with_entries_group;
|
|||
config = Criterion::default()
|
|||
//.measurement_time(Duration::from_secs(960))
|
|||
.measurement_time(Duration::from_secs(60))
|
|||
.sample_size(10)
|
|||
.configure_from_args();
|
|||
targets = smt_with_entries
|
|||
}
|
|||
criterion_main!(smt_with_entries_group);
|
|||
|
|||
// HELPER FUNCTIONS
|
|||
// --------------------------------------------------------------------------------------------
|
|||
|
|||
fn prepare_entries(pair_count: u64, seed: &mut [u8; 32]) -> Vec<(RpoDigest, [Felt; 4])> {
|
|||
let entries: Vec<(RpoDigest, Word)> = (0..pair_count)
|
|||
.map(|i| {
|
|||
let count = pair_count as f64;
|
|||
let idx = ((i as f64 / count) * (count)) as u64;
|
|||
let key = RpoDigest::new([generate_value(seed), ONE, Felt::new(i), Felt::new(idx)]);
|
|||
let value = generate_word(seed);
|
|||
(key, value)
|
|||
})
|
|||
.collect();
|
|||
entries
|
|||
}
|
|||
|
|||
fn generate_value<T: Copy + Debug + Randomizable>(seed: &mut [u8; 32]) -> T {
|
|||
mem::swap(seed, &mut prng_array(*seed));
|
|||
let value: [T; 1] = rand_utils::prng_array(*seed);
|
|||
value[0]
|
|||
}
|
|||
|
|||
fn generate_word(seed: &mut [u8; 32]) -> Word {
|
|||
mem::swap(seed, &mut prng_array(*seed));
|
|||
let nums: [u64; 4] = prng_array(*seed);
|
|||
[Felt::new(nums[0]), Felt::new(nums[1]), Felt::new(nums[2]), Felt::new(nums[3])]
|
|||
}
|
@ -0,0 +1,417 @@ |
|||
use alloc::{collections::BTreeMap, vec::Vec};
|
|||
|
|||
use super::{
|
|||
build_subtree, InnerNode, LeafIndex, NodeIndex, PairComputations, SmtLeaf, SparseMerkleTree,
|
|||
SubtreeLeaf, SubtreeLeavesIter, COLS_PER_SUBTREE, SUBTREE_DEPTH,
|
|||
};
|
|||
use crate::{
|
|||
hash::rpo::RpoDigest,
|
|||
merkle::{Smt, SMT_DEPTH},
|
|||
Felt, Word, ONE,
|
|||
};
|
|||
|
|||
fn smtleaf_to_subtree_leaf(leaf: &SmtLeaf) -> SubtreeLeaf {
|
|||
SubtreeLeaf {
|
|||
col: leaf.index().index.value(),
|
|||
hash: leaf.hash(),
|
|||
}
|
|||
}
|
|||
|
|||
#[test]
|
|||
fn test_sorted_pairs_to_leaves() {
|
|||
let entries: Vec<(RpoDigest, Word)> = vec![
|
|||
// Subtree 0.
|
|||
(RpoDigest::new([ONE, ONE, ONE, Felt::new(16)]), [ONE; 4]),
|
|||
(RpoDigest::new([ONE, ONE, ONE, Felt::new(17)]), [ONE; 4]),
|
|||
// Leaf index collision.
|
|||
(RpoDigest::new([ONE, ONE, Felt::new(10), Felt::new(20)]), [ONE; 4]),
|
|||
(RpoDigest::new([ONE, ONE, Felt::new(20), Felt::new(20)]), [ONE; 4]),
|
|||
// Subtree 1. Normal single leaf again.
|
|||
(RpoDigest::new([ONE, ONE, ONE, Felt::new(400)]), [ONE; 4]), // Subtree boundary.
|
|||
(RpoDigest::new([ONE, ONE, ONE, Felt::new(401)]), [ONE; 4]),
|
|||
// Subtree 2. Another normal leaf.
|
|||
(RpoDigest::new([ONE, ONE, ONE, Felt::new(1024)]), [ONE; 4]),
|
|||
];
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
let control_leaves: Vec<SmtLeaf> = {
|
|||
let mut entries_iter = entries.iter().cloned();
|
|||
let mut next_entry = || entries_iter.next().unwrap();
|
|||
let control_leaves = vec![
|
|||
// Subtree 0.
|
|||
SmtLeaf::Single(next_entry()),
|
|||
SmtLeaf::Single(next_entry()),
|
|||
SmtLeaf::new_multiple(vec![next_entry(), next_entry()]).unwrap(),
|
|||
// Subtree 1.
|
|||
SmtLeaf::Single(next_entry()),
|
|||
SmtLeaf::Single(next_entry()),
|
|||
// Subtree 2.
|
|||
SmtLeaf::Single(next_entry()),
|
|||
];
|
|||
assert_eq!(entries_iter.next(), None);
|
|||
control_leaves
|
|||
};
|
|||
|
|||
let control_subtree_leaves: Vec<Vec<SubtreeLeaf>> = {
|
|||
let mut control_leaves_iter = control_leaves.iter();
|
|||
let mut next_leaf = || control_leaves_iter.next().unwrap();
|
|||
|
|||
let control_subtree_leaves: Vec<Vec<SubtreeLeaf>> = [
|
|||
// Subtree 0.
|
|||
vec![next_leaf(), next_leaf(), next_leaf()],
|
|||
// Subtree 1.
|
|||
vec![next_leaf(), next_leaf()],
|
|||
// Subtree 2.
|
|||
vec![next_leaf()],
|
|||
]
|
|||
.map(|subtree| subtree.into_iter().map(smtleaf_to_subtree_leaf).collect())
|
|||
.to_vec();
|
|||
assert_eq!(control_leaves_iter.next(), None);
|
|||
control_subtree_leaves
|
|||
};
|
|||
|
|||
let subtrees: PairComputations<u64, SmtLeaf> = Smt::sorted_pairs_to_leaves(entries);
|
|||
// This will check that the hashes, columns, and subtree assignments all match.
|
|||
assert_eq!(subtrees.leaves, control_subtree_leaves);
|
|||
|
|||
// Flattening and re-separating out the leaves into subtrees should have the same result.
|
|||
let mut all_leaves: Vec<SubtreeLeaf> = subtrees.leaves.clone().into_iter().flatten().collect();
|
|||
let re_grouped: Vec<Vec<_>> = SubtreeLeavesIter::from_leaves(&mut all_leaves).collect();
|
|||
assert_eq!(subtrees.leaves, re_grouped);
|
|||
|
|||
// Then finally we might as well check the computed leaf nodes too.
|
|||
let control_leaves: BTreeMap<u64, SmtLeaf> = control
|
|||
.leaves()
|
|||
.map(|(index, value)| (index.index.value(), value.clone()))
|
|||
.collect();
|
|||
|
|||
for (column, test_leaf) in subtrees.nodes {
|
|||
if test_leaf.is_empty() {
|
|||
continue;
|
|||
}
|
|||
let control_leaf = control_leaves
|
|||
.get(&column)
|
|||
.unwrap_or_else(|| panic!("no leaf node found for column {column}"));
|
|||
assert_eq!(control_leaf, &test_leaf);
|
|||
}
|
|||
}
|
|||
|
|||
// Helper for the below tests.
|
|||
fn generate_entries(pair_count: u64) -> Vec<(RpoDigest, Word)> {
|
|||
(0..pair_count)
|
|||
.map(|i| {
|
|||
let leaf_index = ((i as f64 / pair_count as f64) * (pair_count as f64)) as u64;
|
|||
let key = RpoDigest::new([ONE, ONE, Felt::new(i), Felt::new(leaf_index)]);
|
|||
let value = [ONE, ONE, ONE, Felt::new(i)];
|
|||
(key, value)
|
|||
})
|
|||
.collect()
|
|||
}
|
|||
|
|||
#[test]
|
|||
fn test_single_subtree() {
|
|||
// A single subtree's worth of leaves.
|
|||
const PAIR_COUNT: u64 = COLS_PER_SUBTREE;
|
|||
|
|||
let entries = generate_entries(PAIR_COUNT);
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
// `entries` should already be sorted by nature of how we constructed it.
|
|||
let leaves = Smt::sorted_pairs_to_leaves(entries).leaves;
|
|||
let leaves = leaves.into_iter().next().unwrap();
|
|||
|
|||
let (first_subtree, subtree_root) = build_subtree(leaves, SMT_DEPTH, SMT_DEPTH);
|
|||
assert!(!first_subtree.is_empty());
|
|||
|
|||
// The inner nodes computed from that subtree should match the nodes in our control tree.
|
|||
for (index, node) in first_subtree.into_iter() {
|
|||
let control = control.get_inner_node(index);
|
|||
assert_eq!(
|
|||
control, node,
|
|||
"subtree-computed node at index {index:?} does not match control",
|
|||
);
|
|||
}
|
|||
|
|||
// The root returned should also match the equivalent node in the control tree.
|
|||
let control_root_index =
|
|||
NodeIndex::new(SMT_DEPTH - SUBTREE_DEPTH, subtree_root.col).expect("Valid root index");
|
|||
let control_root_node = control.get_inner_node(control_root_index);
|
|||
let control_hash = control_root_node.hash();
|
|||
assert_eq!(
|
|||
control_hash, subtree_root.hash,
|
|||
"Subtree-computed root at index {control_root_index:?} does not match control"
|
|||
);
|
|||
}
|
|||
|
|||
// Test that not just can we compute a subtree correctly, but we can feed the results of one
|
|||
// subtree into computing another. In other words, test that `build_subtree()` is correctly
|
|||
// composable.
|
|||
#[test]
|
|||
fn test_two_subtrees() {
|
|||
// Two subtrees' worth of leaves.
|
|||
const PAIR_COUNT: u64 = COLS_PER_SUBTREE * 2;
|
|||
|
|||
let entries = generate_entries(PAIR_COUNT);
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
let PairComputations { leaves, .. } = Smt::sorted_pairs_to_leaves(entries);
|
|||
// With two subtrees' worth of leaves, we should have exactly two subtrees.
|
|||
let [first, second]: [Vec<_>; 2] = leaves.try_into().unwrap();
|
|||
assert_eq!(first.len() as u64, PAIR_COUNT / 2);
|
|||
assert_eq!(first.len(), second.len());
|
|||
|
|||
let mut current_depth = SMT_DEPTH;
|
|||
let mut next_leaves: Vec<SubtreeLeaf> = Default::default();
|
|||
|
|||
let (first_nodes, first_root) = build_subtree(first, SMT_DEPTH, current_depth);
|
|||
next_leaves.push(first_root);
|
|||
|
|||
let (second_nodes, second_root) = build_subtree(second, SMT_DEPTH, current_depth);
|
|||
next_leaves.push(second_root);
|
|||
|
|||
// All new inner nodes + the new subtree-leaves should be 512, for one depth-cycle.
|
|||
let total_computed = first_nodes.len() + second_nodes.len() + next_leaves.len();
|
|||
assert_eq!(total_computed as u64, PAIR_COUNT);
|
|||
|
|||
// Verify the computed nodes of both subtrees.
|
|||
let computed_nodes = first_nodes.clone().into_iter().chain(second_nodes);
|
|||
for (index, test_node) in computed_nodes {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(
|
|||
control_node, test_node,
|
|||
"subtree-computed node at index {index:?} does not match control",
|
|||
);
|
|||
}
|
|||
|
|||
current_depth -= SUBTREE_DEPTH;
|
|||
|
|||
let (nodes, root_leaf) = build_subtree(next_leaves, SMT_DEPTH, current_depth);
|
|||
assert_eq!(nodes.len(), SUBTREE_DEPTH as usize);
|
|||
assert_eq!(root_leaf.col, 0);
|
|||
|
|||
for (index, test_node) in nodes {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(
|
|||
control_node, test_node,
|
|||
"subtree-computed node at index {index:?} does not match control",
|
|||
);
|
|||
}
|
|||
|
|||
let index = NodeIndex::new(current_depth - SUBTREE_DEPTH, root_leaf.col).unwrap();
|
|||
let control_root = control.get_inner_node(index).hash();
|
|||
assert_eq!(control_root, root_leaf.hash, "Root mismatch");
|
|||
}
|
|||
|
|||
#[test]
|
|||
fn test_singlethreaded_subtrees() {
|
|||
const PAIR_COUNT: u64 = COLS_PER_SUBTREE * 64;
|
|||
|
|||
let entries = generate_entries(PAIR_COUNT);
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
let mut accumulated_nodes: BTreeMap<NodeIndex, InnerNode> = Default::default();
|
|||
|
|||
let PairComputations {
|
|||
leaves: mut leaf_subtrees,
|
|||
nodes: test_leaves,
|
|||
} = Smt::sorted_pairs_to_leaves(entries);
|
|||
|
|||
for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() {
|
|||
// There's no flat_map_unzip(), so this is the best we can do.
|
|||
let (nodes, mut subtree_roots): (Vec<BTreeMap<_, _>>, Vec<SubtreeLeaf>) = leaf_subtrees
|
|||
.into_iter()
|
|||
.enumerate()
|
|||
.map(|(i, subtree)| {
|
|||
// Pre-assertions.
|
|||
assert!(
|
|||
subtree.is_sorted(),
|
|||
"subtree {i} at bottom-depth {current_depth} is not sorted",
|
|||
);
|
|||
assert!(
|
|||
!subtree.is_empty(),
|
|||
"subtree {i} at bottom-depth {current_depth} is empty!",
|
|||
);
|
|||
|
|||
// Do actual things.
|
|||
let (nodes, subtree_root) = build_subtree(subtree, SMT_DEPTH, current_depth);
|
|||
|
|||
// Post-assertions.
|
|||
for (&index, test_node) in nodes.iter() {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(
|
|||
test_node, &control_node,
|
|||
"depth {} subtree {}: test node does not match control at index {:?}",
|
|||
current_depth, i, index,
|
|||
);
|
|||
}
|
|||
|
|||
(nodes, subtree_root)
|
|||
})
|
|||
.unzip();
|
|||
|
|||
// Update state between each depth iteration.
|
|||
|
|||
leaf_subtrees = SubtreeLeavesIter::from_leaves(&mut subtree_roots).collect();
|
|||
accumulated_nodes.extend(nodes.into_iter().flatten());
|
|||
|
|||
assert!(!leaf_subtrees.is_empty(), "on depth {current_depth}");
|
|||
}
|
|||
|
|||
// Make sure the true leaves match, first checking length and then checking each individual
|
|||
// leaf.
|
|||
let control_leaves: BTreeMap<_, _> = control.leaves().collect();
|
|||
let control_leaves_len = control_leaves.len();
|
|||
let test_leaves_len = test_leaves.len();
|
|||
assert_eq!(test_leaves_len, control_leaves_len);
|
|||
for (col, ref test_leaf) in test_leaves {
|
|||
let index = LeafIndex::new_max_depth(col);
|
|||
let &control_leaf = control_leaves.get(&index).unwrap();
|
|||
assert_eq!(test_leaf, control_leaf, "test leaf at column {col} does not match control");
|
|||
}
|
|||
|
|||
// Make sure the inner nodes match, checking length first and then each individual leaf.
|
|||
let control_nodes_len = control.inner_nodes().count();
|
|||
let test_nodes_len = accumulated_nodes.len();
|
|||
assert_eq!(test_nodes_len, control_nodes_len);
|
|||
for (index, test_node) in accumulated_nodes.clone() {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(test_node, control_node, "test node does not match control at {index:?}");
|
|||
}
|
|||
|
|||
// After the last iteration of the above for loop, we should have the new root node actually
|
|||
// in two places: one in `accumulated_nodes`, and the other as the "next leaves" return from
|
|||
// `build_subtree()`. So let's check both!
|
|||
|
|||
let control_root = control.get_inner_node(NodeIndex::root());
|
|||
|
|||
// That for loop should have left us with only one leaf subtree...
|
|||
let [leaf_subtree]: [Vec<_>; 1] = leaf_subtrees.try_into().unwrap();
|
|||
// which itself contains only one 'leaf'...
|
|||
let [root_leaf]: [SubtreeLeaf; 1] = leaf_subtree.try_into().unwrap();
|
|||
// which matches the expected root.
|
|||
assert_eq!(control.root(), root_leaf.hash);
|
|||
|
|||
// Likewise `accumulated_nodes` should contain a node at the root index...
|
|||
assert!(accumulated_nodes.contains_key(&NodeIndex::root()));
|
|||
// and it should match our actual root.
|
|||
let test_root = accumulated_nodes.get(&NodeIndex::root()).unwrap();
|
|||
assert_eq!(control_root, *test_root);
|
|||
// And of course the root we got from each place should match.
|
|||
assert_eq!(control.root(), root_leaf.hash);
|
|||
}
|
|||
|
|||
/// The parallel version of `test_singlethreaded_subtree()`.
|
|||
#[test]
|
|||
#[cfg(feature = "concurrent")]
|
|||
fn test_multithreaded_subtrees() {
|
|||
use rayon::prelude::*;
|
|||
|
|||
const PAIR_COUNT: u64 = COLS_PER_SUBTREE * 64;
|
|||
|
|||
let entries = generate_entries(PAIR_COUNT);
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
let mut accumulated_nodes: BTreeMap<NodeIndex, InnerNode> = Default::default();
|
|||
|
|||
let PairComputations {
|
|||
leaves: mut leaf_subtrees,
|
|||
nodes: test_leaves,
|
|||
} = Smt::sorted_pairs_to_leaves(entries);
|
|||
|
|||
for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() {
|
|||
let (nodes, mut subtree_roots): (Vec<BTreeMap<_, _>>, Vec<SubtreeLeaf>) = leaf_subtrees
|
|||
.into_par_iter()
|
|||
.enumerate()
|
|||
.map(|(i, subtree)| {
|
|||
// Pre-assertions.
|
|||
assert!(
|
|||
subtree.is_sorted(),
|
|||
"subtree {i} at bottom-depth {current_depth} is not sorted",
|
|||
);
|
|||
assert!(
|
|||
!subtree.is_empty(),
|
|||
"subtree {i} at bottom-depth {current_depth} is empty!",
|
|||
);
|
|||
|
|||
let (nodes, subtree_root) = build_subtree(subtree, SMT_DEPTH, current_depth);
|
|||
|
|||
// Post-assertions.
|
|||
for (&index, test_node) in nodes.iter() {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(
|
|||
test_node, &control_node,
|
|||
"depth {} subtree {}: test node does not match control at index {:?}",
|
|||
current_depth, i, index,
|
|||
);
|
|||
}
|
|||
|
|||
(nodes, subtree_root)
|
|||
})
|
|||
.unzip();
|
|||
|
|||
leaf_subtrees = SubtreeLeavesIter::from_leaves(&mut subtree_roots).collect();
|
|||
accumulated_nodes.extend(nodes.into_iter().flatten());
|
|||
|
|||
assert!(!leaf_subtrees.is_empty(), "on depth {current_depth}");
|
|||
}
|
|||
|
|||
// Make sure the true leaves match, checking length first and then each individual leaf.
|
|||
let control_leaves: BTreeMap<_, _> = control.leaves().collect();
|
|||
let control_leaves_len = control_leaves.len();
|
|||
let test_leaves_len = test_leaves.len();
|
|||
assert_eq!(test_leaves_len, control_leaves_len);
|
|||
for (col, ref test_leaf) in test_leaves {
|
|||
let index = LeafIndex::new_max_depth(col);
|
|||
let &control_leaf = control_leaves.get(&index).unwrap();
|
|||
assert_eq!(test_leaf, control_leaf);
|
|||
}
|
|||
|
|||
// Make sure the inner nodes match, checking length first and then each individual leaf.
|
|||
let control_nodes_len = control.inner_nodes().count();
|
|||
let test_nodes_len = accumulated_nodes.len();
|
|||
assert_eq!(test_nodes_len, control_nodes_len);
|
|||
for (index, test_node) in accumulated_nodes.clone() {
|
|||
let control_node = control.get_inner_node(index);
|
|||
assert_eq!(test_node, control_node, "test node does not match control at {index:?}");
|
|||
}
|
|||
|
|||
// After the last iteration of the above for loop, we should have the new root node actually
|
|||
// in two places: one in `accumulated_nodes`, and the other as the "next leaves" return from
|
|||
// `build_subtree()`. So let's check both!
|
|||
|
|||
let control_root = control.get_inner_node(NodeIndex::root());
|
|||
|
|||
// That for loop should have left us with only one leaf subtree...
|
|||
let [leaf_subtree]: [_; 1] = leaf_subtrees.try_into().unwrap();
|
|||
// which itself contains only one 'leaf'...
|
|||
let [root_leaf]: [_; 1] = leaf_subtree.try_into().unwrap();
|
|||
// which matches the expected root.
|
|||
assert_eq!(control.root(), root_leaf.hash);
|
|||
|
|||
// Likewise `accumulated_nodes` should contain a node at the root index...
|
|||
assert!(accumulated_nodes.contains_key(&NodeIndex::root()));
|
|||
// and it should match our actual root.
|
|||
let test_root = accumulated_nodes.get(&NodeIndex::root()).unwrap();
|
|||
assert_eq!(control_root, *test_root);
|
|||
// And of course the root we got from each place should match.
|
|||
assert_eq!(control.root(), root_leaf.hash);
|
|||
}
|
|||
|
|||
#[test]
|
|||
#[cfg(feature = "concurrent")]
|
|||
fn test_with_entries_parallel() {
|
|||
const PAIR_COUNT: u64 = COLS_PER_SUBTREE * 64;
|
|||
|
|||
let entries = generate_entries(PAIR_COUNT);
|
|||
|
|||
let control = Smt::with_entries_sequential(entries.clone()).unwrap();
|
|||
|
|||
let smt = Smt::with_entries(entries.clone()).unwrap();
|
|||
assert_eq!(smt.root(), control.root());
|
|||
assert_eq!(smt, control);
|
|||
}
|