Skip to content

Commit ee50a50

Browse files
Feature: Merkle Tree implementation as Orion pcs building block (#143)
* init commit for merkle tree implementation * benchmarked and optimized, shit runs fast * allocate rather than concat * use move for intermediate results that can be dropped later * shorten bench time * additional benchmarks
1 parent 30c80f8 commit ee50a50

File tree

10 files changed

+895
-0
lines changed

10 files changed

+895
-0
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"pcs",
1414
"sumcheck",
1515
"transcript",
16+
"tree"
1617
]
1718
resolver = "2"
1819

tree/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "tree"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
arith = { path = "../arith" }
8+
9+
# babybear = { path = "../arith/babybear" }
10+
# poseidon = { path = "../poseidon" }
11+
12+
ark-std.workspace = true
13+
sha2.workspace = true
14+
rayon.workspace = true
15+
16+
[dev-dependencies]
17+
criterion.workspace = true
18+
gf2 = { path = "../arith/gf2" }
19+
gf2_128 = { path = "../arith/gf2_128" }
20+
mersenne31 = { path = "../arith/mersenne31" }
21+
tynm.workspace = true
22+
23+
# p3-baby-bear.workspace = true
24+
# p3-field.workspace = true
25+
# p3-monty-31.workspace = true
26+
27+
[[bench]]
28+
name = "tree"
29+
harness = false

tree/benches/tree.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use std::time::Duration;
2+
3+
use arith::{Field, FieldSerde, SimdField};
4+
use ark_std::{rand::RngCore, test_rng};
5+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
6+
use gf2::{GF2x128, GF2x64, GF2x8, GF2};
7+
use mersenne31::{M31x16, M31};
8+
use tree::{Leaf, Tree, LEAF_BYTES};
9+
use tynm::type_name;
10+
11+
const FINAL_MT_LEAVES_LOG2: usize = 15;
12+
13+
fn tree_building_benchmark(c: &mut Criterion) {
14+
let mut group = c.benchmark_group("SHA512-256 merkle tree");
15+
16+
let mut rng = test_rng();
17+
let mut data_buffer = [0u8; LEAF_BYTES];
18+
let leaves: Vec<_> = (0..(1 << FINAL_MT_LEAVES_LOG2))
19+
.map(|_| {
20+
Leaf::new({
21+
rng.fill_bytes(&mut data_buffer);
22+
data_buffer.clone()
23+
})
24+
})
25+
.collect();
26+
27+
for i in 10..=FINAL_MT_LEAVES_LOG2 {
28+
group
29+
.bench_function(BenchmarkId::new(format!("2^{i} leaves"), i), |b| {
30+
let leaves_benchmark = leaves[..(1 << i)].to_vec();
31+
32+
b.iter(|| {
33+
Tree::new_with_leaves(leaves_benchmark.clone());
34+
})
35+
})
36+
.sample_size(10)
37+
.measurement_time(Duration::from_secs(5));
38+
}
39+
}
40+
41+
fn compact_field_elem_tree_building_benchmark_generic<F, PackF>(c: &mut Criterion)
42+
where
43+
F: Field + FieldSerde,
44+
PackF: SimdField<Scalar = F>,
45+
{
46+
let mut group = c.benchmark_group(format!(
47+
"SHA512-256 merkle tree with field element {} packed by SIMD field element {}",
48+
type_name::<F>(),
49+
type_name::<PackF>()
50+
));
51+
let num_of_elems_in_leaf = LEAF_BYTES * 8 / F::FIELD_SIZE;
52+
53+
let mut rng = test_rng();
54+
let field_elems: Vec<_> = (0..(1 << FINAL_MT_LEAVES_LOG2) * num_of_elems_in_leaf)
55+
.map(|_| F::random_unsafe(&mut rng))
56+
.collect();
57+
58+
for i in 10..=FINAL_MT_LEAVES_LOG2 {
59+
group
60+
.bench_function(BenchmarkId::new(format!("2^{i} leaves"), i), |b| {
61+
let field_elems_benchmark = field_elems[..(1 << i) * num_of_elems_in_leaf].to_vec();
62+
63+
b.iter(|| {
64+
Tree::compact_new_with_field_elems::<F, PackF>(&field_elems_benchmark);
65+
})
66+
})
67+
.sample_size(10)
68+
.measurement_time(Duration::from_secs(5));
69+
}
70+
}
71+
72+
fn compact_field_elem_tree_building_benchmark(c: &mut Criterion) {
73+
compact_field_elem_tree_building_benchmark_generic::<GF2, GF2x8>(c);
74+
compact_field_elem_tree_building_benchmark_generic::<GF2, GF2x64>(c);
75+
compact_field_elem_tree_building_benchmark_generic::<GF2, GF2x128>(c);
76+
compact_field_elem_tree_building_benchmark_generic::<M31, M31x16>(c)
77+
}
78+
79+
fn compact_packed_field_elem_tree_building_benchmark_generic<F, PackF>(c: &mut Criterion)
80+
where
81+
F: Field + FieldSerde,
82+
PackF: SimdField<Scalar = F>,
83+
{
84+
let mut group = c.benchmark_group(format!(
85+
"SHA512-256 merkle tree with SIMD field element {}",
86+
type_name::<PackF>()
87+
));
88+
let num_of_elems_in_leaf = LEAF_BYTES / PackF::SIZE;
89+
90+
let mut rng = test_rng();
91+
let field_elems: Vec<_> = (0..(1 << FINAL_MT_LEAVES_LOG2) * num_of_elems_in_leaf)
92+
.map(|_| PackF::random_unsafe(&mut rng))
93+
.collect();
94+
95+
for i in 10..=FINAL_MT_LEAVES_LOG2 {
96+
group
97+
.bench_function(BenchmarkId::new(format!("2^{i} leaves"), i), |b| {
98+
let field_elems_benchmark = field_elems[..(1 << i) * num_of_elems_in_leaf].to_vec();
99+
100+
b.iter(|| {
101+
Tree::compact_new_with_packed_field_elems::<F, PackF>(&field_elems_benchmark);
102+
})
103+
})
104+
.sample_size(10)
105+
.measurement_time(Duration::from_secs(5));
106+
}
107+
}
108+
109+
fn compact_packed_field_elem_tree_building_benchmark(c: &mut Criterion) {
110+
compact_packed_field_elem_tree_building_benchmark_generic::<GF2, GF2x8>(c);
111+
compact_packed_field_elem_tree_building_benchmark_generic::<GF2, GF2x64>(c);
112+
}
113+
114+
criterion_group!(
115+
bench,
116+
tree_building_benchmark,
117+
compact_field_elem_tree_building_benchmark,
118+
compact_packed_field_elem_tree_building_benchmark
119+
);
120+
criterion_main!(bench);

tree/src/leaf.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use std::fmt;
2+
use std::fmt::{Debug, Display};
3+
4+
use arith::Field;
5+
use sha2::{Digest, Sha512_256};
6+
7+
use crate::Node;
8+
9+
/// Each leaf should have 64 bytes or 512 bits
10+
pub const LEAF_BYTES: usize = 64;
11+
12+
/// Each leaf hash should have 32 bytes
13+
pub const LEAF_HASH_BYTES: usize = 32;
14+
15+
#[inline(always)]
16+
pub const fn leaf_adic<F: Field>() -> usize {
17+
LEAF_BYTES * 8 / F::FIELD_SIZE
18+
}
19+
20+
/// Represents a leaf in the Merkle tree, containing 64 bytes of data stored in a BabyBearx16.
21+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
22+
pub struct Leaf {
23+
pub data: [u8; LEAF_BYTES],
24+
}
25+
26+
impl Default for Leaf {
27+
fn default() -> Self {
28+
Self {
29+
data: [0u8; LEAF_BYTES],
30+
}
31+
}
32+
}
33+
34+
impl Display for Leaf {
35+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36+
// Display the first and last byte of the leaf data for brevity
37+
write!(f, "leaf: 0x{:02x?}", self.data)
38+
}
39+
}
40+
41+
impl Leaf {
42+
/// Creates a new Leaf with the given data.
43+
pub fn new(data: [u8; LEAF_BYTES]) -> Self {
44+
Self { data }
45+
}
46+
47+
pub fn leaf_hash(&self) -> Node {
48+
let mut hasher = Sha512_256::new();
49+
hasher.update(self.data);
50+
let res: [u8; LEAF_HASH_BYTES] = hasher.finalize().into();
51+
52+
Node { data: res }
53+
}
54+
55+
// /// Computes the hash of the leaf using Poseidon hash function.
56+
// ///
57+
// /// # Arguments
58+
// ///
59+
// /// * `hash_param` - The Poseidon hash parameters
60+
// ///
61+
// /// # Returns
62+
// ///
63+
// /// A Node containing the hash of the leaf data.
64+
// pub fn leaf_hash(&self, hash_param: &PoseidonBabyBearParams) -> Node {
65+
// // Use Poseidon hash for leaf nodes
66+
// // Note: This could be replaced with SHA2 if performance requires
67+
// let mut state = PoseidonBabyBearState { state: self.data };
68+
// hash_param.permute(&mut state);
69+
// Node {
70+
// data: unsafe {
71+
// transmute::<BabyBearx16, [u8; 64]>(state.state)[..32]
72+
// .try_into()
73+
// .unwrap()
74+
// },
75+
// }
76+
// }
77+
}

tree/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//! This module defines the core components of a Merkle tree implementation.
2+
//! It includes definitions for tree structures, nodes, leaves, and paths.
3+
4+
mod tree;
5+
pub use tree::*;
6+
7+
mod node;
8+
pub use node::*;
9+
10+
mod leaf;
11+
pub use leaf::*;
12+
13+
mod path;
14+
pub use path::*;
15+
16+
#[cfg(test)]
17+
mod tests;

tree/src/node.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::fmt;
2+
use std::fmt::Display;
3+
4+
use sha2::{Digest, Sha512_256};
5+
6+
use crate::LEAF_HASH_BYTES;
7+
8+
/// A node in the Merkle tree, representing 32 bytes of data.
9+
#[derive(Debug, Copy, Clone, PartialEq, Default)]
10+
pub struct Node {
11+
pub(crate) data: [u8; LEAF_HASH_BYTES],
12+
}
13+
14+
impl Display for Node {
15+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
16+
// Display the first and last byte of the node for brevity
17+
write!(f, "node: 0x{:02x?}...{:02x?}", self.data[0], self.data[31])
18+
}
19+
}
20+
21+
impl Node {
22+
/// Creates a new Node with the given data.
23+
pub fn new(data: [u8; LEAF_HASH_BYTES]) -> Self {
24+
Self { data }
25+
}
26+
27+
/// Computes the hash of two child nodes to create a parent node.
28+
///
29+
/// This function uses SHA-512 for hashing and takes the first 32 bytes of the result.
30+
///
31+
/// # Arguments
32+
///
33+
/// * `left` - The left child node
34+
/// * `right` - The right child node
35+
///
36+
/// # Returns
37+
///
38+
/// A new Node containing the hash of the two input nodes.
39+
#[inline]
40+
pub fn node_hash(left: &Node, right: &Node) -> Node {
41+
let mut hasher = Sha512_256::new();
42+
hasher.update(left.data);
43+
hasher.update(right.data);
44+
let result = hasher.finalize();
45+
Node {
46+
data: result.into(),
47+
}
48+
}
49+
50+
/// Returns the data of the node as a slice of bytes.
51+
pub fn as_bytes(&self) -> &[u8] {
52+
&self.data
53+
}
54+
}

0 commit comments

Comments
 (0)