Skip to content

Commit

Permalink
WIP(smt): impl simple subtree8 hashing and benchmarks for it
Browse files Browse the repository at this point in the history
  • Loading branch information
Qyriad committed Oct 24, 2024
1 parent 04dbbf9 commit 9638969
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ harness = false
name = "smt"
harness = false

[[bench]]
name = "smt-subtree"
harness = false

[[bench]]
name = "store"
harness = false
Expand Down
137 changes: 137 additions & 0 deletions benches/smt-subtree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::{fmt::Debug, hint, mem, time::Duration};

use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
use miden_crypto::{
hash::rpo::RpoDigest,
merkle::{NodeIndex, Smt, SmtLeaf, SMT_DEPTH},
Felt, Word, ONE,
};
use rand_utils::prng_array;
use winter_utils::Randomizable;

fn smt_subtree_even(c: &mut Criterion) {
let mut seed = [0u8; 32];

let mut group = c.benchmark_group("subtree8-even");

for pair_count in (64..=256).step_by(64) {
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 / pair_count) * 255;
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();
(col, hash)
})
.collect();
leaves.sort();
leaves
},
|leaves| {
// Benchmarked function.
let subtree =
Smt::build_subtree(hint::black_box(leaves), 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 (64..=256).step_by(64) {
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();
(col, hash)
})
.collect();
leaves.sort();
let before = leaves.len();
leaves.dedup();
let after = leaves.len();
assert_eq!(before, after);
leaves
},
|leaves| {
let subtree =
Smt::build_subtree(hint::black_box(leaves), 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])]
}
7 changes: 7 additions & 0 deletions src/merkle/smt/full/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ impl Smt {
None
}
}

pub fn build_subtree(
leaves: Vec<(u64, RpoDigest)>,
bottom_depth: u8,
) -> BTreeMap<NodeIndex, InnerNode> {
<Self as SparseMerkleTree<SMT_DEPTH>>::build_subtree(leaves, bottom_depth)
}
}

impl SparseMerkleTree<SMT_DEPTH> for Smt {
Expand Down
171 changes: 170 additions & 1 deletion src/merkle/smt/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use alloc::{collections::BTreeMap, vec::Vec};
use core::mem;

use num::Integer;

use super::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, NodeIndex};
use crate::{
Expand Down Expand Up @@ -339,14 +342,116 @@ pub(crate) trait SparseMerkleTree<const DEPTH: u8> {
///
/// The length `path` is guaranteed to be equal to `DEPTH`
fn path_and_leaf_to_opening(path: MerklePath, leaf: Self::Leaf) -> Self::Opening;

/// Builds Merkle nodes from a bottom layer of tuples of horizontal indices and their hashes,
/// sorted by their position.
///
/// The leaves are 'conceptual' leaves, simply being entities at the bottom of some subtree, not
/// [`Self::Leaf`].
///
/// # Panics
/// With debug assertions on, this function panics under invalid inputs: if `leaves` contains
/// more entries than can fit in a depth-8 subtree (more than 256), if `bottom_depth` is
/// lower in the tree than the specified maximum depth (`DEPTH`), or if `leaves` is not sorted.
// FIXME: more complete docstring.
#[cfg_attr(not(test), allow(dead_code))]
fn build_subtree(
mut leaves: Vec<(u64, RpoDigest)>,
bottom_depth: u8,
) -> BTreeMap<NodeIndex, InnerNode> {
debug_assert!(bottom_depth <= DEPTH);
debug_assert!(bottom_depth.is_multiple_of(&8));
debug_assert!(leaves.len() <= usize::pow(2, 8));

let subtree_root = bottom_depth - 8;

let mut inner_nodes: BTreeMap<NodeIndex, InnerNode> = Default::default();

let mut next_leaves: Vec<(u64, RpoDigest)> = Vec::with_capacity(leaves.len() / 2);

for next_depth in (subtree_root..bottom_depth).rev() {
debug_assert!(next_depth <= bottom_depth);

// `next_depth` is the stuff we're making.
// `current_depth` is the stuff we have.
let current_depth = next_depth + 1;

let mut iter = leaves.drain(..).map(SubtreeLeaf::from_tuple).peekable();
while let Some(first) = iter.next() {
// On non-continuous iterations, including the first iteration, `first_column` may
// be a left or right node. On subsequent continuous iterations, we will always call
// `iter.next()` twice.

// On non-continuous iterations (including the very first iteration), this column
// could be either on the left or the right. If the next iteration is not
// discontinuous with our right node, then the next iteration's

let is_right = first.col.is_odd();
let (left, right) = if is_right {
// Discontinuous iteration: we have no left node, so it must be empty.

let left = SubtreeLeaf {
col: first.col - 1,
hash: *EmptySubtreeRoots::entry(DEPTH, current_depth),
};
let right = first;

(left, right)
} else {
let left = first;

let right_col = first.col + 1;
let right = match iter.peek().copied() {
Some(SubtreeLeaf { col, .. }) if col == right_col => {
// Our inputs must be sorted.
debug_assert!(left.col <= col);
// The next leaf in the iterator is our sibling. Use it and consume it!
iter.next().unwrap()
},
// Otherwise, the leaves don't contain our sibling, so our sibling must be
// empty.
_ => SubtreeLeaf {
col: right_col,
hash: *EmptySubtreeRoots::entry(DEPTH, current_depth),
},
};

(left, right)
};

let index = NodeIndex::new_unchecked(current_depth, left.col).parent();
let node = InnerNode { left: left.hash, right: right.hash };
let hash = node.hash();

let &equivalent_empty_hash = EmptySubtreeRoots::entry(DEPTH, next_depth);
// If this hash is empty, then it doesn't become a new inner node, nor does it count
// as a leaf for the next depth.
if hash != equivalent_empty_hash {
inner_nodes.insert(index, node);
// FIXME: is it possible for this to end up not being sorted? I don't think so.
next_leaves.push((index.value(), hash));
}
}

// Stop borrowing `leaves`, so we can swap it.
// The iterator is empty at this point anyway.
drop(iter);

// After each depth, consider the stuff we just made the new "leaves", and empty the
// other collection.
mem::swap(&mut leaves, &mut next_leaves);
}

inner_nodes
}
}

// INNER NODE
// ================================================================================================

#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct InnerNode {
pub struct InnerNode {
pub left: RpoDigest,
pub right: RpoDigest,
}
Expand Down Expand Up @@ -456,3 +561,67 @@ impl<const DEPTH: u8, K, V> MutationSet<DEPTH, K, V> {
self.new_root
}
}

// HELPERS
// ================================================================================================
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
struct SubtreeLeaf {
col: u64,
hash: RpoDigest,
}
impl SubtreeLeaf {
const fn from_tuple((col, hash): (u64, RpoDigest)) -> Self {
Self { col, hash }
}
}

// TESTS
// ================================================================================================
#[cfg(test)]
mod test {
use alloc::vec::Vec;

use super::SparseMerkleTree;
use crate::{
hash::rpo::RpoDigest,
merkle::{Smt, SmtLeaf, SMT_DEPTH},
Felt, Word, ONE,
};

#[test]
fn test_build_subtree_from_leaves() {
const PAIR_COUNT: u64 = u64::pow(2, 8);

let entries: Vec<(RpoDigest, Word)> = (0..PAIR_COUNT)
.map(|i| {
let leaf_index = u64::MAX / (i + 1);
let key = RpoDigest::new([ONE, ONE, Felt::new(i), Felt::new(leaf_index)]);
let value = [ONE, ONE, ONE, Felt::new(i)];
(key, value)
})
.collect();

let control = Smt::with_entries(entries.clone()).unwrap();

let leaves: Vec<(u64, RpoDigest)> = entries
.iter()
.map(|(key, value)| {
let leaf = SmtLeaf::new_single(*key, *value);
let col = leaf.index().index.value();
let hash = leaf.hash();
(col, hash)
})
.collect();

let first_subtree = Smt::build_subtree(leaves, SMT_DEPTH);
assert!(!first_subtree.is_empty());

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",
);
}
}
}

0 comments on commit 9638969

Please sign in to comment.