Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] implement subtree-based SMT computations #341

Merged
merged 20 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b585f9c
merkle: add parent() helper function on NodeIndex
Qyriad Nov 13, 2024
ae772d2
smt: add pairs_to_leaf() to trait
Qyriad Nov 13, 2024
8b10465
smt: add sorted_pairs_to_leaves() and test for it
Qyriad Nov 13, 2024
16456aa
smt: implement single subtree-8 hashing, w/ benchmarks & tests
Qyriad Nov 14, 2024
1863dab
merkle: add a benchmark for constructing 256-balanced trees
Qyriad Nov 14, 2024
cd1dc7c
smt: test that SparseMerkleTree::build_subtree() is composable
Qyriad Nov 14, 2024
475c826
smt: test that subtree logic can correctly construct an entire tree
Qyriad Nov 14, 2024
5b9480a
smt: implement test for basic parallelized subtree computation w/ rayon
Qyriad Nov 14, 2024
38422f5
smt: add from_raw_parts() to trait interface
Qyriad Nov 15, 2024
cc144a6
smt: add parallel constructors to Smt and SimpleSmt
Qyriad Nov 15, 2024
ec2dfdf
smt: add benchmarks for parallel construction
Qyriad Nov 15, 2024
3f52ef3
add news item for smt parallel subtree construction
Qyriad Nov 15, 2024
c9b4682
refactor: integrate parallel implementations
Nov 22, 2024
6d93c0d
remove concurrent `SimpleSmt::with_leaves`
krushimir Nov 25, 2024
5cdd3fc
refactor: `build_subtree`
krushimir Nov 27, 2024
3cbfbaf
chore: address review comments
krushimir Nov 29, 2024
a07eddc
refactor: adjust bench feature requirements
krushimir Dec 2, 2024
3b4f141
chore: remove `concurrent` from `internal`
krushimir Dec 3, 2024
824adc9
Merge branch 'next' into qyriad/parallel-construction
krushimir Dec 3, 2024
d38984d
chore: fix formatting issues
krushimir Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 0.11.0 (2024-10-30)

- [BREAKING] Updated Winterfell dependency to v0.10 (#338).
- Added parallel implementation of `Smt::with_entries()` with significantly better performance when the `concurrent` feature is enabled (#341).

## 0.11.0 (2024-10-17)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,26 @@ harness = false
name = "smt"
harness = false

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

[[bench]]
name = "merkle"
harness = false

[[bench]]
name = "parallel-subtree"
harness = false
required-features = ["concurrent"]

[[bench]]
name = "store"
harness = false

[features]
default = ["std"]
concurrent = ["dep:rayon"]
default = ["std", "concurrent"]
executable = ["dep:clap", "dep:rand-utils", "std"]
serde = ["dep:serde", "serde?/alloc", "winter-math/serde"]
std = [
Expand All @@ -53,6 +67,7 @@ num-complex = { version = "0.4", default-features = false }
rand = { version = "0.8", default-features = false }
rand_core = { version = "0.6", default-features = false }
rand-utils = { version = "0.10", package = "winter-rand-utils", optional = true }
rayon = { version = "1.10", optional = true }
serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] }
sha3 = { version = "0.10", default-features = false }
winter-crypto = { version = "0.10", default-features = false }
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ make

This crate can be compiled with the following features:

- `concurrent`- enabled by default; enables multi-threaded implementation of `Smt::with_entries()` with significantly improved performance.
- `std` - enabled by default and relies on the Rust standard library.
- `no_std` does not rely on the Rust standard library and enables compilation to WebAssembly.

Both of these features imply the use of [alloc](https://doc.rust-lang.org/alloc/) to support heap-allocated collections.
All of these features imply the use of [alloc](https://doc.rust-lang.org/alloc/) to support heap-allocated collections.

To compile with `no_std`, disable default features via `--no-default-features` flag or using the following command:

Expand Down
66 changes: 66 additions & 0 deletions benches/merkle.rs
Original file line number Diff line number Diff line change
@@ -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])]
}
75 changes: 75 additions & 0 deletions benches/parallel-subtree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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_parallel_subtree(c: &mut Criterion) {
let mut seed = [0u8; 32];

let mut group = c.benchmark_group("parallel-subtrees");

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 count = pair_count as f64;
let idx = ((i as f64 / count) * (count)) as u64;
let key = RpoDigest::new([
generate_value(&mut seed),
ONE,
Felt::new(i),
Felt::new(idx),
]);
let value = generate_word(&mut seed);
(key, value)
})
.collect();

let control = Smt::with_entries_sequential(entries.clone()).unwrap();
(entries, control)
},
|(entries, control)| {
// Benchmarked function.
let tree = Smt::with_entries(hint::black_box(entries)).unwrap();
assert_eq!(tree.root(), control.root());
},
BatchSize::SmallInput,
);
});
}
}

criterion_group! {
name = smt_subtree_group;
config = Criterion::default()
//.measurement_time(Duration::from_secs(960))
.measurement_time(Duration::from_secs(60))
.sample_size(10)
.configure_from_args();
targets = smt_parallel_subtree
}
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])]
}
136 changes: 136 additions & 0 deletions benches/smt-subtree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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, 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, _) =
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 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, _) =
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])]
}
8 changes: 8 additions & 0 deletions src/merkle/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ impl NodeIndex {
self
}

/// Returns the parent of the current node. This is the same as [`Self::move_up()`], but returns
/// a new value instead of mutating `self`.
pub const fn parent(mut self) -> Self {
self.depth = self.depth.saturating_sub(1);
self.value >>= 1;
self
}

// PROVIDERS
// --------------------------------------------------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion src/merkle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use path::{MerklePath, RootPath, ValuePath};
mod smt;
pub use smt::{
LeafIndex, MutationSet, SimpleSmt, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError,
SMT_DEPTH, SMT_MAX_DEPTH, SMT_MIN_DEPTH,
SubtreeLeaf, SMT_DEPTH, SMT_MAX_DEPTH, SMT_MIN_DEPTH,
};

mod mmr;
Expand Down
Loading