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

Implementation of Parallel Merkle Tree #125

Merged
merged 24 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f76ac13
template for parallel implementation
Dec 14, 2023
63dc5fd
added a maybe useful snippet of rayon mut_par_iter as comment in the …
Dec 14, 2023
6c745c7
to do: try build and run, then test
Dec 14, 2023
fcf8843
solved build issues
Dec 18, 2023
3d11411
solved bug where in new_with_leaf_digest_parallel the index of non le…
Dec 18, 2023
4fec1c5
modified mod.rs to build function only if feature is enabled rather t…
Dec 19, 2023
571b651
adapted unit tests for parallel merkle_tree in merkle_tree/tests/mod.rs
Dec 20, 2023
f44806f
implemented benches for merkle_tree
Dec 20, 2023
4ebc098
modified field merkle tree bench
Dec 20, 2023
5eecd5a
fmt
WizardOfMenlo Dec 21, 2023
0056600
cleanup
WizardOfMenlo Dec 21, 2023
f888171
Fix compilation issue
WizardOfMenlo Dec 21, 2023
4b6be6c
Move to SHA256
WizardOfMenlo Dec 21, 2023
a37041b
More realistic benches
WizardOfMenlo Dec 21, 2023
278445e
spacing in new (parallel)
intx4 Dec 21, 2023
efc0d61
refactored code inside merkle_tree/mod.rs
intx4 Dec 23, 2023
007aeef
refactored merkle_tree/mod.rs with cfg_into_iter macro. Refactored ac…
intx4 Dec 24, 2023
f9ae45a
refactored merkle_tree benches and tests with cfg_iter macro
intx4 Dec 24, 2023
a7684e9
modified merkle_tree/mod.rs new_with_leaf_digest to use macro cfg_ite…
intx4 Dec 24, 2023
8f0a6d4
Make error send
Pratyush Dec 28, 2023
a781861
Parallel iteration fixes
Pratyush Dec 28, 2023
c17337f
Fixes
Pratyush Dec 28, 2023
93e0e8d
Change signature to enable correct iteration
Pratyush Dec 28, 2023
bf076db
Change back to taking `Vec<LeafDigest>`
Pratyush Dec 28, 2023
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Cargo.lock
params
*.swp
*.swo

.vscode
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ path = "benches/signature.rs"
harness = false
required-features = [ "signature" ]

[[bench]]
name = "merkle_tree"
path = "benches/merkle_tree.rs"
harness = false
required-features = [ "merkle_tree" ]

[patch.crates-io]
ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std/" }
ark-ff = { git = "https://github.com/arkworks-rs/algebra/" }
Expand Down
83 changes: 83 additions & 0 deletions benches/merkle_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#[macro_use]
extern crate criterion;

static NUM_LEAVES: i32 = 1 << 20;

mod bytes_mt_benches {
use ark_crypto_primitives::crh::*;
use ark_crypto_primitives::merkle_tree::*;
use ark_crypto_primitives::to_uncompressed_bytes;
use ark_ff::BigInteger256;
use ark_serialize::CanonicalSerialize;
use ark_std::{test_rng, UniformRand};
use criterion::Criterion;
use std::borrow::Borrow;

#[cfg(feature = "parallel")]
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};

use crate::NUM_LEAVES;

type LeafH = sha2::Sha256;
type CompressH = sha2::Sha256;

struct Sha256MerkleTreeParams;

impl Config for Sha256MerkleTreeParams {
type Leaf = [u8];

type LeafDigest = <LeafH as CRHScheme>::Output;
type LeafInnerDigestConverter = ByteDigestConverter<Self::LeafDigest>;
type InnerDigest = <CompressH as TwoToOneCRHScheme>::Output;

type LeafHash = LeafH;
type TwoToOneHash = CompressH;
}
type Sha256MerkleTree = MerkleTree<Sha256MerkleTreeParams>;

pub fn merkle_tree_create(c: &mut Criterion) {
let mut rng = test_rng();
let leaves: Vec<_> = (0..NUM_LEAVES)
.map(|_| {
let rnd = BigInteger256::rand(&mut rng);
to_uncompressed_bytes!(rnd).unwrap()
})
.collect();
let leaf_crh_params = <LeafH as CRHScheme>::setup(&mut rng).unwrap();
let two_to_one_params = <CompressH as TwoToOneCRHScheme>::setup(&mut rng)
.unwrap()
.clone();
c.bench_function("Merkle Tree Create (Leaves as [u8])", move |b| {
b.iter(|| {
#[cfg(not(feature = "parallel"))]
{
_ = Sha256MerkleTree::new(
&leaf_crh_params.clone(),
&two_to_one_params.clone(),
leaves.iter().map(|x| x.as_slice()),
)
.unwrap();
}
#[cfg(feature = "parallel")]
{
_ = Sha256MerkleTree::new(
&leaf_crh_params.clone(),
&two_to_one_params.clone(),
leaves.par_iter().map(|x| x.as_slice()),
)
.unwrap();
}
})
});
}

criterion_group! {
name = mt_create;
config = Criterion::default().sample_size(10);
targets = merkle_tree_create
}
}

criterion_main!(
crate::bytes_mt_benches::mt_create,
);
138 changes: 131 additions & 7 deletions src/merkle_tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use ark_std::borrow::Borrow;
use ark_std::hash::Hash;
use ark_std::vec::Vec;

#[cfg(feature = "parallel")]
use rayon::{iter::IntoParallelIterator, prelude::*};

#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -60,7 +63,8 @@ pub trait Config {
+ Hash
+ Default
+ CanonicalSerialize
+ CanonicalDeserialize;
+ CanonicalDeserialize
+ Send;
// transition between leaf layer to inner layer
type LeafInnerDigestConverter: DigestConverter<
Self::LeafDigest,
Expand All @@ -73,7 +77,8 @@ pub trait Config {
+ Hash
+ Default
+ CanonicalSerialize
+ CanonicalDeserialize;
+ CanonicalDeserialize
+ Send;

// Tom's Note: in the future, if we want different hash function, we can simply add more
// types of digest here and specify a digest converter. Same for constraints.
Expand Down Expand Up @@ -234,26 +239,145 @@ impl<P: Config> MerkleTree<P> {
}

/// Returns a new merkle tree. `leaves.len()` should be power of two.
#[cfg(not(feature = "parallel"))]
pub fn new<L: Borrow<P::Leaf>>(
leaf_hash_param: &LeafParam<P>,
two_to_one_hash_param: &TwoToOneParam<P>,
leaves: impl IntoIterator<Item = L>,
) -> Result<Self, crate::Error> {
let mut leaves_digests = Vec::new();
// Serial version of new
// Compute and store hash values for each leaf
let leaves_digest: Result<Vec<_>, _> = leaves
.into_iter()
.map(|leaf| P::LeafHash::evaluate(leaf_hash_param, leaf))
.collect();

Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaves_digest?)
}

/// Returns a new merkle tree. `leaves.len()` should be power of two.
#[cfg(feature = "parallel")]
Pratyush marked this conversation as resolved.
Show resolved Hide resolved
pub fn new<L: Borrow<P::Leaf>>(
leaf_hash_param: &LeafParam<P>,
two_to_one_hash_param: &TwoToOneParam<P>,
leaves: impl IntoParallelIterator<Item = L>,
) -> Result<Self, crate::Error> {
// Parallel version of new
// TODO: Box<Error> is not Send, I have used .unwrap() but this changes semantics between
// parallel and serial version. We should fix this in the future.
let leaves_digest: Vec<_> = leaves
.into_par_iter()
.map(|leaf| P::LeafHash::evaluate(leaf_hash_param, leaf).unwrap())
.collect();

Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaves_digest)
}

#[cfg(feature = "parallel")]
pub fn new_with_leaf_digest(
leaf_hash_param: &LeafParam<P>,
two_to_one_hash_param: &TwoToOneParam<P>,
leaves_digest: Vec<P::LeafDigest>,
) -> Result<Self, crate::Error> {
// Parallel implementation of new_with_leaf_digest

let leaf_nodes_size = leaves_digest.len();
assert!(
leaf_nodes_size.is_power_of_two() && leaf_nodes_size > 1,
"`leaves.len() should be power of two and greater than one"
);
let non_leaf_nodes_size = leaf_nodes_size - 1;

let tree_height = tree_height(leaf_nodes_size);

let hash_of_empty: P::InnerDigest = P::InnerDigest::default();

// initialize the merkle tree as array of nodes in level order
let mut non_leaf_nodes: Vec<P::InnerDigest> = (0..non_leaf_nodes_size)
.into_par_iter()
.map(|_| hash_of_empty.clone())
.collect();

// Compute the starting indices for each non-leaf level of the tree
let mut index = 0;
let mut level_indices = Vec::with_capacity(tree_height - 1);
for _ in 0..(tree_height - 1) {
level_indices.push(index);
index = left_child(index);
}

// compute and store hash values for each leaf
for leaf in leaves.into_iter() {
leaves_digests.push(P::LeafHash::evaluate(leaf_hash_param, leaf)?)
// compute the hash values for the non-leaf bottom layer
{
let start_index = level_indices.pop().unwrap();
let upper_bound = left_child(start_index);
let bottom_nodes = &mut non_leaf_nodes[start_index..upper_bound];

bottom_nodes.par_iter_mut().enumerate().for_each(|(i, n)| {
// `left_child(current_index)` and `right_child(current_index) returns the position of
// leaf in the whole tree (represented as a list in level order). We need to shift it
// by `-upper_bound` to get the index in `leaf_nodes` list.

//similarly, we need to rescale i by start_index
//to get the index outside the slice and in the level-ordered list of nodes

let current_index = i + start_index;
let left_leaf_index = left_child(current_index) - upper_bound;
let right_leaf_index = right_child(current_index) - upper_bound;
// compute hash
*n = P::TwoToOneHash::evaluate(
two_to_one_hash_param,
P::LeafInnerDigestConverter::convert(leaves_digest[left_leaf_index].clone())
.unwrap(),
Pratyush marked this conversation as resolved.
Show resolved Hide resolved
P::LeafInnerDigestConverter::convert(leaves_digest[right_leaf_index].clone())
.unwrap(),
)
.unwrap()
Pratyush marked this conversation as resolved.
Show resolved Hide resolved
});
}

// compute the hash values for nodes in every other layer in the tree
level_indices.reverse();
for &start_index in &level_indices {
// The layer beginning `start_index` ends at `upper_bound` (exclusive).

let upper_bound = left_child(start_index);

let (nodes_at_level, nodes_at_prev_level) =
non_leaf_nodes[..].split_at_mut(upper_bound);
nodes_at_level[start_index..]
.par_iter_mut()
.enumerate()
.for_each(|(i, n)| {
let current_index = i + start_index;
let left_leaf_index = left_child(current_index) - upper_bound;
let right_leaf_index = right_child(current_index) - upper_bound;

*n = P::TwoToOneHash::compress(
two_to_one_hash_param,
nodes_at_prev_level[left_leaf_index].clone(),
nodes_at_prev_level[right_leaf_index].clone(),
)
.unwrap()
});
}

Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaves_digests)
Ok(MerkleTree {
leaf_nodes: leaves_digest,
non_leaf_nodes,
height: tree_height,
leaf_hash_param: leaf_hash_param.clone(),
two_to_one_hash_param: two_to_one_hash_param.clone(),
})
}

#[cfg(not(feature = "parallel"))]
pub fn new_with_leaf_digest(
leaf_hash_param: &LeafParam<P>,
two_to_one_hash_param: &TwoToOneParam<P>,
leaves_digest: Vec<P::LeafDigest>,
) -> Result<Self, crate::Error> {
// Serial implementation of new_with_leaf_digest

let leaf_nodes_size = leaves_digest.len();
assert!(
leaf_nodes_size.is_power_of_two() && leaf_nodes_size > 1,
Expand Down
Loading