WIP(smt): add simple benchmark for single subtree computation
This commit is contained in:
parent
a35c11abfe
commit
6addcd0226
5 changed files with 238 additions and 8 deletions
|
@ -31,6 +31,10 @@ harness = false
|
||||||
name = "store"
|
name = "store"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "subtree"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std", "async"]
|
default = ["std", "async"]
|
||||||
executable = ["dep:clap", "dep:rand-utils", "std"]
|
executable = ["dep:clap", "dep:rand-utils", "std"]
|
||||||
|
|
66
benches/subtree.rs
Normal file
66
benches/subtree.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion};
|
||||||
|
use miden_crypto::{
|
||||||
|
hash::rpo::RpoDigest,
|
||||||
|
merkle::{NodeIndex, NodeSubtreeComputer, Smt, SparseMerkleTree},
|
||||||
|
Felt, Word, ONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SUBTREE_INTERVAL: u8 = 8;
|
||||||
|
|
||||||
|
fn setup_subtree8(tree_size: u64) -> (Smt, NodeIndex, Arc<BTreeMap<RpoDigest, Word>>, RpoDigest) {
|
||||||
|
let entries: BTreeMap<RpoDigest, Word> = (0..tree_size)
|
||||||
|
.into_iter()
|
||||||
|
.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 subtree = entries
|
||||||
|
.keys()
|
||||||
|
.map(|key| {
|
||||||
|
let index_for_key = NodeIndex::from(Smt::key_to_leaf_index(key));
|
||||||
|
index_for_key.parent_n(SUBTREE_INTERVAL)
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
let control_hash = control.get_inner_node(subtree).hash();
|
||||||
|
(Smt::new(), subtree, Arc::new(entries), control_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_subtree8(
|
||||||
|
(smt, subtree, entries, control_hash): (
|
||||||
|
Smt,
|
||||||
|
NodeIndex,
|
||||||
|
Arc<BTreeMap<RpoDigest, Word>>,
|
||||||
|
RpoDigest,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
let mut state = NodeSubtreeComputer::with_smt(&smt, Default::default(), entries);
|
||||||
|
let hash = state.get_or_make_hash(subtree);
|
||||||
|
assert_eq!(control_hash, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smt_subtree8(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("subtree8");
|
||||||
|
|
||||||
|
group.measurement_time(Duration::from_secs(120));
|
||||||
|
group.sample_size(30);
|
||||||
|
|
||||||
|
for &tree_size in [32, 128, 512, 1024].iter() {
|
||||||
|
let bench_id = BenchmarkId::from_parameter(tree_size);
|
||||||
|
//group.throughput(Throughput::Elements(tree_size));
|
||||||
|
group.bench_with_input(bench_id, &tree_size, |bench, &tree_size| {
|
||||||
|
bench.iter_batched(|| setup_subtree8(tree_size), bench_subtree8, BatchSize::SmallInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(subtree_group, smt_subtree8);
|
||||||
|
criterion_main!(subtree_group);
|
|
@ -22,8 +22,8 @@ pub use path::{MerklePath, RootPath, ValuePath};
|
||||||
|
|
||||||
mod smt;
|
mod smt;
|
||||||
pub use smt::{
|
pub use smt::{
|
||||||
LeafIndex, MutationSet, SimpleSmt, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError,
|
InnerNode, LeafIndex, MutationSet, NodeSubtreeComputer, SimpleSmt, Smt, SmtLeaf, SmtLeafError,
|
||||||
SMT_DEPTH, SMT_MAX_DEPTH, SMT_MIN_DEPTH,
|
SmtProof, SmtProofError, SparseMerkleTree, SMT_DEPTH, SMT_MAX_DEPTH, SMT_MIN_DEPTH,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod mmr;
|
mod mmr;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use alloc::{
|
use alloc::{
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
|
@ -12,6 +12,9 @@ use super::{
|
||||||
MutationSet, NodeIndex, Rpo256, RpoDigest, SparseMerkleTree, Word, EMPTY_WORD,
|
MutationSet, NodeIndex, Rpo256, RpoDigest, SparseMerkleTree, Word, EMPTY_WORD,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use super::NodeMutation;
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::{SmtLeafError, SmtProofError};
|
pub use error::{SmtLeafError, SmtProofError};
|
||||||
|
|
||||||
|
@ -297,6 +300,27 @@ impl Smt {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn construct_prospective_leaf(
|
||||||
|
mut existing_leaf: SmtLeaf,
|
||||||
|
key: &RpoDigest,
|
||||||
|
value: &Word,
|
||||||
|
) -> SmtLeaf {
|
||||||
|
debug_assert_eq!(existing_leaf.index(), Self::key_to_leaf_index(key));
|
||||||
|
|
||||||
|
match existing_leaf {
|
||||||
|
SmtLeaf::Empty(_) => SmtLeaf::new_single(*key, *value),
|
||||||
|
_ => {
|
||||||
|
if *value != EMPTY_WORD {
|
||||||
|
existing_leaf.insert(*key, *value);
|
||||||
|
} else {
|
||||||
|
existing_leaf.remove(*key);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_leaf
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SparseMerkleTree<SMT_DEPTH> for Smt {
|
impl SparseMerkleTree<SMT_DEPTH> for Smt {
|
||||||
|
@ -399,6 +423,141 @@ impl Default for Smt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just a [`NodeMutation`] with its hash already computed and stored.
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
pub struct ComputedNodeMutation {
|
||||||
|
pub mutation: NodeMutation,
|
||||||
|
pub hash: RpoDigest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
pub struct NodeSubtreeComputer {
|
||||||
|
inner_nodes: Arc<BTreeMap<NodeIndex, InnerNode>>,
|
||||||
|
leaves: Arc<BTreeMap<u64, SmtLeaf>>,
|
||||||
|
existing_mutations: Arc<HashMap<NodeIndex, ComputedNodeMutation>>,
|
||||||
|
new_mutations: HashMap<NodeIndex, ComputedNodeMutation>,
|
||||||
|
new_pairs: Arc<BTreeMap<RpoDigest, Word>>,
|
||||||
|
/// Cache indices we know to be dirty.
|
||||||
|
dirtied_indices: HashMap<NodeIndex, bool>,
|
||||||
|
cached_leaf_hashes: HashMap<LeafIndex<SMT_DEPTH>, RpoDigest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
impl NodeSubtreeComputer {
|
||||||
|
pub fn with_smt(
|
||||||
|
smt: &Smt,
|
||||||
|
existing_mutations: Arc<HashMap<NodeIndex, ComputedNodeMutation>>,
|
||||||
|
new_pairs: Arc<BTreeMap<RpoDigest, Word>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inner_nodes: Arc::clone(&smt.inner_nodes),
|
||||||
|
leaves: Arc::clone(&smt.leaves),
|
||||||
|
existing_mutations,
|
||||||
|
new_mutations: Default::default(),
|
||||||
|
new_pairs,
|
||||||
|
dirtied_indices: Default::default(),
|
||||||
|
cached_leaf_hashes: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_index_dirty(&mut self, index_to_check: NodeIndex) -> bool {
|
||||||
|
if let Some(cached) = self.dirtied_indices.get(&index_to_check) {
|
||||||
|
return *cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An index is dirty if there is a new pair at it, an known existing mutation at it, or an
|
||||||
|
// ancestor of one of those.
|
||||||
|
let is_dirty = self
|
||||||
|
.existing_mutations
|
||||||
|
.iter()
|
||||||
|
.map(|(index, _)| *index)
|
||||||
|
.chain(self.new_pairs.iter().map(|(key, _v)| Smt::key_to_leaf_index(key).index))
|
||||||
|
.filter(|&dirtied_index| index_to_check.contains(dirtied_index))
|
||||||
|
.next()
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
// This is somewhat expensive to compute, so cache this.
|
||||||
|
self.dirtied_indices.insert(index_to_check, is_dirty);
|
||||||
|
is_dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_effective_leaf(&self, index: LeafIndex<SMT_DEPTH>) -> SmtLeaf {
|
||||||
|
let pairs_at_index = self
|
||||||
|
.new_pairs
|
||||||
|
.iter()
|
||||||
|
.filter(|&(new_key, _)| Smt::key_to_leaf_index(new_key) == index);
|
||||||
|
|
||||||
|
let existing_leaf = self
|
||||||
|
.leaves
|
||||||
|
.get(&index.index.value())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| SmtLeaf::new_empty(index));
|
||||||
|
|
||||||
|
pairs_at_index.fold(existing_leaf, |acc, (k, v)| {
|
||||||
|
let existing_leaf = acc.clone();
|
||||||
|
Smt::construct_prospective_leaf(existing_leaf, k, v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does NOT check `new_mutations`.
|
||||||
|
pub(crate) fn get_clean_hash(&self, index: NodeIndex) -> Option<RpoDigest> {
|
||||||
|
self.existing_mutations
|
||||||
|
.get(&index)
|
||||||
|
.map(|ComputedNodeMutation { hash, .. }| *hash)
|
||||||
|
.or_else(|| self.inner_nodes.get(&index).map(|inner_node| InnerNode::hash(&inner_node)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve a cached hash, or recursively compute it.
|
||||||
|
pub fn get_or_make_hash(&mut self, index: NodeIndex) -> RpoDigest {
|
||||||
|
use NodeMutation::*;
|
||||||
|
|
||||||
|
// If this is a leaf, then only do leaf stuff.
|
||||||
|
if index.depth() == SMT_DEPTH {
|
||||||
|
let index = LeafIndex::new(index.value()).unwrap();
|
||||||
|
return match self.cached_leaf_hashes.get(&index) {
|
||||||
|
Some(cached_hash) => cached_hash.clone(),
|
||||||
|
None => {
|
||||||
|
let leaf = self.get_effective_leaf(index);
|
||||||
|
let hash = Smt::hash_leaf(&leaf);
|
||||||
|
self.cached_leaf_hashes.insert(index, hash);
|
||||||
|
hash
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already computed this one earlier as a mutation, just return it.
|
||||||
|
if let Some(ComputedNodeMutation { hash, .. }) = self.new_mutations.get(&index) {
|
||||||
|
return *hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we need to know if this node is one of the nodes we're in the process of
|
||||||
|
// recomputing, or if we can safely use the node already in the Merkle tree.
|
||||||
|
if !self.is_index_dirty(index) {
|
||||||
|
return self
|
||||||
|
.get_clean_hash(index)
|
||||||
|
.unwrap_or_else(|| *EmptySubtreeRoots::entry(SMT_DEPTH, index.depth()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here, then we have to make, rather than get, this hash.
|
||||||
|
// Make sure we mark this index as now dirty.
|
||||||
|
self.dirtied_indices.insert(index, true);
|
||||||
|
|
||||||
|
// Recurse for the left and right sides.
|
||||||
|
let left = self.get_or_make_hash(index.left_child());
|
||||||
|
let right = self.get_or_make_hash(index.right_child());
|
||||||
|
let node = InnerNode { left, right };
|
||||||
|
let hash = node.hash();
|
||||||
|
let &equivalent_empty_hash = EmptySubtreeRoots::entry(SMT_DEPTH, index.depth());
|
||||||
|
let is_removal = hash == equivalent_empty_hash;
|
||||||
|
let new_entry = if is_removal { Removal } else { Addition(node) };
|
||||||
|
|
||||||
|
self.new_mutations
|
||||||
|
.insert(index, ComputedNodeMutation { hash, mutation: new_entry });
|
||||||
|
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CONVERSIONS
|
// CONVERSIONS
|
||||||
// ================================================================================================
|
// ================================================================================================
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,9 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod full;
|
mod full;
|
||||||
pub use full::{Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError, SMT_DEPTH};
|
pub use full::{
|
||||||
|
NodeSubtreeComputer, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError, SMT_DEPTH,
|
||||||
|
};
|
||||||
|
|
||||||
mod simple;
|
mod simple;
|
||||||
pub use simple::SimpleSmt;
|
pub use simple::SimpleSmt;
|
||||||
|
@ -43,7 +45,7 @@ pub const SMT_MAX_DEPTH: u8 = 64;
|
||||||
/// must accomodate all keys that map to the same leaf.
|
/// must accomodate all keys that map to the same leaf.
|
||||||
///
|
///
|
||||||
/// [SparseMerkleTree] currently doesn't support optimizations that compress Merkle proofs.
|
/// [SparseMerkleTree] currently doesn't support optimizations that compress Merkle proofs.
|
||||||
pub(crate) trait SparseMerkleTree<const DEPTH: u8> {
|
pub trait SparseMerkleTree<const DEPTH: u8> {
|
||||||
/// The type for a key
|
/// The type for a key
|
||||||
type Key: Clone + Ord;
|
type Key: Clone + Ord;
|
||||||
/// The type for a value
|
/// The type for a value
|
||||||
|
@ -346,7 +348,7 @@ pub(crate) trait SparseMerkleTree<const DEPTH: u8> {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub(crate) struct InnerNode {
|
pub struct InnerNode {
|
||||||
pub left: RpoDigest,
|
pub left: RpoDigest,
|
||||||
pub right: RpoDigest,
|
pub right: RpoDigest,
|
||||||
}
|
}
|
||||||
|
@ -459,7 +461,7 @@ impl<const DEPTH: u8> TryFrom<NodeIndex> for LeafIndex<DEPTH> {
|
||||||
/// [`MutationSet`] stores this type in relation to a [`NodeIndex`] to keep track of what changes
|
/// [`MutationSet`] stores this type in relation to a [`NodeIndex`] to keep track of what changes
|
||||||
/// need to occur at which node indices.
|
/// need to occur at which node indices.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) enum NodeMutation {
|
pub enum NodeMutation {
|
||||||
/// Corresponds to [`SparseMerkleTree::remove_inner_node()`].
|
/// Corresponds to [`SparseMerkleTree::remove_inner_node()`].
|
||||||
Removal,
|
Removal,
|
||||||
/// Corresponds to [`SparseMerkleTree::insert_inner_node()`].
|
/// Corresponds to [`SparseMerkleTree::insert_inner_node()`].
|
||||||
|
@ -499,7 +501,6 @@ impl<const DEPTH: u8, K, V> MutationSet<DEPTH, K, V> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
Loading…
Add table
Reference in a new issue