From bf0d63888555407522f854835e8d34d9793d900b Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Sun, 2 Nov 2025 11:58:48 +0100 Subject: [PATCH 01/22] tests: serialise merkle leaves via CanonicalSerialize instead of deprecated ToBytes --- .../src/merkle_tree/tests/mod.rs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index 7b690d58..e2f596a5 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -2,11 +2,15 @@ mod constraints; mod test_utils; +// TODO: add duplicate and OOB index handling (defensive checks) +// and size/structure sanity checks + mod bytes_mt_tests { use crate::{crh::*, merkle_tree::*}; use ark_ed_on_bls12_381::EdwardsProjective as JubJub; use ark_ff::BigInteger256; + use ark_serialize::CanonicalSerialize; use ark_std::{iter::zip, test_rng, UniformRand}; #[derive(Clone)] @@ -33,13 +37,17 @@ mod bytes_mt_tests { } type JubJubMerkleTree = MerkleTree; - /// Pedersen only takes bytes as leaf, so we use `ToBytes` trait. + /// Pedersen only takes bytes as leaf, so we serialise leaves canonically into bytes. fn merkle_tree_test(leaves: &[L], update_query: &[(usize, L)]) -> () { let mut rng = ark_std::test_rng(); - let mut leaves: Vec<_> = leaves + let mut leaves: Vec> = leaves .iter() - .map(|leaf| crate::to_uncompressed_bytes!(leaf).unwrap()) + .map(|leaf| { + let mut bytes = Vec::new(); + leaf.serialize_uncompressed(&mut bytes).unwrap(); + bytes + }) .collect(); let leaf_crh_params = ::setup(&mut rng).unwrap(); @@ -68,9 +76,10 @@ mod bytes_mt_tests { // test merkle tree update functionality for (i, v) in update_query { - let v = crate::to_uncompressed_bytes!(v).unwrap(); - tree.update(*i, &v).unwrap(); - leaves[*i] = v.clone(); + let mut bytes = Vec::new(); + v.serialize_uncompressed(&mut bytes).unwrap(); + tree.update(*i, &bytes).unwrap(); + leaves[*i] = bytes.clone(); } // update the root root = tree.root(); @@ -140,9 +149,13 @@ mod bytes_mt_tests { } assert_eq!(leaves.len(), 8); - let serialized_leaves: Vec<_> = leaves + let serialized_leaves: Vec> = leaves .iter() - .map(|leaf| crate::to_uncompressed_bytes!(leaf).unwrap()) + .map(|leaf| { + let mut bytes = Vec::new(); + leaf.serialize_uncompressed(&mut bytes).unwrap(); + bytes + }) .collect(); let leaf_crh_params = ::setup(&mut rng).unwrap(); From fdc9e35bd1866c472a050e82ff9ce12afc81a833 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Sun, 2 Nov 2025 13:45:14 +0100 Subject: [PATCH 02/22] tests: add cost guardrail for multiproof encoding --- .../src/merkle_tree/tests/mod.rs | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index e2f596a5..d32896a6 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -2,9 +2,6 @@ mod constraints; mod test_utils; -// TODO: add duplicate and OOB index handling (defensive checks) -// and size/structure sanity checks - mod bytes_mt_tests { use crate::{crh::*, merkle_tree::*}; @@ -300,6 +297,61 @@ mod field_mt_tests { .unwrap()); } + #[test] + fn multiproof_prefix_encoding_sanity() { + use ark_std::collections::BTreeSet; + + let leaves: Vec> = (0..32u64).map(|i| vec![F::from(i)]).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + + let query_indexes = vec![9usize, 0, 12, 5, 3, 3, 17, 24, 31, 0]; + let multi_proof = tree + .generate_multi_proof(query_indexes.clone()) + .unwrap(); + + let sorted_unique: Vec<_> = query_indexes + .into_iter() + .collect::>() + .into_iter() + .collect(); + + let mut prev_path: Vec = Vec::new(); + let mut expected_prefix_lengths = Vec::new(); + let mut expected_suffixes: Vec> = Vec::new(); + let mut expected_suffix_total = 0usize; + + for &index in &sorted_unique { + let path = tree.generate_proof(index).unwrap().auth_path; + let prefix_len = prev_path + .iter() + .zip(path.iter()) + .take_while(|(a, b)| a == b) + .count(); + let suffix = path[prefix_len..].to_vec(); + expected_suffix_total += suffix.len(); + expected_prefix_lengths.push(prefix_len); + expected_suffixes.push(suffix); + prev_path = path; + } + + let actual_suffix_total: usize = multi_proof + .auth_paths_suffixes + .iter() + .map(|suffix| suffix.len()) + .sum(); + + assert_eq!(multi_proof.leaf_indexes, sorted_unique); + assert_eq!(multi_proof.auth_paths_prefix_lenghts, expected_prefix_lengths); + assert_eq!(multi_proof.auth_paths_suffixes, expected_suffixes); + assert_eq!(multi_proof.leaf_siblings_hashes.len(), sorted_unique.len()); + assert_eq!( + multi_proof.leaf_siblings_hashes.len() + actual_suffix_total, + sorted_unique.len() + expected_suffix_total + ); + } + #[test] fn good_root_test() { let mut rng = test_rng(); From af2341134594eeb94ccf86942ab1fee95136af30 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Tue, 4 Nov 2025 10:54:06 +0100 Subject: [PATCH 03/22] merkle_tree: add multipath v2 copath encoding --- crypto-primitives/src/merkle_tree/mod.rs | 483 ++++++++++++++++++++++- 1 file changed, 482 insertions(+), 1 deletion(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 167b4db9..b95abdad 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -11,7 +11,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::vec::Vec; use ark_std::{ borrow::Borrow, - collections::BTreeSet, + collections::{BTreeSet, BTreeMap}, fmt::Debug, hash::{BuildHasherDefault, Hash}, }; @@ -350,6 +350,300 @@ impl MultiPath

{ } } +/// Optimized data structure to store multiple nodes proofs. +/// For example: +/// ```tree_diagram +/// [A] d = 0 +/// / \ +/// [B] C d = 1 +/// / \ / \ +/// D [E] F H d = 2 +/// ... / \ / \ .... +/// [I] J L M d = 3 +/// ``` +/// Suppose we want to prove I and J, then: +/// `tree_height`: `4` +/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) +/// `leaf_copath`: `[]` +/// `inner_copath`: `[(2,0,D), (1,1,C)]` +/// +/// ---- Legacy fields ---- +/// `leaf_siblings_hashes`: `[J,I]` +/// `auth_paths_prefix_lenghts`: `[0,2]` +/// `auth_paths_suffixes`: `[ [C,D], []]` +/// +/// We can reconstruct upfront the minimal copath needed for the proof: +/// First, we reconstruct the minimal copath at the leaf layer (`depth = tree_height-1`). +/// This is only those sibling leaf digests that are required to complete parents of on-path leaves but are not themselves on-path. +/// The leaf copath is thus `[J,I]/[I,J]=[]`. +/// We then repeat this for each inner layer, computing only the non-on-path siblings needed to complete parents of the union of all single paths. +/// The inner copath digests are stored as (depth, index, digest) tuples ordered by (depth, index). +/// Thus, inner copath is `[(2,0,D), (1,1,C)]`. +/// Intuitively, CoSet transmits only what's missing to recompute every parent on the shared union-of-paths. +/// +/// ---- Legacy prefix encoding (kept for compatibility): ---- +/// We can reconstruct the paths incrementally: +/// First, we reconstruct the first path. The prefix length is 0, hence we do not have any prefix encoding. +/// The path is thus `[C,D]`. +/// Once the first path is verified, we can reconstruct the second path. +/// The prefix length of 2 means that the path prefix will be `previous_path[:2] -> [C,D]`. +/// Since the Merkle Tree branch is the same, the authentication path is the same (which means in this case that there is no suffix). +/// The second path is hence `[C,D] + []` (i.e., plus the empty suffix). We can verify the second path as the first one. + +#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] +#[derivative( + Clone(bound = "P: Config"), + Debug(bound = "P: Config"), + Default(bound = "P: Config") +)] +pub struct MultiPathV2 { + /// ---- MultiPathV2 new fields ---- + /// stores the height of the tree (>= 2) to drive CoSet decoding + pub tree_height: usize, + /// TODO: reorder leaf_indexes last in proof for legacy use compatibility + /// stores the leaf indexes of the nodes to prove + pub leaf_indexes: Vec, + /// For leaf layer, stores co-path digests (B*_{d-1}) in ascending sibling index order + pub leaf_copath: Vec, + /// For inner layers, stores co-path entries as (depth, index, digest) tuples ordered by (depth, index) + pub inner_copath: Vec<(usize, usize, P::InnerDigest)>, + + /// ---- Legacy fields (kept for compatibility/tests) ---- + /// For node i, stores the hash of node i's sibling + pub leaf_siblings_hashes: Vec, + /// For node i path, stores at index i the prefix length of the path, for Incremental encoding + pub auth_paths_prefix_lenghts: Vec, + /// For node i path, stores at index i the suffix of the path for Incremental Encoding (as vector of symbols to be resolved with self.lut). Order is from higher layer to lower layer (does not include root node). + pub auth_paths_suffixes: Vec>, +} + +impl MultiPathV2

{ + /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. + /// Note that the order of the leaves hashes should match the leaves respective indexes + /// * `leaf_size`: leaf size in number of bytes + /// + /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` + pub fn verify + Clone>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + leaves: impl IntoIterator, + ) -> Result { + // TODO: when multi-proof logic is overhauled, clarify the semantics for empty + // batches (this index access panics if `leaf_indexes` is empty) + use log::{debug, trace}; + + let have_coset = + self.tree_height >= 2 && + (!self.leaf_indexes.is_empty() // accept valid batch of size 0 proof without path work + || !self.inner_copath.is_empty() + || !self.leaf_copath.is_empty()); + + if have_coset { + let d = self.tree_height; + let leaf_depth = d - 1; + debug!("coset.verify: k={}, height={}", self.leaf_indexes.len(), d); + + if d < 2 { + return Ok(false); + } + + // hash opened leaves and build map containing all leaf digests needed at bottom layer + let mut leaves = leaves.into_iter(); + let mut leaf_level: BTreeMap = BTreeMap::new(); + for &idx in &self.leaf_indexes { + let leaf = leaves.next().ok_or_else(|| crate::Error::Other("coset.verify: insufficient leaves".into()))?; + let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; + leaf_level.insert(idx, leaf_hash); + } + if leaves.next().is_some() { + return Err(crate::Error::Other("coset.verify: extra leaves".into())); + } + + // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j + let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); + let on_path = compute_on_path(leaf_depth, &index_set); // holds indices of on-path nodes at depth d + + // compute minimal copath at leaf layer (B*_{d-1}) + let mut expected_leaf_coset: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if !on_path[leaf_depth].contains(&sibling_idx) { + expected_leaf_coset.push(sibling_idx); // copath element needed for proof + } + } + expected_leaf_coset.sort_unstable(); // canonical order + + if expected_leaf_coset.len() != self.leaf_copath.len() { + return Ok(false); + } + + for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { + match leaf_level.get(&sibling_idx) { + Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one + _ => { + leaf_level.insert(sibling_idx, sibling_digest.clone()); + } + } + } + + // prepare inner-level maps for non-on-path siblings and computed parents + let mut inner_levels: Vec> = + (0..d).map(|_| BTreeMap::new()).collect(); + + // LookUp table to speedup computation avoid redundant hash computations + let mut hash_lut: HashMap = + HashMap::with_hasher(BuildHasherDefault::::default()); + + for &(depth, idx, ref copath_digest) in &self.inner_copath { + if depth == 0 || depth >= d { + return Ok(false); + } + // store the sibling digest at its depth/index + if let Some(existing) = inner_levels[depth].get(&idx) { + if existing != copath_digest { + return Ok(false); + } + } else { + inner_levels[depth].insert(idx, copath_digest.clone()); + } + // seed LUT with known siblings + let heap_idx = level_index(depth, idx); + hash_lut.entry(heap_idx).or_insert_with(|| copath_digest.clone()); + } + + // Recomputation + // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs + for &parent_index in on_path[leaf_depth - 1].iter() { + let left = leaf_level.get(&(parent_index * 2)).cloned(); + let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), _ => return Ok(false) }; + let parent = P::TwoToOneHash::evaluate( + two_to_one_params, + P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type + P::LeafInnerDigestConverter::convert(right)?, + )?; + inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(leaf_depth - 1, parent_index); + hash_lut.insert(heap_idx, parent); + } + + // compute inner layers up to root using TwoToOne::compress to hash inner digests + for depth in (1..=leaf_depth - 1).rev() { + let parent_depth = depth - 1; + for &parent_index in on_path[parent_depth].iter() { + let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); + let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), _ => return Ok(false) }; + let parent = P::TwoToOneHash::compress( + two_to_one_params, + &left, + &right, + )?; + inner_levels[parent_depth].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(parent_depth, parent_index); + hash_lut.insert(heap_idx, parent); + } + } + + // check root + match inner_levels[0].get(&0) { + Some(h) => { + trace!("coset.verify: root computed, success={}", h == root_hash); + Ok(h == root_hash) + } + None => Ok(false), + } + } else { + // --- Legacy prefix-decoder path --- + let tree_height = self.auth_paths_suffixes.get(0).map(|v| v.len()).unwrap_or(0) + 2; + let mut leaves = leaves.into_iter(); + + // LookUp table to speedup computation avoid redundant hash computations + let mut hash_lut: hashbrown::HashMap = + hashbrown::HashMap::with_hasher(BuildHasherDefault::::default()); + + // init prev path for decoding + let mut prev_path: Vec<_> = self.auth_paths_suffixes[0].clone(); + + for i in 0..self.leaf_indexes.len() { + let leaf_index = self.leaf_indexes[i]; + let leaf = leaves.next().unwrap(); + let leaf_sibling_hash = &self.leaf_siblings_hashes[i]; + + // decode i-th auth path + let auth_path = prefix_decode_path( + &prev_path, + self.auth_paths_prefix_lenghts[i], + &self.auth_paths_suffixes[i], + ); + // update prev path for decoding next one + prev_path = auth_path.clone(); + + let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf.clone())?; + let (left_child, right_child) = + select_left_right_child(leaf_index, &claimed_leaf_hash, &leaf_sibling_hash)?; + // check hash along the path from bottom to root + + // leaf layer to inner layer conversion + let left_child = P::LeafInnerDigestConverter::convert(left_child)?; + let right_child = P::LeafInnerDigestConverter::convert(right_child)?; + + // we will use `index` variable to track the position of path + let mut index = leaf_index; + let mut index_in_tree = convert_index_to_last_level(leaf_index, tree_height); + index >>= 1; + index_in_tree = parent(index_in_tree).unwrap(); + + let mut curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { + P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap() + }); + + // Check levels between leaf level and root + for level in (0..auth_path.len()).rev() { + // check if path node at this level is left or right + let (left, right) = + select_left_right_child(index, curr_path_node, &auth_path[level])?; + // update curr_path_node + index >>= 1; + index_in_tree = parent(index_in_tree).unwrap(); + curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { + P::TwoToOneHash::compress(&two_to_one_params, left, right).unwrap() + }); + } + + // check if final hash is root + if curr_path_node != root_hash { + return Ok(false); + } + } + Ok(true) + } + } + + /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. + /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. + /// + /// This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. + #[allow(unused)] // this function is actually used when r1cs feature is on + fn position_list(&'_ self) -> impl '_ + Iterator> { + let path_len = self.auth_paths_suffixes[0].len(); + + cfg_into_iter!(self.leaf_indexes.clone()) + .map(move |i| { + (0..path_len + 1) + .map(move |j| ((i >> j) & 1) != 0) + .rev() + .collect() + }) + .collect::>() + .into_iter() + } +} + /// `index` is the first `path.len()` bits of /// the position of tree. /// @@ -624,6 +918,170 @@ impl MerkleTree

{ }) } + /// Returns a MultiPathV2 (a compressed membership proof for a set of leaves), + /// sufficient to verify each leaf up to the root. + /// Note that for compatibility, indexes are internally sorted and legacy prefix fields still populated. + /// + /// With the CoSet (minimal co-path) encoding, we do not store full per-leaf authentication paths. + /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. + /// This yields a smaller proof than front-incremental prefix encoding in the typical case, + /// while preserving the same verification interface. + /// + /// For sorted indexes, the CoSet proof carries: + /// * `tree_height`; + /// * `leaf_indexes` (ascending, unique); + /// * `leaf_copath`: the leaf-layer co-path digests `B*_{d-1}`, in ascending sibling index order; + /// * `inner_copath`: the inner co-path as `(depth, index, digest)` tuples ordered by `(depth, index)`. + /// + /// The lagacy prefix fields (`auth_paths_prefix_lenghts`, `auth_paths_suffixes`, and `leaf_siblings_hashes`) + /// are still filled so existing tests and callers that expect prefix decoding continue to work. + /// + /// When verifying the proof, leaves hashes should be supplied in order of `leaf_indexes`, that is: + /// ```ignore + /// let ordered_leaves: Vec<_> = self.leaf_indexes.into_iter().map(|i| leaves[i]).collect(); + /// ``` + /// Notes: + /// * Empty input (`indexes` is empty) returns a structurally valid empty proof carrying `tree_height`, + /// and verification succeeds vacuously against the claimed root. + pub fn generate_multi_proof_V2( + &self, + indexes: impl IntoIterator, + ) -> Result, crate::Error> { + // TODO: remove logging and debugging for production + use log::{debug, trace}; + + // pruned and sorted for encoding efficiency + let indexes: BTreeSet = indexes.into_iter().collect(); + let d = self.height(); + debug!("coset.generate_multi_proof: k={}, height={}", indexes.len(), d); + + // TODO: should empty query return structurally valid empty proof + if indexes.is_empty() { + return Ok(MultiPathv2 { + tree_height: d, + leaf_indexes: Vec::new(), + leaf_copath: Vec::new(), + inner_copath: Vec::new(), + // legacy + leaf_siblings_hashes: Vec::new(), + auth_paths_prefix_lenghts: Vec::new(), + auth_paths_suffixes: Vec::new(), + }); + } + + // legacy + let mut auth_path_prefix_lengths = Vec::with_capacity(indexes.len()); + let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); + + let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); + + let mut prev_path = Vec::new(); + // end of legacy + + let leaf_depth = d - 1; + let mut leaf_candidates: BTreeMap<(usize /*depth*/, usize /*idx*/), P::LeafDigest> = + BTreeMap::new(); + let mut inner_candidates: BTreeMap<(usize /*depth*/, usize /*idx*/), P::InnerDigest> = + BTreeMap::new(); + + // TODO: loop over index in &indexes with ref to *index + // or switch to &i in indexes.iter() with ref to i and promise ? + for &i in indexes.iter() { + let path = self.generate_proof(i)?; + + // Legacy prefix-encoding (for compatibility) + let prefix_len = { + let l = prev_path + .iter() + .zip(path.auth_path.iter()) + .take_while(|(a, b)| a == b) + .count(); + l + }; + let suffix = path.auth_path[prefix_len..].to_vec(); + auth_paths_prefix_lenghts.push(prefix_len); + auth_paths_suffixes.push(suffix); + prev_path = path.auth_path.clone(); + + leaf_siblings_hashes.push(path.leaf_sibling_hash.clone()); + // end of legacy + + // CoSet candidates at leaf layer: sibling at depth d-1 is i xor 1 + let sib_leaf_idx = i ^ 1; + leaf_candidates + .entry((leaf_depth, sib_leaf_idx)) + .or_insert(path.leaf_sibling_hash); + + // CoSet candidates on inner layers + for (offset, digest) in path.auth_path.into_iter().enumerate() { + let depth = offset + 1; // inner depths are 1..(d-2) + let shift = leaf_depth - depth; + let on_path_index = if shift == 0 { i } else { i >> shift }; + let sibling_index = on_path_index ^ 1; + inner_candidates + .entry((depth, sibling_index)) + .or_insert(digest); + } + } + + // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j + let on_path = compute_on_path(leaf_depth, &indexes); + + // leaf layer (depth = d-1) + let mut leaf_coset_ids: BTreeSet = BTreeSet::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if !on_path[leaf_depth].contains(&sibling_idx) { + leaf_coset_ids.insert(sibling_idx); + } + } + + let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); + for sibling_idx in leaf_coset_ids.iter().copied() { + if let Some(sibling_digest) = leaf_candidates.get(&(leaf_depth, sibling_idx)) { + leaf_copath.push(sibling_digest.clone()); + } else { + // This should not happen; fallback to computing from stored leaves if needed + return Err(crate::Error::Other("coset: missing leaf copath candidate".into())); + } + } + + // inner layers (depth 1..d-2) + let mut inner_copath = Vec::new(); + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if !on_path[depth].contains(&sibling_idx) { + if let Some(sibling_digest) = inner_candidates.get(&(depth, sibling_idx)) { + inner_copath.push((depth, sibling_idx, sibling_digest.clone())); + } else { + return Err(crate::Error::Other("coset: missing inner copath candidate".into())); + } + } + } + } + // canonicalise order + inner_copath.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); + + // TODO: later remove trace + trace!( + "coset.generate_multi_proof: leaf_copath={}, inner_copath={}", + leaf_copath.len(), + inner_copath.len() + ); + + Ok(MultiPathV2 { + tree_height: d, + leaf_indexes: Vec::from_iter(indexes), + leaf_copath, + inner_copath, + // legacy fields still populated for tests + leaf_siblings_hashes, + auth_paths_prefix_lenghts, + auth_paths_suffixes, + }) + } + /// Given the index and new leaf, return the hash of leaf and an updated path in order from root to bottom non-leaf level. /// This does not mutate the underlying tree. fn updated_path>( @@ -734,6 +1192,12 @@ fn tree_height(num_leaves: usize) -> usize { (ark_std::log2(num_leaves) as usize) + 1 } +/// Return level-order index encoded in global heap. +/// Node at `depth` (root=0) and position `pos` (0-based at that depth) -> heap index `(1< usize { + ((1usize << depth) - 1) + pos +} /// Returns true iff the index represents the root. #[inline] fn is_root(index: usize) -> bool { @@ -815,3 +1279,20 @@ where vec![prev_path[0..prefix_len].to_vec(), suffix.clone()].concat() } } + +/// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. +/// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. +fn compute_on_path(depth_leaves: usize, indexes: &ark_std::collections::BTreeSet) + -> Vec> +{ + use ark_std::collections::BTreeSet; + let mut path_sets = vec![BTreeSet::new(); depth_leaves + 1]; + for &leaf_index in indexes.iter() { + for depth in 0..=depth_leaves { + let shift = depth_leaves - depth; + let path_index = if shift == 0 { leaf_index } else { leaf_index >> shift }; + path_sets[depth].insert(path_index); + } + } + path_sets +} From 4b0531fedf42b42c71a4b68176cfe261eec737c4 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Sun, 9 Nov 2025 14:19:39 +0100 Subject: [PATCH 04/22] tests: exercise multiproof v2 encoding --- crypto-primitives/src/merkle_tree/mod.rs | 30 +++++-------------- .../src/merkle_tree/tests/mod.rs | 26 ++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index b95abdad..517e286e 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -432,8 +432,6 @@ impl MultiPathV2

{ ) -> Result { // TODO: when multi-proof logic is overhauled, clarify the semantics for empty // batches (this index access panics if `leaf_indexes` is empty) - use log::{debug, trace}; - let have_coset = self.tree_height >= 2 && (!self.leaf_indexes.is_empty() // accept valid batch of size 0 proof without path work @@ -443,8 +441,6 @@ impl MultiPathV2

{ if have_coset { let d = self.tree_height; let leaf_depth = d - 1; - debug!("coset.verify: k={}, height={}", self.leaf_indexes.len(), d); - if d < 2 { return Ok(false); } @@ -453,12 +449,12 @@ impl MultiPathV2

{ let mut leaves = leaves.into_iter(); let mut leaf_level: BTreeMap = BTreeMap::new(); for &idx in &self.leaf_indexes { - let leaf = leaves.next().ok_or_else(|| crate::Error::Other("coset.verify: insufficient leaves".into()))?; + let leaf = leaves.next().ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_indexes.len()))?; let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; leaf_level.insert(idx, leaf_hash); } if leaves.next().is_some() { - return Err(crate::Error::Other("coset.verify: extra leaves".into())); + return Err(crate::Error::IncorrectInputLength(self.leaf_indexes.len())); } // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j @@ -552,7 +548,6 @@ impl MultiPathV2

{ // check root match inner_levels[0].get(&0) { Some(h) => { - trace!("coset.verify: root computed, success={}", h == root_hash); Ok(h == root_hash) } None => Ok(false), @@ -943,21 +938,17 @@ impl MerkleTree

{ /// Notes: /// * Empty input (`indexes` is empty) returns a structurally valid empty proof carrying `tree_height`, /// and verification succeeds vacuously against the claimed root. - pub fn generate_multi_proof_V2( + pub fn generate_multi_proof_v2( &self, indexes: impl IntoIterator, ) -> Result, crate::Error> { - // TODO: remove logging and debugging for production - use log::{debug, trace}; - // pruned and sorted for encoding efficiency let indexes: BTreeSet = indexes.into_iter().collect(); let d = self.height(); - debug!("coset.generate_multi_proof: k={}, height={}", indexes.len(), d); // TODO: should empty query return structurally valid empty proof if indexes.is_empty() { - return Ok(MultiPathv2 { + return Ok(MultiPathV2 { tree_height: d, leaf_indexes: Vec::new(), leaf_copath: Vec::new(), @@ -970,7 +961,7 @@ impl MerkleTree

{ } // legacy - let mut auth_path_prefix_lengths = Vec::with_capacity(indexes.len()); + let mut auth_paths_prefix_lenghts = Vec::with_capacity(indexes.len()); let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); @@ -1041,8 +1032,7 @@ impl MerkleTree

{ if let Some(sibling_digest) = leaf_candidates.get(&(leaf_depth, sibling_idx)) { leaf_copath.push(sibling_digest.clone()); } else { - // This should not happen; fallback to computing from stored leaves if needed - return Err(crate::Error::Other("coset: missing leaf copath candidate".into())); + return Err(crate::Error::IncorrectInputLength(self.leaf_nodes.len())); } } @@ -1055,7 +1045,7 @@ impl MerkleTree

{ if let Some(sibling_digest) = inner_candidates.get(&(depth, sibling_idx)) { inner_copath.push((depth, sibling_idx, sibling_digest.clone())); } else { - return Err(crate::Error::Other("coset: missing inner copath candidate".into())); + return Err(crate::Error::IncorrectInputLength(self.leaf_nodes.len())); } } } @@ -1064,12 +1054,6 @@ impl MerkleTree

{ inner_copath.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); // TODO: later remove trace - trace!( - "coset.generate_multi_proof: leaf_copath={}, inner_copath={}", - leaf_copath.len(), - inner_copath.len() - ); - Ok(MultiPathV2 { tree_height: d, leaf_indexes: Vec::from_iter(indexes), diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index d32896a6..393b99d6 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -67,9 +67,16 @@ mod bytes_mt_tests { .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); + let mut coset_multi_proof = tree + .generate_multi_proof_v2((0..leaves.len()).collect::>()) + .unwrap(); + assert!(multi_proof .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) .unwrap()); + assert!(coset_multi_proof + .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .unwrap()); // test merkle tree update functionality for (i, v) in update_query { @@ -92,10 +99,16 @@ mod bytes_mt_tests { multi_proof = tree .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); + coset_multi_proof = tree + .generate_multi_proof_v2((0..leaves.len()).collect::>()) + .unwrap(); assert!(multi_proof .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) .unwrap()); + assert!(coset_multi_proof + .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .unwrap()); } #[test] @@ -171,6 +184,10 @@ mod bytes_mt_tests { .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); + let coset_multi_proof = tree + .generate_multi_proof_v2((0..leaves.len()).collect::>()) + .unwrap(); + // test compression theretical prefix lengths for size 8 Tree: // we should send 6 hashes instead of 2*8 = 16 let theoretical_prefix_lengths = vec![0, 2, 1, 2, 0, 2, 1, 2]; @@ -189,6 +206,15 @@ mod bytes_mt_tests { ) { assert_eq!(prefix_len + suffix.len(), proofs[0].auth_path.len()); } + + assert!(coset_multi_proof + .verify( + &leaf_crh_params, + &two_to_one_params, + &tree.root(), + serialized_leaves.clone() + ) + .unwrap()); } } From 36281190f758cae86e874bd869dadbe39849a474 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 24 Nov 2025 20:22:54 +0100 Subject: [PATCH 05/22] merkle_tree: optimise multiproof generation to reuse tree digests and add benchmarks --- crypto-primitives/Cargo.toml | 1 + crypto-primitives/src/merkle_tree/mod.rs | 278 ++++--- .../src/merkle_tree/tests/bench_report.rs | 690 ++++++++++++++++++ .../src/merkle_tree/tests/mod.rs | 2 + crypto-primitives/src/merkle_tree/v2_bench.rs | 376 ++++++++++ 5 files changed, 1247 insertions(+), 100 deletions(-) create mode 100644 crypto-primitives/src/merkle_tree/tests/bench_report.rs create mode 100644 crypto-primitives/src/merkle_tree/v2_bench.rs diff --git a/crypto-primitives/Cargo.toml b/crypto-primitives/Cargo.toml index 87c4046c..4c0e7159 100644 --- a/crypto-primitives/Cargo.toml +++ b/crypto-primitives/Cargo.toml @@ -74,6 +74,7 @@ ark-bls12-381 = { git = "https://github.com/arkworks-rs/algebra", default-featur ark-mnt4-298 = { git = "https://github.com/arkworks-rs/algebra", default-features = false, features = [ "curve", "r1cs" ] } ark-mnt6-298 = { git = "https://github.com/arkworks-rs/algebra", default-features = false, features = [ "r1cs" ] } criterion = { version = "0.6" } +plotters = { version = "0.3", default-features = false, features = ["svg_backend", "line_series", "ttf"] } ################################# Benchmarks ################################## diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 517e286e..56b5f035 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -25,6 +25,9 @@ pub mod constraints; #[cfg(test)] mod tests; +pub mod v2_bench; +pub use v2_bench::MultiPathV2Bench; + #[cfg(all( target_has_atomic = "8", target_has_atomic = "16", @@ -43,6 +46,8 @@ type DefaultHasher = ahash::AHasher; )))] type DefaultHasher = fnv::FnvHasher; +type PackedInnerCopath

= (usize, usize, Vec, Vec<

::InnerDigest>); + /// Convert the hash digest in different layers by converting previous layer's output to /// `TargetType`, which is a `Borrow` to next layer's input. pub trait DigestConverter { @@ -405,10 +410,11 @@ pub struct MultiPathV2 { pub leaf_indexes: Vec, /// For leaf layer, stores co-path digests (B*_{d-1}) in ascending sibling index order pub leaf_copath: Vec, - /// For inner layers, stores co-path entries as (depth, index, digest) tuples ordered by (depth, index) - pub inner_copath: Vec<(usize, usize, P::InnerDigest)>, + /// For inner layers, stores co-path entries packed as (start_depth, start_index, packed deltas, digests) + pub inner_copath: Option>, /// ---- Legacy fields (kept for compatibility/tests) ---- + /// These vectors are left empty /// For node i, stores the hash of node i's sibling pub leaf_siblings_hashes: Vec, /// For node i path, stores at index i the prefix length of the path, for Incremental encoding @@ -417,6 +423,42 @@ pub struct MultiPathV2 { pub auth_paths_suffixes: Vec>, } +#[allow(dead_code)] +struct CoSetLevel { + entries: Vec<(usize, P::InnerDigest)>, +} + +impl CoSetLevel

{ + #[allow(dead_code)] + fn new() -> Self { + Self { entries: Vec::new() } + } + + #[allow(dead_code)] + fn get(&self, idx: usize) -> Option<&P::InnerDigest> { + self.entries.iter().find(|(i, _)| *i == idx).map(|(_, d)| d) + } + + #[allow(dead_code)] + fn insert_or_check(&mut self, idx: usize, digest: &P::InnerDigest) -> bool { + if let Some((_, existing)) = self.entries.iter().find(|(i, _)| *i == idx) { + existing == digest + } else { + self.entries.push((idx, digest.clone())); + true + } + } + + #[allow(dead_code)] + fn set(&mut self, idx: usize, digest: P::InnerDigest) { + if let Some((_, existing)) = self.entries.iter_mut().find(|(i, _)| *i == idx) { + *existing = digest; + } else { + self.entries.push((idx, digest)); + } + } +} + impl MultiPathV2

{ /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. /// Note that the order of the leaves hashes should match the leaves respective indexes @@ -432,10 +474,10 @@ impl MultiPathV2

{ ) -> Result { // TODO: when multi-proof logic is overhauled, clarify the semantics for empty // batches (this index access panics if `leaf_indexes` is empty) - let have_coset = - self.tree_height >= 2 && + let have_coset = + self.tree_height >= 2 && (!self.leaf_indexes.is_empty() // accept valid batch of size 0 proof without path work - || !self.inner_copath.is_empty() + || self.inner_copath.is_some() || !self.leaf_copath.is_empty()); if have_coset { @@ -465,7 +507,7 @@ impl MultiPathV2

{ let mut expected_leaf_coset: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; - if !on_path[leaf_depth].contains(&sibling_idx) { + if !contains_sorted(&on_path[leaf_depth], sibling_idx) { expected_leaf_coset.push(sibling_idx); // copath element needed for proof } } @@ -492,21 +534,86 @@ impl MultiPathV2

{ let mut hash_lut: HashMap = HashMap::with_hasher(BuildHasherDefault::::default()); - for &(depth, idx, ref copath_digest) in &self.inner_copath { - if depth == 0 || depth >= d { - return Ok(false); - } - // store the sibling digest at its depth/index - if let Some(existing) = inner_levels[depth].get(&idx) { - if existing != copath_digest { + if let Some((start_depth, start_index, deltas, digests)) = &self.inner_copath { + if digests.is_empty() { + if !deltas.is_empty() { return Ok(false); } } else { - inner_levels[depth].insert(idx, copath_digest.clone()); + let mut depth = match i64::try_from(*start_depth) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + let mut index = match i64::try_from(*start_index) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + let mut cursor = 0usize; + let mut prev_coord: Option<(usize, usize)> = None; + let mut push_entry = |depth_i64: i64, + index_i64: i64, + digest: &P::InnerDigest| + -> bool { + let depth_usize = match usize::try_from(depth_i64) { + Ok(v) => v, + Err(_) => return false, + }; + let index_usize = match usize::try_from(index_i64) { + Ok(v) => v, + Err(_) => return false, + }; + if depth_usize == 0 || depth_usize >= d { + return false; + } + if let Some((pd, pi)) = prev_coord { + if (depth_usize, index_usize) < (pd, pi) { + return false; + } + } + if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { + if existing != digest { + return false; + } + } else { + inner_levels[depth_usize].insert(index_usize, digest.clone()); + } + // seed LUT with known siblings + let heap_idx = level_index(depth_usize, index_usize); + hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); + prev_coord = Some((depth_usize, index_usize)); + true + }; + + if !push_entry(depth, index, &digests[0]) { + return Ok(false); + } + + for digest in digests.iter().skip(1) { + let depth_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return Ok(false), + }; + let index_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return Ok(false), + }; + depth = match depth.checked_add(depth_delta) { + Some(value) => value, + None => return Ok(false), + }; + index = match index.checked_add(index_delta) { + Some(value) => value, + None => return Ok(false), + }; + if !push_entry(depth, index, digest) { + return Ok(false); + } + } + + if cursor != deltas.len() { + return Ok(false); + } } - // seed LUT with known siblings - let heap_idx = level_index(depth, idx); - hash_lut.entry(heap_idx).or_insert_with(|| copath_digest.clone()); } // Recomputation @@ -915,7 +1022,8 @@ impl MerkleTree

{ /// Returns a MultiPathV2 (a compressed membership proof for a set of leaves), /// sufficient to verify each leaf up to the root. - /// Note that for compatibility, indexes are internally sorted and legacy prefix fields still populated. + /// Note that for compatibility, indexes are internally sorted and legacy prefix fields remain in the + /// struct (but are emitted empty for new proofs) /// /// With the CoSet (minimal co-path) encoding, we do not store full per-leaf authentication paths. /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. @@ -960,80 +1068,27 @@ impl MerkleTree

{ }); } - // legacy - let mut auth_paths_prefix_lenghts = Vec::with_capacity(indexes.len()); - let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); - - let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); - - let mut prev_path = Vec::new(); - // end of legacy - let leaf_depth = d - 1; - let mut leaf_candidates: BTreeMap<(usize /*depth*/, usize /*idx*/), P::LeafDigest> = - BTreeMap::new(); - let mut inner_candidates: BTreeMap<(usize /*depth*/, usize /*idx*/), P::InnerDigest> = - BTreeMap::new(); - - // TODO: loop over index in &indexes with ref to *index - // or switch to &i in indexes.iter() with ref to i and promise ? - for &i in indexes.iter() { - let path = self.generate_proof(i)?; - - // Legacy prefix-encoding (for compatibility) - let prefix_len = { - let l = prev_path - .iter() - .zip(path.auth_path.iter()) - .take_while(|(a, b)| a == b) - .count(); - l - }; - let suffix = path.auth_path[prefix_len..].to_vec(); - auth_paths_prefix_lenghts.push(prefix_len); - auth_paths_suffixes.push(suffix); - prev_path = path.auth_path.clone(); - - leaf_siblings_hashes.push(path.leaf_sibling_hash.clone()); - // end of legacy - - // CoSet candidates at leaf layer: sibling at depth d-1 is i xor 1 - let sib_leaf_idx = i ^ 1; - leaf_candidates - .entry((leaf_depth, sib_leaf_idx)) - .or_insert(path.leaf_sibling_hash); - - // CoSet candidates on inner layers - for (offset, digest) in path.auth_path.into_iter().enumerate() { - let depth = offset + 1; // inner depths are 1..(d-2) - let shift = leaf_depth - depth; - let on_path_index = if shift == 0 { i } else { i >> shift }; - let sibling_index = on_path_index ^ 1; - inner_candidates - .entry((depth, sibling_index)) - .or_insert(digest); - } - } - // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j let on_path = compute_on_path(leaf_depth, &indexes); // leaf layer (depth = d-1) - let mut leaf_coset_ids: BTreeSet = BTreeSet::new(); + let mut leaf_coset_ids: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; - if !on_path[leaf_depth].contains(&sibling_idx) { - leaf_coset_ids.insert(sibling_idx); + if !contains_sorted(&on_path[leaf_depth], sibling_idx) { + leaf_coset_ids.push(sibling_idx); } } + leaf_coset_ids.sort_unstable(); let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); for sibling_idx in leaf_coset_ids.iter().copied() { - if let Some(sibling_digest) = leaf_candidates.get(&(leaf_depth, sibling_idx)) { - leaf_copath.push(sibling_digest.clone()); - } else { - return Err(crate::Error::IncorrectInputLength(self.leaf_nodes.len())); - } + let sibling_digest = self + .leaf_nodes + .get(sibling_idx) + .ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_nodes.len()))?; + leaf_copath.push(sibling_digest.clone()); } // inner layers (depth 1..d-2) @@ -1041,12 +1096,13 @@ impl MerkleTree

{ for depth in 1..leaf_depth { for &path_idx in on_path[depth].iter() { let sibling_idx = path_idx ^ 1; - if !on_path[depth].contains(&sibling_idx) { - if let Some(sibling_digest) = inner_candidates.get(&(depth, sibling_idx)) { - inner_copath.push((depth, sibling_idx, sibling_digest.clone())); - } else { - return Err(crate::Error::IncorrectInputLength(self.leaf_nodes.len())); - } + if !contains_sorted(&on_path[depth], sibling_idx) { + let heap_idx = level_index(depth, sibling_idx); + let sibling_digest = self + .non_leaf_nodes + .get(heap_idx) + .ok_or_else(|| crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()))?; + inner_copath.push((depth, sibling_idx, sibling_digest.clone())); } } } @@ -1059,10 +1115,10 @@ impl MerkleTree

{ leaf_indexes: Vec::from_iter(indexes), leaf_copath, inner_copath, - // legacy fields still populated for tests - leaf_siblings_hashes, - auth_paths_prefix_lenghts, - auth_paths_suffixes, + // legacy prefix data intentionally omitted for compact proofs + leaf_siblings_hashes: Vec::new(), + auth_paths_prefix_lenghts: Vec::new(), + auth_paths_suffixes: Vec::new(), }) } @@ -1264,19 +1320,41 @@ where } } + /// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. /// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. -fn compute_on_path(depth_leaves: usize, indexes: &ark_std::collections::BTreeSet) - -> Vec> -{ - use ark_std::collections::BTreeSet; - let mut path_sets = vec![BTreeSet::new(); depth_leaves + 1]; - for &leaf_index in indexes.iter() { - for depth in 0..=depth_leaves { - let shift = depth_leaves - depth; - let path_index = if shift == 0 { leaf_index } else { leaf_index >> shift }; - path_sets[depth].insert(path_index); +/// +/// Implementation detail: +/// * Uses sorted `Vec` per level to keep the hot loops linear and cache-friendly. +/// * Each leaf contributes one index per depth; we divide by 2 as we walk up and then sort+dedup. +fn compute_on_path( + depth_leaves: usize, + indexes: &ark_std::collections::BTreeSet, +) -> Vec> { + // collect raw indices per depth + let mut path_sets: Vec> = vec![Vec::new(); depth_leaves + 1]; + for &leaf_index in indexes { + let mut idx = leaf_index; + let mut depth = depth_leaves; + loop { + path_sets[depth].push(idx); + if depth == 0 { + break; + } + idx >>= 1; + depth -= 1; } } + + // sort + dedup each level to get canonical, unique, ascending order + for level in 0..=depth_leaves { + let level_vec = &mut path_sets[level]; + level_vec.sort_unstable(); + level_vec.dedup(); + } path_sets } + +fn contains_sorted(haystack: &[usize], needle: usize) -> bool { + haystack.binary_search(&needle).is_ok() +} diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs new file mode 100644 index 00000000..92656055 --- /dev/null +++ b/crypto-primitives/src/merkle_tree/tests/bench_report.rs @@ -0,0 +1,690 @@ +use crate::merkle_tree::{ + tests::test_utils::poseidon_parameters, Config, IdentityDigestConverter, LeafParam, + MerkleTree, MultiPath, MultiPathV2, MultiPathV2Bench, Path as MerklePath, TwoToOneParam, +}; +use ark_ed_on_bls12_381::Fr; +use ark_serialize::CanonicalSerialize; +use ark_std::{ + rand::{rngs::StdRng, Rng, SeedableRng}, + UniformRand, +}; +use plotters::prelude::*; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, + time::{Duration, SystemTime}, +}; + +type F = Fr; +type H = crate::crh::poseidon::CRH; +type TwoToOneH = crate::crh::poseidon::TwoToOneCRH; + +struct FieldMTConfig; +impl Config for FieldMTConfig { + type Leaf = [F]; + type LeafDigest = F; + type LeafInnerDigestConverter = IdentityDigestConverter; + type InnerDigest = F; + type LeafHash = H; + type TwoToOneHash = TwoToOneH; +} + +type FieldMT = MerkleTree; + +const TREE_EXPONENTS: &[u32] = &[12, 14, 16, 18, 20]; +const BATCH_SIZES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; +const LEAF_WIDTH: usize = 3; + +#[derive(Clone, Copy)] +enum IndexPattern { + Random, + Clustered, + Adversarial, +} + +impl IndexPattern { + fn label(self) -> &'static str { + match self { + IndexPattern::Random => "random", + IndexPattern::Clustered => "clustered", + IndexPattern::Adversarial => "adversarial", + } + } + + fn id(self) -> u64 { + match self { + IndexPattern::Random => 0, + IndexPattern::Clustered => 1, + IndexPattern::Adversarial => 2, + } + } +} + +struct TreeFixture { + leaves: Vec>, + tree: FieldMT, + leaf_params: LeafParam, + two_to_one_params: TwoToOneParam, +} + +#[derive(CanonicalSerialize)] +struct NoPruneBatch { + paths: Vec>, +} + +struct ReportRow { + tree_size: usize, + log2_size: u32, + batch: usize, + pattern: &'static str, + strategy: &'static str, + proof_bytes: usize, + proof_nodes: usize, + hashes_per_opening: f64, + prove_ms: f64, + verify_ms: f64, + rss_delta_kb: Option, +} + +struct PlotMetric { + name: &'static str, + filename_prefix: &'static str, + y_label: &'static str, + value: fn(&ReportRow) -> Option, +} + +trait ProofStats { + fn opened(&self) -> usize; + fn total_nodes(&self) -> usize; +} + +impl ProofStats for MultiPath

{ + fn opened(&self) -> usize { + self.leaf_indexes.len() + } + + fn total_nodes(&self) -> usize { + let auth_len: usize = self + .auth_paths_suffixes + .iter() + .map(|path| path.len()) + .sum(); + self.leaf_siblings_hashes.len() + auth_len + } +} + +impl ProofStats for MultiPathV2Bench

{ + fn opened(&self) -> usize { + self.leaf_indexes.len() + } + + fn total_nodes(&self) -> usize { + let inner = self + .inner_copath + .as_ref() + .map(|(_, _, _, digests)| digests.len()) + .unwrap_or(0); + self.leaf_copath.len() + inner + } +} + +impl ProofStats for MultiPathV2

{ + fn opened(&self) -> usize { + self.leaf_indexes.len() + } + + fn total_nodes(&self) -> usize { + self.leaf_copath.len() + self.inner_copath.len() + } +} + +impl ProofStats for NoPruneBatch

{ + fn opened(&self) -> usize { + self.paths.len() + } + + fn total_nodes(&self) -> usize { + self.paths + .iter() + .map(|path| 1 + path.auth_path.len()) + .sum() + } +} + +const PLOT_METRICS: &[PlotMetric] = &[ + PlotMetric { + name: "Proof Size", + filename_prefix: "proof_size", + y_label: "proof size (bytes)", + value: |row: &ReportRow| Some(row.proof_bytes as f64), + }, + PlotMetric { + name: "Proof Nodes", + filename_prefix: "proof_nodes", + y_label: "proof nodes", + value: |row: &ReportRow| Some(row.proof_nodes as f64), + }, + PlotMetric { + name: "Hashes Per Opening", + filename_prefix: "hashes_per_opening", + y_label: "hashes per opened leaf", + value: |row: &ReportRow| Some(row.hashes_per_opening), + }, + PlotMetric { + name: "Proving Time", + filename_prefix: "prove_ms", + y_label: "prove time (ms)", + value: |row: &ReportRow| Some(row.prove_ms), + }, + PlotMetric { + name: "Verification Time", + filename_prefix: "verify_ms", + y_label: "verify time (ms)", + value: |row: &ReportRow| Some(row.verify_ms), + }, + PlotMetric { + name: "RSS Delta", + filename_prefix: "rss_delta_kb", + y_label: "rss delta (kB)", + value: |row: &ReportRow| row.rss_delta_kb.map(|kb| kb as f64), + }, +]; + +#[test] +#[ignore] +fn multiproof_v2_benchmark_report() { + run_report().expect("benchmark report must succeed"); +} + +fn run_report() -> Result<(), Box> { + let mut fixtures = Vec::new(); + for &exp in TREE_EXPONENTS { + fixtures.push(build_fixture(exp)?); + } + + let patterns = [ + IndexPattern::Random, + IndexPattern::Clustered, + IndexPattern::Adversarial, + ]; + + let mut rows = Vec::new(); + for fixture in fixtures.iter() { + for &batch in BATCH_SIZES { + if batch > fixture.leaves.len() { + continue; + } + for &pattern in &patterns { + let mut scenario_rng = StdRng::seed_from_u64( + 0xC057_E771_u64 + ^ ((fixture.leaves.len() as u64) << 16) + ^ ((batch as u64) << 2) + ^ pattern.id(), + ); + let indexes = + sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); + rows.extend(run_scenario(fixture, batch, pattern, &indexes)?); + } + } + } + + let report_dir = PathBuf::from("target/merkle_tree_reports"); + fs::create_dir_all(&report_dir)?; + let plot_files = write_plots(&rows, &report_dir)?; + write_report(&rows, &report_dir, &plot_files)?; + Ok(()) +} + +fn run_scenario( + fixture: &TreeFixture, + batch: usize, + pattern: IndexPattern, + indexes: &[usize], +) -> Result<[ReportRow; 4], Box> { + let root = fixture.tree.root(); + let opened_leaves: Vec> = indexes.iter().map(|&i| fixture.leaves[i].clone()).collect(); + + let legacy_row = benchmark_strategy( + "prefix", + || fixture.tree.generate_multi_proof(indexes.iter().copied()), + |proof, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + leaves, + ) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + )?; + + let no_prune_row = benchmark_strategy( + "no_prune", + || { + let mut paths = Vec::with_capacity(indexes.len()); + for &idx in indexes.iter() { + paths.push(fixture.tree.generate_proof(idx)?); + } + Ok(NoPruneBatch { paths }) + }, + |proof: &NoPruneBatch<_>, leaves| { + if proof.paths.len() != leaves.len() { + return Err(crate::Error::IncorrectInputLength(proof.paths.len())); + } + for (path, leaf) in proof.paths.iter().zip(leaves.iter()) { + let ok = path.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + leaf.as_slice(), + )?; + if !ok { + return Ok(false); + } + } + Ok(true) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + )?; + + let coset_row = benchmark_strategy( + "coset_v2", + || { + fixture + .tree + .generate_multi_proof_v2(indexes.iter().copied()) + }, + |proof: &MultiPathV2<_>, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + leaves, + ) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + )?; + + let coset_bench_row = benchmark_strategy( + "coset_v2_bench", + || { + fixture + .tree + .generate_multi_proof_v2_bench(indexes.iter().copied()) + }, + |proof: &MultiPathV2Bench<_>, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + leaves, + ) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + )?; + + Ok([legacy_row, no_prune_row, coset_row, coset_bench_row]) +} + +fn benchmark_strategy( + strategy: &'static str, + mut generator: Gen, + mut verifier: Verify, + opened_leaves: &[Vec], + tree_size: usize, + log2_size: u32, + batch: usize, + pattern: IndexPattern, +) -> Result> +where + Proof: CanonicalSerialize + ProofStats, + Gen: FnMut() -> Result, + Verify: FnMut(&Proof, Vec>) -> Result, +{ + let rss_before = rss_bytes(); + let prove_start = std::time::Instant::now(); + let proof = generator()?; + let prove_time = prove_start.elapsed(); + let prove_rss = rss_delta_kb(rss_before, rss_bytes()); + + let proof_bytes = serialized_size(&proof); + let proof_nodes = proof.total_nodes(); + let opened = proof.opened().max(1); + let hashes_per_opening = proof_nodes as f64 / opened as f64; + + let verify_input = opened_leaves.to_vec(); + let rss_before_verify = rss_bytes(); + let verify_start = std::time::Instant::now(); + let verify_ok = verifier(&proof, verify_input.clone())?; + let verify_time = verify_start.elapsed(); + let verify_rss = rss_delta_kb(rss_before_verify, rss_bytes()); + assert!(verify_ok, "verification must succeed for {}", strategy); + + let row = ReportRow { + tree_size, + log2_size, + batch, + pattern: pattern.label(), + strategy, + proof_bytes, + proof_nodes, + hashes_per_opening, + prove_ms: duration_ms(prove_time), + verify_ms: duration_ms(verify_time), + rss_delta_kb: combine_rss(prove_rss, verify_rss), + }; + + Ok(row) +} + +fn sample_indexes( + pattern: IndexPattern, + batch: usize, + num_leaves: usize, + rng: &mut StdRng, +) -> Vec { + match pattern { + IndexPattern::Random => { + let mut set = BTreeSet::new(); + while set.len() < batch { + let idx = rng.gen_range(0..num_leaves); + set.insert(idx); + } + set.into_iter().collect() + } + IndexPattern::Clustered => { + let max_start = num_leaves.saturating_sub(batch); + let start = rng.gen_range(0..=max_start); + (start..start + batch).collect() + } + IndexPattern::Adversarial => { + if batch >= num_leaves { + return (0..num_leaves).collect(); + } + let step = num_leaves / batch; + (0..batch).map(|i| (i * step) % num_leaves).collect() + } + } +} + +fn build_fixture(exp: u32) -> Result> { + let leaf_params = poseidon_parameters(); + let two_to_one_params = leaf_params.clone(); + + let num_leaves = 1usize << exp; + let mut rng = StdRng::seed_from_u64(0x5EED_C0DE_u64 ^ (exp as u64)); + let leaves = sample_leaves(num_leaves, &mut rng); + + let tree = FieldMT::new(&leaf_params, &two_to_one_params, &leaves).unwrap(); + + Ok(TreeFixture { + leaves, + tree, + leaf_params, + two_to_one_params, + }) +} + +fn sample_leaves(count: usize, rng: &mut StdRng) -> Vec> { + (0..count) + .map(|_| (0..LEAF_WIDTH).map(|_| F::rand(rng)).collect()) + .collect() +} + +fn write_report( + rows: &[ReportRow], + report_dir: &Path, + plot_files: &BTreeMap<&'static str, Vec>, +) -> Result<(), Box> { + let report_path = report_dir.join("multiproof_v2_report.md"); + let mut file = File::create(&report_path)?; + + writeln!(file, "# Merkle Tree Multiproof Benchmark Report")?; + writeln!( + file, + "\nGenerated: {:?}\n", + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? + )?; + writeln!( + file, + "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms | rss_delta_kb |" + )?; + writeln!( + file, + "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- | ------------ |" + )?; + + for row in rows { + writeln!( + file, + "| {} | {} | {} | {} | {} | {} | {} | {:.2} | {:.2} | {:.2} | {} |", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + row.hashes_per_opening, + row.prove_ms, + row.verify_ms, + row.rss_delta_kb + .map(|kb| kb.to_string()) + .unwrap_or_else(|| "-".into()) + )?; + } + + for (metric, files) in plot_files { + if files.is_empty() { + continue; + } + writeln!(file, "\n## {} Visualizations\n", metric)?; + for plot in files { + writeln!(file, "![{}]({})", metric.replace(' ', "-").to_lowercase(), plot)?; + } + } + + Ok(()) +} + +fn write_plots( + rows: &[ReportRow], + report_dir: &Path, +) -> Result>, Box> { + let mut outputs: BTreeMap<&'static str, Vec> = BTreeMap::new(); + + for metric in PLOT_METRICS { + let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = + BTreeMap::new(); + + for row in rows { + if let Some(value) = (metric.value)(row) { + grouped + .entry((row.tree_size, row.pattern)) + .or_default() + .entry(row.strategy) + .or_default() + .push((row.batch as f64, value)); + } + } + + let mut generated = Vec::new(); + for ((tree_size, pattern), strategies) in grouped { + let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); + for &name in &["prefix", "no_prune", "coset_v2", "coset_v2_bench"] { + if let Some(mut series) = strategies.get(name).cloned() { + if series.is_empty() { + continue; + } + series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + ordered_series.push((name, series)); + } + } + + if ordered_series.len() < 2 + || !ordered_series.iter().any(|(name, _)| *name == "prefix") + { + continue; + } + + let mut min_x = f64::MAX; + let mut max_x = f64::MIN; + let mut min_y = f64::MAX; + let mut max_y = f64::MIN; + + for (_, series) in &ordered_series { + for &(x, y) in series { + min_x = min_x.min(x); + max_x = max_x.max(x); + min_y = min_y.min(y); + max_y = max_y.max(y); + } + } + if min_x == f64::MAX || min_y == f64::MAX { + continue; + } + + let x_pad = ((max_x - min_x) * 0.05).max(1.0); + let y_pad = ((max_y - min_y) * 0.05).max(1.0); + + let filename = format!( + "{}_{}_{}.svg", + metric.filename_prefix, tree_size, pattern + ); + let filepath = report_dir.join(&filename); + let filepath_str = filepath.to_string_lossy().to_string(); + let drawing_area = SVGBackend::new(&filepath_str, (960, 540)).into_drawing_area(); + drawing_area.fill(&WHITE)?; + + let mut chart = ChartBuilder::on(&drawing_area) + .caption( + format!("{} vs k (n={}, pattern={})", metric.name, tree_size, pattern), + ("sans-serif", 26), + ) + .margin(20) + .x_label_area_size(45) + .y_label_area_size(70) + .build_cartesian_2d( + (min_x - x_pad)..(max_x + x_pad), + (min_y - y_pad)..(max_y + y_pad), + )?; + + chart + .configure_mesh() + .x_desc("batch size (k)") + .y_desc(metric.y_label) + .draw()?; + + for (name, series) in &ordered_series { + let color = strategy_color(name); + chart + .draw_series(LineSeries::new(series.clone(), color.clone()))? + .label(strategy_label(name)) + .legend({ + let color = color.clone(); + move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color.clone()) + }); + } + + chart.configure_series_labels().border_style(&BLACK).draw()?; + + generated.push(filename); + } + + outputs.insert(metric.name, generated); + } + + Ok(outputs) +} + +fn strategy_label(name: &str) -> &str { + match name { + "prefix" => "prefix", + "no_prune" => "no pruning", + "coset_v2" => "coset v2", + "coset_v2_bench" => "coset v2 (bench)", + _ => name, + } +} + +fn strategy_color(name: &str) -> RGBColor { + match name { + "prefix" => RED, + "no_prune" => RGBColor(255, 127, 14), + "coset_v2" => BLUE, + "coset_v2_bench" => GREEN, + _ => BLACK, + } +} + +fn rss_bytes() -> Option { + #[cfg(target_os = "linux")] + { + let data = fs::read_to_string("/proc/self/status").ok()?; + for line in data.lines() { + if let Some(rest) = line.strip_prefix("VmRSS:") { + let kb: u64 = rest + .split_whitespace() + .nth(1) + .and_then(|v| v.parse().ok()) + .unwrap_or(0); + return Some(kb * 1024); + } + } + None + } + #[cfg(not(target_os = "linux"))] + { + None + } +} + +fn rss_delta_kb(before: Option, after: Option) -> Option { + match (before, after) { + (Some(b), Some(a)) => Some(((a as i64) - (b as i64)) / 1024), + _ => None, + } +} + +fn combine_rss(a: Option, b: Option) -> Option { + match (a, b) { + (Some(x), Some(y)) => Some(x + y), + _ => a.or(b), + } +} + +fn serialized_size(value: &T) -> usize { + let mut buf = Vec::new(); + value + .serialize_uncompressed(&mut buf) + .expect("serialization must succeed"); + buf.len() +} + +fn duration_ms(duration: Duration) -> f64 { + duration.as_secs_f64() * 1000.0 +} + +impl TreeFixture { + fn log2_size(&self) -> u32 { + (self.leaves.len() as f64).log2().round() as u32 + } +} diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index 393b99d6..bbfe8aa5 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -2,6 +2,8 @@ mod constraints; mod test_utils; +mod bench_report; + mod bytes_mt_tests { use crate::{crh::*, merkle_tree::*}; diff --git a/crypto-primitives/src/merkle_tree/v2_bench.rs b/crypto-primitives/src/merkle_tree/v2_bench.rs new file mode 100644 index 00000000..2667e9fa --- /dev/null +++ b/crypto-primitives/src/merkle_tree/v2_bench.rs @@ -0,0 +1,376 @@ +use core::convert::TryFrom; + +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +#[cfg(not(feature = "std"))] +use ark_std::vec::Vec; +use ark_std::{ + borrow::Borrow, + collections::{BTreeMap, BTreeSet}, + hash::BuildHasherDefault, +}; +use hashbrown::HashMap; + +use super::{ + compute_on_path, level_index, Config, DigestConverter, LeafParam, MerkleTree, TwoToOneParam, + DefaultHasher, +}; +use crate::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + Error, +}; + +type PackedInnerCopath

= (usize, usize, Vec, Vec<

::InnerDigest>); + +/// CoSet proof used for benchmark data +#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] +#[derivative( + Clone(bound = "P: Config"), + Debug(bound = "P: Config"), + Default(bound = "P: Config") +)] +pub struct MultiPathV2Bench { + pub tree_height: usize, + pub leaf_indexes: Vec, + pub leaf_copath: Vec, + /// Inner co-path encoded as (start_depth, start_index, packed deltas, digests). + /// `None` means there are no inner-layer digests required + pub inner_copath: Option>, +} + +impl MultiPathV2Bench

{ + /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. + /// Note that the order of the leaves hashes should match the leaves respective indexes + /// * `leaf_size`: leaf size in number of bytes + /// + /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` + pub fn verify + Clone>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + leaves: impl IntoIterator, + ) -> Result { + if self.tree_height < 2 { + return Ok(false); + } + + // TODO: when multi-proof logic is overhauled, clarify the semantics for empty + // batches (this index access panics if `leaf_indexes` is empty) + // accept valid batch of size 0 proof without path work + if self.leaf_indexes.is_empty() { + return Ok(true); + } + + let d = self.tree_height; + let leaf_depth = d - 1; + + let mut leaves = leaves.into_iter(); + let mut leaf_level: BTreeMap = BTreeMap::new(); + for &idx in &self.leaf_indexes { + let leaf = leaves.next().ok_or_else(|| Error::IncorrectInputLength(self.leaf_indexes.len()))?; + let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; + leaf_level.insert(idx, leaf_hash); + } + if leaves.next().is_some() { + return Err(Error::IncorrectInputLength(self.leaf_indexes.len())); + } + + // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j + let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); + let on_path = compute_on_path(leaf_depth, &index_set); + + // compute minimal copath at leaf layer (B*_{d-1}) + let mut expected_leaf_coset: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { + expected_leaf_coset.push(sibling_idx); // copath element needed for proof + } + } + expected_leaf_coset.sort_unstable(); // canonical order + + if expected_leaf_coset.len() != self.leaf_copath.len() { + return Ok(false); + } + + for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { + match leaf_level.get(&sibling_idx) { + Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one + _ => { + leaf_level.insert(sibling_idx, sibling_digest.clone()); + } + } + } + + // let mut inner_levels: Vec> = (0..d).map(|_| CoSetLevel::new()).collect(); + // prepare inner-level maps for non-on-path siblings and computed parents + let mut inner_levels: Vec> = + (0..d).map(|_| BTreeMap::new()).collect(); + + let mut hash_lut: HashMap = + HashMap::with_hasher(BuildHasherDefault::::default()); + + if let Some((start_depth, start_index, deltas, digests)) = &self.inner_copath { + if digests.is_empty() { + if !deltas.is_empty() { + return Ok(false); + } + } else { + let mut depth = match i64::try_from(*start_depth) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + let mut index = match i64::try_from(*start_index) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + let mut cursor = 0usize; + let mut prev_coord: Option<(usize, usize)> = None; + let mut push_entry = |depth_i64: i64, + index_i64: i64, + digest: &P::InnerDigest| + -> bool { + let depth_usize = match usize::try_from(depth_i64) { + Ok(v) => v, + Err(_) => return false, + }; + let index_usize = match usize::try_from(index_i64) { + Ok(v) => v, + Err(_) => return false, + }; + if depth_usize == 0 || depth_usize >= d { + return false; + } + if let Some((pd, pi)) = prev_coord { + if (depth_usize, index_usize) < (pd, pi) { + return false; + } + } + if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { + if existing != digest { + return false; + } + } else { + inner_levels[depth_usize].insert(index_usize, digest.clone()); + } + let heap_idx = level_index(depth_usize, index_usize); + hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); + prev_coord = Some((depth_usize, index_usize)); + true + }; + + if !push_entry(depth, index, &digests[0]) { + return Ok(false); + } + + for digest in digests.iter().skip(1) { + let depth_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return Ok(false), + }; + let index_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return Ok(false), + }; + depth = match depth.checked_add(depth_delta) { + Some(value) => value, + None => return Ok(false), + }; + index = match index.checked_add(index_delta) { + Some(value) => value, + None => return Ok(false), + }; + if !push_entry(depth, index, digest) { + return Ok(false); + } + } + + if cursor != deltas.len() { + return Ok(false); + } + } + } + + // Recomputation + // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs + for &parent_index in on_path[leaf_depth - 1].iter() { + let left = leaf_level.get(&(parent_index * 2)).cloned(); + let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::evaluate( + two_to_one_params, + P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type + P::LeafInnerDigestConverter::convert(right)?, + )?; + inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(leaf_depth - 1, parent_index); + hash_lut.insert(heap_idx, parent); + } + + // compute inner layers up to root using TwoToOne::compress to hash inner digests + for depth in (1..=leaf_depth - 1).rev() { + let parent_depth = depth - 1; + for &parent_index in on_path[parent_depth].iter() { + let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); + let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::compress( + two_to_one_params, + &left, &right, + )?; + inner_levels[parent_depth].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(parent_depth, parent_index); + hash_lut.insert(heap_idx, parent); + } + } + + match inner_levels[0].get(&0) { + Some(h) => { + Ok(h == root_hash) + } + None => Ok(false), + } + } +} + +impl MerkleTree

{ + pub fn generate_multi_proof_v2_bench( + &self, + indexes: impl IntoIterator, + ) -> Result, Error> { + // pruned and sorted for encoding efficiency + let indexes: BTreeSet = indexes.into_iter().collect(); + let d = self.height(); + + if indexes.is_empty() { + return Ok(MultiPathV2Bench { + tree_height: d, + leaf_indexes: Vec::new(), + leaf_copath: Vec::new(), + inner_copath: None, + }); + } + + let leaf_depth = d - 1; + // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j + let on_path = compute_on_path(leaf_depth, &indexes); + + // leaf layer (depth = d-1) + let mut leaf_coset_ids: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { + leaf_coset_ids.push(sibling_idx); + } + } + leaf_coset_ids.sort_unstable(); + + let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); + for &sibling_idx in &leaf_coset_ids { + let sibling_digest = self + .leaf_nodes + .get(sibling_idx) + .ok_or_else(|| Error::IncorrectInputLength(self.leaf_nodes.len()))?; + leaf_copath.push(sibling_digest.clone()); + } + + // inner layers (depth 1..d-2) + let mut inner_copath_entries: Vec<(usize, usize, P::InnerDigest)> = Vec::new(); + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[depth].binary_search(&sibling_idx).is_err() { + let heap_idx = level_index(depth, sibling_idx); + let sibling_digest = self + .non_leaf_nodes + .get(heap_idx) + .ok_or_else(|| Error::IncorrectInputLength(self.non_leaf_nodes.len()))?; + inner_copath_entries.push((depth, sibling_idx, sibling_digest.clone())); + } + } + } + // canonicalise order + inner_copath_entries.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); + let inner_copath = pack_inner_copath::

(&inner_copath_entries); + + Ok(MultiPathV2Bench { + tree_height: d, + leaf_indexes: Vec::from_iter(indexes), + leaf_copath, + inner_copath, + }) + } +} + +fn pack_inner_copath( + entries: &[(usize, usize, P::InnerDigest)], +) -> Option> { + if entries.is_empty() { + return None; + } + + let first = &entries[0]; + let mut deltas = Vec::new(); + let mut digests = Vec::with_capacity(entries.len()); + let mut prev_depth = i64::try_from(first.0).ok()?; + let mut prev_index = i64::try_from(first.1).ok()?; + digests.push(first.2.clone()); + + for &(depth, index, ref digest) in entries.iter().skip(1) { + let depth_i64 = i64::try_from(depth).ok()?; + let index_i64 = i64::try_from(index).ok()?; + encode_delta(&mut deltas, depth_i64 - prev_depth); + encode_delta(&mut deltas, index_i64 - prev_index); + digests.push(digest.clone()); + prev_depth = depth_i64; + prev_index = index_i64; + } + + Some((first.0, first.1, deltas, digests)) +} + +fn encode_delta(buffer: &mut Vec, value: i64) { + let zigzag = ((value << 1) ^ (value >> 63)) as u64; + encode_varint(buffer, zigzag); +} + +fn decode_delta(bytes: &[u8], cursor: &mut usize) -> Option { + let raw = decode_varint(bytes, cursor)?; + Some(((raw >> 1) as i64) ^ (-((raw & 1) as i64))) +} + +fn encode_varint(buffer: &mut Vec, mut value: u64) { + while value >= 0x80 { + buffer.push(((value as u8) & 0x7F) | 0x80); + value >>= 7; + } + buffer.push(value as u8); +} + +fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { + let mut value = 0u64; + let mut shift = 0u32; + + while *cursor < bytes.len() { + let byte = bytes[*cursor]; + *cursor += 1; + value |= ((byte & 0x7F) as u64) << shift; + if byte & 0x80 == 0 { + return Some(value); + } + shift += 7; + if shift >= 64 { + return None; + } + } + + None +} From 1429c0613894c39ebd1b03736c6cd85d0e64e5a6 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Sun, 30 Nov 2025 20:31:50 +0100 Subject: [PATCH 06/22] merkle_tree: integrate finalised strategy into prover and verifier API --- crypto-primitives/Cargo.toml | 1 + crypto-primitives/src/merkle_tree/bench.rs | 816 ++++++++++++++++ crypto-primitives/src/merkle_tree/mod.rs | 904 +++++++----------- .../src/merkle_tree/tests/bench_report.rs | 211 ++-- .../src/merkle_tree/tests/mod.rs | 250 +++-- crypto-primitives/src/merkle_tree/v2_bench.rs | 376 -------- 6 files changed, 1374 insertions(+), 1184 deletions(-) create mode 100644 crypto-primitives/src/merkle_tree/bench.rs delete mode 100644 crypto-primitives/src/merkle_tree/v2_bench.rs diff --git a/crypto-primitives/Cargo.toml b/crypto-primitives/Cargo.toml index 4c0e7159..70036e4a 100644 --- a/crypto-primitives/Cargo.toml +++ b/crypto-primitives/Cargo.toml @@ -54,6 +54,7 @@ crh = ["sponge"] sponge = ["merlin"] commitment = ["crh"] merkle_tree = ["crh", "hashbrown"] +bench_harness = [] encryption = [] prf = [] snark = [] diff --git a/crypto-primitives/src/merkle_tree/bench.rs b/crypto-primitives/src/merkle_tree/bench.rs new file mode 100644 index 00000000..440f105a --- /dev/null +++ b/crypto-primitives/src/merkle_tree/bench.rs @@ -0,0 +1,816 @@ +#![allow(clippy::needless_range_loop)] +#![allow(dead_code)] + +/// Defines a trait to chain two types of CRHs. +use crate::{ + crh::{CRHScheme, TwoToOneCRHScheme}, + sponge::Absorb, + Error, +}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +#[cfg(not(feature = "std"))] +use ark_std::vec::Vec; +use ark_std::{ + borrow::Borrow, + collections::BTreeSet, + fmt::Debug, + hash::{BuildHasherDefault, Hash}, +}; +use hashbrown::HashMap; +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +#[cfg(feature = "constraints")] +pub mod constraints; + +#[cfg(all( + target_has_atomic = "8", + target_has_atomic = "16", + target_has_atomic = "32", + target_has_atomic = "64", + target_has_atomic = "ptr" +))] +type DefaultHasher = ahash::AHasher; + +#[cfg(not(all( + target_has_atomic = "8", + target_has_atomic = "16", + target_has_atomic = "32", + target_has_atomic = "64", + target_has_atomic = "ptr" +)))] +type DefaultHasher = fnv::FnvHasher; + +/// Convert the hash digest in different layers by converting previous layer's output to +/// `TargetType`, which is a `Borrow` to next layer's input. +pub trait DigestConverter { + type TargetType: Borrow; + fn convert(item: From) -> Result; +} + +/// A trivial converter where digest of previous layer's hash is the same as next layer's input. +pub struct IdentityDigestConverter { + _prev_layer_digest: T, +} + +impl DigestConverter for IdentityDigestConverter { + type TargetType = T; + fn convert(item: T) -> Result { + Ok(item) + } +} + +/// Convert previous layer's digest to bytes and use bytes as input for next layer's digest. +/// TODO: `ToBytes` trait will be deprecated in future versions. +pub struct ByteDigestConverter { + _prev_layer_digest: T, +} + +impl DigestConverter for ByteDigestConverter { + type TargetType = Vec; + + fn convert(item: T) -> Result { + // TODO: In some tests, `serialize` is not consistent with constraints. Try fix those. + Ok(crate::to_uncompressed_bytes!(item)?) + } +} + +/// Merkle tree has two types of hashes. +/// * `LeafHash`: Convert leaf to leaf digest +/// * `TwoToOneHash`: Compress two inner digests to one inner digest +pub trait Config { + type Leaf: ?Sized + Send; // merkle tree does not store the leaf + // leaf layer + type LeafDigest: Clone + + Eq + + Debug + + Hash + + Default + + CanonicalSerialize + + CanonicalDeserialize + + Send + + Sync; + + // transition between leaf layer to inner layer + type LeafInnerDigestConverter: DigestConverter< + Self::LeafDigest, + ::Input, + >; + // inner layer + type InnerDigest: Clone + + Eq + + Debug + + Hash + + Default + + CanonicalSerialize + + CanonicalDeserialize + + Send + + Sync + + Absorb; + + // 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. + + /// leaf -> leaf digest + /// If leaf hash digest and inner hash digest are different, we can create a new + /// leaf hash which wraps the original leaf hash and convert its output to `Digest`. + type LeafHash: CRHScheme; + /// 2 inner digest -> inner digest + type TwoToOneHash: TwoToOneCRHScheme; +} + +pub type TwoToOneParam

= <

::TwoToOneHash as TwoToOneCRHScheme>::Parameters; +pub type LeafParam

= <

::LeafHash as CRHScheme>::Parameters; + +/// Stores the hashes of a particular path (in order) from root to leaf. +/// For example: +/// ```tree_diagram +/// [A] +/// / \ +/// [B] C +/// / \ / \ +/// D [E] F H +/// .. / \ .... +/// [I] J +/// ``` +/// Suppose we want to prove I, then `leaf_sibling_hash` is J, `auth_path` is `[C,D]` +#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] +#[derivative( + PartialEq(bound = "P: Config"), + Clone(bound = "P: Config"), + Debug(bound = "P: Config"), + Default(bound = "P: Config") +)] +pub struct Path { + pub leaf_sibling_hash: P::LeafDigest, + /// The sibling of path node ordered from higher layer to lower layer (does not include root node). + pub auth_path: Vec, + /// stores the leaf index of the node + pub leaf_index: usize, +} + +impl Path

{ + /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. + /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. + /// + /// This function simply converts `self.leaf_index` to boolean array in big endian form. + #[allow(unused)] // this function is actually used when r1cs feature is on + fn position_list(&'_ self) -> impl '_ + Iterator { + (0..self.auth_path.len() + 1) + .map(move |i| ((self.leaf_index >> i) & 1) != 0) + .rev() + } +} + +impl Path

{ + /// Verify that a leaf is at `self.index` of the merkle tree. + /// * `leaf_size`: leaf size in number of bytes + /// + /// `verify` infers the tree height by setting `tree_height = self.auth_path.len() + 2` + #[allow(dead_code)] + pub fn verify>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + leaf: L, + ) -> Result { + // calculate leaf hash + let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf)?; + // check hash along the path from bottom to root + let (left_child, right_child) = + select_left_right_child(self.leaf_index, &claimed_leaf_hash, &self.leaf_sibling_hash)?; + + // leaf layer to inner layer conversion + let left_child = P::LeafInnerDigestConverter::convert(left_child)?; + let right_child = P::LeafInnerDigestConverter::convert(right_child)?; + + let mut curr_path_node = + P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child)?; + + // we will use `index` variable to track the position of path + let mut index = self.leaf_index; + index >>= 1; + + // Check levels between leaf level and root + for level in (0..self.auth_path.len()).rev() { + // check if path node at this level is left or right + let (left, right) = + select_left_right_child(index, &curr_path_node, &self.auth_path[level])?; + // update curr_path_node + curr_path_node = P::TwoToOneHash::compress(&two_to_one_params, &left, &right)?; + index >>= 1; + } + + // check if final hash is root + if &curr_path_node != root_hash { + return Ok(false); + } + + Ok(true) + } +} + +/// Optimized data structure to store multiple nodes proofs. +/// For example: +/// ```tree_diagram +/// [A] +/// / \ +/// [B] C +/// / \ / \ +/// D [E] F H +/// ... / \ / \ .... +/// [I] J L M +/// ``` +/// Suppose we want to prove I and J, then: +/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) +/// `leaf_siblings_hashes`: `[J,I]` +/// `auth_paths_prefix_lenghts`: `[0,2]` +/// `auth_paths_suffixes`: `[ [C,D], []]` +/// We can reconstruct the paths incrementally: +/// First, we reconstruct the first path. The prefix length is 0, hence we do not have any prefix encoding. +/// The path is thus `[C,D]`. +/// Once the first path is verified, we can reconstruct the second path. +/// The prefix length of 2 means that the path prefix will be `previous_path[:2] -> [C,D]`. +/// Since the Merkle Tree branch is the same, the authentication path is the same (which means in this case that there is no suffix). +/// The second path is hence `[C,D] + []` (i.e., plus the empty suffix). We can verify the second path as the first one. + +#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] +#[derivative( + Clone(bound = "P: Config"), + Debug(bound = "P: Config"), + Default(bound = "P: Config") +)] +pub struct MultiPath { + /// For node i, stores the hash of node i's sibling + pub leaf_siblings_hashes: Vec, + /// For node i path, stores at index i the prefix length of the path, for Incremental encoding + pub auth_paths_prefix_lenghts: Vec, + /// For node i path, stores at index i the suffix of the path for Incremental Encoding (as vector of symbols to be resolved with self.lut). Order is from higher layer to lower layer (does not include root node). + pub auth_paths_suffixes: Vec>, + /// stores the leaf indexes of the nodes to prove + pub leaf_indexes: Vec, +} + +impl MultiPath

{ + /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. + /// Note that the order of the leaves hashes should match the leaves respective indexes + /// * `leaf_size`: leaf size in number of bytes + /// + /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` + pub fn verify + Clone>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + leaves: impl IntoIterator, + ) -> Result { + let tree_height = self.auth_paths_suffixes[0].len() + 2; + let mut leaves = leaves.into_iter(); + + // LookUp table to speedup computation avoid redundant hash computations + let mut hash_lut: HashMap = + HashMap::with_hasher(BuildHasherDefault::::default()); + + // init prev path for decoding + let mut prev_path: Vec<_> = self.auth_paths_suffixes[0].clone(); + + for i in 0..self.leaf_indexes.len() { + let leaf_index = self.leaf_indexes[i]; + let leaf = leaves.next().unwrap(); + let leaf_sibling_hash = &self.leaf_siblings_hashes[i]; + + // decode i-th auth path + let auth_path = prefix_decode_path( + &prev_path, + self.auth_paths_prefix_lenghts[i], + &self.auth_paths_suffixes[i], + ); + // update prev path for decoding next one + prev_path = auth_path.clone(); + + let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf.clone())?; + let (left_child, right_child) = + select_left_right_child(leaf_index, &claimed_leaf_hash, &leaf_sibling_hash)?; + // check hash along the path from bottom to root + + // leaf layer to inner layer conversion + let left_child = P::LeafInnerDigestConverter::convert(left_child)?; + let right_child = P::LeafInnerDigestConverter::convert(right_child)?; + + // we will use `index` variable to track the position of path + let mut index = leaf_index; + let mut index_in_tree = convert_index_to_last_level(leaf_index, tree_height); + index >>= 1; + index_in_tree = parent(index_in_tree).unwrap(); + + let mut curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { + P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap() + }); + + // Check levels between leaf level and root + for level in (0..auth_path.len()).rev() { + // check if path node at this level is left or right + let (left, right) = + select_left_right_child(index, curr_path_node, &auth_path[level])?; + // update curr_path_node + index >>= 1; + index_in_tree = parent(index_in_tree).unwrap(); + curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { + P::TwoToOneHash::compress(&two_to_one_params, left, right).unwrap() + }); + } + + // check if final hash is root + if curr_path_node != root_hash { + return Ok(false); + } + } + Ok(true) + } + + /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. + /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. + /// + /// This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. + #[allow(unused)] // this function is actually used when r1cs feature is on + fn position_list(&'_ self) -> impl '_ + Iterator> { + let path_len = self.auth_paths_suffixes[0].len(); + + cfg_into_iter!(self.leaf_indexes.clone()) + .map(move |i| { + (0..path_len + 1) + .map(move |j| ((i >> j) & 1) != 0) + .rev() + .collect() + }) + .collect::>() + .into_iter() + } +} + +/// `index` is the first `path.len()` bits of +/// the position of tree. +/// +/// If the least significant bit of `index` is 0, then `sibling` will be left and `computed` will be right. +/// Otherwise, `sibling` will be right and `computed` will be left. +/// +/// Returns: (left, right) +fn select_left_right_child( + index: usize, + computed_hash: &L, + sibling_hash: &L, +) -> Result<(L, L), crate::Error> { + let is_left = index & 1 == 0; + let mut left_child = computed_hash; + let mut right_child = sibling_hash; + if !is_left { + core::mem::swap(&mut left_child, &mut right_child); + } + Ok((left_child.clone(), right_child.clone())) +} + +/// Defines a merkle tree data structure. +/// This merkle tree has runtime fixed height, and assumes number of leaves is 2^height. +/// +/// TODO: add RFC-6962 compatible merkle tree in the future. +/// For this release, padding will not be supported because of security concerns: if the leaf hash and two to one hash uses same underlying +/// CRH, a malicious prover can prove a leaf while the actual node is an inner node. In the future, we can prefix leaf hashes in different layers to +/// solve the problem. +#[derive(Derivative)] +#[derivative(Clone(bound = "P: Config"))] +pub struct MerkleTree { + /// stores the non-leaf nodes in level order. The first element is the root node. + /// The ith nodes (starting at 1st) children are at indices `2*i`, `2*i+1` + non_leaf_nodes: Vec, + /// store the hash of leaf nodes from left to right + leaf_nodes: Vec, + /// Store the inner hash parameters + two_to_one_hash_param: TwoToOneParam

, + /// Store the leaf hash parameters + leaf_hash_param: LeafParam

, + /// Stores the height of the MerkleTree + height: usize, +} + +impl MerkleTree

{ + /// Create an empty merkle tree such that all leaves are zero-filled. + /// Consider using a sparse merkle tree if you need the tree to be low memory + pub fn blank( + leaf_hash_param: &LeafParam

, + two_to_one_hash_param: &TwoToOneParam

, + height: usize, + ) -> Result { + // use empty leaf digest + let leaf_digests = vec![P::LeafDigest::default(); 1 << (height - 1)]; + Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaf_digests) + } + + /// Returns a new merkle tree. `leaves.len()` should be power of two. + pub fn new + Send>( + leaf_hash_param: &LeafParam

, + two_to_one_hash_param: &TwoToOneParam

, + #[cfg(not(feature = "parallel"))] leaves: impl IntoIterator, + #[cfg(feature = "parallel")] leaves: impl IntoParallelIterator, + ) -> Result { + let leaf_digests: Vec<_> = cfg_into_iter!(leaves) + .map(|input| P::LeafHash::evaluate(leaf_hash_param, input.as_ref())) + .collect::, _>>()?; + + Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaf_digests) + } + + pub fn new_with_leaf_digest( + leaf_hash_param: &LeafParam

, + two_to_one_hash_param: &TwoToOneParam

, + leaf_digests: Vec, + ) -> Result { + let leaf_nodes_size = leaf_digests.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 = cfg_into_iter!(0..non_leaf_nodes_size) + .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 the hash values for the non-leaf bottom layer + { + let start_index = level_indices.pop().unwrap(); + let upper_bound = left_child(start_index); + + cfg_iter_mut!(non_leaf_nodes[start_index..upper_bound]) + .enumerate() + .try_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; + + *n = P::TwoToOneHash::evaluate( + two_to_one_hash_param, + P::LeafInnerDigestConverter::convert( + leaf_digests[left_leaf_index].clone(), + )?, + P::LeafInnerDigestConverter::convert( + leaf_digests[right_leaf_index].clone(), + )?, + )?; + Ok::<(), crate::Error>(()) + })?; + } + + // 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); + // Iterate over the nodes at the current level, and compute the hash of each node + cfg_iter_mut!(nodes_at_level[start_index..]) + .enumerate() + .try_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; + + // need for unwrap as Box does not implement trait Send + *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(), + )?; + Ok::<_, crate::Error>(()) + })?; + } + Ok(MerkleTree { + leaf_nodes: leaf_digests, + non_leaf_nodes, + height: tree_height, + leaf_hash_param: leaf_hash_param.clone(), + two_to_one_hash_param: two_to_one_hash_param.clone(), + }) + } + + /// Returns the root of the Merkle tree. + pub fn root(&self) -> P::InnerDigest { + self.non_leaf_nodes[0].clone() + } + + /// Returns the height of the Merkle tree. + pub fn height(&self) -> usize { + self.height + } + + /// Given the `index` of a leaf, returns the digest of its leaf sibling + pub fn get_leaf_sibling_hash(&self, index: usize) -> P::LeafDigest { + if index & 1 == 0 { + // leaf is left child + self.leaf_nodes[index + 1].clone() + } else { + // leaf is right child + self.leaf_nodes[index - 1].clone() + } + } + + /// Returns the authentication path from leaf at `index` to root, as a Vec of digests + fn compute_auth_path(&self, index: usize) -> Vec { + // gather basic tree information + let tree_height = tree_height(self.leaf_nodes.len()); + + // Get Leaf hash, and leaf sibling hash, + let leaf_index_in_tree = convert_index_to_last_level(index, tree_height); + + // path.len() = `tree height - 2`, the two missing elements being the leaf sibling hash and the root + let mut path = Vec::with_capacity(tree_height - 2); + // Iterate from the bottom layer after the leaves, to the top, storing all sibling node's hash values. + let mut current_node = parent(leaf_index_in_tree).unwrap(); + while !is_root(current_node) { + let sibling_node = sibling(current_node).unwrap(); + path.push(self.non_leaf_nodes[sibling_node].clone()); + current_node = parent(current_node).unwrap(); + } + + debug_assert_eq!(path.len(), tree_height - 2); + + // we want to make path from root to bottom + path.reverse(); + path + } + + /// Returns the authentication path from leaf at `index` to root. + pub fn generate_proof(&self, index: usize) -> Result, crate::Error> { + let path = self.compute_auth_path(index); + Ok(Path { + leaf_index: index, + auth_path: path, + leaf_sibling_hash: self.get_leaf_sibling_hash(index), + }) + } + + /// Returns a MultiPath (multiple authentication paths in compressed form, with Front Incremental Encoding), + /// from every leaf to root. + /// Note that for compression efficiency, the indexes are internally sorted. + /// For sorted indexes, MultiPath contains: + /// `2*( (num_leaves.log2()-1).pow(2) - (num_leaves.log2()-2) )` + /// instead of + /// `num_leaves*(num_leaves.log2()-1)` + /// When verifying the proof, leaves hashes should be supplied in order, that is: + /// ```ignore + /// let ordered_leaves: Vec<_> = self.leaf_indexes.into_iter().map(|i| leaves[i]).collect(); + /// ``` + pub fn generate_multi_proof( + &self, + indexes: impl IntoIterator, + ) -> Result, crate::Error> { + // pruned and sorted for encoding efficiency + let indexes: BTreeSet = indexes.into_iter().collect(); + + //let auth_paths = Vec::with_capacity(indexes.len()); + let mut auth_paths_prefix_lenghts: Vec = Vec::with_capacity(indexes.len()); + let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); + + let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); + + let mut prev_path = Vec::new(); + + for index in &indexes { + leaf_siblings_hashes.push(self.get_leaf_sibling_hash(*index)); + + let path = self.compute_auth_path(*index); + + // incremental encoding + let (prefix_len, suffix) = prefix_encode_path(&prev_path, &path); + auth_paths_prefix_lenghts.push(prefix_len); + auth_paths_suffixes.push(suffix); + prev_path = path; + } + + Ok(MultiPath { + leaf_indexes: Vec::from_iter(indexes), + auth_paths_prefix_lenghts, + auth_paths_suffixes, + leaf_siblings_hashes, + }) + } + + /// Given the index and new leaf, return the hash of leaf and an updated path in order from root to bottom non-leaf level. + /// This does not mutate the underlying tree. + fn updated_path>( + &self, + index: usize, + new_leaf: T, + ) -> Result<(P::LeafDigest, Vec), crate::Error> { + // calculate the hash of leaf + let new_leaf_hash: P::LeafDigest = P::LeafHash::evaluate(&self.leaf_hash_param, new_leaf)?; + + // calculate leaf sibling hash and locate its position (left or right) + let (leaf_left, leaf_right) = if index & 1 == 0 { + // leaf on left + (&new_leaf_hash, &self.leaf_nodes[index + 1]) + } else { + (&self.leaf_nodes[index - 1], &new_leaf_hash) + }; + + // calculate the updated hash at bottom non-leaf-level + let mut path_bottom_to_top = Vec::with_capacity(self.height - 1); + { + path_bottom_to_top.push(P::TwoToOneHash::evaluate( + &self.two_to_one_hash_param, + P::LeafInnerDigestConverter::convert(leaf_left.clone())?, + P::LeafInnerDigestConverter::convert(leaf_right.clone())?, + )?); + } + + // then calculate the updated hash from bottom to root + let leaf_index_in_tree = convert_index_to_last_level(index, self.height); + let mut prev_index = parent(leaf_index_in_tree).unwrap(); + while !is_root(prev_index) { + let (left_child, right_child) = if is_left_child(prev_index) { + ( + path_bottom_to_top.last().unwrap(), + &self.non_leaf_nodes[sibling(prev_index).unwrap()], + ) + } else { + ( + &self.non_leaf_nodes[sibling(prev_index).unwrap()], + path_bottom_to_top.last().unwrap(), + ) + }; + let evaluated = + P::TwoToOneHash::compress(&self.two_to_one_hash_param, left_child, right_child)?; + path_bottom_to_top.push(evaluated); + prev_index = parent(prev_index).unwrap(); + } + + debug_assert_eq!(path_bottom_to_top.len(), self.height - 1); + let path_top_to_bottom: Vec<_> = path_bottom_to_top.into_iter().rev().collect(); + Ok((new_leaf_hash, path_top_to_bottom)) + } + + /// Update the leaf at `index` to updated leaf. + /// ```tree_diagram + /// [A] + /// / \ + /// [B] C + /// / \ / \ + /// D [E] F H + /// .. / \ .... + /// [I] J + /// ``` + /// update(3, {new leaf}) would swap the leaf value at `[I]` and cause a recomputation of `[A]`, `[B]`, and `[E]`. + pub fn update(&mut self, index: usize, new_leaf: &P::Leaf) -> Result<(), crate::Error> { + assert!(index < self.leaf_nodes.len(), "index out of range"); + let (updated_leaf_hash, mut updated_path) = self.updated_path(index, new_leaf)?; + self.leaf_nodes[index] = updated_leaf_hash; + let mut curr_index = convert_index_to_last_level(index, self.height); + for _ in 0..self.height - 1 { + curr_index = parent(curr_index).unwrap(); + self.non_leaf_nodes[curr_index] = updated_path.pop().unwrap(); + } + Ok(()) + } + + /// Update the leaf and check if the updated root is equal to `asserted_new_root`. + /// + /// Tree will not be modified if the check fails. + pub fn check_update>( + &mut self, + index: usize, + new_leaf: &P::Leaf, + asserted_new_root: &P::InnerDigest, + ) -> Result { + assert!(index < self.leaf_nodes.len(), "index out of range"); + let (updated_leaf_hash, mut updated_path) = self.updated_path(index, new_leaf)?; + if &updated_path[0] != asserted_new_root { + return Ok(false); + } + self.leaf_nodes[index] = updated_leaf_hash; + let mut curr_index = convert_index_to_last_level(index, self.height); + for _ in 0..self.height - 1 { + curr_index = parent(curr_index).unwrap(); + self.non_leaf_nodes[curr_index] = updated_path.pop().unwrap(); + } + Ok(true) + } +} + +/// Returns the height of the tree, given the number of leaves. +#[inline] +fn tree_height(num_leaves: usize) -> usize { + if num_leaves == 1 { + return 1; + } + + (ark_std::log2(num_leaves) as usize) + 1 +} +/// Returns true iff the index represents the root. +#[inline] +fn is_root(index: usize) -> bool { + index == 0 +} + +/// Returns the index of the left child, given an index. +#[inline] +fn left_child(index: usize) -> usize { + 2 * index + 1 +} + +/// Returns the index of the right child, given an index. +#[inline] +fn right_child(index: usize) -> usize { + 2 * index + 2 +} + +/// Returns the index of the sibling, given an index. +#[inline] +fn sibling(index: usize) -> Option { + if index == 0 { + None + } else if is_left_child(index) { + Some(index + 1) + } else { + Some(index - 1) + } +} + +/// Returns true iff the given index represents a left child. +#[inline] +fn is_left_child(index: usize) -> bool { + index % 2 == 1 +} + +/// Returns the index of the parent, given an index. +#[inline] +fn parent(index: usize) -> Option { + if index > 0 { + Some((index - 1) >> 1) + } else { + None + } +} + +#[inline] +fn convert_index_to_last_level(index: usize, tree_height: usize) -> usize { + index + (1 << (tree_height - 1)) - 1 +} + +/// Encodes path with Incremental Encoding by comparing with prev_path +/// Returns the prefix length and the suffix to append during decoding +/// Example: +/// If `prev_path` is vec![C,D] and `path` is vec![C,E] (where C,D,E are hashes) +/// `prefix_encode_path` returns 1,vec![E] + +#[inline] +fn prefix_encode_path(prev_path: &Vec, path: &Vec) -> (usize, Vec) +where + T: Eq + Clone, +{ + let prefix_length = prev_path + .iter() + .zip(path.iter()) + .take_while(|(a, b)| a == b) + .count(); + + (prefix_length, path[prefix_length..].to_vec()) +} + +fn prefix_decode_path(prev_path: &Vec, prefix_len: usize, suffix: &Vec) -> Vec +where + T: Eq + Clone, +{ + if prefix_len == 0 { + suffix.clone() + } else { + vec![prev_path[0..prefix_len].to_vec(), suffix.clone()].concat() + } +} diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 56b5f035..f6c4ee05 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -1,5 +1,7 @@ #![allow(clippy::needless_range_loop)] +use core::convert::TryFrom; + /// Defines a trait to chain two types of CRHs. use crate::{ crh::{CRHScheme, TwoToOneCRHScheme}, @@ -25,9 +27,6 @@ pub mod constraints; #[cfg(test)] mod tests; -pub mod v2_bench; -pub use v2_bench::MultiPathV2Bench; - #[cfg(all( target_has_atomic = "8", target_has_atomic = "16", @@ -217,144 +216,6 @@ impl Path

{ } } -/// Optimized data structure to store multiple nodes proofs. -/// For example: -/// ```tree_diagram -/// [A] -/// / \ -/// [B] C -/// / \ / \ -/// D [E] F H -/// ... / \ / \ .... -/// [I] J L M -/// ``` -/// Suppose we want to prove I and J, then: -/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) -/// `leaf_siblings_hashes`: `[J,I]` -/// `auth_paths_prefix_lenghts`: `[0,2]` -/// `auth_paths_suffixes`: `[ [C,D], []]` -/// We can reconstruct the paths incrementally: -/// First, we reconstruct the first path. The prefix length is 0, hence we do not have any prefix encoding. -/// The path is thus `[C,D]`. -/// Once the first path is verified, we can reconstruct the second path. -/// The prefix length of 2 means that the path prefix will be `previous_path[:2] -> [C,D]`. -/// Since the Merkle Tree branch is the same, the authentication path is the same (which means in this case that there is no suffix). -/// The second path is hence `[C,D] + []` (i.e., plus the empty suffix). We can verify the second path as the first one. - -#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] -#[derivative( - Clone(bound = "P: Config"), - Debug(bound = "P: Config"), - Default(bound = "P: Config") -)] -pub struct MultiPath { - /// For node i, stores the hash of node i's sibling - pub leaf_siblings_hashes: Vec, - /// For node i path, stores at index i the prefix length of the path, for Incremental encoding - pub auth_paths_prefix_lenghts: Vec, - /// For node i path, stores at index i the suffix of the path for Incremental Encoding (as vector of symbols to be resolved with self.lut). Order is from higher layer to lower layer (does not include root node). - pub auth_paths_suffixes: Vec>, - /// stores the leaf indexes of the nodes to prove - pub leaf_indexes: Vec, -} - -impl MultiPath

{ - /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. - /// Note that the order of the leaves hashes should match the leaves respective indexes - /// * `leaf_size`: leaf size in number of bytes - /// - /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` - pub fn verify + Clone>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - leaves: impl IntoIterator, - ) -> Result { - let tree_height = self.auth_paths_suffixes[0].len() + 2; - let mut leaves = leaves.into_iter(); - - // LookUp table to speedup computation avoid redundant hash computations - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); - - // init prev path for decoding - let mut prev_path: Vec<_> = self.auth_paths_suffixes[0].clone(); - - for i in 0..self.leaf_indexes.len() { - let leaf_index = self.leaf_indexes[i]; - let leaf = leaves.next().unwrap(); - let leaf_sibling_hash = &self.leaf_siblings_hashes[i]; - - // decode i-th auth path - let auth_path = prefix_decode_path( - &prev_path, - self.auth_paths_prefix_lenghts[i], - &self.auth_paths_suffixes[i], - ); - // update prev path for decoding next one - prev_path = auth_path.clone(); - - let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf.clone())?; - let (left_child, right_child) = - select_left_right_child(leaf_index, &claimed_leaf_hash, &leaf_sibling_hash)?; - // check hash along the path from bottom to root - - // leaf layer to inner layer conversion - let left_child = P::LeafInnerDigestConverter::convert(left_child)?; - let right_child = P::LeafInnerDigestConverter::convert(right_child)?; - - // we will use `index` variable to track the position of path - let mut index = leaf_index; - let mut index_in_tree = convert_index_to_last_level(leaf_index, tree_height); - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - - let mut curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap() - }); - - // Check levels between leaf level and root - for level in (0..auth_path.len()).rev() { - // check if path node at this level is left or right - let (left, right) = - select_left_right_child(index, curr_path_node, &auth_path[level])?; - // update curr_path_node - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::compress(&two_to_one_params, left, right).unwrap() - }); - } - - // check if final hash is root - if curr_path_node != root_hash { - return Ok(false); - } - } - Ok(true) - } - - /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. - /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. - /// - /// This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. - #[allow(unused)] // this function is actually used when r1cs feature is on - fn position_list(&'_ self) -> impl '_ + Iterator> { - let path_len = self.auth_paths_suffixes[0].len(); - - cfg_into_iter!(self.leaf_indexes.clone()) - .map(move |i| { - (0..path_len + 1) - .map(move |j| ((i >> j) & 1) != 0) - .rev() - .collect() - }) - .collect::>() - .into_iter() - } -} - /// Optimized data structure to store multiple nodes proofs. /// For example: /// ```tree_diagram @@ -368,14 +229,9 @@ impl MultiPath

{ /// ``` /// Suppose we want to prove I and J, then: /// `tree_height`: `4` -/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) /// `leaf_copath`: `[]` -/// `inner_copath`: `[(2,0,D), (1,1,C)]` -/// -/// ---- Legacy fields ---- -/// `leaf_siblings_hashes`: `[J,I]` -/// `auth_paths_prefix_lenghts`: `[0,2]` -/// `auth_paths_suffixes`: `[ [C,D], []]` +/// `inner_copath`: `[(2,0,D), (1,1,C)]` (store packed as `(2,0,[1,+1],[D,C])`) +/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) /// /// We can reconstruct upfront the minimal copath needed for the proof: /// First, we reconstruct the minimal copath at the leaf layer (`depth = tree_height-1`). @@ -385,15 +241,6 @@ impl MultiPath

{ /// The inner copath digests are stored as (depth, index, digest) tuples ordered by (depth, index). /// Thus, inner copath is `[(2,0,D), (1,1,C)]`. /// Intuitively, CoSet transmits only what's missing to recompute every parent on the shared union-of-paths. -/// -/// ---- Legacy prefix encoding (kept for compatibility): ---- -/// We can reconstruct the paths incrementally: -/// First, we reconstruct the first path. The prefix length is 0, hence we do not have any prefix encoding. -/// The path is thus `[C,D]`. -/// Once the first path is verified, we can reconstruct the second path. -/// The prefix length of 2 means that the path prefix will be `previous_path[:2] -> [C,D]`. -/// Since the Merkle Tree branch is the same, the authentication path is the same (which means in this case that there is no suffix). -/// The second path is hence `[C,D] + []` (i.e., plus the empty suffix). We can verify the second path as the first one. #[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] #[derivative( @@ -401,65 +248,18 @@ impl MultiPath

{ Debug(bound = "P: Config"), Default(bound = "P: Config") )] -pub struct MultiPathV2 { - /// ---- MultiPathV2 new fields ---- +pub struct CoPath { /// stores the height of the tree (>= 2) to drive CoSet decoding pub tree_height: usize, - /// TODO: reorder leaf_indexes last in proof for legacy use compatibility - /// stores the leaf indexes of the nodes to prove - pub leaf_indexes: Vec, /// For leaf layer, stores co-path digests (B*_{d-1}) in ascending sibling index order pub leaf_copath: Vec, /// For inner layers, stores co-path entries packed as (start_depth, start_index, packed deltas, digests) pub inner_copath: Option>, - - /// ---- Legacy fields (kept for compatibility/tests) ---- - /// These vectors are left empty - /// For node i, stores the hash of node i's sibling - pub leaf_siblings_hashes: Vec, - /// For node i path, stores at index i the prefix length of the path, for Incremental encoding - pub auth_paths_prefix_lenghts: Vec, - /// For node i path, stores at index i the suffix of the path for Incremental Encoding (as vector of symbols to be resolved with self.lut). Order is from higher layer to lower layer (does not include root node). - pub auth_paths_suffixes: Vec>, -} - -#[allow(dead_code)] -struct CoSetLevel { - entries: Vec<(usize, P::InnerDigest)>, -} - -impl CoSetLevel

{ - #[allow(dead_code)] - fn new() -> Self { - Self { entries: Vec::new() } - } - - #[allow(dead_code)] - fn get(&self, idx: usize) -> Option<&P::InnerDigest> { - self.entries.iter().find(|(i, _)| *i == idx).map(|(_, d)| d) - } - - #[allow(dead_code)] - fn insert_or_check(&mut self, idx: usize, digest: &P::InnerDigest) -> bool { - if let Some((_, existing)) = self.entries.iter().find(|(i, _)| *i == idx) { - existing == digest - } else { - self.entries.push((idx, digest.clone())); - true - } - } - - #[allow(dead_code)] - fn set(&mut self, idx: usize, digest: P::InnerDigest) { - if let Some((_, existing)) = self.entries.iter_mut().find(|(i, _)| *i == idx) { - *existing = digest; - } else { - self.entries.push((idx, digest)); - } - } + /// stores the leaf indexes of the nodes to prove + pub leaf_indexes: Vec, } -impl MultiPathV2

{ +impl CoPath

{ /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. /// Note that the order of the leaves hashes should match the leaves respective indexes /// * `leaf_size`: leaf size in number of bytes @@ -472,278 +272,321 @@ impl MultiPathV2

{ root_hash: &P::InnerDigest, leaves: impl IntoIterator, ) -> Result { - // TODO: when multi-proof logic is overhauled, clarify the semantics for empty - // batches (this index access panics if `leaf_indexes` is empty) - let have_coset = - self.tree_height >= 2 && - (!self.leaf_indexes.is_empty() // accept valid batch of size 0 proof without path work - || self.inner_copath.is_some() - || !self.leaf_copath.is_empty()); - - if have_coset { - let d = self.tree_height; - let leaf_depth = d - 1; - if d < 2 { - return Ok(false); - } + if self.leaf_indexes.is_empty() { + return Ok(true) + } - // hash opened leaves and build map containing all leaf digests needed at bottom layer - let mut leaves = leaves.into_iter(); - let mut leaf_level: BTreeMap = BTreeMap::new(); - for &idx in &self.leaf_indexes { - let leaf = leaves.next().ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_indexes.len()))?; - let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; - leaf_level.insert(idx, leaf_hash); - } - if leaves.next().is_some() { - return Err(crate::Error::IncorrectInputLength(self.leaf_indexes.len())); + let d = self.tree_height; + let leaf_depth = d - 1; + if d < 2 { + return Ok(false); + } + + // hash opened leaves and build map containing all leaf digests needed at bottom layer + let mut leaves = leaves.into_iter(); + let mut leaf_level: BTreeMap = BTreeMap::new(); + for &idx in &self.leaf_indexes { + let leaf = leaves.next().ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_indexes.len()))?; + let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; + leaf_level.insert(idx, leaf_hash); + } + if leaves.next().is_some() { + return Err(crate::Error::IncorrectInputLength(self.leaf_indexes.len())); + } + + // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j + let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); + let on_path = compute_on_path(leaf_depth, &index_set); // holds indices of on-path nodes at depth d + + // compute minimal copath at leaf layer (B*_{d-1}) + let mut expected_leaf_coset: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { + expected_leaf_coset.push(sibling_idx); // copath element needed for proof } + } + expected_leaf_coset.sort_unstable(); // canonical order - // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j - let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); - let on_path = compute_on_path(leaf_depth, &index_set); // holds indices of on-path nodes at depth d + if expected_leaf_coset.len() != self.leaf_copath.len() { + return Ok(false); + } - // compute minimal copath at leaf layer (B*_{d-1}) - let mut expected_leaf_coset: Vec = Vec::new(); - for &path_idx in on_path[leaf_depth].iter() { - let sibling_idx = path_idx ^ 1; - if !contains_sorted(&on_path[leaf_depth], sibling_idx) { - expected_leaf_coset.push(sibling_idx); // copath element needed for proof + for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { + match leaf_level.get(&sibling_idx) { + Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one + _ => { + leaf_level.insert(sibling_idx, sibling_digest.clone()); } } - expected_leaf_coset.sort_unstable(); // canonical order + } + + // prepare inner-level maps for non-on-path siblings and computed parents + let mut inner_levels: Vec> = + (0..d).map(|_| BTreeMap::new()).collect(); - if expected_leaf_coset.len() != self.leaf_copath.len() { - return Ok(false); - } + // LookUp table to speedup computation avoid redundant hash computations + let mut hash_lut: HashMap = + HashMap::with_hasher(BuildHasherDefault::::default()); - for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { - match leaf_level.get(&sibling_idx) { - Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one - _ => { - leaf_level.insert(sibling_idx, sibling_digest.clone()); - } - } + // Decode received inner copath and add digests to LUT + if !Self::decode_inner_copath(d, &self.inner_copath, &mut inner_levels, &mut hash_lut) { + return Ok(false); + } + + // Recomputation + // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs + for &parent_index in on_path[leaf_depth - 1].iter() { + let left = leaf_level.get(&(parent_index * 2)).cloned(); + let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::evaluate( + two_to_one_params, + P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type + P::LeafInnerDigestConverter::convert(right)?, + )?; + inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(leaf_depth - 1, parent_index); + hash_lut.insert(heap_idx, parent); + } + + // compute inner layers up to root using TwoToOne::compress to hash inner digests + for depth in (1..=leaf_depth - 1).rev() { + let parent_depth = depth - 1; + for &parent_index in on_path[parent_depth].iter() { + let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); + let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::compress( + two_to_one_params, + &left, + &right, + )?; + inner_levels[parent_depth].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(parent_depth, parent_index); + hash_lut.insert(heap_idx, parent); } + } + + // check root + match inner_levels[0].get(&0) { + Some(h) => Ok(h == root_hash), // valid: Ok(true) + None => Ok(false), + } + } + + /// Encodes the inner co-path entries [(depth, index, digest), ...] as compact delta encodings. + /// Keeps the first (depth, index) entry, then zigzag encodes signed deltas of subsequent paris, + /// collecting the corresponding digests in order. + /// Result is a tuple (start_depth: usize, start_index: usize, deltas: Vec, digests: Vec<

::InnerDigest>) + /// + /// For example: + /// ```tree_diagram + /// [A] d = 0 + /// / \ + /// [B] [C] d = 1 + /// / \ / \ + /// [D] E F [G] d = 2 + /// / \ / \ / \ / \ + /// H I [J] K L [M] N O d = 3 + /// / \ / \ / \ / \ / \ / \ / \ / \ + /// .... 4 5 6 7 8 9 10 11 .... d = 4 + /// ``` + /// + /// Suppose we want to prove the following openings: + /// ```text + /// I = {6, 8} + /// ``` + /// With the CoSet strategy: + /// * we take the union of all single paths and compute + /// `B*_j = siblings(A_j) \ A_j` for each depth `j`, + /// * at the leaf layer we compute `B*_{4} = {(4, 7), (4, 9)}`, + /// * and across inner layers (depths `1..3`) we compute: + /// `B*_{1} = {(1, 0), (1, 1)}`, `B*_{2} = {(2, 0), (2, 3)}`, `B*_{3} = {(3, 2), (3, 5)}`, + /// So the CoPath carries: + /// * `2` leaf-layer digests (`leaf_copath`), + /// * `6` inner-layer digests (`inner_copath`), + /// + /// We keep `leaf_copath` as-is but instead of storing all 6 `(depth, index)` pairs explicitly, we store: + /// + /// * a starting coordinate: + /// ```text + /// start_depth = 1 + /// start_index = 0 + /// ``` + /// + /// * followed by signed deltas between consecutive coordinates: + /// ```text + /// (Δd, Δi) sequence: + /// (0, +1), + /// (+1, -1), + /// (0, +3), + /// (+1, -1), + /// (0, +3) + /// ``` + /// + /// Each `(Δd, Δi)` is encoded to unsigned and then varint-encoded. All of these + /// deltas are very small (−1, 0, +1, +3), so each encoded value fits in a + /// single byte. On a 64-bit platform: + /// + /// * naive coordinate encoding for 6 entries as `(depth: usize, index: usize)` + /// uses roughly `6 × 2 × 8 = 96` bytes, + /// * the packed representation uses: + /// * one `(start_depth, start_index)` pair (16 bytes), + /// * plus `5 × 2` varints (10 bytes/20 bytes for index in a large tree), + /// * for ~ 26/36 bytes of coordinate data. + fn pack_inner_copath( + entries: &[(usize, usize, P::InnerDigest)], + ) -> Option> { + if entries.is_empty() { + return None; + } - // prepare inner-level maps for non-on-path siblings and computed parents - let mut inner_levels: Vec> = - (0..d).map(|_| BTreeMap::new()).collect(); + let first = &entries[0]; + let mut deltas = Vec::new(); + let mut digests = Vec::with_capacity(entries.len()); + let mut prev_depth = i64::try_from(first.0).ok()?; + let mut prev_index = i64::try_from(first.1).ok()?; + digests.push(first.2.clone()); + + for &(depth, index, ref digest) in entries.iter().skip(1) { + let depth_i64 = i64::try_from(depth).ok()?; + let index_i64 = i64::try_from(index).ok()?; + encode_delta(&mut deltas, depth_i64 - prev_depth); + encode_delta(&mut deltas, index_i64 - prev_index); + digests.push(digest.clone()); + prev_depth = depth_i64; + prev_index = index_i64; + } - // LookUp table to speedup computation avoid redundant hash computations - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); + Some((first.0, first.1, deltas, digests)) + } - if let Some((start_depth, start_index, deltas, digests)) = &self.inner_copath { - if digests.is_empty() { - if !deltas.is_empty() { - return Ok(false); - } - } else { - let mut depth = match i64::try_from(*start_depth) { - Ok(value) => value, - Err(_) => return Ok(false), + /// Decodes inner co-path entries back into their usize equivalents + /// Inserts corresponding digest into the LUT for memoisation. + fn decode_inner_copath( + tree_height: usize, + inner_copath: &Option>, + inner_levels: &mut [BTreeMap], + hash_lut: &mut HashMap>, + ) -> bool { + if let Some((start_depth, start_index, deltas, digests)) = inner_copath { + // verifier rejects if remaining deltas with empty digests + if digests.is_empty() { + return deltas.is_empty(); + } + + // init (depth, index) accumulation as i64 + let mut depth = match i64::try_from(*start_depth) { + Ok(value) => value, + Err(_) => return false, + }; + let mut index = match i64::try_from(*start_index) { + Ok(value) => value, + Err(_) => return false, + }; + let mut cursor = 0usize; + let mut prev_coord: Option<(usize, usize)> = None; + + // Helper to insert digest into the LUT + let mut push_entry = + |depth_i64: i64, index_i64: i64, digest: &P::InnerDigest| -> bool { + let depth_usize = match usize::try_from(depth_i64) { + Ok(v) => v, + Err(_) => return false, }; - let mut index = match i64::try_from(*start_index) { - Ok(value) => value, - Err(_) => return Ok(false), + let index_usize = match usize::try_from(index_i64) { + Ok(v) => v, + Err(_) => return false, }; - let mut cursor = 0usize; - let mut prev_coord: Option<(usize, usize)> = None; - let mut push_entry = |depth_i64: i64, - index_i64: i64, - digest: &P::InnerDigest| - -> bool { - let depth_usize = match usize::try_from(depth_i64) { - Ok(v) => v, - Err(_) => return false, - }; - let index_usize = match usize::try_from(index_i64) { - Ok(v) => v, - Err(_) => return false, - }; - if depth_usize == 0 || depth_usize >= d { + if depth_usize == 0 || depth_usize >= tree_height { + return false; + } + // ensure provers ordering is consistent + if let Some((pd, pi)) = prev_coord { + if (depth_usize, index_usize) < (pd, pi) { return false; } - if let Some((pd, pi)) = prev_coord { - if (depth_usize, index_usize) < (pd, pi) { - return false; - } - } - if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { - if existing != digest { - return false; - } - } else { - inner_levels[depth_usize].insert(index_usize, digest.clone()); - } - // seed LUT with known siblings - let heap_idx = level_index(depth_usize, index_usize); - hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); - prev_coord = Some((depth_usize, index_usize)); - true - }; - - if !push_entry(depth, index, &digests[0]) { - return Ok(false); } - - for digest in digests.iter().skip(1) { - let depth_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return Ok(false), - }; - let index_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return Ok(false), - }; - depth = match depth.checked_add(depth_delta) { - Some(value) => value, - None => return Ok(false), - }; - index = match index.checked_add(index_delta) { - Some(value) => value, - None => return Ok(false), - }; - if !push_entry(depth, index, digest) { - return Ok(false); + // check for conflicting siblings at the same coordinate + if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { + if existing != digest { + return false; } + } else { + inner_levels[depth_usize].insert(index_usize, digest.clone()); } - - if cursor != deltas.len() { - return Ok(false); - } - } + // seed LUT with known siblings + let heap_idx = level_index(depth_usize, index_usize); + hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); + prev_coord = Some((depth_usize, index_usize)); + true + }; + + if !push_entry(depth, index, &digests[0]) { + return false; } - // Recomputation - // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs - for &parent_index in on_path[leaf_depth - 1].iter() { - let left = leaf_level.get(&(parent_index * 2)).cloned(); - let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), _ => return Ok(false) }; - let parent = P::TwoToOneHash::evaluate( - two_to_one_params, - P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type - P::LeafInnerDigestConverter::convert(right)?, - )?; - inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(leaf_depth - 1, parent_index); - hash_lut.insert(heap_idx, parent); - } - - // compute inner layers up to root using TwoToOne::compress to hash inner digests - for depth in (1..=leaf_depth - 1).rev() { - let parent_depth = depth - 1; - for &parent_index in on_path[parent_depth].iter() { - let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); - let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), _ => return Ok(false) }; - let parent = P::TwoToOneHash::compress( - two_to_one_params, - &left, - &right, - )?; - inner_levels[parent_depth].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(parent_depth, parent_index); - hash_lut.insert(heap_idx, parent); + // accumulate remaining digests + for digest in digests.iter().skip(1) { + let depth_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return false, + }; + let index_delta = match decode_delta(deltas, &mut cursor) { + Some(delta) => delta, + None => return false, + }; + // next (depth, index) + depth = match depth.checked_add(depth_delta) { + Some(value) => value, + None => return false, + }; + index = match index.checked_add(index_delta) { + Some(value) => value, + None => return false, + }; + // attempt push + if !push_entry(depth, index, digest) { + return false; } } - // check root - match inner_levels[0].get(&0) { - Some(h) => { - Ok(h == root_hash) - } - None => Ok(false), + if cursor != deltas.len() { + return false; } - } else { - // --- Legacy prefix-decoder path --- - let tree_height = self.auth_paths_suffixes.get(0).map(|v| v.len()).unwrap_or(0) + 2; - let mut leaves = leaves.into_iter(); - - // LookUp table to speedup computation avoid redundant hash computations - let mut hash_lut: hashbrown::HashMap = - hashbrown::HashMap::with_hasher(BuildHasherDefault::::default()); - - // init prev path for decoding - let mut prev_path: Vec<_> = self.auth_paths_suffixes[0].clone(); - - for i in 0..self.leaf_indexes.len() { - let leaf_index = self.leaf_indexes[i]; - let leaf = leaves.next().unwrap(); - let leaf_sibling_hash = &self.leaf_siblings_hashes[i]; - - // decode i-th auth path - let auth_path = prefix_decode_path( - &prev_path, - self.auth_paths_prefix_lenghts[i], - &self.auth_paths_suffixes[i], - ); - // update prev path for decoding next one - prev_path = auth_path.clone(); - - let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf.clone())?; - let (left_child, right_child) = - select_left_right_child(leaf_index, &claimed_leaf_hash, &leaf_sibling_hash)?; - // check hash along the path from bottom to root - - // leaf layer to inner layer conversion - let left_child = P::LeafInnerDigestConverter::convert(left_child)?; - let right_child = P::LeafInnerDigestConverter::convert(right_child)?; - - // we will use `index` variable to track the position of path - let mut index = leaf_index; - let mut index_in_tree = convert_index_to_last_level(leaf_index, tree_height); - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - - let mut curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap() - }); - - // Check levels between leaf level and root - for level in (0..auth_path.len()).rev() { - // check if path node at this level is left or right - let (left, right) = - select_left_right_child(index, curr_path_node, &auth_path[level])?; - // update curr_path_node - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::compress(&two_to_one_params, left, right).unwrap() - }); - } - - // check if final hash is root - if curr_path_node != root_hash { - return Ok(false); - } - } - Ok(true) } - } - /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. - /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. - /// - /// This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. - #[allow(unused)] // this function is actually used when r1cs feature is on - fn position_list(&'_ self) -> impl '_ + Iterator> { - let path_len = self.auth_paths_suffixes[0].len(); - - cfg_into_iter!(self.leaf_indexes.clone()) - .map(move |i| { - (0..path_len + 1) - .map(move |j| ((i >> j) & 1) != 0) - .rev() - .collect() - }) - .collect::>() - .into_iter() + true } + + // TODO: git commit changes and then consult this + // The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. + // `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. + // + // This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. + // #[allow(unused)] // this function is actually used when r1cs feature is on + // fn position_list(&'_ self) -> impl '_ + Iterator> { + // let path_len = self.auth_paths_suffixes[0].len(); + + // cfg_into_iter!(self.leaf_indexes.clone()) + // .map(move |i| { + // (0..path_len + 1) + // .map(move |j| ((i >> j) & 1) != 0) + // .rev() + // .collect() + // }) + // .collect::>() + // .into_iter() + // } } /// `index` is the first `path.len()` bits of @@ -974,56 +817,9 @@ impl MerkleTree

{ }) } - /// Returns a MultiPath (multiple authentication paths in compressed form, with Front Incremental Encoding), - /// from every leaf to root. - /// Note that for compression efficiency, the indexes are internally sorted. - /// For sorted indexes, MultiPath contains: - /// `2*( (num_leaves.log2()-1).pow(2) - (num_leaves.log2()-2) )` - /// instead of - /// `num_leaves*(num_leaves.log2()-1)` - /// When verifying the proof, leaves hashes should be supplied in order, that is: - /// ```ignore - /// let ordered_leaves: Vec<_> = self.leaf_indexes.into_iter().map(|i| leaves[i]).collect(); - /// ``` - pub fn generate_multi_proof( - &self, - indexes: impl IntoIterator, - ) -> Result, crate::Error> { - // pruned and sorted for encoding efficiency - let indexes: BTreeSet = indexes.into_iter().collect(); - - //let auth_paths = Vec::with_capacity(indexes.len()); - let mut auth_paths_prefix_lenghts: Vec = Vec::with_capacity(indexes.len()); - let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); - - let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); - - let mut prev_path = Vec::new(); - - for index in &indexes { - leaf_siblings_hashes.push(self.get_leaf_sibling_hash(*index)); - - let path = self.compute_auth_path(*index); - - // incremental encoding - let (prefix_len, suffix) = prefix_encode_path(&prev_path, &path); - auth_paths_prefix_lenghts.push(prefix_len); - auth_paths_suffixes.push(suffix); - prev_path = path; - } - - Ok(MultiPath { - leaf_indexes: Vec::from_iter(indexes), - auth_paths_prefix_lenghts, - auth_paths_suffixes, - leaf_siblings_hashes, - }) - } - - /// Returns a MultiPathV2 (a compressed membership proof for a set of leaves), + /// Returns a CoPath struct (a compressed membership proof for a set of leaves), /// sufficient to verify each leaf up to the root. - /// Note that for compatibility, indexes are internally sorted and legacy prefix fields remain in the - /// struct (but are emitted empty for new proofs) + /// Indexes are internally sorted and emitted in this order. /// /// With the CoSet (minimal co-path) encoding, we do not store full per-leaf authentication paths. /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. @@ -1034,37 +830,30 @@ impl MerkleTree

{ /// * `tree_height`; /// * `leaf_indexes` (ascending, unique); /// * `leaf_copath`: the leaf-layer co-path digests `B*_{d-1}`, in ascending sibling index order; - /// * `inner_copath`: the inner co-path as `(depth, index, digest)` tuples ordered by `(depth, index)`. - /// - /// The lagacy prefix fields (`auth_paths_prefix_lenghts`, `auth_paths_suffixes`, and `leaf_siblings_hashes`) - /// are still filled so existing tests and callers that expect prefix decoding continue to work. + /// * `inner_copath`: the inner co-path packed as `(start_depth, start_index, packed deltas, digests)`. /// /// When verifying the proof, leaves hashes should be supplied in order of `leaf_indexes`, that is: - /// ```ignore - /// let ordered_leaves: Vec<_> = self.leaf_indexes.into_iter().map(|i| leaves[i]).collect(); + /// ```text + /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); /// ``` /// Notes: /// * Empty input (`indexes` is empty) returns a structurally valid empty proof carrying `tree_height`, /// and verification succeeds vacuously against the claimed root. - pub fn generate_multi_proof_v2( + pub fn generate_multi_proof( &self, indexes: impl IntoIterator, - ) -> Result, crate::Error> { + ) -> Result, crate::Error> { // pruned and sorted for encoding efficiency let indexes: BTreeSet = indexes.into_iter().collect(); let d = self.height(); // TODO: should empty query return structurally valid empty proof if indexes.is_empty() { - return Ok(MultiPathV2 { + return Ok(CoPath { tree_height: d, - leaf_indexes: Vec::new(), leaf_copath: Vec::new(), - inner_copath: Vec::new(), - // legacy - leaf_siblings_hashes: Vec::new(), - auth_paths_prefix_lenghts: Vec::new(), - auth_paths_suffixes: Vec::new(), + inner_copath: None, + leaf_indexes: Vec::new(), }); } @@ -1076,7 +865,7 @@ impl MerkleTree

{ let mut leaf_coset_ids: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; - if !contains_sorted(&on_path[leaf_depth], sibling_idx) { + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { leaf_coset_ids.push(sibling_idx); } } @@ -1092,33 +881,29 @@ impl MerkleTree

{ } // inner layers (depth 1..d-2) - let mut inner_copath = Vec::new(); + let mut inner_copath_entries: Vec<(usize, usize, P::InnerDigest)> = Vec::new(); for depth in 1..leaf_depth { for &path_idx in on_path[depth].iter() { let sibling_idx = path_idx ^ 1; - if !contains_sorted(&on_path[depth], sibling_idx) { + if on_path[depth].binary_search(&sibling_idx).is_err() { let heap_idx = level_index(depth, sibling_idx); let sibling_digest = self .non_leaf_nodes .get(heap_idx) .ok_or_else(|| crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()))?; - inner_copath.push((depth, sibling_idx, sibling_digest.clone())); + inner_copath_entries.push((depth, sibling_idx, sibling_digest.clone())); } } } // canonicalise order - inner_copath.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); + inner_copath_entries.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); + let inner_copath = CoPath::

::pack_inner_copath(&inner_copath_entries); - // TODO: later remove trace - Ok(MultiPathV2 { + Ok(CoPath { tree_height: d, - leaf_indexes: Vec::from_iter(indexes), leaf_copath, inner_copath, - // legacy prefix data intentionally omitted for compact proofs - leaf_siblings_hashes: Vec::new(), - auth_paths_prefix_lenghts: Vec::new(), - auth_paths_suffixes: Vec::new(), + leaf_indexes: Vec::from_iter(indexes), }) } @@ -1289,37 +1074,46 @@ fn convert_index_to_last_level(index: usize, tree_height: usize) -> usize { index + (1 << (tree_height - 1)) - 1 } -/// Encodes path with Incremental Encoding by comparing with prev_path -/// Returns the prefix length and the suffix to append during decoding -/// Example: -/// If `prev_path` is vec![C,D] and `path` is vec![C,E] (where C,D,E are hashes) -/// `prefix_encode_path` returns 1,vec![E] - +/// Encodes indexes into the packed delta format #[inline] -fn prefix_encode_path(prev_path: &Vec, path: &Vec) -> (usize, Vec) -where - T: Eq + Clone, -{ - let prefix_length = prev_path - .iter() - .zip(path.iter()) - .take_while(|(a, b)| a == b) - .count(); - - (prefix_length, path[prefix_length..].to_vec()) +fn encode_delta(buffer: &mut Vec, value: i64) { + let zigzag = ((value << 1) ^ (value >> 63)) as u64; + encode_varint(buffer, zigzag); } -fn prefix_decode_path(prev_path: &Vec, prefix_len: usize, suffix: &Vec) -> Vec -where - T: Eq + Clone, -{ - if prefix_len == 0 { - suffix.clone() - } else { - vec![prev_path[0..prefix_len].to_vec(), suffix.clone()].concat() +fn decode_delta(bytes: &[u8], cursor: &mut usize) -> Option { + let raw = decode_varint(bytes, cursor)?; + Some(((raw >> 1) as i64) ^ (-((raw & 1) as i64))) +} + +#[inline] +fn encode_varint(buffer: &mut Vec, mut value: u64) { + while value >= 0x80 { + buffer.push(((value as u8) & 0x7F) | 0x80); + value >>= 7; } + buffer.push(value as u8); } +fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { + let mut value = 0u64; + let mut shift = 0u32; + + while *cursor < bytes.len() { + let byte = bytes[*cursor]; + *cursor += 1; + value |= ((byte & 0x7F) as u64) << shift; + if byte & 0x80 == 0 { + return Some(value); + } + shift += 7; + if shift >= 64 { + return None; + } + } + + None +} /// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. /// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. @@ -1354,7 +1148,3 @@ fn compute_on_path( } path_sets } - -fn contains_sorted(haystack: &[usize], needle: usize) -> bool { - haystack.binary_search(&needle).is_ok() -} diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs index 92656055..8d0e027b 100644 --- a/crypto-primitives/src/merkle_tree/tests/bench_report.rs +++ b/crypto-primitives/src/merkle_tree/tests/bench_report.rs @@ -1,6 +1,8 @@ +#![cfg(feature = "bench_harness")] + use crate::merkle_tree::{ - tests::test_utils::poseidon_parameters, Config, IdentityDigestConverter, LeafParam, - MerkleTree, MultiPath, MultiPathV2, MultiPathV2Bench, Path as MerklePath, TwoToOneParam, + tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, LeafParam, + MerkleTree, TwoToOneParam, }; use ark_ed_on_bls12_381::Fr; use ark_serialize::CanonicalSerialize; @@ -9,6 +11,10 @@ use ark_std::{ UniformRand, }; use plotters::prelude::*; +#[cfg(test)] +#[cfg(feature = "bench_harness")] +#[path = "../bench.rs"] +mod legacy; use std::{ collections::{BTreeMap, BTreeSet}, fs::{self, File}, @@ -31,7 +37,20 @@ impl Config for FieldMTConfig { type TwoToOneHash = TwoToOneH; } +struct LegacyFieldMTConfig; +impl legacy::Config for LegacyFieldMTConfig { + type Leaf = [F]; + type LeafDigest = F; + type LeafInnerDigestConverter = legacy::IdentityDigestConverter; + type InnerDigest = F; + type LeafHash = H; + type TwoToOneHash = TwoToOneH; +} + type FieldMT = MerkleTree; +type LegacyFieldMT = legacy::MerkleTree; +type LegacyLeafParam = legacy::LeafParam; +type LegacyTwoToOneParam = legacy::TwoToOneParam; const TREE_EXPONENTS: &[u32] = &[12, 14, 16, 18, 20]; const BATCH_SIZES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; @@ -65,13 +84,11 @@ impl IndexPattern { struct TreeFixture { leaves: Vec>, tree: FieldMT, + legacy_tree: LegacyFieldMT, leaf_params: LeafParam, + legacy_leaf_params: LegacyLeafParam, two_to_one_params: TwoToOneParam, -} - -#[derive(CanonicalSerialize)] -struct NoPruneBatch { - paths: Vec>, + legacy_two_to_one_params: LegacyTwoToOneParam, } struct ReportRow { @@ -100,56 +117,29 @@ trait ProofStats { fn total_nodes(&self) -> usize; } -impl ProofStats for MultiPath

{ - fn opened(&self) -> usize { - self.leaf_indexes.len() - } - - fn total_nodes(&self) -> usize { - let auth_len: usize = self - .auth_paths_suffixes - .iter() - .map(|path| path.len()) - .sum(); - self.leaf_siblings_hashes.len() + auth_len - } -} - -impl ProofStats for MultiPathV2Bench

{ +impl ProofStats for CoPath

{ fn opened(&self) -> usize { self.leaf_indexes.len() } fn total_nodes(&self) -> usize { - let inner = self - .inner_copath - .as_ref() - .map(|(_, _, _, digests)| digests.len()) - .unwrap_or(0); + let inner = self.inner_copath.as_ref().map(|(_, _, _, digests)| digests.len()).unwrap_or(0); self.leaf_copath.len() + inner } } -impl ProofStats for MultiPathV2

{ +impl ProofStats for legacy::MultiPath

{ fn opened(&self) -> usize { self.leaf_indexes.len() } fn total_nodes(&self) -> usize { - self.leaf_copath.len() + self.inner_copath.len() - } -} - -impl ProofStats for NoPruneBatch

{ - fn opened(&self) -> usize { - self.paths.len() - } - - fn total_nodes(&self) -> usize { - self.paths + let auth_len: usize = self + .auth_paths_suffixes .iter() - .map(|path| 1 + path.auth_path.len()) - .sum() + .map(|path| path.len()) + .sum(); + self.leaf_siblings_hashes.len() + auth_len } } @@ -160,18 +150,6 @@ const PLOT_METRICS: &[PlotMetric] = &[ y_label: "proof size (bytes)", value: |row: &ReportRow| Some(row.proof_bytes as f64), }, - PlotMetric { - name: "Proof Nodes", - filename_prefix: "proof_nodes", - y_label: "proof nodes", - value: |row: &ReportRow| Some(row.proof_nodes as f64), - }, - PlotMetric { - name: "Hashes Per Opening", - filename_prefix: "hashes_per_opening", - y_label: "hashes per opened leaf", - value: |row: &ReportRow| Some(row.hashes_per_opening), - }, PlotMetric { name: "Proving Time", filename_prefix: "prove_ms", @@ -184,12 +162,6 @@ const PLOT_METRICS: &[PlotMetric] = &[ y_label: "verify time (ms)", value: |row: &ReportRow| Some(row.verify_ms), }, - PlotMetric { - name: "RSS Delta", - filename_prefix: "rss_delta_kb", - y_label: "rss delta (kB)", - value: |row: &ReportRow| row.rss_delta_kb.map(|kb| kb as f64), - }, ]; #[test] @@ -242,18 +214,19 @@ fn run_scenario( batch: usize, pattern: IndexPattern, indexes: &[usize], -) -> Result<[ReportRow; 4], Box> { +) -> Result<[ReportRow; 2], Box> { let root = fixture.tree.root(); + let legacy_root = fixture.legacy_tree.root(); let opened_leaves: Vec> = indexes.iter().map(|&i| fixture.leaves[i].clone()).collect(); let legacy_row = benchmark_strategy( "prefix", - || fixture.tree.generate_multi_proof(indexes.iter().copied()), - |proof, leaves| { + || fixture.legacy_tree.generate_multi_proof(indexes.iter().copied()), + |proof: &legacy::MultiPath<_>, leaves| { proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, + &fixture.legacy_leaf_params, + &fixture.legacy_two_to_one_params, + &legacy_root, leaves, ) }, @@ -264,84 +237,22 @@ fn run_scenario( pattern, )?; - let no_prune_row = benchmark_strategy( - "no_prune", - || { - let mut paths = Vec::with_capacity(indexes.len()); - for &idx in indexes.iter() { - paths.push(fixture.tree.generate_proof(idx)?); - } - Ok(NoPruneBatch { paths }) - }, - |proof: &NoPruneBatch<_>, leaves| { - if proof.paths.len() != leaves.len() { - return Err(crate::Error::IncorrectInputLength(proof.paths.len())); - } - for (path, leaf) in proof.paths.iter().zip(leaves.iter()) { - let ok = path.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - leaf.as_slice(), - )?; - if !ok { - return Ok(false); - } - } - Ok(true) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - )?; - let coset_row = benchmark_strategy( - "coset_v2", - || { - fixture - .tree - .generate_multi_proof_v2(indexes.iter().copied()) - }, - |proof: &MultiPathV2<_>, leaves| { - proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - leaves, - ) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - )?; - - let coset_bench_row = benchmark_strategy( - "coset_v2_bench", - || { - fixture - .tree - .generate_multi_proof_v2_bench(indexes.iter().copied()) - }, - |proof: &MultiPathV2Bench<_>, leaves| { - proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - leaves, - ) - }, + "coset", + || fixture.tree.generate_multi_proof(indexes.iter().copied()), + |proof: &CoPath<_>, leaves| proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + leaves, + ), &opened_leaves, fixture.leaves.len(), fixture.log2_size(), batch, pattern, )?; - - Ok([legacy_row, no_prune_row, coset_row, coset_bench_row]) + Ok([legacy_row, coset_row]) } fn benchmark_strategy( @@ -376,7 +287,14 @@ where let verify_ok = verifier(&proof, verify_input.clone())?; let verify_time = verify_start.elapsed(); let verify_rss = rss_delta_kb(rss_before_verify, rss_bytes()); - assert!(verify_ok, "verification must succeed for {}", strategy); + assert!( + verify_ok, + "verification must succeed for {} (tree_n={}, batch={}, pattern={})", + strategy, + tree_size, + batch, + pattern.label() + ); let row = ReportRow { tree_size, @@ -427,19 +345,26 @@ fn sample_indexes( fn build_fixture(exp: u32) -> Result> { let leaf_params = poseidon_parameters(); + let legacy_leaf_params: LegacyLeafParam = leaf_params.clone(); let two_to_one_params = leaf_params.clone(); + let legacy_two_to_one_params: LegacyTwoToOneParam = two_to_one_params.clone(); let num_leaves = 1usize << exp; let mut rng = StdRng::seed_from_u64(0x5EED_C0DE_u64 ^ (exp as u64)); let leaves = sample_leaves(num_leaves, &mut rng); let tree = FieldMT::new(&leaf_params, &two_to_one_params, &leaves).unwrap(); + let legacy_tree = + LegacyFieldMT::new(&legacy_leaf_params, &legacy_two_to_one_params, &leaves).unwrap(); Ok(TreeFixture { leaves, tree, + legacy_tree, leaf_params, + legacy_leaf_params, two_to_one_params, + legacy_two_to_one_params, }) } @@ -529,7 +454,7 @@ fn write_plots( let mut generated = Vec::new(); for ((tree_size, pattern), strategies) in grouped { let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); - for &name in &["prefix", "no_prune", "coset_v2", "coset_v2_bench"] { + for &name in &["prefix", "coset"] { if let Some(mut series) = strategies.get(name).cloned() { if series.is_empty() { continue; @@ -618,9 +543,7 @@ fn write_plots( fn strategy_label(name: &str) -> &str { match name { "prefix" => "prefix", - "no_prune" => "no pruning", - "coset_v2" => "coset v2", - "coset_v2_bench" => "coset v2 (bench)", + "coset" => "coset", _ => name, } } @@ -628,9 +551,7 @@ fn strategy_label(name: &str) -> &str { fn strategy_color(name: &str) -> RGBColor { match name { "prefix" => RED, - "no_prune" => RGBColor(255, 127, 14), - "coset_v2" => BLUE, - "coset_v2_bench" => GREEN, + "coset" => BLUE, _ => BLACK, } } diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index bbfe8aa5..a40c1326 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -2,6 +2,7 @@ mod constraints; mod test_utils; +#[cfg(all(test, feature = "bench_harness"))] mod bench_report; mod bytes_mt_tests { @@ -10,7 +11,7 @@ mod bytes_mt_tests { use ark_ed_on_bls12_381::EdwardsProjective as JubJub; use ark_ff::BigInteger256; use ark_serialize::CanonicalSerialize; - use ark_std::{iter::zip, test_rng, UniformRand}; + use ark_std::{test_rng, UniformRand}; #[derive(Clone)] pub(super) struct Window4x256; @@ -42,11 +43,7 @@ mod bytes_mt_tests { let mut leaves: Vec> = leaves .iter() - .map(|leaf| { - let mut bytes = Vec::new(); - leaf.serialize_uncompressed(&mut bytes).unwrap(); - bytes - }) + .map(|leaf| crate::to_uncompressed_bytes!(leaf).unwrap()) .collect(); let leaf_crh_params = ::setup(&mut rng).unwrap(); @@ -69,21 +66,13 @@ mod bytes_mt_tests { .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); - let mut coset_multi_proof = tree - .generate_multi_proof_v2((0..leaves.len()).collect::>()) - .unwrap(); - assert!(multi_proof .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) .unwrap()); - assert!(coset_multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) - .unwrap()); // test merkle tree update functionality for (i, v) in update_query { - let mut bytes = Vec::new(); - v.serialize_uncompressed(&mut bytes).unwrap(); + let bytes = crate::to_uncompressed_bytes!(v).unwrap(); tree.update(*i, &bytes).unwrap(); leaves[*i] = bytes.clone(); } @@ -101,16 +90,10 @@ mod bytes_mt_tests { multi_proof = tree .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); - coset_multi_proof = tree - .generate_multi_proof_v2((0..leaves.len()).collect::>()) - .unwrap(); assert!(multi_proof .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) .unwrap()); - assert!(coset_multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) - .unwrap()); } #[test] @@ -163,11 +146,7 @@ mod bytes_mt_tests { let serialized_leaves: Vec> = leaves .iter() - .map(|leaf| { - let mut bytes = Vec::new(); - leaf.serialize_uncompressed(&mut bytes).unwrap(); - bytes - }) + .map(|leaf| crate::to_uncompressed_bytes!(leaf).unwrap()) .collect(); let leaf_crh_params = ::setup(&mut rng).unwrap(); @@ -186,30 +165,8 @@ mod bytes_mt_tests { .generate_multi_proof((0..leaves.len()).collect::>()) .unwrap(); - let coset_multi_proof = tree - .generate_multi_proof_v2((0..leaves.len()).collect::>()) - .unwrap(); - - // test compression theretical prefix lengths for size 8 Tree: - // we should send 6 hashes instead of 2*8 = 16 - let theoretical_prefix_lengths = vec![0, 2, 1, 2, 0, 2, 1, 2]; - - for (comp_len, exp_len) in zip( - &multi_proof.auth_paths_prefix_lenghts, - &theoretical_prefix_lengths, - ) { - assert_eq!(comp_len, exp_len); - } - - // test that the compressed paths can expand to expected len - for (prefix_len, suffix) in zip( - &multi_proof.auth_paths_prefix_lenghts, - &multi_proof.auth_paths_suffixes, - ) { - assert_eq!(prefix_len + suffix.len(), proofs[0].auth_path.len()); - } - - assert!(coset_multi_proof + // multi-proof should verify and contain co-set data consistent with expected on-path sets + assert!(multi_proof .verify( &leaf_crh_params, &two_to_one_params, @@ -227,7 +184,7 @@ mod field_mt_tests { tests::test_utils::poseidon_parameters, Config, IdentityDigestConverter, MerkleTree, }, }; - use ark_std::{test_rng, One, UniformRand}; + use ark_std::{test_rng, UniformRand, One}; type F = ark_ed_on_bls12_381::Fr; type H = poseidon::CRH; @@ -325,61 +282,6 @@ mod field_mt_tests { .unwrap()); } - #[test] - fn multiproof_prefix_encoding_sanity() { - use ark_std::collections::BTreeSet; - - let leaves: Vec> = (0..32u64).map(|i| vec![F::from(i)]).collect(); - let leaf_crh_params = poseidon_parameters(); - let two_to_one_params = leaf_crh_params.clone(); - let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); - - let query_indexes = vec![9usize, 0, 12, 5, 3, 3, 17, 24, 31, 0]; - let multi_proof = tree - .generate_multi_proof(query_indexes.clone()) - .unwrap(); - - let sorted_unique: Vec<_> = query_indexes - .into_iter() - .collect::>() - .into_iter() - .collect(); - - let mut prev_path: Vec = Vec::new(); - let mut expected_prefix_lengths = Vec::new(); - let mut expected_suffixes: Vec> = Vec::new(); - let mut expected_suffix_total = 0usize; - - for &index in &sorted_unique { - let path = tree.generate_proof(index).unwrap().auth_path; - let prefix_len = prev_path - .iter() - .zip(path.iter()) - .take_while(|(a, b)| a == b) - .count(); - let suffix = path[prefix_len..].to_vec(); - expected_suffix_total += suffix.len(); - expected_prefix_lengths.push(prefix_len); - expected_suffixes.push(suffix); - prev_path = path; - } - - let actual_suffix_total: usize = multi_proof - .auth_paths_suffixes - .iter() - .map(|suffix| suffix.len()) - .sum(); - - assert_eq!(multi_proof.leaf_indexes, sorted_unique); - assert_eq!(multi_proof.auth_paths_prefix_lenghts, expected_prefix_lengths); - assert_eq!(multi_proof.auth_paths_suffixes, expected_suffixes); - assert_eq!(multi_proof.leaf_siblings_hashes.len(), sorted_unique.len()); - assert_eq!( - multi_proof.leaf_siblings_hashes.len() + actual_suffix_total, - sorted_unique.len() + expected_suffix_total - ); - } - #[test] fn good_root_test() { let mut rng = test_rng(); @@ -400,4 +302,140 @@ mod field_mt_tests { ], ) } + + #[test] + fn multiproof_empty_batch_verifies() { + let mut rng = test_rng(); + let leaves: Vec> = (0..4).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + let root = tree.root(); + + let proof = tree.generate_multi_proof(Vec::::new()).unwrap(); + assert!( + proof + .verify(&leaf_crh_params, &two_to_one_params, &root, Vec::>::new()) + .unwrap(), + "empty batch proof should verify" + ); + assert_eq!(proof.leaf_indexes.len(), 0); + } + + #[test] + fn multiproof_duplicate_indices_deduped() { + let mut rng = test_rng(); + let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + let root = tree.root(); + + let indexes = vec![3usize, 1, 3, 1, 5]; + let proof = tree.generate_multi_proof(indexes.clone()).unwrap(); + assert_eq!(proof.leaf_indexes, vec![1, 3, 5], "indexes should be sorted & deduped"); + + let opened: Vec<_> = proof + .leaf_indexes + .iter() + .map(|&i| leaves[i].clone()) + .collect(); + + assert!( + proof + .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .unwrap(), + "proof with duplicate input indices should verify after deduplication" + ); + } + + #[test] + fn multiproof_wrong_leaf_copath_fails() { + let mut rng = test_rng(); + let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + let root = tree.root(); + + let proof = tree.generate_multi_proof(vec![1usize, 6]).unwrap(); + let mut bad = proof.clone(); + if let Some(first) = bad.leaf_copath.get_mut(0) { + *first += F::one(); // flip one sibling digest + } + let opened: Vec<_> = bad + .leaf_indexes + .iter() + .map(|&i| leaves[i].clone()) + .collect(); + + let ok = bad + .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .unwrap(); + assert!(!ok, "tampered leaf_copath digest must fail verification"); + } + + #[test] + fn multiproof_missing_inner_entry_fails() { + let mut rng = test_rng(); + let leaves: Vec> = (0..16).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + let root = tree.root(); + + let proof = tree.generate_multi_proof(vec![2usize, 5, 9]).unwrap(); + let mut bad = proof.clone(); + if let Some((_, _, _, digests)) = bad.inner_copath.as_mut() { + if !digests.is_empty() { + digests.pop(); // drop one inner sibling digest + } + } + let opened: Vec<_> = bad + .leaf_indexes + .iter() + .map(|&i| leaves[i].clone()) + .collect(); + let ok = bad + .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .unwrap(); + assert!(!ok, "missing inner copath entry must invalidate the proof"); + } + + #[test] + fn multiproof_open_order_robustness() { + let mut rng = test_rng(); + let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaf_crh_params = poseidon_parameters(); + let two_to_one_params = leaf_crh_params.clone(); + let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); + let root = tree.root(); + + let indexes = vec![4usize, 1, 6]; + let proof = tree.generate_multi_proof(indexes.clone()).unwrap(); + + // verification should use leaves ordered by proof.leaf_indexes (sorted) + let ordered_leaves: Vec<_> = proof + .leaf_indexes + .iter() + .map(|&i| leaves[i].clone()) + .collect(); + assert!( + proof + .verify(&leaf_crh_params, &two_to_one_params, &root, ordered_leaves.clone()) + .unwrap(), + "proof should verify when leaves follow proof.leaf_indexes order" + ); + + // providing leaves in shuffled query order should fail + let shuffled_leaves: Vec<_> = indexes + .iter() + .map(|&i| leaves[i].clone()) + .collect(); + let ok = proof + .verify(&leaf_crh_params, &two_to_one_params, &root, shuffled_leaves) + .unwrap(); + assert!(!ok, "mismatched leaf ordering must fail verification"); + } + } diff --git a/crypto-primitives/src/merkle_tree/v2_bench.rs b/crypto-primitives/src/merkle_tree/v2_bench.rs deleted file mode 100644 index 2667e9fa..00000000 --- a/crypto-primitives/src/merkle_tree/v2_bench.rs +++ /dev/null @@ -1,376 +0,0 @@ -use core::convert::TryFrom; - -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -#[cfg(not(feature = "std"))] -use ark_std::vec::Vec; -use ark_std::{ - borrow::Borrow, - collections::{BTreeMap, BTreeSet}, - hash::BuildHasherDefault, -}; -use hashbrown::HashMap; - -use super::{ - compute_on_path, level_index, Config, DigestConverter, LeafParam, MerkleTree, TwoToOneParam, - DefaultHasher, -}; -use crate::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - Error, -}; - -type PackedInnerCopath

= (usize, usize, Vec, Vec<

::InnerDigest>); - -/// CoSet proof used for benchmark data -#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] -#[derivative( - Clone(bound = "P: Config"), - Debug(bound = "P: Config"), - Default(bound = "P: Config") -)] -pub struct MultiPathV2Bench { - pub tree_height: usize, - pub leaf_indexes: Vec, - pub leaf_copath: Vec, - /// Inner co-path encoded as (start_depth, start_index, packed deltas, digests). - /// `None` means there are no inner-layer digests required - pub inner_copath: Option>, -} - -impl MultiPathV2Bench

{ - /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. - /// Note that the order of the leaves hashes should match the leaves respective indexes - /// * `leaf_size`: leaf size in number of bytes - /// - /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` - pub fn verify + Clone>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - leaves: impl IntoIterator, - ) -> Result { - if self.tree_height < 2 { - return Ok(false); - } - - // TODO: when multi-proof logic is overhauled, clarify the semantics for empty - // batches (this index access panics if `leaf_indexes` is empty) - // accept valid batch of size 0 proof without path work - if self.leaf_indexes.is_empty() { - return Ok(true); - } - - let d = self.tree_height; - let leaf_depth = d - 1; - - let mut leaves = leaves.into_iter(); - let mut leaf_level: BTreeMap = BTreeMap::new(); - for &idx in &self.leaf_indexes { - let leaf = leaves.next().ok_or_else(|| Error::IncorrectInputLength(self.leaf_indexes.len()))?; - let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; - leaf_level.insert(idx, leaf_hash); - } - if leaves.next().is_some() { - return Err(Error::IncorrectInputLength(self.leaf_indexes.len())); - } - - // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j - let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); - let on_path = compute_on_path(leaf_depth, &index_set); - - // compute minimal copath at leaf layer (B*_{d-1}) - let mut expected_leaf_coset: Vec = Vec::new(); - for &path_idx in on_path[leaf_depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { - expected_leaf_coset.push(sibling_idx); // copath element needed for proof - } - } - expected_leaf_coset.sort_unstable(); // canonical order - - if expected_leaf_coset.len() != self.leaf_copath.len() { - return Ok(false); - } - - for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { - match leaf_level.get(&sibling_idx) { - Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one - _ => { - leaf_level.insert(sibling_idx, sibling_digest.clone()); - } - } - } - - // let mut inner_levels: Vec> = (0..d).map(|_| CoSetLevel::new()).collect(); - // prepare inner-level maps for non-on-path siblings and computed parents - let mut inner_levels: Vec> = - (0..d).map(|_| BTreeMap::new()).collect(); - - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); - - if let Some((start_depth, start_index, deltas, digests)) = &self.inner_copath { - if digests.is_empty() { - if !deltas.is_empty() { - return Ok(false); - } - } else { - let mut depth = match i64::try_from(*start_depth) { - Ok(value) => value, - Err(_) => return Ok(false), - }; - let mut index = match i64::try_from(*start_index) { - Ok(value) => value, - Err(_) => return Ok(false), - }; - let mut cursor = 0usize; - let mut prev_coord: Option<(usize, usize)> = None; - let mut push_entry = |depth_i64: i64, - index_i64: i64, - digest: &P::InnerDigest| - -> bool { - let depth_usize = match usize::try_from(depth_i64) { - Ok(v) => v, - Err(_) => return false, - }; - let index_usize = match usize::try_from(index_i64) { - Ok(v) => v, - Err(_) => return false, - }; - if depth_usize == 0 || depth_usize >= d { - return false; - } - if let Some((pd, pi)) = prev_coord { - if (depth_usize, index_usize) < (pd, pi) { - return false; - } - } - if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { - if existing != digest { - return false; - } - } else { - inner_levels[depth_usize].insert(index_usize, digest.clone()); - } - let heap_idx = level_index(depth_usize, index_usize); - hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); - prev_coord = Some((depth_usize, index_usize)); - true - }; - - if !push_entry(depth, index, &digests[0]) { - return Ok(false); - } - - for digest in digests.iter().skip(1) { - let depth_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return Ok(false), - }; - let index_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return Ok(false), - }; - depth = match depth.checked_add(depth_delta) { - Some(value) => value, - None => return Ok(false), - }; - index = match index.checked_add(index_delta) { - Some(value) => value, - None => return Ok(false), - }; - if !push_entry(depth, index, digest) { - return Ok(false); - } - } - - if cursor != deltas.len() { - return Ok(false); - } - } - } - - // Recomputation - // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs - for &parent_index in on_path[leaf_depth - 1].iter() { - let left = leaf_level.get(&(parent_index * 2)).cloned(); - let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { - (Some(left), Some(right)) => (left, right), - _ => return Ok(false), - }; - let parent = P::TwoToOneHash::evaluate( - two_to_one_params, - P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type - P::LeafInnerDigestConverter::convert(right)?, - )?; - inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(leaf_depth - 1, parent_index); - hash_lut.insert(heap_idx, parent); - } - - // compute inner layers up to root using TwoToOne::compress to hash inner digests - for depth in (1..=leaf_depth - 1).rev() { - let parent_depth = depth - 1; - for &parent_index in on_path[parent_depth].iter() { - let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); - let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { - (Some(left), Some(right)) => (left, right), - _ => return Ok(false), - }; - let parent = P::TwoToOneHash::compress( - two_to_one_params, - &left, &right, - )?; - inner_levels[parent_depth].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(parent_depth, parent_index); - hash_lut.insert(heap_idx, parent); - } - } - - match inner_levels[0].get(&0) { - Some(h) => { - Ok(h == root_hash) - } - None => Ok(false), - } - } -} - -impl MerkleTree

{ - pub fn generate_multi_proof_v2_bench( - &self, - indexes: impl IntoIterator, - ) -> Result, Error> { - // pruned and sorted for encoding efficiency - let indexes: BTreeSet = indexes.into_iter().collect(); - let d = self.height(); - - if indexes.is_empty() { - return Ok(MultiPathV2Bench { - tree_height: d, - leaf_indexes: Vec::new(), - leaf_copath: Vec::new(), - inner_copath: None, - }); - } - - let leaf_depth = d - 1; - // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j - let on_path = compute_on_path(leaf_depth, &indexes); - - // leaf layer (depth = d-1) - let mut leaf_coset_ids: Vec = Vec::new(); - for &path_idx in on_path[leaf_depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { - leaf_coset_ids.push(sibling_idx); - } - } - leaf_coset_ids.sort_unstable(); - - let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); - for &sibling_idx in &leaf_coset_ids { - let sibling_digest = self - .leaf_nodes - .get(sibling_idx) - .ok_or_else(|| Error::IncorrectInputLength(self.leaf_nodes.len()))?; - leaf_copath.push(sibling_digest.clone()); - } - - // inner layers (depth 1..d-2) - let mut inner_copath_entries: Vec<(usize, usize, P::InnerDigest)> = Vec::new(); - for depth in 1..leaf_depth { - for &path_idx in on_path[depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[depth].binary_search(&sibling_idx).is_err() { - let heap_idx = level_index(depth, sibling_idx); - let sibling_digest = self - .non_leaf_nodes - .get(heap_idx) - .ok_or_else(|| Error::IncorrectInputLength(self.non_leaf_nodes.len()))?; - inner_copath_entries.push((depth, sibling_idx, sibling_digest.clone())); - } - } - } - // canonicalise order - inner_copath_entries.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); - let inner_copath = pack_inner_copath::

(&inner_copath_entries); - - Ok(MultiPathV2Bench { - tree_height: d, - leaf_indexes: Vec::from_iter(indexes), - leaf_copath, - inner_copath, - }) - } -} - -fn pack_inner_copath( - entries: &[(usize, usize, P::InnerDigest)], -) -> Option> { - if entries.is_empty() { - return None; - } - - let first = &entries[0]; - let mut deltas = Vec::new(); - let mut digests = Vec::with_capacity(entries.len()); - let mut prev_depth = i64::try_from(first.0).ok()?; - let mut prev_index = i64::try_from(first.1).ok()?; - digests.push(first.2.clone()); - - for &(depth, index, ref digest) in entries.iter().skip(1) { - let depth_i64 = i64::try_from(depth).ok()?; - let index_i64 = i64::try_from(index).ok()?; - encode_delta(&mut deltas, depth_i64 - prev_depth); - encode_delta(&mut deltas, index_i64 - prev_index); - digests.push(digest.clone()); - prev_depth = depth_i64; - prev_index = index_i64; - } - - Some((first.0, first.1, deltas, digests)) -} - -fn encode_delta(buffer: &mut Vec, value: i64) { - let zigzag = ((value << 1) ^ (value >> 63)) as u64; - encode_varint(buffer, zigzag); -} - -fn decode_delta(bytes: &[u8], cursor: &mut usize) -> Option { - let raw = decode_varint(bytes, cursor)?; - Some(((raw >> 1) as i64) ^ (-((raw & 1) as i64))) -} - -fn encode_varint(buffer: &mut Vec, mut value: u64) { - while value >= 0x80 { - buffer.push(((value as u8) & 0x7F) | 0x80); - value >>= 7; - } - buffer.push(value as u8); -} - -fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { - let mut value = 0u64; - let mut shift = 0u32; - - while *cursor < bytes.len() { - let byte = bytes[*cursor]; - *cursor += 1; - value |= ((byte & 0x7F) as u64) << shift; - if byte & 0x80 == 0 { - return Some(value); - } - shift += 7; - if shift >= 64 { - return None; - } - } - - None -} From 0228f7191bc8fafd7fd263a2d2251e56e4996e47 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 1 Dec 2025 06:57:21 +0100 Subject: [PATCH 07/22] add merkle tree multiproof benchmark report --- .../multiproof_v2_report.md | 450 ++++++++++++++++++ .../proof_size_1048576_adversarial.svg | 278 +++++++++++ .../proof_size_1048576_clustered.svg | 280 +++++++++++ .../proof_size_1048576_random.svg | 267 +++++++++++ .../proof_size_16384_adversarial.svg | 265 +++++++++++ .../proof_size_16384_clustered.svg | 280 +++++++++++ .../proof_size_16384_random.svg | 252 ++++++++++ .../proof_size_262144_adversarial.svg | 259 ++++++++++ .../proof_size_262144_clustered.svg | 280 +++++++++++ .../proof_size_262144_random.svg | 273 +++++++++++ .../proof_size_4096_adversarial.svg | 280 +++++++++++ .../proof_size_4096_clustered.svg | 280 +++++++++++ .../proof_size_4096_random.svg | 280 +++++++++++ .../proof_size_65536_adversarial.svg | 308 ++++++++++++ .../proof_size_65536_clustered.svg | 280 +++++++++++ .../proof_size_65536_random.svg | 292 ++++++++++++ .../prove_ms_1048576_adversarial.svg | 252 ++++++++++ .../prove_ms_1048576_clustered.svg | 241 ++++++++++ .../prove_ms_1048576_random.svg | 270 +++++++++++ .../prove_ms_16384_adversarial.svg | 242 ++++++++++ .../prove_ms_16384_clustered.svg | 242 ++++++++++ .../prove_ms_16384_random.svg | 256 ++++++++++ .../prove_ms_262144_adversarial.svg | 275 +++++++++++ .../prove_ms_262144_clustered.svg | 252 ++++++++++ .../prove_ms_262144_random.svg | 286 +++++++++++ .../prove_ms_4096_adversarial.svg | 236 +++++++++ .../prove_ms_4096_clustered.svg | 237 +++++++++ .../prove_ms_4096_random.svg | 243 ++++++++++ .../prove_ms_65536_adversarial.svg | 260 ++++++++++ .../prove_ms_65536_clustered.svg | 241 ++++++++++ .../prove_ms_65536_random.svg | 268 +++++++++++ .../verify_ms_1048576_adversarial.svg | 291 +++++++++++ .../verify_ms_1048576_clustered.svg | 296 ++++++++++++ .../verify_ms_1048576_random.svg | 281 +++++++++++ .../verify_ms_16384_adversarial.svg | 277 +++++++++++ .../verify_ms_16384_clustered.svg | 295 ++++++++++++ .../verify_ms_16384_random.svg | 265 +++++++++++ .../verify_ms_262144_adversarial.svg | 266 +++++++++++ .../verify_ms_262144_clustered.svg | 298 ++++++++++++ .../verify_ms_262144_random.svg | 253 ++++++++++ .../verify_ms_4096_adversarial.svg | 298 ++++++++++++ .../verify_ms_4096_clustered.svg | 298 ++++++++++++ .../verify_ms_4096_random.svg | 295 ++++++++++++ .../verify_ms_65536_adversarial.svg | 325 +++++++++++++ .../verify_ms_65536_clustered.svg | 298 ++++++++++++ .../verify_ms_65536_random.svg | 306 ++++++++++++ 46 files changed, 12747 insertions(+) create mode 100644 crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg create mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg diff --git a/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md b/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md new file mode 100644 index 00000000..dc5df06b --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md @@ -0,0 +1,450 @@ +# Merkle Tree Multiproof Benchmark Report + +Generated: 1764530071.130155803s + +| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms | rss_delta_kb | +| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- | ------------ | +| 4096 | 12 | 1 | random | prefix | 440 | 12 | 12.00 | 0.01 | 0.45 | 0 | +| 4096 | 12 | 1 | random | coset | 471 | 12 | 12.00 | 0.87 | 0.72 | 0 | +| 4096 | 12 | 1 | clustered | prefix | 440 | 12 | 12.00 | 0.00 | 0.43 | 0 | +| 4096 | 12 | 1 | clustered | coset | 471 | 12 | 12.00 | 0.00 | 0.45 | 0 | +| 4096 | 12 | 1 | adversarial | prefix | 440 | 12 | 12.00 | 0.03 | 0.49 | 0 | +| 4096 | 12 | 1 | adversarial | coset | 469 | 12 | 12.00 | 0.00 | 0.43 | 0 | +| 4096 | 12 | 2 | random | prefix | 848 | 24 | 12.00 | 0.00 | 0.84 | 0 | +| 4096 | 12 | 2 | random | coset | 824 | 22 | 11.00 | 0.03 | 0.83 | 0 | +| 4096 | 12 | 2 | clustered | prefix | 496 | 13 | 6.50 | 0.00 | 0.51 | 0 | +| 4096 | 12 | 2 | clustered | coset | 449 | 11 | 5.50 | 0.00 | 0.49 | 0 | +| 4096 | 12 | 2 | adversarial | prefix | 848 | 24 | 12.00 | 0.00 | 0.92 | 0 | +| 4096 | 12 | 2 | adversarial | coset | 823 | 22 | 11.00 | 0.00 | 0.85 | 0 | +| 4096 | 12 | 4 | random | prefix | 1536 | 44 | 11.00 | 0.00 | 1.49 | 0 | +| 4096 | 12 | 4 | random | coset | 1385 | 38 | 9.50 | 0.01 | 1.55 | 0 | +| 4096 | 12 | 4 | clustered | prefix | 704 | 18 | 4.50 | 0.00 | 0.78 | 0 | +| 4096 | 12 | 4 | clustered | coset | 493 | 12 | 3.00 | 0.01 | 0.75 | 0 | +| 4096 | 12 | 4 | adversarial | prefix | 1600 | 46 | 11.50 | 0.00 | 1.55 | 0 | +| 4096 | 12 | 4 | adversarial | coset | 1455 | 40 | 10.00 | 0.01 | 2.21 | 0 | +| 4096 | 12 | 8 | random | prefix | 2752 | 79 | 9.88 | 0.07 | 3.35 | 0 | +| 4096 | 12 | 8 | random | coset | 2325 | 65 | 8.12 | 0.06 | 2.92 | 0 | +| 4096 | 12 | 8 | clustered | prefix | 1024 | 25 | 3.12 | 0.00 | 1.24 | 0 | +| 4096 | 12 | 8 | clustered | coset | 497 | 11 | 1.38 | 0.01 | 1.10 | 0 | +| 4096 | 12 | 8 | adversarial | prefix | 2976 | 86 | 10.75 | 0.00 | 3.68 | 0 | +| 4096 | 12 | 8 | adversarial | coset | 2576 | 72 | 9.00 | 0.01 | 3.49 | 0 | +| 4096 | 12 | 16 | random | prefix | 4640 | 132 | 8.25 | 0.05 | 6.92 | 0 | +| 4096 | 12 | 16 | random | coset | 3636 | 102 | 6.38 | 0.24 | 4.98 | 0 | +| 4096 | 12 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.01 | 1.90 | 0 | +| 4096 | 12 | 16 | clustered | coset | 723 | 16 | 1.00 | 0.01 | 1.90 | 0 | +| 4096 | 12 | 16 | adversarial | prefix | 5472 | 158 | 9.88 | 0.01 | 5.47 | 0 | +| 4096 | 12 | 16 | adversarial | coset | 4537 | 128 | 8.00 | 0.05 | 5.68 | 0 | +| 4096 | 12 | 32 | random | prefix | 9472 | 271 | 8.47 | 0.02 | 9.11 | 0 | +| 4096 | 12 | 32 | random | coset | 7372 | 209 | 6.53 | 0.03 | 10.08 | 0 | +| 4096 | 12 | 32 | clustered | prefix | 3200 | 75 | 2.34 | 0.01 | 3.48 | 0 | +| 4096 | 12 | 32 | clustered | coset | 754 | 13 | 0.41 | 0.02 | 3.76 | 0 | +| 4096 | 12 | 32 | adversarial | prefix | 9952 | 286 | 8.94 | 0.02 | 11.75 | 0 | +| 4096 | 12 | 32 | adversarial | coset | 7898 | 224 | 7.00 | 0.08 | 11.86 | 0 | +| 4096 | 12 | 64 | random | prefix | 15776 | 444 | 6.94 | 0.02 | 17.67 | 0 | +| 4096 | 12 | 64 | random | coset | 11265 | 318 | 4.97 | 0.04 | 18.72 | 0 | +| 4096 | 12 | 64 | clustered | prefix | 6080 | 141 | 2.20 | 0.01 | 7.20 | 0 | +| 4096 | 12 | 64 | clustered | coset | 1081 | 15 | 0.23 | 0.03 | 6.97 | 0 | +| 4096 | 12 | 64 | adversarial | prefix | 17888 | 510 | 7.97 | 0.03 | 19.63 | 0 | +| 4096 | 12 | 64 | adversarial | coset | 13499 | 384 | 6.00 | 0.04 | 20.32 | 0 | +| 4096 | 12 | 128 | random | prefix | 28896 | 806 | 6.30 | 0.04 | 31.51 | 0 | +| 4096 | 12 | 128 | random | coset | 19600 | 552 | 4.31 | 0.06 | 33.19 | 0 | +| 4096 | 12 | 128 | clustered | prefix | 11584 | 265 | 2.07 | 0.02 | 14.31 | 0 | +| 4096 | 12 | 128 | clustered | coset | 1458 | 11 | 0.09 | 0.06 | 12.89 | 0 | +| 4096 | 12 | 128 | adversarial | prefix | 31712 | 894 | 6.98 | 0.04 | 37.36 | 0 | +| 4096 | 12 | 128 | adversarial | coset | 22586 | 640 | 5.00 | 0.06 | 35.57 | 0 | +| 4096 | 12 | 256 | random | prefix | 49376 | 1350 | 5.27 | 0.07 | 52.54 | 0 | +| 4096 | 12 | 256 | random | coset | 30183 | 840 | 3.28 | 0.14 | 55.84 | 0 | +| 4096 | 12 | 256 | clustered | prefix | 22880 | 522 | 2.04 | 0.04 | 28.28 | 0 | +| 4096 | 12 | 256 | clustered | coset | 2515 | 12 | 0.05 | 0.07 | 32.59 | 0 | +| 4096 | 12 | 256 | adversarial | prefix | 55264 | 1534 | 5.99 | 0.05 | 57.32 | 0 | +| 4096 | 12 | 256 | adversarial | coset | 36409 | 1024 | 4.00 | 0.13 | 63.94 | 0 | +| 4096 | 12 | 512 | random | prefix | 83584 | 2227 | 4.35 | 0.12 | 94.54 | 0 | +| 4096 | 12 | 512 | random | coset | 44256 | 1205 | 2.35 | 0.14 | 100.43 | 0 | +| 4096 | 12 | 512 | clustered | prefix | 45408 | 1034 | 2.02 | 0.07 | 57.55 | 0 | +| 4096 | 12 | 512 | clustered | coset | 4556 | 12 | 0.02 | 0.08 | 50.02 | 0 | +| 4096 | 12 | 512 | adversarial | prefix | 94176 | 2558 | 5.00 | 0.08 | 100.27 | 0 | +| 4096 | 12 | 512 | adversarial | coset | 55352 | 1536 | 3.00 | 0.14 | 106.47 | 0 | +| 4096 | 12 | 1024 | random | prefix | 139264 | 3583 | 3.50 | 0.17 | 154.62 | 0 | +| 4096 | 12 | 1024 | random | coset | 58977 | 1537 | 1.50 | 0.22 | 149.52 | 0 | +| 4096 | 12 | 1024 | clustered | prefix | 90464 | 2058 | 2.01 | 0.14 | 107.70 | 0 | +| 4096 | 12 | 1024 | clustered | coset | 8655 | 12 | 0.01 | 0.15 | 100.56 | 0 | +| 4096 | 12 | 1024 | adversarial | prefix | 155616 | 4094 | 4.00 | 0.15 | 160.98 | 0 | +| 4096 | 12 | 1024 | adversarial | coset | 75831 | 2048 | 2.00 | 0.22 | 172.59 | 0 | +| 4096 | 12 | 2048 | random | prefix | 226720 | 5548 | 2.71 | 0.29 | 264.00 | 0 | +| 4096 | 12 | 2048 | random | coset | 63902 | 1454 | 0.71 | 0.43 | 261.51 | 0 | +| 4096 | 12 | 2048 | clustered | prefix | 180512 | 4104 | 2.00 | 0.25 | 213.80 | 0 | +| 4096 | 12 | 2048 | clustered | coset | 16782 | 10 | 0.00 | 0.30 | 195.58 | 0 | +| 4096 | 12 | 2048 | adversarial | prefix | 245728 | 6142 | 3.00 | 0.28 | 278.58 | 0 | +| 4096 | 12 | 2048 | adversarial | coset | 81945 | 2048 | 1.00 | 0.29 | 270.10 | 0 | +| 4096 | 12 | 4096 | random | prefix | 360416 | 8190 | 2.00 | 0.47 | 407.58 | 0 | +| 4096 | 12 | 4096 | random | coset | 32793 | 0 | 0.00 | 0.52 | 394.06 | 0 | +| 4096 | 12 | 4096 | clustered | prefix | 360416 | 8190 | 2.00 | 0.47 | 417.15 | 0 | +| 4096 | 12 | 4096 | clustered | coset | 32793 | 0 | 0.00 | 0.56 | 418.08 | 0 | +| 4096 | 12 | 4096 | adversarial | prefix | 360416 | 8190 | 2.00 | 0.49 | 403.72 | 0 | +| 4096 | 12 | 4096 | adversarial | coset | 32793 | 0 | 0.00 | 0.51 | 418.19 | 0 | +| 16384 | 14 | 1 | random | prefix | 504 | 14 | 14.00 | 0.05 | 0.79 | 0 | +| 16384 | 14 | 1 | random | coset | 540 | 14 | 14.00 | 0.10 | 0.80 | 0 | +| 16384 | 14 | 1 | clustered | prefix | 504 | 14 | 14.00 | 0.04 | 0.49 | 0 | +| 16384 | 14 | 1 | clustered | coset | 543 | 14 | 14.00 | 0.01 | 0.51 | 0 | +| 16384 | 14 | 1 | adversarial | prefix | 504 | 14 | 14.00 | 0.00 | 0.48 | 0 | +| 16384 | 14 | 1 | adversarial | coset | 537 | 14 | 14.00 | 0.00 | 0.54 | 0 | +| 16384 | 14 | 2 | random | prefix | 976 | 28 | 14.00 | 0.05 | 0.99 | 0 | +| 16384 | 14 | 2 | random | coset | 962 | 26 | 13.00 | 0.01 | 1.26 | 0 | +| 16384 | 14 | 2 | clustered | prefix | 560 | 15 | 7.50 | 0.00 | 0.71 | 0 | +| 16384 | 14 | 2 | clustered | coset | 519 | 13 | 6.50 | 0.01 | 0.55 | 0 | +| 16384 | 14 | 2 | adversarial | prefix | 976 | 28 | 14.00 | 0.00 | 0.96 | 0 | +| 16384 | 14 | 2 | adversarial | coset | 963 | 26 | 13.00 | 0.01 | 0.95 | 0 | +| 16384 | 14 | 4 | random | prefix | 1728 | 50 | 12.50 | 0.04 | 1.77 | 0 | +| 16384 | 14 | 4 | random | coset | 1595 | 44 | 11.00 | 0.04 | 2.56 | 0 | +| 16384 | 14 | 4 | clustered | prefix | 704 | 18 | 4.50 | 0.02 | 0.72 | 0 | +| 16384 | 14 | 4 | clustered | coset | 499 | 12 | 3.00 | 0.02 | 0.71 | 0 | +| 16384 | 14 | 4 | adversarial | prefix | 1856 | 54 | 13.50 | 0.03 | 2.16 | 0 | +| 16384 | 14 | 4 | adversarial | coset | 1735 | 48 | 12.00 | 0.04 | 1.80 | 0 | +| 16384 | 14 | 8 | random | prefix | 3264 | 95 | 11.88 | 0.02 | 3.54 | 0 | +| 16384 | 14 | 8 | random | coset | 2890 | 81 | 10.12 | 0.08 | 3.28 | 0 | +| 16384 | 14 | 8 | clustered | prefix | 1152 | 29 | 3.62 | 0.00 | 1.35 | 0 | +| 16384 | 14 | 8 | clustered | coset | 630 | 15 | 1.88 | 0.02 | 1.74 | 0 | +| 16384 | 14 | 8 | adversarial | prefix | 3488 | 102 | 12.75 | 0.04 | 3.42 | 0 | +| 16384 | 14 | 8 | adversarial | coset | 3136 | 88 | 11.00 | 0.01 | 3.96 | 0 | +| 16384 | 14 | 16 | random | prefix | 6144 | 179 | 11.19 | 0.11 | 8.62 | 0 | +| 16384 | 14 | 16 | random | coset | 5264 | 149 | 9.31 | 0.15 | 7.48 | 0 | +| 16384 | 14 | 16 | clustered | prefix | 1856 | 45 | 2.81 | 0.01 | 1.88 | 0 | +| 16384 | 14 | 16 | clustered | coset | 691 | 15 | 0.94 | 0.07 | 1.89 | 0 | +| 16384 | 14 | 16 | adversarial | prefix | 6496 | 190 | 11.88 | 0.06 | 6.33 | 0 | +| 16384 | 14 | 16 | adversarial | coset | 5657 | 160 | 10.00 | 0.19 | 6.44 | 0 | +| 16384 | 14 | 32 | random | prefix | 10880 | 315 | 9.84 | 0.09 | 11.29 | 0 | +| 16384 | 14 | 32 | random | coset | 8911 | 253 | 7.91 | 0.12 | 10.71 | 0 | +| 16384 | 14 | 32 | clustered | prefix | 3232 | 76 | 2.38 | 0.01 | 3.39 | 0 | +| 16384 | 14 | 32 | clustered | coset | 787 | 14 | 0.44 | 0.02 | 3.29 | 0 | +| 16384 | 14 | 32 | adversarial | prefix | 12000 | 350 | 10.94 | 0.10 | 11.67 | 0 | +| 16384 | 14 | 32 | adversarial | coset | 10138 | 288 | 9.00 | 0.09 | 11.82 | 0 | +| 16384 | 14 | 64 | random | prefix | 19904 | 573 | 8.95 | 0.05 | 20.74 | 0 | +| 16384 | 14 | 64 | random | coset | 15714 | 447 | 6.98 | 0.13 | 19.71 | 0 | +| 16384 | 14 | 64 | clustered | prefix | 6016 | 139 | 2.17 | 0.01 | 6.14 | 0 | +| 16384 | 14 | 64 | clustered | coset | 1014 | 13 | 0.20 | 0.02 | 6.28 | 0 | +| 16384 | 14 | 64 | adversarial | prefix | 21984 | 638 | 9.97 | 0.03 | 22.37 | 0 | +| 16384 | 14 | 64 | adversarial | coset | 17979 | 512 | 8.00 | 0.05 | 22.49 | 0 | +| 16384 | 14 | 128 | random | prefix | 36640 | 1048 | 8.19 | 0.10 | 37.27 | 0 | +| 16384 | 14 | 128 | random | coset | 27885 | 794 | 6.20 | 0.16 | 38.35 | 0 | +| 16384 | 14 | 128 | clustered | prefix | 11680 | 268 | 2.09 | 0.02 | 12.54 | 0 | +| 16384 | 14 | 128 | clustered | coset | 1553 | 14 | 0.11 | 0.03 | 12.49 | 0 | +| 16384 | 14 | 128 | adversarial | prefix | 39904 | 1150 | 8.98 | 0.10 | 42.08 | 0 | +| 16384 | 14 | 128 | adversarial | coset | 31419 | 896 | 7.00 | 0.16 | 44.56 | 0 | +| 16384 | 14 | 256 | random | prefix | 63648 | 1796 | 7.02 | 0.14 | 71.58 | 0 | +| 16384 | 14 | 256 | random | coset | 45385 | 1286 | 5.02 | 0.15 | 70.52 | 0 | +| 16384 | 14 | 256 | clustered | prefix | 22912 | 523 | 2.04 | 0.04 | 25.62 | 0 | +| 16384 | 14 | 256 | clustered | coset | 2550 | 13 | 0.05 | 0.05 | 27.82 | 0 | +| 16384 | 14 | 256 | adversarial | prefix | 71648 | 2046 | 7.99 | 0.08 | 79.97 | 0 | +| 16384 | 14 | 256 | adversarial | coset | 53819 | 1536 | 6.00 | 0.12 | 78.00 | 0 | +| 16384 | 14 | 512 | random | prefix | 113408 | 3159 | 6.17 | 0.17 | 117.88 | 0 | +| 16384 | 14 | 512 | random | coset | 75820 | 2137 | 4.17 | 0.23 | 126.40 | 0 | +| 16384 | 14 | 512 | clustered | prefix | 45536 | 1038 | 2.03 | 0.07 | 50.77 | 0 | +| 16384 | 14 | 512 | clustered | coset | 4694 | 16 | 0.03 | 0.09 | 55.49 | 0 | +| 16384 | 14 | 512 | adversarial | prefix | 126944 | 3582 | 7.00 | 0.16 | 142.52 | 0 | +| 16384 | 14 | 512 | adversarial | coset | 90170 | 2560 | 5.00 | 0.22 | 137.58 | 0 | +| 16384 | 14 | 1024 | random | prefix | 195712 | 5347 | 5.22 | 0.38 | 220.34 | 0 | +| 16384 | 14 | 1024 | random | coset | 118567 | 3301 | 3.22 | 0.34 | 207.68 | 0 | +| 16384 | 14 | 1024 | clustered | prefix | 90560 | 2061 | 2.01 | 0.15 | 101.42 | 0 | +| 16384 | 14 | 1024 | clustered | coset | 8756 | 15 | 0.01 | 0.17 | 94.09 | 0 | +| 16384 | 14 | 1024 | adversarial | prefix | 221152 | 6142 | 6.00 | 0.29 | 252.24 | 0 | +| 16384 | 14 | 1024 | adversarial | coset | 145465 | 4096 | 4.00 | 0.32 | 232.68 | 0 | +| 16384 | 14 | 2048 | random | prefix | 335680 | 8953 | 4.37 | 0.43 | 364.78 | 0 | +| 16384 | 14 | 2048 | random | coset | 178065 | 4859 | 2.37 | 0.52 | 363.75 | 0 | +| 16384 | 14 | 2048 | clustered | prefix | 180672 | 4109 | 2.01 | 0.27 | 216.93 | 0 | +| 16384 | 14 | 2048 | clustered | coset | 16951 | 15 | 0.01 | 0.25 | 206.32 | 0 | +| 16384 | 14 | 2048 | adversarial | prefix | 376800 | 10238 | 5.00 | 0.32 | 404.15 | 0 | +| 16384 | 14 | 2048 | adversarial | coset | 221240 | 6144 | 3.00 | 0.47 | 408.73 | 0 | +| 16384 | 14 | 4096 | random | prefix | 560288 | 14436 | 3.52 | 0.73 | 581.70 | 0 | +| 16384 | 14 | 4096 | random | coset | 238926 | 6246 | 1.52 | 1.30 | 624.38 | 0 | +| 16384 | 14 | 4096 | clustered | prefix | 360896 | 8205 | 2.00 | 0.53 | 409.04 | 0 | +| 16384 | 14 | 4096 | clustered | coset | 33335 | 15 | 0.00 | 0.80 | 397.47 | 0 | +| 16384 | 14 | 4096 | adversarial | prefix | 622560 | 16382 | 4.00 | 0.59 | 689.98 | 0 | +| 16384 | 14 | 4096 | adversarial | coset | 303159 | 8192 | 2.00 | 0.81 | 670.59 | 0 | +| 65536 | 16 | 1 | random | prefix | 568 | 16 | 16.00 | 0.00 | 0.68 | 0 | +| 65536 | 16 | 1 | random | coset | 612 | 16 | 16.00 | 0.06 | 1.26 | 0 | +| 65536 | 16 | 1 | clustered | prefix | 568 | 16 | 16.00 | 0.06 | 0.96 | 0 | +| 65536 | 16 | 1 | clustered | coset | 611 | 16 | 16.00 | 0.01 | 0.94 | 0 | +| 65536 | 16 | 1 | adversarial | prefix | 568 | 16 | 16.00 | 0.06 | 0.63 | 0 | +| 65536 | 16 | 1 | adversarial | coset | 605 | 16 | 16.00 | 0.01 | 0.70 | 0 | +| 65536 | 16 | 2 | random | prefix | 1104 | 32 | 16.00 | 0.01 | 1.31 | 0 | +| 65536 | 16 | 2 | random | coset | 1107 | 30 | 15.00 | 0.07 | 1.20 | 0 | +| 65536 | 16 | 2 | clustered | prefix | 688 | 19 | 9.50 | 0.03 | 0.69 | 0 | +| 65536 | 16 | 2 | clustered | coset | 653 | 17 | 8.50 | 0.01 | 0.67 | 0 | +| 65536 | 16 | 2 | adversarial | prefix | 1104 | 32 | 16.00 | 0.03 | 1.19 | 0 | +| 65536 | 16 | 2 | adversarial | coset | 1105 | 30 | 15.00 | 0.07 | 1.67 | 0 | +| 65536 | 16 | 4 | random | prefix | 1728 | 50 | 12.50 | 0.01 | 1.67 | 0 | +| 65536 | 16 | 4 | random | coset | 1594 | 44 | 11.00 | 0.03 | 2.04 | 0 | +| 65536 | 16 | 4 | clustered | prefix | 800 | 21 | 5.25 | 0.02 | 0.95 | 0 | +| 65536 | 16 | 4 | clustered | coset | 605 | 15 | 3.75 | 0.01 | 0.89 | 0 | +| 65536 | 16 | 4 | adversarial | prefix | 2112 | 62 | 15.50 | 0.02 | 2.10 | 0 | +| 65536 | 16 | 4 | adversarial | coset | 2019 | 56 | 14.00 | 0.03 | 2.60 | 0 | +| 65536 | 16 | 8 | random | prefix | 3936 | 116 | 14.50 | 0.14 | 4.12 | 0 | +| 65536 | 16 | 8 | random | coset | 3622 | 102 | 12.75 | 0.09 | 3.96 | 0 | +| 65536 | 16 | 8 | clustered | prefix | 1248 | 32 | 4.00 | 0.01 | 1.30 | 0 | +| 65536 | 16 | 8 | clustered | coset | 735 | 18 | 2.25 | 0.05 | 1.76 | 0 | +| 65536 | 16 | 8 | adversarial | prefix | 4000 | 118 | 14.75 | 0.01 | 4.06 | 0 | +| 65536 | 16 | 8 | adversarial | coset | 3697 | 104 | 13.00 | 0.04 | 4.01 | 0 | +| 65536 | 16 | 16 | random | prefix | 6944 | 204 | 12.75 | 0.08 | 7.56 | 0 | +| 65536 | 16 | 16 | random | coset | 6143 | 174 | 10.88 | 0.12 | 7.27 | 0 | +| 65536 | 16 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.02 | 1.89 | 0 | +| 65536 | 16 | 16 | clustered | coset | 736 | 16 | 1.00 | 0.01 | 2.75 | 0 | +| 65536 | 16 | 16 | adversarial | prefix | 7520 | 222 | 13.88 | 0.05 | 8.05 | 0 | +| 65536 | 16 | 16 | adversarial | coset | 6778 | 192 | 12.00 | 0.09 | 7.72 | 0 | +| 65536 | 16 | 32 | random | prefix | 13440 | 395 | 12.34 | 0.11 | 14.61 | 0 | +| 65536 | 16 | 32 | random | coset | 11696 | 333 | 10.41 | 0.18 | 13.93 | 0 | +| 65536 | 16 | 32 | clustered | prefix | 3264 | 77 | 2.41 | 0.01 | 3.46 | 0 | +| 65536 | 16 | 32 | clustered | coset | 827 | 15 | 0.47 | 0.02 | 3.34 | 0 | +| 65536 | 16 | 32 | adversarial | prefix | 14048 | 414 | 12.94 | 0.10 | 14.71 | 0 | +| 65536 | 16 | 32 | adversarial | coset | 12379 | 352 | 11.00 | 0.14 | 14.03 | 0 | +| 65536 | 16 | 64 | random | prefix | 24608 | 720 | 11.25 | 0.20 | 25.19 | 0 | +| 65536 | 16 | 64 | random | coset | 20825 | 594 | 9.28 | 0.25 | 25.97 | 0 | +| 65536 | 16 | 64 | clustered | prefix | 6176 | 144 | 2.25 | 0.02 | 7.13 | 0 | +| 65536 | 16 | 64 | clustered | coset | 1183 | 18 | 0.28 | 0.02 | 6.40 | 0 | +| 65536 | 16 | 64 | adversarial | prefix | 26080 | 766 | 11.97 | 0.12 | 25.83 | 0 | +| 65536 | 16 | 64 | adversarial | coset | 22460 | 640 | 10.00 | 0.16 | 27.80 | 0 | +| 65536 | 16 | 128 | random | prefix | 44224 | 1285 | 10.04 | 0.17 | 44.88 | 0 | +| 65536 | 16 | 128 | random | coset | 36127 | 1031 | 8.05 | 0.77 | 45.09 | 0 | +| 65536 | 16 | 128 | clustered | prefix | 11776 | 271 | 2.12 | 0.03 | 12.65 | 0 | +| 65536 | 16 | 128 | clustered | coset | 1659 | 17 | 0.13 | 0.03 | 12.18 | 0 | +| 65536 | 16 | 128 | adversarial | prefix | 48096 | 1406 | 10.98 | 0.12 | 50.78 | 0 | +| 65536 | 16 | 128 | adversarial | coset | 40380 | 1152 | 9.00 | 0.24 | 50.79 | 0 | +| 65536 | 16 | 256 | random | prefix | 80384 | 2319 | 9.06 | 0.23 | 83.15 | 0 | +| 65536 | 16 | 256 | random | coset | 63408 | 1809 | 7.07 | 0.43 | 85.16 | 0 | +| 65536 | 16 | 256 | clustered | prefix | 22880 | 522 | 2.04 | 0.04 | 27.42 | 0 | +| 65536 | 16 | 256 | clustered | coset | 2515 | 12 | 0.05 | 0.06 | 25.53 | 0 | +| 65536 | 16 | 256 | adversarial | prefix | 88032 | 2558 | 9.99 | 0.22 | 99.07 | 0 | +| 65536 | 16 | 256 | adversarial | coset | 71740 | 2048 | 8.00 | 0.32 | 94.96 | 0 | +| 65536 | 16 | 512 | random | prefix | 144544 | 4132 | 8.07 | 0.26 | 164.78 | 0 | +| 65536 | 16 | 512 | random | coset | 109154 | 3110 | 6.07 | 0.41 | 157.91 | 0 | +| 65536 | 16 | 512 | clustered | prefix | 45600 | 1040 | 2.03 | 0.08 | 53.33 | 0 | +| 65536 | 16 | 512 | clustered | coset | 4768 | 18 | 0.04 | 0.10 | 53.25 | 0 | +| 65536 | 16 | 512 | adversarial | prefix | 159712 | 4606 | 9.00 | 0.22 | 169.87 | 0 | +| 65536 | 16 | 512 | adversarial | coset | 125500 | 3584 | 7.00 | 0.32 | 187.96 | 0 | +| 65536 | 16 | 1024 | random | prefix | 259136 | 7329 | 7.16 | 0.34 | 281.81 | 0 | +| 65536 | 16 | 1024 | random | coset | 186031 | 5283 | 5.16 | 0.57 | 290.11 | 0 | +| 65536 | 16 | 1024 | clustered | prefix | 90560 | 2061 | 2.01 | 0.14 | 97.01 | 0 | +| 65536 | 16 | 1024 | clustered | coset | 8764 | 15 | 0.01 | 0.17 | 105.35 | 0 | +| 65536 | 16 | 1024 | adversarial | prefix | 286688 | 8190 | 8.00 | 0.32 | 309.03 | 0 | +| 65536 | 16 | 1024 | adversarial | coset | 215100 | 6144 | 6.00 | 0.47 | 306.22 | 0 | +| 65536 | 16 | 2048 | random | prefix | 454496 | 12666 | 6.18 | 0.74 | 509.36 | 0 | +| 65536 | 16 | 2048 | random | coset | 303958 | 8572 | 4.19 | 1.27 | 492.45 | 0 | +| 65536 | 16 | 2048 | clustered | prefix | 180736 | 4111 | 2.01 | 0.28 | 212.77 | 0 | +| 65536 | 16 | 2048 | clustered | coset | 17022 | 17 | 0.01 | 0.35 | 211.18 | 0 | +| 65536 | 16 | 2048 | adversarial | prefix | 507872 | 14334 | 7.00 | 0.55 | 549.20 | 0 | +| 65536 | 16 | 2048 | adversarial | coset | 360507 | 10240 | 5.00 | 1.34 | 574.17 | 0 | +| 65536 | 16 | 4096 | random | prefix | 788544 | 21569 | 5.27 | 1.03 | 855.42 | 0 | +| 65536 | 16 | 4096 | random | coset | 480070 | 13379 | 3.27 | 1.64 | 864.68 | 0 | +| 65536 | 16 | 4096 | clustered | prefix | 360960 | 8207 | 2.00 | 0.75 | 418.24 | 0 | +| 65536 | 16 | 4096 | clustered | coset | 33406 | 17 | 0.00 | 0.67 | 406.56 | 0 | +| 65536 | 16 | 4096 | adversarial | prefix | 884704 | 24574 | 6.00 | 1.03 | 985.23 | 0 | +| 65536 | 16 | 4096 | adversarial | coset | 581690 | 16384 | 4.00 | 1.46 | 993.18 | 0 | +| 262144 | 18 | 1 | random | prefix | 632 | 18 | 18.00 | 0.01 | 0.62 | 0 | +| 262144 | 18 | 1 | random | coset | 686 | 18 | 18.00 | 0.06 | 0.62 | 0 | +| 262144 | 18 | 1 | clustered | prefix | 632 | 18 | 18.00 | 0.00 | 0.64 | 0 | +| 262144 | 18 | 1 | clustered | coset | 686 | 18 | 18.00 | 0.05 | 0.62 | 0 | +| 262144 | 18 | 1 | adversarial | prefix | 632 | 18 | 18.00 | 0.00 | 0.91 | 0 | +| 262144 | 18 | 1 | adversarial | coset | 673 | 18 | 18.00 | 0.05 | 0.91 | 0 | +| 262144 | 18 | 2 | random | prefix | 1200 | 35 | 17.50 | 0.01 | 1.81 | 0 | +| 262144 | 18 | 2 | random | coset | 1211 | 33 | 16.50 | 0.10 | 1.21 | 0 | +| 262144 | 18 | 2 | clustered | prefix | 688 | 19 | 9.50 | 0.03 | 0.79 | 0 | +| 262144 | 18 | 2 | clustered | coset | 662 | 17 | 8.50 | 0.05 | 0.78 | 0 | +| 262144 | 18 | 2 | adversarial | prefix | 1232 | 36 | 18.00 | 0.00 | 1.26 | 0 | +| 262144 | 18 | 2 | adversarial | coset | 1249 | 34 | 17.00 | 0.04 | 1.59 | 0 | +| 262144 | 18 | 4 | random | prefix | 2176 | 64 | 16.00 | 0.04 | 3.47 | 0 | +| 262144 | 18 | 4 | random | coset | 2094 | 58 | 14.50 | 0.07 | 2.64 | 0 | +| 262144 | 18 | 4 | clustered | prefix | 1024 | 28 | 7.00 | 0.01 | 1.02 | 0 | +| 262144 | 18 | 4 | clustered | coset | 843 | 22 | 5.50 | 0.05 | 1.18 | 0 | +| 262144 | 18 | 4 | adversarial | prefix | 2368 | 70 | 17.50 | 0.01 | 2.81 | 0 | +| 262144 | 18 | 4 | adversarial | coset | 2307 | 64 | 16.00 | 0.08 | 2.86 | 0 | +| 262144 | 18 | 8 | random | prefix | 4384 | 130 | 16.25 | 0.07 | 4.46 | 0 | +| 262144 | 18 | 8 | random | coset | 4121 | 116 | 14.50 | 0.26 | 4.87 | 0 | +| 262144 | 18 | 8 | clustered | prefix | 1280 | 33 | 4.12 | 0.00 | 1.46 | 0 | +| 262144 | 18 | 8 | clustered | coset | 773 | 19 | 2.38 | 0.01 | 1.75 | 0 | +| 262144 | 18 | 8 | adversarial | prefix | 4512 | 134 | 16.75 | 0.01 | 5.09 | 0 | +| 262144 | 18 | 8 | adversarial | coset | 4273 | 120 | 15.00 | 0.09 | 5.98 | 0 | +| 262144 | 18 | 16 | random | prefix | 8096 | 240 | 15.00 | 0.13 | 8.37 | 0 | +| 262144 | 18 | 16 | random | coset | 7413 | 210 | 13.12 | 0.36 | 10.14 | 0 | +| 262144 | 18 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.01 | 1.94 | 0 | +| 262144 | 18 | 16 | clustered | coset | 736 | 16 | 1.00 | 0.01 | 2.23 | 0 | +| 262144 | 18 | 16 | adversarial | prefix | 8544 | 254 | 15.88 | 0.10 | 9.37 | 0 | +| 262144 | 18 | 16 | adversarial | coset | 7915 | 224 | 14.00 | 0.19 | 10.12 | 0 | +| 262144 | 18 | 32 | random | prefix | 15296 | 453 | 14.16 | 0.11 | 19.29 | 0 | +| 262144 | 18 | 32 | random | coset | 13737 | 391 | 12.22 | 0.45 | 16.44 | 0 | +| 262144 | 18 | 32 | clustered | prefix | 3296 | 78 | 2.44 | 0.01 | 3.42 | 0 | +| 262144 | 18 | 32 | clustered | coset | 863 | 16 | 0.50 | 0.02 | 4.19 | 0 | +| 262144 | 18 | 32 | adversarial | prefix | 16096 | 478 | 14.94 | 0.06 | 17.81 | 0 | +| 262144 | 18 | 32 | adversarial | coset | 14621 | 416 | 13.00 | 0.38 | 16.71 | 0 | +| 262144 | 18 | 64 | random | prefix | 28000 | 826 | 12.91 | 0.21 | 29.38 | 0 | +| 262144 | 18 | 64 | random | coset | 24537 | 700 | 10.94 | 0.57 | 32.65 | 0 | +| 262144 | 18 | 64 | clustered | prefix | 6112 | 142 | 2.22 | 0.02 | 6.92 | 0 | +| 262144 | 18 | 64 | clustered | coset | 1119 | 16 | 0.25 | 0.03 | 7.47 | 0 | +| 262144 | 18 | 64 | adversarial | prefix | 30176 | 894 | 13.97 | 0.13 | 32.97 | 0 | +| 262144 | 18 | 64 | adversarial | coset | 26942 | 768 | 12.00 | 0.39 | 32.41 | 0 | +| 262144 | 18 | 128 | random | prefix | 52704 | 1550 | 12.11 | 0.36 | 62.19 | 0 | +| 262144 | 18 | 128 | random | coset | 45367 | 1296 | 10.12 | 0.89 | 56.12 | 0 | +| 262144 | 18 | 128 | clustered | prefix | 11808 | 272 | 2.12 | 0.03 | 13.44 | 0 | +| 262144 | 18 | 128 | clustered | coset | 1704 | 18 | 0.14 | 0.04 | 13.48 | 0 | +| 262144 | 18 | 128 | adversarial | prefix | 56288 | 1662 | 12.98 | 0.15 | 59.01 | 0 | +| 262144 | 18 | 128 | adversarial | coset | 49342 | 1408 | 11.00 | 0.49 | 61.67 | 0 | +| 262144 | 18 | 256 | random | prefix | 96864 | 2834 | 11.07 | 0.54 | 101.68 | 0 | +| 262144 | 18 | 256 | random | coset | 81322 | 2324 | 9.08 | 1.00 | 105.37 | 0 | +| 262144 | 18 | 256 | clustered | prefix | 23072 | 528 | 2.06 | 0.04 | 25.73 | 0 | +| 262144 | 18 | 256 | clustered | coset | 2723 | 18 | 0.07 | 0.21 | 24.48 | 0 | +| 262144 | 18 | 256 | adversarial | prefix | 104416 | 3070 | 11.99 | 0.29 | 103.90 | 0 | +| 262144 | 18 | 256 | adversarial | coset | 89662 | 2560 | 10.00 | 0.71 | 117.14 | 0 | +| 262144 | 18 | 512 | random | prefix | 178784 | 5202 | 10.16 | 0.91 | 186.87 | 0 | +| 262144 | 18 | 512 | random | coset | 146264 | 4180 | 8.16 | 1.32 | 201.69 | 0 | +| 262144 | 18 | 512 | clustered | prefix | 45536 | 1038 | 2.03 | 0.08 | 50.81 | 0 | +| 262144 | 18 | 512 | clustered | coset | 4706 | 16 | 0.03 | 0.10 | 50.21 | 0 | +| 262144 | 18 | 512 | adversarial | prefix | 192480 | 5630 | 11.00 | 0.45 | 210.60 | 0 | +| 262144 | 18 | 512 | adversarial | coset | 161342 | 4608 | 9.00 | 0.89 | 221.99 | 0 | +| 262144 | 18 | 1024 | random | prefix | 324928 | 9385 | 9.17 | 0.98 | 341.50 | 0 | +| 262144 | 18 | 1024 | random | coset | 256923 | 7339 | 7.17 | 1.66 | 352.00 | 0 | +| 262144 | 18 | 1024 | clustered | prefix | 90656 | 2064 | 2.02 | 0.17 | 109.46 | 0 | +| 262144 | 18 | 1024 | clustered | coset | 8867 | 18 | 0.02 | 0.27 | 119.73 | 0 | +| 262144 | 18 | 1024 | adversarial | prefix | 352224 | 10238 | 10.00 | 0.85 | 359.38 | 0 | +| 262144 | 18 | 1024 | adversarial | coset | 286782 | 8192 | 8.00 | 1.05 | 381.45 | 0 | +| 262144 | 18 | 2048 | random | prefix | 582720 | 16673 | 8.14 | 1.57 | 625.73 | 0 | +| 262144 | 18 | 2048 | random | coset | 441167 | 12579 | 6.14 | 1.63 | 676.73 | 0 | +| 262144 | 18 | 2048 | clustered | prefix | 180800 | 4113 | 2.01 | 0.30 | 213.70 | 0 | +| 262144 | 18 | 2048 | clustered | coset | 17099 | 19 | 0.01 | 0.38 | 212.47 | 0 | +| 262144 | 18 | 2048 | adversarial | prefix | 638944 | 18430 | 9.00 | 1.26 | 689.96 | 0 | +| 262144 | 18 | 2048 | adversarial | coset | 501822 | 14336 | 7.00 | 1.93 | 692.39 | 0 | +| 262144 | 18 | 4096 | random | prefix | 1034592 | 29258 | 7.14 | 1.89 | 1128.74 | 0 | +| 262144 | 18 | 4096 | random | coset | 741754 | 21068 | 5.14 | 2.25 | 1136.17 | 0 | +| 262144 | 18 | 4096 | clustered | prefix | 360768 | 8201 | 2.00 | 1.09 | 413.40 | 0 | +| 262144 | 18 | 4096 | clustered | coset | 33201 | 11 | 0.00 | 0.80 | 417.63 | 0 | +| 262144 | 18 | 4096 | adversarial | prefix | 1146848 | 32766 | 8.00 | 1.25 | 1243.74 | 0 | +| 262144 | 18 | 4096 | adversarial | coset | 860222 | 24576 | 6.00 | 2.00 | 1274.75 | 0 | +| 1048576 | 20 | 1 | random | prefix | 696 | 20 | 20.00 | 0.00 | 0.86 | 0 | +| 1048576 | 20 | 1 | random | coset | 756 | 20 | 20.00 | 0.08 | 1.08 | 0 | +| 1048576 | 20 | 1 | clustered | prefix | 696 | 20 | 20.00 | 0.00 | 0.68 | 0 | +| 1048576 | 20 | 1 | clustered | coset | 758 | 20 | 20.00 | 0.03 | 0.73 | 0 | +| 1048576 | 20 | 1 | adversarial | prefix | 696 | 20 | 20.00 | 0.00 | 1.77 | 0 | +| 1048576 | 20 | 1 | adversarial | coset | 741 | 20 | 20.00 | 0.06 | 0.71 | 0 | +| 1048576 | 20 | 2 | random | prefix | 1168 | 34 | 17.00 | 0.00 | 1.24 | 0 | +| 1048576 | 20 | 2 | random | coset | 1172 | 32 | 16.00 | 0.04 | 1.16 | 0 | +| 1048576 | 20 | 2 | clustered | prefix | 752 | 21 | 10.50 | 0.00 | 0.82 | 0 | +| 1048576 | 20 | 2 | clustered | coset | 732 | 19 | 9.50 | 0.01 | 0.92 | 0 | +| 1048576 | 20 | 2 | adversarial | prefix | 1360 | 40 | 20.00 | 0.05 | 1.45 | 0 | +| 1048576 | 20 | 2 | adversarial | coset | 1393 | 38 | 19.00 | 0.01 | 1.85 | 0 | +| 1048576 | 20 | 4 | random | prefix | 2560 | 76 | 19.00 | 0.00 | 2.48 | 0 | +| 1048576 | 20 | 4 | random | coset | 2519 | 70 | 17.50 | 0.13 | 2.75 | 0 | +| 1048576 | 20 | 4 | clustered | prefix | 960 | 26 | 6.50 | 0.00 | 0.96 | 0 | +| 1048576 | 20 | 4 | clustered | coset | 778 | 20 | 5.00 | 0.01 | 0.98 | 0 | +| 1048576 | 20 | 4 | adversarial | prefix | 2624 | 78 | 19.50 | 0.00 | 2.86 | 0 | +| 1048576 | 20 | 4 | adversarial | coset | 2595 | 72 | 18.00 | 0.12 | 3.11 | 0 | +| 1048576 | 20 | 8 | random | prefix | 4960 | 148 | 18.50 | 0.01 | 5.68 | 0 | +| 1048576 | 20 | 8 | random | coset | 4769 | 134 | 16.75 | 0.11 | 5.71 | 0 | +| 1048576 | 20 | 8 | clustered | prefix | 1344 | 35 | 4.38 | 0.02 | 2.23 | 0 | +| 1048576 | 20 | 8 | clustered | coset | 840 | 21 | 2.62 | 0.03 | 1.55 | 0 | +| 1048576 | 20 | 8 | adversarial | prefix | 5024 | 150 | 18.75 | 0.06 | 5.05 | 0 | +| 1048576 | 20 | 8 | adversarial | coset | 4849 | 136 | 17.00 | 0.11 | 5.69 | 0 | +| 1048576 | 20 | 16 | random | prefix | 8896 | 265 | 16.56 | 0.07 | 8.73 | 0 | +| 1048576 | 20 | 16 | random | coset | 8298 | 235 | 14.69 | 0.16 | 9.23 | 0 | +| 1048576 | 20 | 16 | clustered | prefix | 1984 | 49 | 3.06 | 0.01 | 2.01 | 0 | +| 1048576 | 20 | 16 | clustered | coset | 842 | 19 | 1.19 | 0.01 | 2.10 | 0 | +| 1048576 | 20 | 16 | adversarial | prefix | 9568 | 286 | 17.88 | 0.07 | 9.68 | 0 | +| 1048576 | 20 | 16 | adversarial | coset | 9067 | 256 | 16.00 | 0.16 | 9.73 | 0 | +| 1048576 | 20 | 32 | random | prefix | 17120 | 510 | 15.94 | 0.18 | 20.40 | 0 | +| 1048576 | 20 | 32 | random | coset | 15764 | 448 | 14.00 | 0.22 | 18.71 | 0 | +| 1048576 | 20 | 32 | clustered | prefix | 3616 | 88 | 2.75 | 0.08 | 4.05 | 0 | +| 1048576 | 20 | 32 | clustered | coset | 1203 | 26 | 0.81 | 0.05 | 3.94 | 0 | +| 1048576 | 20 | 32 | adversarial | prefix | 18144 | 542 | 16.94 | 0.05 | 19.48 | 0 | +| 1048576 | 20 | 32 | adversarial | coset | 16925 | 480 | 15.00 | 0.21 | 18.38 | 0 | +| 1048576 | 20 | 64 | random | prefix | 32288 | 960 | 15.00 | 0.22 | 34.70 | 0 | +| 1048576 | 20 | 64 | random | coset | 29252 | 834 | 13.03 | 0.49 | 35.37 | 0 | +| 1048576 | 20 | 64 | clustered | prefix | 6240 | 146 | 2.28 | 0.02 | 6.43 | 0 | +| 1048576 | 20 | 64 | clustered | coset | 1259 | 20 | 0.31 | 0.03 | 6.84 | 0 | +| 1048576 | 20 | 64 | adversarial | prefix | 34272 | 1022 | 15.97 | 0.13 | 34.48 | 0 | +| 1048576 | 20 | 64 | adversarial | coset | 31487 | 896 | 14.00 | 0.32 | 35.10 | 0 | +| 1048576 | 20 | 128 | random | prefix | 60320 | 1788 | 13.97 | 0.89 | 61.89 | 0 | +| 1048576 | 20 | 128 | random | coset | 53716 | 1534 | 11.98 | 0.94 | 63.93 | 0 | +| 1048576 | 20 | 128 | clustered | prefix | 11776 | 271 | 2.12 | 0.07 | 13.35 | 0 | +| 1048576 | 20 | 128 | clustered | coset | 1667 | 17 | 0.13 | 0.06 | 13.00 | 0 | +| 1048576 | 20 | 128 | adversarial | prefix | 64480 | 1918 | 14.98 | 0.18 | 75.30 | 0 | +| 1048576 | 20 | 128 | adversarial | coset | 58304 | 1664 | 13.00 | 0.42 | 64.41 | 0 | +| 1048576 | 20 | 256 | random | prefix | 113376 | 3350 | 13.09 | 0.65 | 113.34 | 0 | +| 1048576 | 20 | 256 | random | coset | 99363 | 2840 | 11.09 | 2.03 | 119.55 | 0 | +| 1048576 | 20 | 256 | clustered | prefix | 23136 | 530 | 2.07 | 0.05 | 24.83 | 0 | +| 1048576 | 20 | 256 | clustered | coset | 2793 | 20 | 0.08 | 0.07 | 24.50 | 0 | +| 1048576 | 20 | 256 | adversarial | prefix | 120800 | 3582 | 13.99 | 0.42 | 122.99 | 0 | +| 1048576 | 20 | 256 | adversarial | coset | 107584 | 3072 | 12.00 | 0.48 | 126.79 | 0 | +| 1048576 | 20 | 512 | random | prefix | 211360 | 6220 | 12.15 | 1.45 | 231.07 | 0 | +| 1048576 | 20 | 512 | random | coset | 181802 | 5198 | 10.15 | 1.86 | 229.12 | 0 | +| 1048576 | 20 | 512 | clustered | prefix | 45792 | 1046 | 2.04 | 0.13 | 51.40 | 0 | +| 1048576 | 20 | 512 | clustered | coset | 4979 | 24 | 0.05 | 0.11 | 49.51 | 0 | +| 1048576 | 20 | 512 | adversarial | prefix | 225248 | 6654 | 13.00 | 0.56 | 248.60 | 0 | +| 1048576 | 20 | 512 | adversarial | coset | 197184 | 5632 | 11.00 | 1.22 | 250.42 | 0 | +| 1048576 | 20 | 1024 | random | prefix | 389600 | 11406 | 11.14 | 1.90 | 429.63 | 0 | +| 1048576 | 20 | 1024 | random | coset | 327322 | 9360 | 9.14 | 2.83 | 417.59 | 0 | +| 1048576 | 20 | 1024 | clustered | prefix | 90752 | 2067 | 2.02 | 0.20 | 99.00 | 0 | +| 1048576 | 20 | 1024 | clustered | coset | 8973 | 21 | 0.02 | 0.21 | 105.35 | 0 | +| 1048576 | 20 | 1024 | adversarial | prefix | 417760 | 12286 | 12.00 | 1.59 | 466.12 | 0 | +| 1048576 | 20 | 1024 | adversarial | coset | 358464 | 10240 | 10.00 | 1.56 | 478.71 | 0 | +| 1048576 | 20 | 2048 | random | prefix | 714016 | 20776 | 10.14 | 2.66 | 770.21 | 0 | +| 1048576 | 20 | 2048 | random | coset | 583495 | 16682 | 8.15 | 3.92 | 766.82 | 0 | +| 1048576 | 20 | 2048 | clustered | prefix | 180768 | 4112 | 2.01 | 0.32 | 230.26 | 0 | +| 1048576 | 20 | 2048 | clustered | coset | 17064 | 18 | 0.01 | 0.37 | 218.32 | 0 | +| 1048576 | 20 | 2048 | adversarial | prefix | 770016 | 22526 | 11.00 | 1.73 | 866.86 | 0 | +| 1048576 | 20 | 2048 | adversarial | coset | 645184 | 18432 | 9.00 | 2.91 | 842.14 | 0 | +| 1048576 | 20 | 4096 | random | prefix | 1294176 | 37370 | 9.12 | 3.96 | 1453.29 | 0 | +| 1048576 | 20 | 4096 | random | coset | 1021468 | 29180 | 7.12 | 5.45 | 1439.72 | 0 | +| 1048576 | 20 | 4096 | clustered | prefix | 361056 | 8210 | 2.00 | 0.63 | 404.03 | 0 | +| 1048576 | 20 | 4096 | clustered | coset | 33521 | 20 | 0.00 | 0.78 | 412.28 | 0 | +| 1048576 | 20 | 4096 | adversarial | prefix | 1408992 | 40958 | 10.00 | 2.34 | 1529.41 | 0 | +| 1048576 | 20 | 4096 | adversarial | coset | 1146944 | 32768 | 8.00 | 4.18 | 1553.60 | 0 | + +## Proof Size Visualizations + +![proof-size](proof_size_4096_adversarial.svg) +![proof-size](proof_size_4096_clustered.svg) +![proof-size](proof_size_4096_random.svg) +![proof-size](proof_size_16384_adversarial.svg) +![proof-size](proof_size_16384_clustered.svg) +![proof-size](proof_size_16384_random.svg) +![proof-size](proof_size_65536_adversarial.svg) +![proof-size](proof_size_65536_clustered.svg) +![proof-size](proof_size_65536_random.svg) +![proof-size](proof_size_262144_adversarial.svg) +![proof-size](proof_size_262144_clustered.svg) +![proof-size](proof_size_262144_random.svg) +![proof-size](proof_size_1048576_adversarial.svg) +![proof-size](proof_size_1048576_clustered.svg) +![proof-size](proof_size_1048576_random.svg) + +## Proving Time Visualizations + +![proving-time](prove_ms_4096_adversarial.svg) +![proving-time](prove_ms_4096_clustered.svg) +![proving-time](prove_ms_4096_random.svg) +![proving-time](prove_ms_16384_adversarial.svg) +![proving-time](prove_ms_16384_clustered.svg) +![proving-time](prove_ms_16384_random.svg) +![proving-time](prove_ms_65536_adversarial.svg) +![proving-time](prove_ms_65536_clustered.svg) +![proving-time](prove_ms_65536_random.svg) +![proving-time](prove_ms_262144_adversarial.svg) +![proving-time](prove_ms_262144_clustered.svg) +![proving-time](prove_ms_262144_random.svg) +![proving-time](prove_ms_1048576_adversarial.svg) +![proving-time](prove_ms_1048576_clustered.svg) +![proving-time](prove_ms_1048576_random.svg) + +## Verification Time Visualizations + +![verification-time](verify_ms_4096_adversarial.svg) +![verification-time](verify_ms_4096_clustered.svg) +![verification-time](verify_ms_4096_random.svg) +![verification-time](verify_ms_16384_adversarial.svg) +![verification-time](verify_ms_16384_clustered.svg) +![verification-time](verify_ms_16384_random.svg) +![verification-time](verify_ms_65536_adversarial.svg) +![verification-time](verify_ms_65536_clustered.svg) +![verification-time](verify_ms_65536_random.svg) +![verification-time](verify_ms_262144_adversarial.svg) +![verification-time](verify_ms_262144_clustered.svg) +![verification-time](verify_ms_262144_random.svg) +![verification-time](verify_ms_1048576_adversarial.svg) +![verification-time](verify_ms_1048576_clustered.svg) +![verification-time](verify_ms_1048576_random.svg) diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg new file mode 100644 index 00000000..48fd0333 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg @@ -0,0 +1,278 @@ + + + +Proof Size vs k (n=1048576, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +200000.0 + + + +400000.0 + + + +600000.0 + + + +800000.0 + + + +1000000.0 + + + +1200000.0 + + + +1400000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg new file mode 100644 index 00000000..2dc2e549 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=1048576, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg new file mode 100644 index 00000000..54f617fd --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg @@ -0,0 +1,267 @@ + + + +Proof Size vs k (n=1048576, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +200000.0 + + + +400000.0 + + + +600000.0 + + + +800000.0 + + + +1000000.0 + + + +1200000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg new file mode 100644 index 00000000..005b01b5 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg @@ -0,0 +1,265 @@ + + + +Proof Size vs k (n=16384, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +100000.0 + + + +200000.0 + + + +300000.0 + + + +400000.0 + + + +500000.0 + + + +600000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg new file mode 100644 index 00000000..0bdf91db --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=16384, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg new file mode 100644 index 00000000..4f98899f --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg @@ -0,0 +1,252 @@ + + + +Proof Size vs k (n=16384, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + +0.0 + + + +100000.0 + + + +200000.0 + + + +300000.0 + + + +400000.0 + + + +500000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg new file mode 100644 index 00000000..3dc39608 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg @@ -0,0 +1,259 @@ + + + +Proof Size vs k (n=262144, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +200000.0 + + + +400000.0 + + + +600000.0 + + + +800000.0 + + + +1000000.0 + + + +1200000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg new file mode 100644 index 00000000..abd9bbc2 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=262144, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg new file mode 100644 index 00000000..9bfc23f8 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg @@ -0,0 +1,273 @@ + + + +Proof Size vs k (n=262144, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100000.0 + + + +200000.0 + + + +300000.0 + + + +400000.0 + + + +500000.0 + + + +600000.0 + + + +700000.0 + + + +800000.0 + + + +900000.0 + + + +1000000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg new file mode 100644 index 00000000..52aedd57 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=4096, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg new file mode 100644 index 00000000..6b66f996 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=4096, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg new file mode 100644 index 00000000..24a836c0 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=4096, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg new file mode 100644 index 00000000..c64b9c4e --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg @@ -0,0 +1,308 @@ + + + +Proof Size vs k (n=65536, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100000.0 + + + +200000.0 + + + +300000.0 + + + +400000.0 + + + +500000.0 + + + +600000.0 + + + +700000.0 + + + +800000.0 + + + +900000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg new file mode 100644 index 00000000..e7435a21 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg @@ -0,0 +1,280 @@ + + + +Proof Size vs k (n=65536, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50000.0 + + + +100000.0 + + + +150000.0 + + + +200000.0 + + + +250000.0 + + + +300000.0 + + + +350000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg new file mode 100644 index 00000000..5334f4b0 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg @@ -0,0 +1,292 @@ + + + +Proof Size vs k (n=65536, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +proof size (bytes) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100000.0 + + + +200000.0 + + + +300000.0 + + + +400000.0 + + + +500000.0 + + + +600000.0 + + + +700000.0 + + + +800000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg new file mode 100644 index 00000000..1284346d --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg @@ -0,0 +1,252 @@ + + + +Proving Time vs k (n=1048576, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + +0.0 + + + +1.0 + + + +2.0 + + + +3.0 + + + +4.0 + + + +5.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg new file mode 100644 index 00000000..e4b86812 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg @@ -0,0 +1,241 @@ + + + +Proving Time vs k (n=1048576, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg new file mode 100644 index 00000000..af620f7e --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg @@ -0,0 +1,270 @@ + + + +Proving Time vs k (n=1048576, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +1.0 + + + +2.0 + + + +3.0 + + + +4.0 + + + +5.0 + + + +6.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg new file mode 100644 index 00000000..4c5d0e68 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg @@ -0,0 +1,242 @@ + + + +Proving Time vs k (n=16384, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg new file mode 100644 index 00000000..e1de88bf --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg @@ -0,0 +1,242 @@ + + + +Proving Time vs k (n=16384, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg new file mode 100644 index 00000000..e284eff0 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg @@ -0,0 +1,256 @@ + + + +Proving Time vs k (n=16384, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg new file mode 100644 index 00000000..3f2bfd82 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg @@ -0,0 +1,275 @@ + + + +Proving Time vs k (n=262144, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + +2.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg new file mode 100644 index 00000000..84183d4c --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg @@ -0,0 +1,252 @@ + + + +Proving Time vs k (n=262144, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg new file mode 100644 index 00000000..462fa3d9 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg @@ -0,0 +1,286 @@ + + + +Proving Time vs k (n=262144, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + +2.5 + + + +3.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg new file mode 100644 index 00000000..5ca34884 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg @@ -0,0 +1,236 @@ + + + +Proving Time vs k (n=4096, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg new file mode 100644 index 00000000..2980d334 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg @@ -0,0 +1,237 @@ + + + +Proving Time vs k (n=4096, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg new file mode 100644 index 00000000..0202d057 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg @@ -0,0 +1,243 @@ + + + +Proving Time vs k (n=4096, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg new file mode 100644 index 00000000..32002427 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg @@ -0,0 +1,260 @@ + + + +Proving Time vs k (n=65536, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg new file mode 100644 index 00000000..fc20c98a --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg @@ -0,0 +1,241 @@ + + + +Proving Time vs k (n=65536, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg new file mode 100644 index 00000000..494f7677 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg @@ -0,0 +1,268 @@ + + + +Proving Time vs k (n=65536, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +prove time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +-0.5 + + + +0.0 + + + +0.5 + + + +1.0 + + + +1.5 + + + +2.0 + + + +2.5 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg new file mode 100644 index 00000000..e509e699 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg @@ -0,0 +1,291 @@ + + + +Verification Time vs k (n=1048576, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +200.0 + + + +400.0 + + + +600.0 + + + +800.0 + + + +1000.0 + + + +1200.0 + + + +1400.0 + + + +1600.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg new file mode 100644 index 00000000..cb1db92e --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg @@ -0,0 +1,296 @@ + + + +Verification Time vs k (n=1048576, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg new file mode 100644 index 00000000..7e86027d --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg @@ -0,0 +1,281 @@ + + + +Verification Time vs k (n=1048576, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +200.0 + + + +400.0 + + + +600.0 + + + +800.0 + + + +1000.0 + + + +1200.0 + + + +1400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg new file mode 100644 index 00000000..37361f1c --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg @@ -0,0 +1,277 @@ + + + +Verification Time vs k (n=16384, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100.0 + + + +200.0 + + + +300.0 + + + +400.0 + + + +500.0 + + + +600.0 + + + +700.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg new file mode 100644 index 00000000..4abae825 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg @@ -0,0 +1,295 @@ + + + +Verification Time vs k (n=16384, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg new file mode 100644 index 00000000..668bd411 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg @@ -0,0 +1,265 @@ + + + +Verification Time vs k (n=16384, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +100.0 + + + +200.0 + + + +300.0 + + + +400.0 + + + +500.0 + + + +600.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg new file mode 100644 index 00000000..1113d20a --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg @@ -0,0 +1,266 @@ + + + +Verification Time vs k (n=262144, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + +0.0 + + + +200.0 + + + +400.0 + + + +600.0 + + + +800.0 + + + +1000.0 + + + +1200.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg new file mode 100644 index 00000000..e936a534 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg @@ -0,0 +1,298 @@ + + + +Verification Time vs k (n=262144, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg new file mode 100644 index 00000000..2532e2ef --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg @@ -0,0 +1,253 @@ + + + +Verification Time vs k (n=262144, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + +0.0 + + + +200.0 + + + +400.0 + + + +600.0 + + + +800.0 + + + +1000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg new file mode 100644 index 00000000..7419c8ac --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg @@ -0,0 +1,298 @@ + + + +Verification Time vs k (n=4096, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg new file mode 100644 index 00000000..18e446e9 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg @@ -0,0 +1,298 @@ + + + +Verification Time vs k (n=4096, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg new file mode 100644 index 00000000..955f1fd8 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg @@ -0,0 +1,295 @@ + + + +Verification Time vs k (n=4096, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg new file mode 100644 index 00000000..164fcd8d --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg @@ -0,0 +1,325 @@ + + + +Verification Time vs k (n=65536, pattern=adversarial) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100.0 + + + +200.0 + + + +300.0 + + + +400.0 + + + +500.0 + + + +600.0 + + + +700.0 + + + +800.0 + + + +900.0 + + + +1000.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg new file mode 100644 index 00000000..b22a6654 --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg @@ -0,0 +1,298 @@ + + + +Verification Time vs k (n=65536, pattern=clustered) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +50.0 + + + +100.0 + + + +150.0 + + + +200.0 + + + +250.0 + + + +300.0 + + + +350.0 + + + +400.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg new file mode 100644 index 00000000..a93a2f2f --- /dev/null +++ b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg @@ -0,0 +1,306 @@ + + + +Verification Time vs k (n=65536, pattern=random) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +verify time (ms) + + +batch size (k) + + + + + + + + + + + + + + + + + + + + + + +0.0 + + + +100.0 + + + +200.0 + + + +300.0 + + + +400.0 + + + +500.0 + + + +600.0 + + + +700.0 + + + +800.0 + + + +900.0 + + + + +0.0 + + + +500.0 + + + +1000.0 + + + +1500.0 + + + +2000.0 + + + +2500.0 + + + +3000.0 + + + +3500.0 + + + +4000.0 + + + + + + +prefix + + +coset + + + + From 9fde6406beaaf63ca903008c3edcd904fce5f537 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 1 Dec 2025 07:53:22 +0100 Subject: [PATCH 08/22] update changelog with breaking change and improvement --- CHANGELOG.md | 7 + .../multiproof_v2_report.md | 450 ------------------ .../proof_size_1048576_adversarial.svg | 278 ----------- .../proof_size_1048576_clustered.svg | 280 ----------- .../proof_size_1048576_random.svg | 267 ----------- .../proof_size_16384_adversarial.svg | 265 ----------- .../proof_size_16384_clustered.svg | 280 ----------- .../proof_size_16384_random.svg | 252 ---------- .../proof_size_262144_adversarial.svg | 259 ---------- .../proof_size_262144_clustered.svg | 280 ----------- .../proof_size_262144_random.svg | 273 ----------- .../proof_size_4096_adversarial.svg | 280 ----------- .../proof_size_4096_clustered.svg | 280 ----------- .../proof_size_4096_random.svg | 280 ----------- .../proof_size_65536_adversarial.svg | 308 ------------ .../proof_size_65536_clustered.svg | 280 ----------- .../proof_size_65536_random.svg | 292 ------------ .../prove_ms_1048576_adversarial.svg | 252 ---------- .../prove_ms_1048576_clustered.svg | 241 ---------- .../prove_ms_1048576_random.svg | 270 ----------- .../prove_ms_16384_adversarial.svg | 242 ---------- .../prove_ms_16384_clustered.svg | 242 ---------- .../prove_ms_16384_random.svg | 256 ---------- .../prove_ms_262144_adversarial.svg | 275 ----------- .../prove_ms_262144_clustered.svg | 252 ---------- .../prove_ms_262144_random.svg | 286 ----------- .../prove_ms_4096_adversarial.svg | 236 --------- .../prove_ms_4096_clustered.svg | 237 --------- .../prove_ms_4096_random.svg | 243 ---------- .../prove_ms_65536_adversarial.svg | 260 ---------- .../prove_ms_65536_clustered.svg | 241 ---------- .../prove_ms_65536_random.svg | 268 ----------- .../verify_ms_1048576_adversarial.svg | 291 ----------- .../verify_ms_1048576_clustered.svg | 296 ------------ .../verify_ms_1048576_random.svg | 281 ----------- .../verify_ms_16384_adversarial.svg | 277 ----------- .../verify_ms_16384_clustered.svg | 295 ------------ .../verify_ms_16384_random.svg | 265 ----------- .../verify_ms_262144_adversarial.svg | 266 ----------- .../verify_ms_262144_clustered.svg | 298 ------------ .../verify_ms_262144_random.svg | 253 ---------- .../verify_ms_4096_adversarial.svg | 298 ------------ .../verify_ms_4096_clustered.svg | 298 ------------ .../verify_ms_4096_random.svg | 295 ------------ .../verify_ms_65536_adversarial.svg | 325 ------------- .../verify_ms_65536_clustered.svg | 298 ------------ .../verify_ms_65536_random.svg | 306 ------------ 47 files changed, 7 insertions(+), 12747 deletions(-) delete mode 100644 crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg delete mode 100644 crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be1ddaa..9b73da58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,17 @@ ### Breaking changes +- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/) Replace the prefix-encoded `MultiPath` Merkle multiproof with a +CoSet-based `CoPath` representation and update `MerkleTree::generate_multi_proof` to return `CoPath`. This changes the proof +encoding for batch openings and removes the old `MultiPath` type from the public API. + ### Features ### Improvements +- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/) Implement CoSet (minimal copath) pruning and delta-encoding for Merkle +multiproofs, reducing proof size and redundant hashing in batched openings. + ### Bugfixes ## v0.5.0 diff --git a/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md b/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md deleted file mode 100644 index dc5df06b..00000000 --- a/crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md +++ /dev/null @@ -1,450 +0,0 @@ -# Merkle Tree Multiproof Benchmark Report - -Generated: 1764530071.130155803s - -| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms | rss_delta_kb | -| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- | ------------ | -| 4096 | 12 | 1 | random | prefix | 440 | 12 | 12.00 | 0.01 | 0.45 | 0 | -| 4096 | 12 | 1 | random | coset | 471 | 12 | 12.00 | 0.87 | 0.72 | 0 | -| 4096 | 12 | 1 | clustered | prefix | 440 | 12 | 12.00 | 0.00 | 0.43 | 0 | -| 4096 | 12 | 1 | clustered | coset | 471 | 12 | 12.00 | 0.00 | 0.45 | 0 | -| 4096 | 12 | 1 | adversarial | prefix | 440 | 12 | 12.00 | 0.03 | 0.49 | 0 | -| 4096 | 12 | 1 | adversarial | coset | 469 | 12 | 12.00 | 0.00 | 0.43 | 0 | -| 4096 | 12 | 2 | random | prefix | 848 | 24 | 12.00 | 0.00 | 0.84 | 0 | -| 4096 | 12 | 2 | random | coset | 824 | 22 | 11.00 | 0.03 | 0.83 | 0 | -| 4096 | 12 | 2 | clustered | prefix | 496 | 13 | 6.50 | 0.00 | 0.51 | 0 | -| 4096 | 12 | 2 | clustered | coset | 449 | 11 | 5.50 | 0.00 | 0.49 | 0 | -| 4096 | 12 | 2 | adversarial | prefix | 848 | 24 | 12.00 | 0.00 | 0.92 | 0 | -| 4096 | 12 | 2 | adversarial | coset | 823 | 22 | 11.00 | 0.00 | 0.85 | 0 | -| 4096 | 12 | 4 | random | prefix | 1536 | 44 | 11.00 | 0.00 | 1.49 | 0 | -| 4096 | 12 | 4 | random | coset | 1385 | 38 | 9.50 | 0.01 | 1.55 | 0 | -| 4096 | 12 | 4 | clustered | prefix | 704 | 18 | 4.50 | 0.00 | 0.78 | 0 | -| 4096 | 12 | 4 | clustered | coset | 493 | 12 | 3.00 | 0.01 | 0.75 | 0 | -| 4096 | 12 | 4 | adversarial | prefix | 1600 | 46 | 11.50 | 0.00 | 1.55 | 0 | -| 4096 | 12 | 4 | adversarial | coset | 1455 | 40 | 10.00 | 0.01 | 2.21 | 0 | -| 4096 | 12 | 8 | random | prefix | 2752 | 79 | 9.88 | 0.07 | 3.35 | 0 | -| 4096 | 12 | 8 | random | coset | 2325 | 65 | 8.12 | 0.06 | 2.92 | 0 | -| 4096 | 12 | 8 | clustered | prefix | 1024 | 25 | 3.12 | 0.00 | 1.24 | 0 | -| 4096 | 12 | 8 | clustered | coset | 497 | 11 | 1.38 | 0.01 | 1.10 | 0 | -| 4096 | 12 | 8 | adversarial | prefix | 2976 | 86 | 10.75 | 0.00 | 3.68 | 0 | -| 4096 | 12 | 8 | adversarial | coset | 2576 | 72 | 9.00 | 0.01 | 3.49 | 0 | -| 4096 | 12 | 16 | random | prefix | 4640 | 132 | 8.25 | 0.05 | 6.92 | 0 | -| 4096 | 12 | 16 | random | coset | 3636 | 102 | 6.38 | 0.24 | 4.98 | 0 | -| 4096 | 12 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.01 | 1.90 | 0 | -| 4096 | 12 | 16 | clustered | coset | 723 | 16 | 1.00 | 0.01 | 1.90 | 0 | -| 4096 | 12 | 16 | adversarial | prefix | 5472 | 158 | 9.88 | 0.01 | 5.47 | 0 | -| 4096 | 12 | 16 | adversarial | coset | 4537 | 128 | 8.00 | 0.05 | 5.68 | 0 | -| 4096 | 12 | 32 | random | prefix | 9472 | 271 | 8.47 | 0.02 | 9.11 | 0 | -| 4096 | 12 | 32 | random | coset | 7372 | 209 | 6.53 | 0.03 | 10.08 | 0 | -| 4096 | 12 | 32 | clustered | prefix | 3200 | 75 | 2.34 | 0.01 | 3.48 | 0 | -| 4096 | 12 | 32 | clustered | coset | 754 | 13 | 0.41 | 0.02 | 3.76 | 0 | -| 4096 | 12 | 32 | adversarial | prefix | 9952 | 286 | 8.94 | 0.02 | 11.75 | 0 | -| 4096 | 12 | 32 | adversarial | coset | 7898 | 224 | 7.00 | 0.08 | 11.86 | 0 | -| 4096 | 12 | 64 | random | prefix | 15776 | 444 | 6.94 | 0.02 | 17.67 | 0 | -| 4096 | 12 | 64 | random | coset | 11265 | 318 | 4.97 | 0.04 | 18.72 | 0 | -| 4096 | 12 | 64 | clustered | prefix | 6080 | 141 | 2.20 | 0.01 | 7.20 | 0 | -| 4096 | 12 | 64 | clustered | coset | 1081 | 15 | 0.23 | 0.03 | 6.97 | 0 | -| 4096 | 12 | 64 | adversarial | prefix | 17888 | 510 | 7.97 | 0.03 | 19.63 | 0 | -| 4096 | 12 | 64 | adversarial | coset | 13499 | 384 | 6.00 | 0.04 | 20.32 | 0 | -| 4096 | 12 | 128 | random | prefix | 28896 | 806 | 6.30 | 0.04 | 31.51 | 0 | -| 4096 | 12 | 128 | random | coset | 19600 | 552 | 4.31 | 0.06 | 33.19 | 0 | -| 4096 | 12 | 128 | clustered | prefix | 11584 | 265 | 2.07 | 0.02 | 14.31 | 0 | -| 4096 | 12 | 128 | clustered | coset | 1458 | 11 | 0.09 | 0.06 | 12.89 | 0 | -| 4096 | 12 | 128 | adversarial | prefix | 31712 | 894 | 6.98 | 0.04 | 37.36 | 0 | -| 4096 | 12 | 128 | adversarial | coset | 22586 | 640 | 5.00 | 0.06 | 35.57 | 0 | -| 4096 | 12 | 256 | random | prefix | 49376 | 1350 | 5.27 | 0.07 | 52.54 | 0 | -| 4096 | 12 | 256 | random | coset | 30183 | 840 | 3.28 | 0.14 | 55.84 | 0 | -| 4096 | 12 | 256 | clustered | prefix | 22880 | 522 | 2.04 | 0.04 | 28.28 | 0 | -| 4096 | 12 | 256 | clustered | coset | 2515 | 12 | 0.05 | 0.07 | 32.59 | 0 | -| 4096 | 12 | 256 | adversarial | prefix | 55264 | 1534 | 5.99 | 0.05 | 57.32 | 0 | -| 4096 | 12 | 256 | adversarial | coset | 36409 | 1024 | 4.00 | 0.13 | 63.94 | 0 | -| 4096 | 12 | 512 | random | prefix | 83584 | 2227 | 4.35 | 0.12 | 94.54 | 0 | -| 4096 | 12 | 512 | random | coset | 44256 | 1205 | 2.35 | 0.14 | 100.43 | 0 | -| 4096 | 12 | 512 | clustered | prefix | 45408 | 1034 | 2.02 | 0.07 | 57.55 | 0 | -| 4096 | 12 | 512 | clustered | coset | 4556 | 12 | 0.02 | 0.08 | 50.02 | 0 | -| 4096 | 12 | 512 | adversarial | prefix | 94176 | 2558 | 5.00 | 0.08 | 100.27 | 0 | -| 4096 | 12 | 512 | adversarial | coset | 55352 | 1536 | 3.00 | 0.14 | 106.47 | 0 | -| 4096 | 12 | 1024 | random | prefix | 139264 | 3583 | 3.50 | 0.17 | 154.62 | 0 | -| 4096 | 12 | 1024 | random | coset | 58977 | 1537 | 1.50 | 0.22 | 149.52 | 0 | -| 4096 | 12 | 1024 | clustered | prefix | 90464 | 2058 | 2.01 | 0.14 | 107.70 | 0 | -| 4096 | 12 | 1024 | clustered | coset | 8655 | 12 | 0.01 | 0.15 | 100.56 | 0 | -| 4096 | 12 | 1024 | adversarial | prefix | 155616 | 4094 | 4.00 | 0.15 | 160.98 | 0 | -| 4096 | 12 | 1024 | adversarial | coset | 75831 | 2048 | 2.00 | 0.22 | 172.59 | 0 | -| 4096 | 12 | 2048 | random | prefix | 226720 | 5548 | 2.71 | 0.29 | 264.00 | 0 | -| 4096 | 12 | 2048 | random | coset | 63902 | 1454 | 0.71 | 0.43 | 261.51 | 0 | -| 4096 | 12 | 2048 | clustered | prefix | 180512 | 4104 | 2.00 | 0.25 | 213.80 | 0 | -| 4096 | 12 | 2048 | clustered | coset | 16782 | 10 | 0.00 | 0.30 | 195.58 | 0 | -| 4096 | 12 | 2048 | adversarial | prefix | 245728 | 6142 | 3.00 | 0.28 | 278.58 | 0 | -| 4096 | 12 | 2048 | adversarial | coset | 81945 | 2048 | 1.00 | 0.29 | 270.10 | 0 | -| 4096 | 12 | 4096 | random | prefix | 360416 | 8190 | 2.00 | 0.47 | 407.58 | 0 | -| 4096 | 12 | 4096 | random | coset | 32793 | 0 | 0.00 | 0.52 | 394.06 | 0 | -| 4096 | 12 | 4096 | clustered | prefix | 360416 | 8190 | 2.00 | 0.47 | 417.15 | 0 | -| 4096 | 12 | 4096 | clustered | coset | 32793 | 0 | 0.00 | 0.56 | 418.08 | 0 | -| 4096 | 12 | 4096 | adversarial | prefix | 360416 | 8190 | 2.00 | 0.49 | 403.72 | 0 | -| 4096 | 12 | 4096 | adversarial | coset | 32793 | 0 | 0.00 | 0.51 | 418.19 | 0 | -| 16384 | 14 | 1 | random | prefix | 504 | 14 | 14.00 | 0.05 | 0.79 | 0 | -| 16384 | 14 | 1 | random | coset | 540 | 14 | 14.00 | 0.10 | 0.80 | 0 | -| 16384 | 14 | 1 | clustered | prefix | 504 | 14 | 14.00 | 0.04 | 0.49 | 0 | -| 16384 | 14 | 1 | clustered | coset | 543 | 14 | 14.00 | 0.01 | 0.51 | 0 | -| 16384 | 14 | 1 | adversarial | prefix | 504 | 14 | 14.00 | 0.00 | 0.48 | 0 | -| 16384 | 14 | 1 | adversarial | coset | 537 | 14 | 14.00 | 0.00 | 0.54 | 0 | -| 16384 | 14 | 2 | random | prefix | 976 | 28 | 14.00 | 0.05 | 0.99 | 0 | -| 16384 | 14 | 2 | random | coset | 962 | 26 | 13.00 | 0.01 | 1.26 | 0 | -| 16384 | 14 | 2 | clustered | prefix | 560 | 15 | 7.50 | 0.00 | 0.71 | 0 | -| 16384 | 14 | 2 | clustered | coset | 519 | 13 | 6.50 | 0.01 | 0.55 | 0 | -| 16384 | 14 | 2 | adversarial | prefix | 976 | 28 | 14.00 | 0.00 | 0.96 | 0 | -| 16384 | 14 | 2 | adversarial | coset | 963 | 26 | 13.00 | 0.01 | 0.95 | 0 | -| 16384 | 14 | 4 | random | prefix | 1728 | 50 | 12.50 | 0.04 | 1.77 | 0 | -| 16384 | 14 | 4 | random | coset | 1595 | 44 | 11.00 | 0.04 | 2.56 | 0 | -| 16384 | 14 | 4 | clustered | prefix | 704 | 18 | 4.50 | 0.02 | 0.72 | 0 | -| 16384 | 14 | 4 | clustered | coset | 499 | 12 | 3.00 | 0.02 | 0.71 | 0 | -| 16384 | 14 | 4 | adversarial | prefix | 1856 | 54 | 13.50 | 0.03 | 2.16 | 0 | -| 16384 | 14 | 4 | adversarial | coset | 1735 | 48 | 12.00 | 0.04 | 1.80 | 0 | -| 16384 | 14 | 8 | random | prefix | 3264 | 95 | 11.88 | 0.02 | 3.54 | 0 | -| 16384 | 14 | 8 | random | coset | 2890 | 81 | 10.12 | 0.08 | 3.28 | 0 | -| 16384 | 14 | 8 | clustered | prefix | 1152 | 29 | 3.62 | 0.00 | 1.35 | 0 | -| 16384 | 14 | 8 | clustered | coset | 630 | 15 | 1.88 | 0.02 | 1.74 | 0 | -| 16384 | 14 | 8 | adversarial | prefix | 3488 | 102 | 12.75 | 0.04 | 3.42 | 0 | -| 16384 | 14 | 8 | adversarial | coset | 3136 | 88 | 11.00 | 0.01 | 3.96 | 0 | -| 16384 | 14 | 16 | random | prefix | 6144 | 179 | 11.19 | 0.11 | 8.62 | 0 | -| 16384 | 14 | 16 | random | coset | 5264 | 149 | 9.31 | 0.15 | 7.48 | 0 | -| 16384 | 14 | 16 | clustered | prefix | 1856 | 45 | 2.81 | 0.01 | 1.88 | 0 | -| 16384 | 14 | 16 | clustered | coset | 691 | 15 | 0.94 | 0.07 | 1.89 | 0 | -| 16384 | 14 | 16 | adversarial | prefix | 6496 | 190 | 11.88 | 0.06 | 6.33 | 0 | -| 16384 | 14 | 16 | adversarial | coset | 5657 | 160 | 10.00 | 0.19 | 6.44 | 0 | -| 16384 | 14 | 32 | random | prefix | 10880 | 315 | 9.84 | 0.09 | 11.29 | 0 | -| 16384 | 14 | 32 | random | coset | 8911 | 253 | 7.91 | 0.12 | 10.71 | 0 | -| 16384 | 14 | 32 | clustered | prefix | 3232 | 76 | 2.38 | 0.01 | 3.39 | 0 | -| 16384 | 14 | 32 | clustered | coset | 787 | 14 | 0.44 | 0.02 | 3.29 | 0 | -| 16384 | 14 | 32 | adversarial | prefix | 12000 | 350 | 10.94 | 0.10 | 11.67 | 0 | -| 16384 | 14 | 32 | adversarial | coset | 10138 | 288 | 9.00 | 0.09 | 11.82 | 0 | -| 16384 | 14 | 64 | random | prefix | 19904 | 573 | 8.95 | 0.05 | 20.74 | 0 | -| 16384 | 14 | 64 | random | coset | 15714 | 447 | 6.98 | 0.13 | 19.71 | 0 | -| 16384 | 14 | 64 | clustered | prefix | 6016 | 139 | 2.17 | 0.01 | 6.14 | 0 | -| 16384 | 14 | 64 | clustered | coset | 1014 | 13 | 0.20 | 0.02 | 6.28 | 0 | -| 16384 | 14 | 64 | adversarial | prefix | 21984 | 638 | 9.97 | 0.03 | 22.37 | 0 | -| 16384 | 14 | 64 | adversarial | coset | 17979 | 512 | 8.00 | 0.05 | 22.49 | 0 | -| 16384 | 14 | 128 | random | prefix | 36640 | 1048 | 8.19 | 0.10 | 37.27 | 0 | -| 16384 | 14 | 128 | random | coset | 27885 | 794 | 6.20 | 0.16 | 38.35 | 0 | -| 16384 | 14 | 128 | clustered | prefix | 11680 | 268 | 2.09 | 0.02 | 12.54 | 0 | -| 16384 | 14 | 128 | clustered | coset | 1553 | 14 | 0.11 | 0.03 | 12.49 | 0 | -| 16384 | 14 | 128 | adversarial | prefix | 39904 | 1150 | 8.98 | 0.10 | 42.08 | 0 | -| 16384 | 14 | 128 | adversarial | coset | 31419 | 896 | 7.00 | 0.16 | 44.56 | 0 | -| 16384 | 14 | 256 | random | prefix | 63648 | 1796 | 7.02 | 0.14 | 71.58 | 0 | -| 16384 | 14 | 256 | random | coset | 45385 | 1286 | 5.02 | 0.15 | 70.52 | 0 | -| 16384 | 14 | 256 | clustered | prefix | 22912 | 523 | 2.04 | 0.04 | 25.62 | 0 | -| 16384 | 14 | 256 | clustered | coset | 2550 | 13 | 0.05 | 0.05 | 27.82 | 0 | -| 16384 | 14 | 256 | adversarial | prefix | 71648 | 2046 | 7.99 | 0.08 | 79.97 | 0 | -| 16384 | 14 | 256 | adversarial | coset | 53819 | 1536 | 6.00 | 0.12 | 78.00 | 0 | -| 16384 | 14 | 512 | random | prefix | 113408 | 3159 | 6.17 | 0.17 | 117.88 | 0 | -| 16384 | 14 | 512 | random | coset | 75820 | 2137 | 4.17 | 0.23 | 126.40 | 0 | -| 16384 | 14 | 512 | clustered | prefix | 45536 | 1038 | 2.03 | 0.07 | 50.77 | 0 | -| 16384 | 14 | 512 | clustered | coset | 4694 | 16 | 0.03 | 0.09 | 55.49 | 0 | -| 16384 | 14 | 512 | adversarial | prefix | 126944 | 3582 | 7.00 | 0.16 | 142.52 | 0 | -| 16384 | 14 | 512 | adversarial | coset | 90170 | 2560 | 5.00 | 0.22 | 137.58 | 0 | -| 16384 | 14 | 1024 | random | prefix | 195712 | 5347 | 5.22 | 0.38 | 220.34 | 0 | -| 16384 | 14 | 1024 | random | coset | 118567 | 3301 | 3.22 | 0.34 | 207.68 | 0 | -| 16384 | 14 | 1024 | clustered | prefix | 90560 | 2061 | 2.01 | 0.15 | 101.42 | 0 | -| 16384 | 14 | 1024 | clustered | coset | 8756 | 15 | 0.01 | 0.17 | 94.09 | 0 | -| 16384 | 14 | 1024 | adversarial | prefix | 221152 | 6142 | 6.00 | 0.29 | 252.24 | 0 | -| 16384 | 14 | 1024 | adversarial | coset | 145465 | 4096 | 4.00 | 0.32 | 232.68 | 0 | -| 16384 | 14 | 2048 | random | prefix | 335680 | 8953 | 4.37 | 0.43 | 364.78 | 0 | -| 16384 | 14 | 2048 | random | coset | 178065 | 4859 | 2.37 | 0.52 | 363.75 | 0 | -| 16384 | 14 | 2048 | clustered | prefix | 180672 | 4109 | 2.01 | 0.27 | 216.93 | 0 | -| 16384 | 14 | 2048 | clustered | coset | 16951 | 15 | 0.01 | 0.25 | 206.32 | 0 | -| 16384 | 14 | 2048 | adversarial | prefix | 376800 | 10238 | 5.00 | 0.32 | 404.15 | 0 | -| 16384 | 14 | 2048 | adversarial | coset | 221240 | 6144 | 3.00 | 0.47 | 408.73 | 0 | -| 16384 | 14 | 4096 | random | prefix | 560288 | 14436 | 3.52 | 0.73 | 581.70 | 0 | -| 16384 | 14 | 4096 | random | coset | 238926 | 6246 | 1.52 | 1.30 | 624.38 | 0 | -| 16384 | 14 | 4096 | clustered | prefix | 360896 | 8205 | 2.00 | 0.53 | 409.04 | 0 | -| 16384 | 14 | 4096 | clustered | coset | 33335 | 15 | 0.00 | 0.80 | 397.47 | 0 | -| 16384 | 14 | 4096 | adversarial | prefix | 622560 | 16382 | 4.00 | 0.59 | 689.98 | 0 | -| 16384 | 14 | 4096 | adversarial | coset | 303159 | 8192 | 2.00 | 0.81 | 670.59 | 0 | -| 65536 | 16 | 1 | random | prefix | 568 | 16 | 16.00 | 0.00 | 0.68 | 0 | -| 65536 | 16 | 1 | random | coset | 612 | 16 | 16.00 | 0.06 | 1.26 | 0 | -| 65536 | 16 | 1 | clustered | prefix | 568 | 16 | 16.00 | 0.06 | 0.96 | 0 | -| 65536 | 16 | 1 | clustered | coset | 611 | 16 | 16.00 | 0.01 | 0.94 | 0 | -| 65536 | 16 | 1 | adversarial | prefix | 568 | 16 | 16.00 | 0.06 | 0.63 | 0 | -| 65536 | 16 | 1 | adversarial | coset | 605 | 16 | 16.00 | 0.01 | 0.70 | 0 | -| 65536 | 16 | 2 | random | prefix | 1104 | 32 | 16.00 | 0.01 | 1.31 | 0 | -| 65536 | 16 | 2 | random | coset | 1107 | 30 | 15.00 | 0.07 | 1.20 | 0 | -| 65536 | 16 | 2 | clustered | prefix | 688 | 19 | 9.50 | 0.03 | 0.69 | 0 | -| 65536 | 16 | 2 | clustered | coset | 653 | 17 | 8.50 | 0.01 | 0.67 | 0 | -| 65536 | 16 | 2 | adversarial | prefix | 1104 | 32 | 16.00 | 0.03 | 1.19 | 0 | -| 65536 | 16 | 2 | adversarial | coset | 1105 | 30 | 15.00 | 0.07 | 1.67 | 0 | -| 65536 | 16 | 4 | random | prefix | 1728 | 50 | 12.50 | 0.01 | 1.67 | 0 | -| 65536 | 16 | 4 | random | coset | 1594 | 44 | 11.00 | 0.03 | 2.04 | 0 | -| 65536 | 16 | 4 | clustered | prefix | 800 | 21 | 5.25 | 0.02 | 0.95 | 0 | -| 65536 | 16 | 4 | clustered | coset | 605 | 15 | 3.75 | 0.01 | 0.89 | 0 | -| 65536 | 16 | 4 | adversarial | prefix | 2112 | 62 | 15.50 | 0.02 | 2.10 | 0 | -| 65536 | 16 | 4 | adversarial | coset | 2019 | 56 | 14.00 | 0.03 | 2.60 | 0 | -| 65536 | 16 | 8 | random | prefix | 3936 | 116 | 14.50 | 0.14 | 4.12 | 0 | -| 65536 | 16 | 8 | random | coset | 3622 | 102 | 12.75 | 0.09 | 3.96 | 0 | -| 65536 | 16 | 8 | clustered | prefix | 1248 | 32 | 4.00 | 0.01 | 1.30 | 0 | -| 65536 | 16 | 8 | clustered | coset | 735 | 18 | 2.25 | 0.05 | 1.76 | 0 | -| 65536 | 16 | 8 | adversarial | prefix | 4000 | 118 | 14.75 | 0.01 | 4.06 | 0 | -| 65536 | 16 | 8 | adversarial | coset | 3697 | 104 | 13.00 | 0.04 | 4.01 | 0 | -| 65536 | 16 | 16 | random | prefix | 6944 | 204 | 12.75 | 0.08 | 7.56 | 0 | -| 65536 | 16 | 16 | random | coset | 6143 | 174 | 10.88 | 0.12 | 7.27 | 0 | -| 65536 | 16 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.02 | 1.89 | 0 | -| 65536 | 16 | 16 | clustered | coset | 736 | 16 | 1.00 | 0.01 | 2.75 | 0 | -| 65536 | 16 | 16 | adversarial | prefix | 7520 | 222 | 13.88 | 0.05 | 8.05 | 0 | -| 65536 | 16 | 16 | adversarial | coset | 6778 | 192 | 12.00 | 0.09 | 7.72 | 0 | -| 65536 | 16 | 32 | random | prefix | 13440 | 395 | 12.34 | 0.11 | 14.61 | 0 | -| 65536 | 16 | 32 | random | coset | 11696 | 333 | 10.41 | 0.18 | 13.93 | 0 | -| 65536 | 16 | 32 | clustered | prefix | 3264 | 77 | 2.41 | 0.01 | 3.46 | 0 | -| 65536 | 16 | 32 | clustered | coset | 827 | 15 | 0.47 | 0.02 | 3.34 | 0 | -| 65536 | 16 | 32 | adversarial | prefix | 14048 | 414 | 12.94 | 0.10 | 14.71 | 0 | -| 65536 | 16 | 32 | adversarial | coset | 12379 | 352 | 11.00 | 0.14 | 14.03 | 0 | -| 65536 | 16 | 64 | random | prefix | 24608 | 720 | 11.25 | 0.20 | 25.19 | 0 | -| 65536 | 16 | 64 | random | coset | 20825 | 594 | 9.28 | 0.25 | 25.97 | 0 | -| 65536 | 16 | 64 | clustered | prefix | 6176 | 144 | 2.25 | 0.02 | 7.13 | 0 | -| 65536 | 16 | 64 | clustered | coset | 1183 | 18 | 0.28 | 0.02 | 6.40 | 0 | -| 65536 | 16 | 64 | adversarial | prefix | 26080 | 766 | 11.97 | 0.12 | 25.83 | 0 | -| 65536 | 16 | 64 | adversarial | coset | 22460 | 640 | 10.00 | 0.16 | 27.80 | 0 | -| 65536 | 16 | 128 | random | prefix | 44224 | 1285 | 10.04 | 0.17 | 44.88 | 0 | -| 65536 | 16 | 128 | random | coset | 36127 | 1031 | 8.05 | 0.77 | 45.09 | 0 | -| 65536 | 16 | 128 | clustered | prefix | 11776 | 271 | 2.12 | 0.03 | 12.65 | 0 | -| 65536 | 16 | 128 | clustered | coset | 1659 | 17 | 0.13 | 0.03 | 12.18 | 0 | -| 65536 | 16 | 128 | adversarial | prefix | 48096 | 1406 | 10.98 | 0.12 | 50.78 | 0 | -| 65536 | 16 | 128 | adversarial | coset | 40380 | 1152 | 9.00 | 0.24 | 50.79 | 0 | -| 65536 | 16 | 256 | random | prefix | 80384 | 2319 | 9.06 | 0.23 | 83.15 | 0 | -| 65536 | 16 | 256 | random | coset | 63408 | 1809 | 7.07 | 0.43 | 85.16 | 0 | -| 65536 | 16 | 256 | clustered | prefix | 22880 | 522 | 2.04 | 0.04 | 27.42 | 0 | -| 65536 | 16 | 256 | clustered | coset | 2515 | 12 | 0.05 | 0.06 | 25.53 | 0 | -| 65536 | 16 | 256 | adversarial | prefix | 88032 | 2558 | 9.99 | 0.22 | 99.07 | 0 | -| 65536 | 16 | 256 | adversarial | coset | 71740 | 2048 | 8.00 | 0.32 | 94.96 | 0 | -| 65536 | 16 | 512 | random | prefix | 144544 | 4132 | 8.07 | 0.26 | 164.78 | 0 | -| 65536 | 16 | 512 | random | coset | 109154 | 3110 | 6.07 | 0.41 | 157.91 | 0 | -| 65536 | 16 | 512 | clustered | prefix | 45600 | 1040 | 2.03 | 0.08 | 53.33 | 0 | -| 65536 | 16 | 512 | clustered | coset | 4768 | 18 | 0.04 | 0.10 | 53.25 | 0 | -| 65536 | 16 | 512 | adversarial | prefix | 159712 | 4606 | 9.00 | 0.22 | 169.87 | 0 | -| 65536 | 16 | 512 | adversarial | coset | 125500 | 3584 | 7.00 | 0.32 | 187.96 | 0 | -| 65536 | 16 | 1024 | random | prefix | 259136 | 7329 | 7.16 | 0.34 | 281.81 | 0 | -| 65536 | 16 | 1024 | random | coset | 186031 | 5283 | 5.16 | 0.57 | 290.11 | 0 | -| 65536 | 16 | 1024 | clustered | prefix | 90560 | 2061 | 2.01 | 0.14 | 97.01 | 0 | -| 65536 | 16 | 1024 | clustered | coset | 8764 | 15 | 0.01 | 0.17 | 105.35 | 0 | -| 65536 | 16 | 1024 | adversarial | prefix | 286688 | 8190 | 8.00 | 0.32 | 309.03 | 0 | -| 65536 | 16 | 1024 | adversarial | coset | 215100 | 6144 | 6.00 | 0.47 | 306.22 | 0 | -| 65536 | 16 | 2048 | random | prefix | 454496 | 12666 | 6.18 | 0.74 | 509.36 | 0 | -| 65536 | 16 | 2048 | random | coset | 303958 | 8572 | 4.19 | 1.27 | 492.45 | 0 | -| 65536 | 16 | 2048 | clustered | prefix | 180736 | 4111 | 2.01 | 0.28 | 212.77 | 0 | -| 65536 | 16 | 2048 | clustered | coset | 17022 | 17 | 0.01 | 0.35 | 211.18 | 0 | -| 65536 | 16 | 2048 | adversarial | prefix | 507872 | 14334 | 7.00 | 0.55 | 549.20 | 0 | -| 65536 | 16 | 2048 | adversarial | coset | 360507 | 10240 | 5.00 | 1.34 | 574.17 | 0 | -| 65536 | 16 | 4096 | random | prefix | 788544 | 21569 | 5.27 | 1.03 | 855.42 | 0 | -| 65536 | 16 | 4096 | random | coset | 480070 | 13379 | 3.27 | 1.64 | 864.68 | 0 | -| 65536 | 16 | 4096 | clustered | prefix | 360960 | 8207 | 2.00 | 0.75 | 418.24 | 0 | -| 65536 | 16 | 4096 | clustered | coset | 33406 | 17 | 0.00 | 0.67 | 406.56 | 0 | -| 65536 | 16 | 4096 | adversarial | prefix | 884704 | 24574 | 6.00 | 1.03 | 985.23 | 0 | -| 65536 | 16 | 4096 | adversarial | coset | 581690 | 16384 | 4.00 | 1.46 | 993.18 | 0 | -| 262144 | 18 | 1 | random | prefix | 632 | 18 | 18.00 | 0.01 | 0.62 | 0 | -| 262144 | 18 | 1 | random | coset | 686 | 18 | 18.00 | 0.06 | 0.62 | 0 | -| 262144 | 18 | 1 | clustered | prefix | 632 | 18 | 18.00 | 0.00 | 0.64 | 0 | -| 262144 | 18 | 1 | clustered | coset | 686 | 18 | 18.00 | 0.05 | 0.62 | 0 | -| 262144 | 18 | 1 | adversarial | prefix | 632 | 18 | 18.00 | 0.00 | 0.91 | 0 | -| 262144 | 18 | 1 | adversarial | coset | 673 | 18 | 18.00 | 0.05 | 0.91 | 0 | -| 262144 | 18 | 2 | random | prefix | 1200 | 35 | 17.50 | 0.01 | 1.81 | 0 | -| 262144 | 18 | 2 | random | coset | 1211 | 33 | 16.50 | 0.10 | 1.21 | 0 | -| 262144 | 18 | 2 | clustered | prefix | 688 | 19 | 9.50 | 0.03 | 0.79 | 0 | -| 262144 | 18 | 2 | clustered | coset | 662 | 17 | 8.50 | 0.05 | 0.78 | 0 | -| 262144 | 18 | 2 | adversarial | prefix | 1232 | 36 | 18.00 | 0.00 | 1.26 | 0 | -| 262144 | 18 | 2 | adversarial | coset | 1249 | 34 | 17.00 | 0.04 | 1.59 | 0 | -| 262144 | 18 | 4 | random | prefix | 2176 | 64 | 16.00 | 0.04 | 3.47 | 0 | -| 262144 | 18 | 4 | random | coset | 2094 | 58 | 14.50 | 0.07 | 2.64 | 0 | -| 262144 | 18 | 4 | clustered | prefix | 1024 | 28 | 7.00 | 0.01 | 1.02 | 0 | -| 262144 | 18 | 4 | clustered | coset | 843 | 22 | 5.50 | 0.05 | 1.18 | 0 | -| 262144 | 18 | 4 | adversarial | prefix | 2368 | 70 | 17.50 | 0.01 | 2.81 | 0 | -| 262144 | 18 | 4 | adversarial | coset | 2307 | 64 | 16.00 | 0.08 | 2.86 | 0 | -| 262144 | 18 | 8 | random | prefix | 4384 | 130 | 16.25 | 0.07 | 4.46 | 0 | -| 262144 | 18 | 8 | random | coset | 4121 | 116 | 14.50 | 0.26 | 4.87 | 0 | -| 262144 | 18 | 8 | clustered | prefix | 1280 | 33 | 4.12 | 0.00 | 1.46 | 0 | -| 262144 | 18 | 8 | clustered | coset | 773 | 19 | 2.38 | 0.01 | 1.75 | 0 | -| 262144 | 18 | 8 | adversarial | prefix | 4512 | 134 | 16.75 | 0.01 | 5.09 | 0 | -| 262144 | 18 | 8 | adversarial | coset | 4273 | 120 | 15.00 | 0.09 | 5.98 | 0 | -| 262144 | 18 | 16 | random | prefix | 8096 | 240 | 15.00 | 0.13 | 8.37 | 0 | -| 262144 | 18 | 16 | random | coset | 7413 | 210 | 13.12 | 0.36 | 10.14 | 0 | -| 262144 | 18 | 16 | clustered | prefix | 1888 | 46 | 2.88 | 0.01 | 1.94 | 0 | -| 262144 | 18 | 16 | clustered | coset | 736 | 16 | 1.00 | 0.01 | 2.23 | 0 | -| 262144 | 18 | 16 | adversarial | prefix | 8544 | 254 | 15.88 | 0.10 | 9.37 | 0 | -| 262144 | 18 | 16 | adversarial | coset | 7915 | 224 | 14.00 | 0.19 | 10.12 | 0 | -| 262144 | 18 | 32 | random | prefix | 15296 | 453 | 14.16 | 0.11 | 19.29 | 0 | -| 262144 | 18 | 32 | random | coset | 13737 | 391 | 12.22 | 0.45 | 16.44 | 0 | -| 262144 | 18 | 32 | clustered | prefix | 3296 | 78 | 2.44 | 0.01 | 3.42 | 0 | -| 262144 | 18 | 32 | clustered | coset | 863 | 16 | 0.50 | 0.02 | 4.19 | 0 | -| 262144 | 18 | 32 | adversarial | prefix | 16096 | 478 | 14.94 | 0.06 | 17.81 | 0 | -| 262144 | 18 | 32 | adversarial | coset | 14621 | 416 | 13.00 | 0.38 | 16.71 | 0 | -| 262144 | 18 | 64 | random | prefix | 28000 | 826 | 12.91 | 0.21 | 29.38 | 0 | -| 262144 | 18 | 64 | random | coset | 24537 | 700 | 10.94 | 0.57 | 32.65 | 0 | -| 262144 | 18 | 64 | clustered | prefix | 6112 | 142 | 2.22 | 0.02 | 6.92 | 0 | -| 262144 | 18 | 64 | clustered | coset | 1119 | 16 | 0.25 | 0.03 | 7.47 | 0 | -| 262144 | 18 | 64 | adversarial | prefix | 30176 | 894 | 13.97 | 0.13 | 32.97 | 0 | -| 262144 | 18 | 64 | adversarial | coset | 26942 | 768 | 12.00 | 0.39 | 32.41 | 0 | -| 262144 | 18 | 128 | random | prefix | 52704 | 1550 | 12.11 | 0.36 | 62.19 | 0 | -| 262144 | 18 | 128 | random | coset | 45367 | 1296 | 10.12 | 0.89 | 56.12 | 0 | -| 262144 | 18 | 128 | clustered | prefix | 11808 | 272 | 2.12 | 0.03 | 13.44 | 0 | -| 262144 | 18 | 128 | clustered | coset | 1704 | 18 | 0.14 | 0.04 | 13.48 | 0 | -| 262144 | 18 | 128 | adversarial | prefix | 56288 | 1662 | 12.98 | 0.15 | 59.01 | 0 | -| 262144 | 18 | 128 | adversarial | coset | 49342 | 1408 | 11.00 | 0.49 | 61.67 | 0 | -| 262144 | 18 | 256 | random | prefix | 96864 | 2834 | 11.07 | 0.54 | 101.68 | 0 | -| 262144 | 18 | 256 | random | coset | 81322 | 2324 | 9.08 | 1.00 | 105.37 | 0 | -| 262144 | 18 | 256 | clustered | prefix | 23072 | 528 | 2.06 | 0.04 | 25.73 | 0 | -| 262144 | 18 | 256 | clustered | coset | 2723 | 18 | 0.07 | 0.21 | 24.48 | 0 | -| 262144 | 18 | 256 | adversarial | prefix | 104416 | 3070 | 11.99 | 0.29 | 103.90 | 0 | -| 262144 | 18 | 256 | adversarial | coset | 89662 | 2560 | 10.00 | 0.71 | 117.14 | 0 | -| 262144 | 18 | 512 | random | prefix | 178784 | 5202 | 10.16 | 0.91 | 186.87 | 0 | -| 262144 | 18 | 512 | random | coset | 146264 | 4180 | 8.16 | 1.32 | 201.69 | 0 | -| 262144 | 18 | 512 | clustered | prefix | 45536 | 1038 | 2.03 | 0.08 | 50.81 | 0 | -| 262144 | 18 | 512 | clustered | coset | 4706 | 16 | 0.03 | 0.10 | 50.21 | 0 | -| 262144 | 18 | 512 | adversarial | prefix | 192480 | 5630 | 11.00 | 0.45 | 210.60 | 0 | -| 262144 | 18 | 512 | adversarial | coset | 161342 | 4608 | 9.00 | 0.89 | 221.99 | 0 | -| 262144 | 18 | 1024 | random | prefix | 324928 | 9385 | 9.17 | 0.98 | 341.50 | 0 | -| 262144 | 18 | 1024 | random | coset | 256923 | 7339 | 7.17 | 1.66 | 352.00 | 0 | -| 262144 | 18 | 1024 | clustered | prefix | 90656 | 2064 | 2.02 | 0.17 | 109.46 | 0 | -| 262144 | 18 | 1024 | clustered | coset | 8867 | 18 | 0.02 | 0.27 | 119.73 | 0 | -| 262144 | 18 | 1024 | adversarial | prefix | 352224 | 10238 | 10.00 | 0.85 | 359.38 | 0 | -| 262144 | 18 | 1024 | adversarial | coset | 286782 | 8192 | 8.00 | 1.05 | 381.45 | 0 | -| 262144 | 18 | 2048 | random | prefix | 582720 | 16673 | 8.14 | 1.57 | 625.73 | 0 | -| 262144 | 18 | 2048 | random | coset | 441167 | 12579 | 6.14 | 1.63 | 676.73 | 0 | -| 262144 | 18 | 2048 | clustered | prefix | 180800 | 4113 | 2.01 | 0.30 | 213.70 | 0 | -| 262144 | 18 | 2048 | clustered | coset | 17099 | 19 | 0.01 | 0.38 | 212.47 | 0 | -| 262144 | 18 | 2048 | adversarial | prefix | 638944 | 18430 | 9.00 | 1.26 | 689.96 | 0 | -| 262144 | 18 | 2048 | adversarial | coset | 501822 | 14336 | 7.00 | 1.93 | 692.39 | 0 | -| 262144 | 18 | 4096 | random | prefix | 1034592 | 29258 | 7.14 | 1.89 | 1128.74 | 0 | -| 262144 | 18 | 4096 | random | coset | 741754 | 21068 | 5.14 | 2.25 | 1136.17 | 0 | -| 262144 | 18 | 4096 | clustered | prefix | 360768 | 8201 | 2.00 | 1.09 | 413.40 | 0 | -| 262144 | 18 | 4096 | clustered | coset | 33201 | 11 | 0.00 | 0.80 | 417.63 | 0 | -| 262144 | 18 | 4096 | adversarial | prefix | 1146848 | 32766 | 8.00 | 1.25 | 1243.74 | 0 | -| 262144 | 18 | 4096 | adversarial | coset | 860222 | 24576 | 6.00 | 2.00 | 1274.75 | 0 | -| 1048576 | 20 | 1 | random | prefix | 696 | 20 | 20.00 | 0.00 | 0.86 | 0 | -| 1048576 | 20 | 1 | random | coset | 756 | 20 | 20.00 | 0.08 | 1.08 | 0 | -| 1048576 | 20 | 1 | clustered | prefix | 696 | 20 | 20.00 | 0.00 | 0.68 | 0 | -| 1048576 | 20 | 1 | clustered | coset | 758 | 20 | 20.00 | 0.03 | 0.73 | 0 | -| 1048576 | 20 | 1 | adversarial | prefix | 696 | 20 | 20.00 | 0.00 | 1.77 | 0 | -| 1048576 | 20 | 1 | adversarial | coset | 741 | 20 | 20.00 | 0.06 | 0.71 | 0 | -| 1048576 | 20 | 2 | random | prefix | 1168 | 34 | 17.00 | 0.00 | 1.24 | 0 | -| 1048576 | 20 | 2 | random | coset | 1172 | 32 | 16.00 | 0.04 | 1.16 | 0 | -| 1048576 | 20 | 2 | clustered | prefix | 752 | 21 | 10.50 | 0.00 | 0.82 | 0 | -| 1048576 | 20 | 2 | clustered | coset | 732 | 19 | 9.50 | 0.01 | 0.92 | 0 | -| 1048576 | 20 | 2 | adversarial | prefix | 1360 | 40 | 20.00 | 0.05 | 1.45 | 0 | -| 1048576 | 20 | 2 | adversarial | coset | 1393 | 38 | 19.00 | 0.01 | 1.85 | 0 | -| 1048576 | 20 | 4 | random | prefix | 2560 | 76 | 19.00 | 0.00 | 2.48 | 0 | -| 1048576 | 20 | 4 | random | coset | 2519 | 70 | 17.50 | 0.13 | 2.75 | 0 | -| 1048576 | 20 | 4 | clustered | prefix | 960 | 26 | 6.50 | 0.00 | 0.96 | 0 | -| 1048576 | 20 | 4 | clustered | coset | 778 | 20 | 5.00 | 0.01 | 0.98 | 0 | -| 1048576 | 20 | 4 | adversarial | prefix | 2624 | 78 | 19.50 | 0.00 | 2.86 | 0 | -| 1048576 | 20 | 4 | adversarial | coset | 2595 | 72 | 18.00 | 0.12 | 3.11 | 0 | -| 1048576 | 20 | 8 | random | prefix | 4960 | 148 | 18.50 | 0.01 | 5.68 | 0 | -| 1048576 | 20 | 8 | random | coset | 4769 | 134 | 16.75 | 0.11 | 5.71 | 0 | -| 1048576 | 20 | 8 | clustered | prefix | 1344 | 35 | 4.38 | 0.02 | 2.23 | 0 | -| 1048576 | 20 | 8 | clustered | coset | 840 | 21 | 2.62 | 0.03 | 1.55 | 0 | -| 1048576 | 20 | 8 | adversarial | prefix | 5024 | 150 | 18.75 | 0.06 | 5.05 | 0 | -| 1048576 | 20 | 8 | adversarial | coset | 4849 | 136 | 17.00 | 0.11 | 5.69 | 0 | -| 1048576 | 20 | 16 | random | prefix | 8896 | 265 | 16.56 | 0.07 | 8.73 | 0 | -| 1048576 | 20 | 16 | random | coset | 8298 | 235 | 14.69 | 0.16 | 9.23 | 0 | -| 1048576 | 20 | 16 | clustered | prefix | 1984 | 49 | 3.06 | 0.01 | 2.01 | 0 | -| 1048576 | 20 | 16 | clustered | coset | 842 | 19 | 1.19 | 0.01 | 2.10 | 0 | -| 1048576 | 20 | 16 | adversarial | prefix | 9568 | 286 | 17.88 | 0.07 | 9.68 | 0 | -| 1048576 | 20 | 16 | adversarial | coset | 9067 | 256 | 16.00 | 0.16 | 9.73 | 0 | -| 1048576 | 20 | 32 | random | prefix | 17120 | 510 | 15.94 | 0.18 | 20.40 | 0 | -| 1048576 | 20 | 32 | random | coset | 15764 | 448 | 14.00 | 0.22 | 18.71 | 0 | -| 1048576 | 20 | 32 | clustered | prefix | 3616 | 88 | 2.75 | 0.08 | 4.05 | 0 | -| 1048576 | 20 | 32 | clustered | coset | 1203 | 26 | 0.81 | 0.05 | 3.94 | 0 | -| 1048576 | 20 | 32 | adversarial | prefix | 18144 | 542 | 16.94 | 0.05 | 19.48 | 0 | -| 1048576 | 20 | 32 | adversarial | coset | 16925 | 480 | 15.00 | 0.21 | 18.38 | 0 | -| 1048576 | 20 | 64 | random | prefix | 32288 | 960 | 15.00 | 0.22 | 34.70 | 0 | -| 1048576 | 20 | 64 | random | coset | 29252 | 834 | 13.03 | 0.49 | 35.37 | 0 | -| 1048576 | 20 | 64 | clustered | prefix | 6240 | 146 | 2.28 | 0.02 | 6.43 | 0 | -| 1048576 | 20 | 64 | clustered | coset | 1259 | 20 | 0.31 | 0.03 | 6.84 | 0 | -| 1048576 | 20 | 64 | adversarial | prefix | 34272 | 1022 | 15.97 | 0.13 | 34.48 | 0 | -| 1048576 | 20 | 64 | adversarial | coset | 31487 | 896 | 14.00 | 0.32 | 35.10 | 0 | -| 1048576 | 20 | 128 | random | prefix | 60320 | 1788 | 13.97 | 0.89 | 61.89 | 0 | -| 1048576 | 20 | 128 | random | coset | 53716 | 1534 | 11.98 | 0.94 | 63.93 | 0 | -| 1048576 | 20 | 128 | clustered | prefix | 11776 | 271 | 2.12 | 0.07 | 13.35 | 0 | -| 1048576 | 20 | 128 | clustered | coset | 1667 | 17 | 0.13 | 0.06 | 13.00 | 0 | -| 1048576 | 20 | 128 | adversarial | prefix | 64480 | 1918 | 14.98 | 0.18 | 75.30 | 0 | -| 1048576 | 20 | 128 | adversarial | coset | 58304 | 1664 | 13.00 | 0.42 | 64.41 | 0 | -| 1048576 | 20 | 256 | random | prefix | 113376 | 3350 | 13.09 | 0.65 | 113.34 | 0 | -| 1048576 | 20 | 256 | random | coset | 99363 | 2840 | 11.09 | 2.03 | 119.55 | 0 | -| 1048576 | 20 | 256 | clustered | prefix | 23136 | 530 | 2.07 | 0.05 | 24.83 | 0 | -| 1048576 | 20 | 256 | clustered | coset | 2793 | 20 | 0.08 | 0.07 | 24.50 | 0 | -| 1048576 | 20 | 256 | adversarial | prefix | 120800 | 3582 | 13.99 | 0.42 | 122.99 | 0 | -| 1048576 | 20 | 256 | adversarial | coset | 107584 | 3072 | 12.00 | 0.48 | 126.79 | 0 | -| 1048576 | 20 | 512 | random | prefix | 211360 | 6220 | 12.15 | 1.45 | 231.07 | 0 | -| 1048576 | 20 | 512 | random | coset | 181802 | 5198 | 10.15 | 1.86 | 229.12 | 0 | -| 1048576 | 20 | 512 | clustered | prefix | 45792 | 1046 | 2.04 | 0.13 | 51.40 | 0 | -| 1048576 | 20 | 512 | clustered | coset | 4979 | 24 | 0.05 | 0.11 | 49.51 | 0 | -| 1048576 | 20 | 512 | adversarial | prefix | 225248 | 6654 | 13.00 | 0.56 | 248.60 | 0 | -| 1048576 | 20 | 512 | adversarial | coset | 197184 | 5632 | 11.00 | 1.22 | 250.42 | 0 | -| 1048576 | 20 | 1024 | random | prefix | 389600 | 11406 | 11.14 | 1.90 | 429.63 | 0 | -| 1048576 | 20 | 1024 | random | coset | 327322 | 9360 | 9.14 | 2.83 | 417.59 | 0 | -| 1048576 | 20 | 1024 | clustered | prefix | 90752 | 2067 | 2.02 | 0.20 | 99.00 | 0 | -| 1048576 | 20 | 1024 | clustered | coset | 8973 | 21 | 0.02 | 0.21 | 105.35 | 0 | -| 1048576 | 20 | 1024 | adversarial | prefix | 417760 | 12286 | 12.00 | 1.59 | 466.12 | 0 | -| 1048576 | 20 | 1024 | adversarial | coset | 358464 | 10240 | 10.00 | 1.56 | 478.71 | 0 | -| 1048576 | 20 | 2048 | random | prefix | 714016 | 20776 | 10.14 | 2.66 | 770.21 | 0 | -| 1048576 | 20 | 2048 | random | coset | 583495 | 16682 | 8.15 | 3.92 | 766.82 | 0 | -| 1048576 | 20 | 2048 | clustered | prefix | 180768 | 4112 | 2.01 | 0.32 | 230.26 | 0 | -| 1048576 | 20 | 2048 | clustered | coset | 17064 | 18 | 0.01 | 0.37 | 218.32 | 0 | -| 1048576 | 20 | 2048 | adversarial | prefix | 770016 | 22526 | 11.00 | 1.73 | 866.86 | 0 | -| 1048576 | 20 | 2048 | adversarial | coset | 645184 | 18432 | 9.00 | 2.91 | 842.14 | 0 | -| 1048576 | 20 | 4096 | random | prefix | 1294176 | 37370 | 9.12 | 3.96 | 1453.29 | 0 | -| 1048576 | 20 | 4096 | random | coset | 1021468 | 29180 | 7.12 | 5.45 | 1439.72 | 0 | -| 1048576 | 20 | 4096 | clustered | prefix | 361056 | 8210 | 2.00 | 0.63 | 404.03 | 0 | -| 1048576 | 20 | 4096 | clustered | coset | 33521 | 20 | 0.00 | 0.78 | 412.28 | 0 | -| 1048576 | 20 | 4096 | adversarial | prefix | 1408992 | 40958 | 10.00 | 2.34 | 1529.41 | 0 | -| 1048576 | 20 | 4096 | adversarial | coset | 1146944 | 32768 | 8.00 | 4.18 | 1553.60 | 0 | - -## Proof Size Visualizations - -![proof-size](proof_size_4096_adversarial.svg) -![proof-size](proof_size_4096_clustered.svg) -![proof-size](proof_size_4096_random.svg) -![proof-size](proof_size_16384_adversarial.svg) -![proof-size](proof_size_16384_clustered.svg) -![proof-size](proof_size_16384_random.svg) -![proof-size](proof_size_65536_adversarial.svg) -![proof-size](proof_size_65536_clustered.svg) -![proof-size](proof_size_65536_random.svg) -![proof-size](proof_size_262144_adversarial.svg) -![proof-size](proof_size_262144_clustered.svg) -![proof-size](proof_size_262144_random.svg) -![proof-size](proof_size_1048576_adversarial.svg) -![proof-size](proof_size_1048576_clustered.svg) -![proof-size](proof_size_1048576_random.svg) - -## Proving Time Visualizations - -![proving-time](prove_ms_4096_adversarial.svg) -![proving-time](prove_ms_4096_clustered.svg) -![proving-time](prove_ms_4096_random.svg) -![proving-time](prove_ms_16384_adversarial.svg) -![proving-time](prove_ms_16384_clustered.svg) -![proving-time](prove_ms_16384_random.svg) -![proving-time](prove_ms_65536_adversarial.svg) -![proving-time](prove_ms_65536_clustered.svg) -![proving-time](prove_ms_65536_random.svg) -![proving-time](prove_ms_262144_adversarial.svg) -![proving-time](prove_ms_262144_clustered.svg) -![proving-time](prove_ms_262144_random.svg) -![proving-time](prove_ms_1048576_adversarial.svg) -![proving-time](prove_ms_1048576_clustered.svg) -![proving-time](prove_ms_1048576_random.svg) - -## Verification Time Visualizations - -![verification-time](verify_ms_4096_adversarial.svg) -![verification-time](verify_ms_4096_clustered.svg) -![verification-time](verify_ms_4096_random.svg) -![verification-time](verify_ms_16384_adversarial.svg) -![verification-time](verify_ms_16384_clustered.svg) -![verification-time](verify_ms_16384_random.svg) -![verification-time](verify_ms_65536_adversarial.svg) -![verification-time](verify_ms_65536_clustered.svg) -![verification-time](verify_ms_65536_random.svg) -![verification-time](verify_ms_262144_adversarial.svg) -![verification-time](verify_ms_262144_clustered.svg) -![verification-time](verify_ms_262144_random.svg) -![verification-time](verify_ms_1048576_adversarial.svg) -![verification-time](verify_ms_1048576_clustered.svg) -![verification-time](verify_ms_1048576_random.svg) diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg deleted file mode 100644 index 48fd0333..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_adversarial.svg +++ /dev/null @@ -1,278 +0,0 @@ - - - -Proof Size vs k (n=1048576, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -200000.0 - - - -400000.0 - - - -600000.0 - - - -800000.0 - - - -1000000.0 - - - -1200000.0 - - - -1400000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg deleted file mode 100644 index 2dc2e549..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_clustered.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=1048576, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg deleted file mode 100644 index 54f617fd..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_1048576_random.svg +++ /dev/null @@ -1,267 +0,0 @@ - - - -Proof Size vs k (n=1048576, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -200000.0 - - - -400000.0 - - - -600000.0 - - - -800000.0 - - - -1000000.0 - - - -1200000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg deleted file mode 100644 index 005b01b5..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_adversarial.svg +++ /dev/null @@ -1,265 +0,0 @@ - - - -Proof Size vs k (n=16384, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -100000.0 - - - -200000.0 - - - -300000.0 - - - -400000.0 - - - -500000.0 - - - -600000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg deleted file mode 100644 index 0bdf91db..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_clustered.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=16384, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg deleted file mode 100644 index 4f98899f..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_16384_random.svg +++ /dev/null @@ -1,252 +0,0 @@ - - - -Proof Size vs k (n=16384, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - -0.0 - - - -100000.0 - - - -200000.0 - - - -300000.0 - - - -400000.0 - - - -500000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg deleted file mode 100644 index 3dc39608..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_adversarial.svg +++ /dev/null @@ -1,259 +0,0 @@ - - - -Proof Size vs k (n=262144, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -200000.0 - - - -400000.0 - - - -600000.0 - - - -800000.0 - - - -1000000.0 - - - -1200000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg deleted file mode 100644 index abd9bbc2..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_clustered.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=262144, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg deleted file mode 100644 index 9bfc23f8..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_262144_random.svg +++ /dev/null @@ -1,273 +0,0 @@ - - - -Proof Size vs k (n=262144, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100000.0 - - - -200000.0 - - - -300000.0 - - - -400000.0 - - - -500000.0 - - - -600000.0 - - - -700000.0 - - - -800000.0 - - - -900000.0 - - - -1000000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg deleted file mode 100644 index 52aedd57..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_adversarial.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=4096, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg deleted file mode 100644 index 6b66f996..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_clustered.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=4096, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg deleted file mode 100644 index 24a836c0..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_4096_random.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=4096, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg deleted file mode 100644 index c64b9c4e..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_adversarial.svg +++ /dev/null @@ -1,308 +0,0 @@ - - - -Proof Size vs k (n=65536, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100000.0 - - - -200000.0 - - - -300000.0 - - - -400000.0 - - - -500000.0 - - - -600000.0 - - - -700000.0 - - - -800000.0 - - - -900000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg deleted file mode 100644 index e7435a21..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_clustered.svg +++ /dev/null @@ -1,280 +0,0 @@ - - - -Proof Size vs k (n=65536, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50000.0 - - - -100000.0 - - - -150000.0 - - - -200000.0 - - - -250000.0 - - - -300000.0 - - - -350000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg deleted file mode 100644 index 5334f4b0..00000000 --- a/crypto-primitives/target/merkle_tree_reports/proof_size_65536_random.svg +++ /dev/null @@ -1,292 +0,0 @@ - - - -Proof Size vs k (n=65536, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -proof size (bytes) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100000.0 - - - -200000.0 - - - -300000.0 - - - -400000.0 - - - -500000.0 - - - -600000.0 - - - -700000.0 - - - -800000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg deleted file mode 100644 index 1284346d..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_adversarial.svg +++ /dev/null @@ -1,252 +0,0 @@ - - - -Proving Time vs k (n=1048576, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - -0.0 - - - -1.0 - - - -2.0 - - - -3.0 - - - -4.0 - - - -5.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg deleted file mode 100644 index e4b86812..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_clustered.svg +++ /dev/null @@ -1,241 +0,0 @@ - - - -Proving Time vs k (n=1048576, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg deleted file mode 100644 index af620f7e..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_1048576_random.svg +++ /dev/null @@ -1,270 +0,0 @@ - - - -Proving Time vs k (n=1048576, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -1.0 - - - -2.0 - - - -3.0 - - - -4.0 - - - -5.0 - - - -6.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg deleted file mode 100644 index 4c5d0e68..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_adversarial.svg +++ /dev/null @@ -1,242 +0,0 @@ - - - -Proving Time vs k (n=16384, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg deleted file mode 100644 index e1de88bf..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_clustered.svg +++ /dev/null @@ -1,242 +0,0 @@ - - - -Proving Time vs k (n=16384, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg deleted file mode 100644 index e284eff0..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_16384_random.svg +++ /dev/null @@ -1,256 +0,0 @@ - - - -Proving Time vs k (n=16384, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg deleted file mode 100644 index 3f2bfd82..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_adversarial.svg +++ /dev/null @@ -1,275 +0,0 @@ - - - -Proving Time vs k (n=262144, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - -2.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg deleted file mode 100644 index 84183d4c..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_clustered.svg +++ /dev/null @@ -1,252 +0,0 @@ - - - -Proving Time vs k (n=262144, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg deleted file mode 100644 index 462fa3d9..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_262144_random.svg +++ /dev/null @@ -1,286 +0,0 @@ - - - -Proving Time vs k (n=262144, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - -2.5 - - - -3.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg deleted file mode 100644 index 5ca34884..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_adversarial.svg +++ /dev/null @@ -1,236 +0,0 @@ - - - -Proving Time vs k (n=4096, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg deleted file mode 100644 index 2980d334..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_clustered.svg +++ /dev/null @@ -1,237 +0,0 @@ - - - -Proving Time vs k (n=4096, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg deleted file mode 100644 index 0202d057..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_4096_random.svg +++ /dev/null @@ -1,243 +0,0 @@ - - - -Proving Time vs k (n=4096, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg deleted file mode 100644 index 32002427..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_adversarial.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - -Proving Time vs k (n=65536, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg deleted file mode 100644 index fc20c98a..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_clustered.svg +++ /dev/null @@ -1,241 +0,0 @@ - - - -Proving Time vs k (n=65536, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg deleted file mode 100644 index 494f7677..00000000 --- a/crypto-primitives/target/merkle_tree_reports/prove_ms_65536_random.svg +++ /dev/null @@ -1,268 +0,0 @@ - - - -Proving Time vs k (n=65536, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -prove time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - --0.5 - - - -0.0 - - - -0.5 - - - -1.0 - - - -1.5 - - - -2.0 - - - -2.5 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg deleted file mode 100644 index e509e699..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_adversarial.svg +++ /dev/null @@ -1,291 +0,0 @@ - - - -Verification Time vs k (n=1048576, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -200.0 - - - -400.0 - - - -600.0 - - - -800.0 - - - -1000.0 - - - -1200.0 - - - -1400.0 - - - -1600.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg deleted file mode 100644 index cb1db92e..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_clustered.svg +++ /dev/null @@ -1,296 +0,0 @@ - - - -Verification Time vs k (n=1048576, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg deleted file mode 100644 index 7e86027d..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_1048576_random.svg +++ /dev/null @@ -1,281 +0,0 @@ - - - -Verification Time vs k (n=1048576, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -200.0 - - - -400.0 - - - -600.0 - - - -800.0 - - - -1000.0 - - - -1200.0 - - - -1400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg deleted file mode 100644 index 37361f1c..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_adversarial.svg +++ /dev/null @@ -1,277 +0,0 @@ - - - -Verification Time vs k (n=16384, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100.0 - - - -200.0 - - - -300.0 - - - -400.0 - - - -500.0 - - - -600.0 - - - -700.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg deleted file mode 100644 index 4abae825..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_clustered.svg +++ /dev/null @@ -1,295 +0,0 @@ - - - -Verification Time vs k (n=16384, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg deleted file mode 100644 index 668bd411..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_16384_random.svg +++ /dev/null @@ -1,265 +0,0 @@ - - - -Verification Time vs k (n=16384, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -100.0 - - - -200.0 - - - -300.0 - - - -400.0 - - - -500.0 - - - -600.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg deleted file mode 100644 index 1113d20a..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_adversarial.svg +++ /dev/null @@ -1,266 +0,0 @@ - - - -Verification Time vs k (n=262144, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - -0.0 - - - -200.0 - - - -400.0 - - - -600.0 - - - -800.0 - - - -1000.0 - - - -1200.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg deleted file mode 100644 index e936a534..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_clustered.svg +++ /dev/null @@ -1,298 +0,0 @@ - - - -Verification Time vs k (n=262144, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg deleted file mode 100644 index 2532e2ef..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_262144_random.svg +++ /dev/null @@ -1,253 +0,0 @@ - - - -Verification Time vs k (n=262144, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - -0.0 - - - -200.0 - - - -400.0 - - - -600.0 - - - -800.0 - - - -1000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg deleted file mode 100644 index 7419c8ac..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_adversarial.svg +++ /dev/null @@ -1,298 +0,0 @@ - - - -Verification Time vs k (n=4096, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg deleted file mode 100644 index 18e446e9..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_clustered.svg +++ /dev/null @@ -1,298 +0,0 @@ - - - -Verification Time vs k (n=4096, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg deleted file mode 100644 index 955f1fd8..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_4096_random.svg +++ /dev/null @@ -1,295 +0,0 @@ - - - -Verification Time vs k (n=4096, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg deleted file mode 100644 index 164fcd8d..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_adversarial.svg +++ /dev/null @@ -1,325 +0,0 @@ - - - -Verification Time vs k (n=65536, pattern=adversarial) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100.0 - - - -200.0 - - - -300.0 - - - -400.0 - - - -500.0 - - - -600.0 - - - -700.0 - - - -800.0 - - - -900.0 - - - -1000.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg deleted file mode 100644 index b22a6654..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_clustered.svg +++ /dev/null @@ -1,298 +0,0 @@ - - - -Verification Time vs k (n=65536, pattern=clustered) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -50.0 - - - -100.0 - - - -150.0 - - - -200.0 - - - -250.0 - - - -300.0 - - - -350.0 - - - -400.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - diff --git a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg b/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg deleted file mode 100644 index a93a2f2f..00000000 --- a/crypto-primitives/target/merkle_tree_reports/verify_ms_65536_random.svg +++ /dev/null @@ -1,306 +0,0 @@ - - - -Verification Time vs k (n=65536, pattern=random) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -verify time (ms) - - -batch size (k) - - - - - - - - - - - - - - - - - - - - - - -0.0 - - - -100.0 - - - -200.0 - - - -300.0 - - - -400.0 - - - -500.0 - - - -600.0 - - - -700.0 - - - -800.0 - - - -900.0 - - - - -0.0 - - - -500.0 - - - -1000.0 - - - -1500.0 - - - -2000.0 - - - -2500.0 - - - -3000.0 - - - -3500.0 - - - -4000.0 - - - - - - -prefix - - -coset - - - - From 01bed5229cc7ce8269a37eb1d6a17a52a292ea3a Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 8 Dec 2025 16:19:45 +0100 Subject: [PATCH 09/22] tests: minor corrections to benchmark tests --- .../src/merkle_tree/{bench.rs => legacy.rs} | 18 ++++++++---------- crypto-primitives/src/merkle_tree/mod.rs | 3 +++ .../src/merkle_tree/tests/bench_report.rs | 8 ++------ 3 files changed, 13 insertions(+), 16 deletions(-) rename crypto-primitives/src/merkle_tree/{bench.rs => legacy.rs} (97%) diff --git a/crypto-primitives/src/merkle_tree/bench.rs b/crypto-primitives/src/merkle_tree/legacy.rs similarity index 97% rename from crypto-primitives/src/merkle_tree/bench.rs rename to crypto-primitives/src/merkle_tree/legacy.rs index 440f105a..000eb4bc 100644 --- a/crypto-primitives/src/merkle_tree/bench.rs +++ b/crypto-primitives/src/merkle_tree/legacy.rs @@ -1,5 +1,4 @@ -#![allow(clippy::needless_range_loop)] -#![allow(dead_code)] +#![allow(clippy::needless_range_loop)] /// Defines a trait to chain two types of CRHs. use crate::{ @@ -149,7 +148,7 @@ pub struct Path { pub leaf_index: usize, } -impl Path

{ +impl Path

{ /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. /// @@ -167,13 +166,12 @@ impl Path

{ /// * `leaf_size`: leaf size in number of bytes /// /// `verify` infers the tree height by setting `tree_height = self.auth_path.len() + 2` - #[allow(dead_code)] - pub fn verify>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - leaf: L, + pub fn verify>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + leaf: L, ) -> Result { // calculate leaf hash let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf)?; diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index f6c4ee05..e827ca04 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -27,6 +27,9 @@ pub mod constraints; #[cfg(test)] mod tests; +#[cfg(any(test, feature = "bench_harness"))] +pub mod legacy; + #[cfg(all( target_has_atomic = "8", target_has_atomic = "16", diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs index 8d0e027b..6e81f3ce 100644 --- a/crypto-primitives/src/merkle_tree/tests/bench_report.rs +++ b/crypto-primitives/src/merkle_tree/tests/bench_report.rs @@ -1,8 +1,8 @@ #![cfg(feature = "bench_harness")] use crate::merkle_tree::{ - tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, LeafParam, - MerkleTree, TwoToOneParam, + legacy, tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, + LeafParam, MerkleTree, TwoToOneParam, }; use ark_ed_on_bls12_381::Fr; use ark_serialize::CanonicalSerialize; @@ -11,10 +11,6 @@ use ark_std::{ UniformRand, }; use plotters::prelude::*; -#[cfg(test)] -#[cfg(feature = "bench_harness")] -#[path = "../bench.rs"] -mod legacy; use std::{ collections::{BTreeMap, BTreeSet}, fs::{self, File}, From eef6d799b27f0af199c3aff6c1641269d29a9d53 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 8 Dec 2025 17:50:06 +0100 Subject: [PATCH 10/22] verify: remove unnecessary depth check --- crypto-primitives/src/merkle_tree/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index e827ca04..fcecb6aa 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -281,9 +281,6 @@ impl CoPath

{ let d = self.tree_height; let leaf_depth = d - 1; - if d < 2 { - return Ok(false); - } // hash opened leaves and build map containing all leaf digests needed at bottom layer let mut leaves = leaves.into_iter(); From 69f05883ce60c219d30596f7af7099cb8acaffa0 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 8 Dec 2025 21:10:19 +0100 Subject: [PATCH 11/22] verify: reduce cog complexity of CoPath verification - Refactored CoPath::verify into smaller helpers (ingest_leaves, expected_leaf_coset, validate_leaf_copath, recompute_bottom_parents, recompute_inner_layers) to cut control-flow complexity. - Clarified iterator naming in verify (now leaves_iter) to avoid shadowing and improve readability. - Added brief doc comments to the new helpers to document their roles (leaf hashing, copath expectations/validation, and parent recomputation). --- crypto-primitives/src/merkle_tree/mod.rs | 207 +++++++++++++++-------- 1 file changed, 140 insertions(+), 67 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index fcecb6aa..d0451f35 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -283,43 +283,19 @@ impl CoPath

{ let leaf_depth = d - 1; // hash opened leaves and build map containing all leaf digests needed at bottom layer - let mut leaves = leaves.into_iter(); - let mut leaf_level: BTreeMap = BTreeMap::new(); - for &idx in &self.leaf_indexes { - let leaf = leaves.next().ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_indexes.len()))?; - let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; - leaf_level.insert(idx, leaf_hash); - } - if leaves.next().is_some() { - return Err(crate::Error::IncorrectInputLength(self.leaf_indexes.len())); - } + let mut leaves_iter = leaves.into_iter(); + let mut leaf_level = + Self::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params)?; // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); let on_path = compute_on_path(leaf_depth, &index_set); // holds indices of on-path nodes at depth d // compute minimal copath at leaf layer (B*_{d-1}) - let mut expected_leaf_coset: Vec = Vec::new(); - for &path_idx in on_path[leaf_depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { - expected_leaf_coset.push(sibling_idx); // copath element needed for proof - } - } - expected_leaf_coset.sort_unstable(); // canonical order - - if expected_leaf_coset.len() != self.leaf_copath.len() { + let expected_leaf_coset = Self::expected_leaf_coset(leaf_depth, &on_path); + if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { return Ok(false); } - - for (sibling_idx, sibling_digest) in expected_leaf_coset.into_iter().zip(self.leaf_copath.iter()) { - match leaf_level.get(&sibling_idx) { - Some(existing) if existing != sibling_digest => return Ok(false), // digest must match new one - _ => { - leaf_level.insert(sibling_idx, sibling_digest.clone()); - } - } - } // prepare inner-level maps for non-on-path siblings and computed parents let mut inner_levels: Vec> = @@ -334,46 +310,25 @@ impl CoPath

{ return Ok(false); } - // Recomputation - // compute parents at depth d-2 using TwoToOne::evaluate to hash inputs - for &parent_index in on_path[leaf_depth - 1].iter() { - let left = leaf_level.get(&(parent_index * 2)).cloned(); - let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { - (Some(left), Some(right)) => (left, right), - _ => return Ok(false), - }; - let parent = P::TwoToOneHash::evaluate( - two_to_one_params, - P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type - P::LeafInnerDigestConverter::convert(right)?, - )?; - inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(leaf_depth - 1, parent_index); - hash_lut.insert(heap_idx, parent); + if !Self::recompute_bottom_parents( + leaf_depth, + &on_path, + &leaf_level, + two_to_one_params, + &mut hash_lut, + &mut inner_levels, + )? { + return Ok(false); } - // compute inner layers up to root using TwoToOne::compress to hash inner digests - for depth in (1..=leaf_depth - 1).rev() { - let parent_depth = depth - 1; - for &parent_index in on_path[parent_depth].iter() { - let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); - let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); - let (left, right) = match (left, right) { - (Some(left), Some(right)) => (left, right), - _ => return Ok(false), - }; - let parent = P::TwoToOneHash::compress( - two_to_one_params, - &left, - &right, - )?; - inner_levels[parent_depth].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(parent_depth, parent_index); - hash_lut.insert(heap_idx, parent); - } + if !Self::recompute_inner_layers( + leaf_depth, + &on_path, + two_to_one_params, + &mut hash_lut, + &mut inner_levels, + )? { + return Ok(false); } // check root @@ -470,6 +425,124 @@ impl CoPath

{ Some((first.0, first.1, deltas, digests)) } + /// Hashes provided leaves (ordered by `leaf_indexes`) and returns a map from leaf index to digest. + fn ingest_leaves( + leaf_indexes: &[usize], + leaves: &mut I, + leaf_hash_params: &LeafParam

, + ) -> Result, crate::Error> + where + L: Borrow, + I: Iterator, + { + let mut leaf_level: BTreeMap = BTreeMap::new(); + for &idx in leaf_indexes { + let leaf = leaves + .next() + .ok_or_else(|| crate::Error::IncorrectInputLength(leaf_indexes.len()))?; + let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; + leaf_level.insert(idx, leaf_hash); + } + if leaves.next().is_some() { + return Err(crate::Error::IncorrectInputLength(leaf_indexes.len())); + } + Ok(leaf_level) + } + + /// Computes the minimal leaf-layer copath indices `B*_{d-1}` (siblings of on-path nodes not on-path). + fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { + let mut expected_leaf_coset: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { + expected_leaf_coset.push(sibling_idx); // copath element needed for proof + } + } + expected_leaf_coset.sort_unstable(); // canonical order + expected_leaf_coset + } + + /// Confirms provided leaf copath matches the expected indices and augments `leaf_level` with them. + fn validate_leaf_copath( + expected_leaf_coset: &[usize], + provided_leaf_copath: &[P::LeafDigest], + leaf_level: &mut BTreeMap, + ) -> bool { + if expected_leaf_coset.len() != provided_leaf_copath.len() { + return false; + } + + for (sibling_idx, sibling_digest) in expected_leaf_coset.iter().zip(provided_leaf_copath) { + match leaf_level.get(sibling_idx) { + Some(existing) if existing != sibling_digest => return false, // digest must match new one + _ => { + leaf_level.insert(*sibling_idx, sibling_digest.clone()); + } + } + } + true + } + + /// Recomputes parents at depth `d-2` (immediately above leaves) using the leaf digests and LUT. + fn recompute_bottom_parents( + leaf_depth: usize, + on_path: &[Vec], + leaf_level: &BTreeMap, + two_to_one_params: &TwoToOneParam

, + hash_lut: &mut HashMap>, + inner_levels: &mut [BTreeMap], + ) -> Result { + for &parent_index in on_path[leaf_depth - 1].iter() { + let left = leaf_level.get(&(parent_index * 2)).cloned(); + let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::evaluate( + two_to_one_params, + P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type + P::LeafInnerDigestConverter::convert(right)?, + )?; + inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(leaf_depth - 1, parent_index); + hash_lut.insert(heap_idx, parent); + } + Ok(true) + } + + /// Recomputes inner layers up to the root using cached inner digests and stores results in LUT. + fn recompute_inner_layers( + leaf_depth: usize, + on_path: &[Vec], + two_to_one_params: &TwoToOneParam

, + hash_lut: &mut HashMap>, + inner_levels: &mut [BTreeMap], + ) -> Result { + for depth in (1..=leaf_depth - 1).rev() { + let parent_depth = depth - 1; + for &parent_index in on_path[parent_depth].iter() { + let left = inner_levels[depth].get(&(parent_index * 2)).cloned(); + let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); + let (left, right) = match (left, right) { + (Some(left), Some(right)) => (left, right), + _ => return Ok(false), + }; + let parent = P::TwoToOneHash::compress( + two_to_one_params, + &left, + &right, + )?; + inner_levels[parent_depth].insert(parent_index, parent.clone()); + // add parent to LUT at heap index + let heap_idx = level_index(parent_depth, parent_index); + hash_lut.insert(heap_idx, parent); + } + } + Ok(true) + } + /// Decodes inner co-path entries back into their usize equivalents /// Inserts corresponding digest into the LUT for memoisation. fn decode_inner_copath( From 861ee9c6caf1a3c595d6019ae07b4a880a30435a Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Mon, 8 Dec 2025 21:44:02 +0100 Subject: [PATCH 12/22] r1cs: fix r1cs compatibility of CoSet struct --- crypto-primitives/src/merkle_tree/mod.rs | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index d0451f35..a64716b7 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -641,25 +641,24 @@ impl CoPath

{ true } - // TODO: git commit changes and then consult this // The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. // `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. // // This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. - // #[allow(unused)] // this function is actually used when r1cs feature is on - // fn position_list(&'_ self) -> impl '_ + Iterator> { - // let path_len = self.auth_paths_suffixes[0].len(); - - // cfg_into_iter!(self.leaf_indexes.clone()) - // .map(move |i| { - // (0..path_len + 1) - // .map(move |j| ((i >> j) & 1) != 0) - // .rev() - // .collect() - // }) - // .collect::>() - // .into_iter() - // } + #[allow(unused)] // this function is actually used when r1cs feature is on + fn position_list(&'_ self) -> impl '_ + Iterator> { + let path_len = self.tree_height.saturating_sub(2); + + cfg_into_iter!(self.leaf_indexes.clone()) + .map(move |i| { + (0..path_len + 1) + .map(move |j| ((i >> j) & 1) != 0) + .rev() + .collect() + }) + .collect::>() + .into_iter() + } } /// `index` is the first `path.len()` bits of From 1b2b1c9d5783f65484979d1affeb0b30136fa051 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Tue, 16 Dec 2025 10:03:44 +0100 Subject: [PATCH 13/22] tests: add unit tests for the delta encoding to ensure correct savings --- crypto-primitives/src/merkle_tree/mod.rs | 4 +- .../merkle_tree/tests/delta_encoding_tests.rs | 151 ++++++++++++++++++ .../src/merkle_tree/tests/mod.rs | 104 ++++++++++++ 3 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index a64716b7..e85988cb 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -1161,10 +1161,10 @@ fn decode_delta(bytes: &[u8], cursor: &mut usize) -> Option { #[inline] fn encode_varint(buffer: &mut Vec, mut value: u64) { while value >= 0x80 { - buffer.push(((value as u8) & 0x7F) | 0x80); + buffer.push(((value as u8) & 0x7F) | 0x80); // MSB = 1 => more bytes follow value >>= 7; } - buffer.push(value as u8); + buffer.push(value as u8); // last byte, MSB = 0 } fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { diff --git a/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs b/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs new file mode 100644 index 00000000..23fe320d --- /dev/null +++ b/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs @@ -0,0 +1,151 @@ +use crate::crh::{CRHScheme, TwoToOneCRHScheme}; +use crate::merkle_tree::{ + decode_delta, decode_varint, encode_delta, encode_varint, CoPath, Config, DefaultHasher, + IdentityDigestConverter, +}; +use ark_std::{borrow::Borrow, collections::BTreeMap, hash::BuildHasherDefault}; +use hashbrown::HashMap; + +struct DummyCfg; +impl Config for DummyCfg { + type Leaf = (); + type LeafDigest = u8; + type LeafInnerDigestConverter = IdentityDigestConverter; + type InnerDigest = u8; + type LeafHash = DummyLeafHash; + type TwoToOneHash = DummyTwoToOne; +} + +struct DummyLeafHash; +impl CRHScheme for DummyLeafHash { + type Input = (); + type Output = u8; + type Parameters = (); + + fn setup(_rng: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + _parameters: &Self::Parameters, + _input: T, + ) -> Result { + Ok(0) + } +} + +struct DummyTwoToOne; +impl TwoToOneCRHScheme for DummyTwoToOne { + type Input = u8; + type Output = u8; + type Parameters = (); + + fn setup(_rng: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + _parameters: &Self::Parameters, + left: T, + right: T, + ) -> Result { + Ok(*left.borrow() ^ *right.borrow()) + } + + fn compress>( + _parameters: &Self::Parameters, + left: T, + right: T, + ) -> Result { + Ok(*left.borrow() ^ *right.borrow()) + } +} + +#[test] +fn varint_roundtrips() { + let samples = [ + 0u64, + 1, + 2, + 42, + 127, + 128, + 255, + 256, + 10_000, + u32::MAX as u64, + u64::MAX / 2, + ]; + + for &value in &samples { + let mut buf = Vec::new(); + encode_varint(&mut buf, value); + let mut cursor = 0usize; + let decoded = decode_varint(&buf, &mut cursor).expect("must decode"); + assert_eq!(decoded, value); + assert_eq!(cursor, buf.len(), "cursor should advance to end"); + } +} + +#[test] +fn delta_roundtrips() { + let samples = [ + 0i64, + 1, + -1, + 5, + -7, + 127, + -128, + 256, + -256, + i32::MAX as i64, + i32::MIN as i64, + ]; + + for &value in &samples { + let mut buf = Vec::new(); + encode_delta(&mut buf, value); + let mut cursor = 0usize; + let decoded = decode_delta(&buf, &mut cursor).expect("must decode"); + assert_eq!(decoded, value); + assert_eq!(cursor, buf.len(), "cursor should advance to end"); + } +} + +#[test] +fn decode_rejects_tampered_deltas() { + let entries: Vec<(usize, usize, u8)> = vec![(1, 0, 10), (1, 1, 20), (2, 2, 30)]; + let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); + + // Tamper with deltas: flip a bit. + let mut tampered = packed.clone(); + tampered.2[0] ^= 0b0000_0001; + + let mut inner_levels: Vec> = (0..4).map(|_| BTreeMap::new()).collect(); + let mut lut = HashMap::with_hasher(BuildHasherDefault::::default()); + let ok = + CoPath::::decode_inner_copath(4, &Some(tampered), &mut inner_levels, &mut lut); + assert!(!ok, "tampered deltas should be rejected"); +} + +#[test] +fn pack_decode_roundtrip_preserves_entries() { + let entries: Vec<(usize, usize, u8)> = vec![(1, 0, 10), (1, 1, 20), (2, 2, 30)]; + let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); + + let mut inner_levels: Vec> = (0..4).map(|_| BTreeMap::new()).collect(); + let mut lut = HashMap::with_hasher(BuildHasherDefault::::default()); + let ok = CoPath::::decode_inner_copath(4, &Some(packed), &mut inner_levels, &mut lut); + assert!(ok, "packed/decoded should succeed"); + + for &(depth, idx, ref digest) in &entries { + assert_eq!( + inner_levels[depth].get(&idx), + Some(digest), + "entry at depth {}, idx {} should roundtrip", + depth, + idx + ); + } +} diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index a40c1326..fc13e2e3 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "constraints")] mod constraints; mod test_utils; +mod delta_encoding_tests; #[cfg(all(test, feature = "bench_harness"))] mod bench_report; @@ -439,3 +440,106 @@ mod field_mt_tests { } } + +mod delta_encoding_spacing_tests { + use super::super::{decode_delta, CoPath, Config, IdentityDigestConverter, CRHScheme, TwoToOneCRHScheme}; + use ark_std::borrow::Borrow; + + struct DummyCfg; + impl Config for DummyCfg { + type Leaf = (); + type LeafDigest = u8; + type LeafInnerDigestConverter = IdentityDigestConverter; + type InnerDigest = u8; + type LeafHash = DummyLeafHash; + type TwoToOneHash = DummyTwoToOne; + } + + struct DummyLeafHash; + impl CRHScheme for DummyLeafHash { + type Input = (); + type Output = u8; + type Parameters = (); + + fn setup(_rng: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + _parameters: &Self::Parameters, + _input: T, + ) -> Result { + Ok(0) + } + } + + struct DummyTwoToOne; + impl TwoToOneCRHScheme for DummyTwoToOne { + type Input = u8; + type Output = u8; + type Parameters = (); + + fn setup(_rng: &mut R) -> Result { + Ok(()) + } + + fn evaluate>( + _parameters: &Self::Parameters, + left: T, + right: T, + ) -> Result { + Ok(*left.borrow() ^ *right.borrow()) + } + + fn compress>( + _parameters: &Self::Parameters, + left: T, + right: T, + ) -> Result { + Ok(*left.borrow() ^ *right.borrow()) + } + } + + #[test] + fn packed_deltas_save_with_large_index_gaps() { + // Coordinate entries are sorted lexicographically by (depth, index), and deltas are taken + // between consecutive coordinates in this order (not relative to a global heap index). + // + // This test demonstrates the worst case spaced openings scenario at the leaf level, plus a + // higher-layer sibling at depth d-2. + let d: usize = 14; + let depth_inner = d - 2; + let depth_leaf = d - 1; + + let mid = 1usize << (d - 2); + let end = (1usize << (d - 1)) - 1; + + let entries: Vec<(usize, usize, u8)> = vec![ + (depth_inner, 0, 10), + (depth_leaf, 0, 20), + (depth_leaf, mid, 30), + (depth_leaf, end, 40), + ]; + + let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); + let (_start_depth, _start_index, deltas, _digests) = packed; // returns `deltas` which is a Vec. + + // The first step is from (d-2,0) -> (d-1,0): depth delta is +1, index delta is 0. + let mut cursor = 0usize; + let depth_delta = decode_delta(&deltas, &mut cursor).expect("depth delta decodes"); + let index_delta = decode_delta(&deltas, &mut cursor).expect("index delta decodes"); + assert_eq!(depth_delta, 1); + assert_eq!(index_delta, 0); + + // Sanity check that the packed coordinate encoding is smaller than storing every (depth,index) + // as a fixed-width pair of `usize`s. + let naive_coord_bytes = entries.len() * 2 * core::mem::size_of::(); + let packed_coord_bytes = 2 * core::mem::size_of::() + deltas.len(); + assert!( + packed_coord_bytes < naive_coord_bytes, + "expected packed coordinates to be smaller (packed={}, naive={})", + packed_coord_bytes, + naive_coord_bytes + ); + } +} From 8ea15a8a05ca7cfe195f61be6522da4d58013b81 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Sun, 8 Mar 2026 22:09:35 +0100 Subject: [PATCH 14/22] fix(overflow): enforce tree_height invariant at trust boundary CoPath::verify could be crashed by a prover-controlled tree_height of 0 or 1, causing usize underflow in debug mode and attacker OOB access. - Validate tree_height >= 2 in extended CanonicalDeserialize trait so malformed proofs are rejected before a CoPath value is constructed - Restrict tree_height to pub(crate) so external code cannot mutate it after deserialization, preserving the invariant throughout the value's lifetime - Add expected_tree_height parameter to CoPath::verify, supplied by the verifier rather than read from the proof -> prevent height confusion attacks --- crypto-primitives/src/merkle_tree/mod.rs | 57 +++++++++++++++++-- .../src/merkle_tree/tests/mod.rs | 22 +++---- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index e85988cb..99e92192 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -8,7 +8,7 @@ use crate::{ sponge::Absorb, Error, }; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate}; #[cfg(not(feature = "std"))] use ark_std::vec::Vec; use ark_std::{ @@ -245,7 +245,7 @@ impl Path

{ /// Thus, inner copath is `[(2,0,D), (1,1,C)]`. /// Intuitively, CoSet transmits only what's missing to recompute every parent on the shared union-of-paths. -#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] +#[derive(Derivative, CanonicalSerialize)] #[derivative( Clone(bound = "P: Config"), Debug(bound = "P: Config"), @@ -253,7 +253,7 @@ impl Path

{ )] pub struct CoPath { /// stores the height of the tree (>= 2) to drive CoSet decoding - pub tree_height: usize, + pub(crate) tree_height: usize, /// For leaf layer, stores co-path digests (B*_{d-1}) in ascending sibling index order pub leaf_copath: Vec, /// For inner layers, stores co-path entries packed as (start_depth, start_index, packed deltas, digests) @@ -262,21 +262,68 @@ pub struct CoPath { pub leaf_indexes: Vec, } +/// Supertrait for CanonicalDeserialize: +impl Valid for CoPath

{ + fn check(&self) -> Result<(), SerializationError> { + if self.tree_height < 2 { + return Err(SerializationError::InvalidData); + } + /// propagate each fields validity check to ensure the entire structure is valid + self.leaf_copath.check()?; + self.inner_copath.check()?; + self.leaf_indexes.check() + } +} + +impl CanonicalDeserialize for CoPath

{ + fn deserialize_with_mode( + mut reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let tree_height = usize::deserialize_with_mode(&mut reader, compress, validate)?; + let leaf_copath = + Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let inner_copath = + Option::>::deserialize_with_mode(&mut reader, compress, validate)?; + let leaf_indexes = + Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + if tree_height < 2 { + return Err(SerializationError::InvalidData); + } + Ok(CoPath { tree_height, leaf_copath, inner_copath, leaf_indexes }) + } +} + impl CoPath

{ /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. /// Note that the order of the leaves hashes should match the leaves respective indexes /// * `leaf_size`: leaf size in number of bytes /// - /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` + /// `expected_tree_height` must equal the height of the tree the proof was generated from; + /// the verifier supplies this value — it is not taken from the (prover-controlled) proof. pub fn verify + Clone>( &self, leaf_hash_params: &LeafParam

, two_to_one_params: &TwoToOneParam

, root_hash: &P::InnerDigest, + expected_tree_height: usize, leaves: impl IntoIterator, ) -> Result { if self.leaf_indexes.is_empty() { - return Ok(true) + return Err(crate::Error::GenericError( + "batch proof must contain at least one leaf index".into() + )); + } + + if self.tree_height < 2 { + return Err(crate::Error::GenericError( + "tree_height must be >= 2".into() + )); + } + + if self.tree_height != expected_tree_height { + return Ok(false); } let d = self.tree_height; diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index fc13e2e3..270b531c 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -68,7 +68,7 @@ mod bytes_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) .unwrap()); // test merkle tree update functionality @@ -93,7 +93,7 @@ mod bytes_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) .unwrap()); } @@ -172,6 +172,7 @@ mod bytes_mt_tests { &leaf_crh_params, &two_to_one_params, &tree.root(), + tree.height(), serialized_leaves.clone() ) .unwrap()); @@ -226,7 +227,7 @@ mod field_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) .unwrap()); { @@ -252,6 +253,7 @@ mod field_mt_tests { &leaf_crh_params, &two_to_one_params, &wrong_root, + tree.height(), leaves.clone() ) .unwrap()); @@ -279,7 +281,7 @@ mod field_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) .unwrap()); } @@ -316,7 +318,7 @@ mod field_mt_tests { let proof = tree.generate_multi_proof(Vec::::new()).unwrap(); assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, Vec::>::new()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), Vec::>::new()) .unwrap(), "empty batch proof should verify" ); @@ -344,7 +346,7 @@ mod field_mt_tests { assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) .unwrap(), "proof with duplicate input indices should verify after deduplication" ); @@ -371,7 +373,7 @@ mod field_mt_tests { .collect(); let ok = bad - .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) .unwrap(); assert!(!ok, "tampered leaf_copath digest must fail verification"); } @@ -398,7 +400,7 @@ mod field_mt_tests { .map(|&i| leaves[i].clone()) .collect(); let ok = bad - .verify(&leaf_crh_params, &two_to_one_params, &root, opened) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) .unwrap(); assert!(!ok, "missing inner copath entry must invalidate the proof"); } @@ -423,7 +425,7 @@ mod field_mt_tests { .collect(); assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, ordered_leaves.clone()) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), ordered_leaves.clone()) .unwrap(), "proof should verify when leaves follow proof.leaf_indexes order" ); @@ -434,7 +436,7 @@ mod field_mt_tests { .map(|&i| leaves[i].clone()) .collect(); let ok = proof - .verify(&leaf_crh_params, &two_to_one_params, &root, shuffled_leaves) + .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), shuffled_leaves) .unwrap(); assert!(!ok, "mismatched leaf ordering must fail verification"); } From 600bfffdae874199c7af8c36f19e62f1ffac57ed Mon Sep 17 00:00:00 2001 From: ajhavlin <116843349+ajhavlin@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:07:46 +0100 Subject: [PATCH 15/22] Add presentation scripts, coordinate encoding benchmarks, and ignore figures - Add scripts/ for presentation plot generation - Extend bench_report with coordinate encoding size benchmarks (natural vs leb128) - Fix error types in CoPath verification to use ark_std::io::Error - Apply rustfmt to mod.rs and tests - Add figures/ to .gitignore Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +- crypto-primitives/src/merkle_tree/mod.rs | 93 +-- .../src/merkle_tree/tests/bench_report.rs | 714 +++++++++++++++++- .../src/merkle_tree/tests/mod.rs | 130 +++- scripts/build_results_pack.py | 495 ++++++++++++ scripts/presentation_coord_encoding_plots.py | 436 +++++++++++ scripts/presentation_plots.py | 560 ++++++++++++++ 7 files changed, 2337 insertions(+), 94 deletions(-) create mode 100644 scripts/build_results_pack.py create mode 100644 scripts/presentation_coord_encoding_plots.py create mode 100644 scripts/presentation_plots.py diff --git a/.gitignore b/.gitignore index 9d1f1106..1ce769b8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ Cargo.lock params *.swp *.swo -.vscode \ No newline at end of file +.vscode +figures/ \ No newline at end of file diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 99e92192..56aff343 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -8,12 +8,14 @@ use crate::{ sponge::Absorb, Error, }; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate}; +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, +}; #[cfg(not(feature = "std"))] use ark_std::vec::Vec; use ark_std::{ borrow::Borrow, - collections::{BTreeSet, BTreeMap}, + collections::{BTreeMap, BTreeSet}, fmt::Debug, hash::{BuildHasherDefault, Hash}, }; @@ -235,9 +237,9 @@ impl Path

{ /// `leaf_copath`: `[]` /// `inner_copath`: `[(2,0,D), (1,1,C)]` (store packed as `(2,0,[1,+1],[D,C])`) /// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) -/// +/// /// We can reconstruct upfront the minimal copath needed for the proof: -/// First, we reconstruct the minimal copath at the leaf layer (`depth = tree_height-1`). +/// First, we reconstruct the minimal copath at the leaf layer (`depth = tree_height-1`). /// This is only those sibling leaf digests that are required to complete parents of on-path leaves but are not themselves on-path. /// The leaf copath is thus `[J,I]/[I,J]=[]`. /// We then repeat this for each inner layer, computing only the non-on-path siblings needed to complete parents of the union of all single paths. @@ -268,7 +270,7 @@ impl Valid for CoPath

{ if self.tree_height < 2 { return Err(SerializationError::InvalidData); } - /// propagate each fields validity check to ensure the entire structure is valid + // Propagate field checks to ensure the whole structure is valid. self.leaf_copath.check()?; self.inner_copath.check()?; self.leaf_indexes.check() @@ -286,12 +288,16 @@ impl CanonicalDeserialize for CoPath

{ Vec::::deserialize_with_mode(&mut reader, compress, validate)?; let inner_copath = Option::>::deserialize_with_mode(&mut reader, compress, validate)?; - let leaf_indexes = - Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let leaf_indexes = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; if tree_height < 2 { return Err(SerializationError::InvalidData); } - Ok(CoPath { tree_height, leaf_copath, inner_copath, leaf_indexes }) + Ok(CoPath { + tree_height, + leaf_copath, + inner_copath, + leaf_indexes, + }) } } @@ -311,15 +317,21 @@ impl CoPath

{ leaves: impl IntoIterator, ) -> Result { if self.leaf_indexes.is_empty() { - return Err(crate::Error::GenericError( - "batch proof must contain at least one leaf index".into() - )); + return Err(crate::Error::GenericError(ark_std::boxed::Box::new( + ark_std::io::Error::new( + ark_std::io::ErrorKind::InvalidInput, + "batch proof must contain at least one leaf index", + ), + ))); } if self.tree_height < 2 { - return Err(crate::Error::GenericError( - "tree_height must be >= 2".into() - )); + return Err(crate::Error::GenericError(ark_std::boxed::Box::new( + ark_std::io::Error::new( + ark_std::io::ErrorKind::InvalidInput, + "tree_height must be >= 2", + ), + ))); } if self.tree_height != expected_tree_height { @@ -343,7 +355,7 @@ impl CoPath

{ if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { return Ok(false); } - + // prepare inner-level maps for non-on-path siblings and computed parents let mut inner_levels: Vec> = (0..d).map(|_| BTreeMap::new()).collect(); @@ -385,11 +397,11 @@ impl CoPath

{ } } - /// Encodes the inner co-path entries [(depth, index, digest), ...] as compact delta encodings. + /// Encodes the inner co-path entries [(depth, index, digest), ...] as compact delta encodings. /// Keeps the first (depth, index) entry, then zigzag encodes signed deltas of subsequent paris, - /// collecting the corresponding digests in order. + /// collecting the corresponding digests in order. /// Result is a tuple (start_depth: usize, start_index: usize, deltas: Vec, digests: Vec<

::InnerDigest>) - /// + /// /// For example: /// ```tree_diagram /// [A] d = 0 @@ -400,9 +412,9 @@ impl CoPath

{ /// / \ / \ / \ / \ /// H I [J] K L [M] N O d = 3 /// / \ / \ / \ / \ / \ / \ / \ / \ - /// .... 4 5 6 7 8 9 10 11 .... d = 4 + /// .... 4 5 6 7 8 9 10 11 .... d = 4 /// ``` - /// + /// /// Suppose we want to prove the following openings: /// ```text /// I = {6, 8} @@ -418,13 +430,13 @@ impl CoPath

{ /// * `6` inner-layer digests (`inner_copath`), /// /// We keep `leaf_copath` as-is but instead of storing all 6 `(depth, index)` pairs explicitly, we store: - /// + /// /// * a starting coordinate: /// ```text /// start_depth = 1 /// start_index = 0 /// ``` - /// + /// /// * followed by signed deltas between consecutive coordinates: /// ```text /// (Δd, Δi) sequence: @@ -438,7 +450,7 @@ impl CoPath

{ /// Each `(Δd, Δi)` is encoded to unsigned and then varint-encoded. All of these /// deltas are very small (−1, 0, +1, +3), so each encoded value fits in a /// single byte. On a 64-bit platform: - /// + /// /// * naive coordinate encoding for 6 entries as `(depth: usize, index: usize)` /// uses roughly `6 × 2 × 8 = 96` bytes, /// * the packed representation uses: @@ -576,11 +588,7 @@ impl CoPath

{ (Some(left), Some(right)) => (left, right), _ => return Ok(false), }; - let parent = P::TwoToOneHash::compress( - two_to_one_params, - &left, - &right, - )?; + let parent = P::TwoToOneHash::compress(two_to_one_params, &left, &right)?; inner_levels[parent_depth].insert(parent_index, parent.clone()); // add parent to LUT at heap index let heap_idx = level_index(parent_depth, parent_index); @@ -591,7 +599,7 @@ impl CoPath

{ } /// Decodes inner co-path entries back into their usize equivalents - /// Inserts corresponding digest into the LUT for memoisation. + /// Inserts corresponding digest into the LUT for memoisation. fn decode_inner_copath( tree_height: usize, inner_copath: &Option>, @@ -599,7 +607,7 @@ impl CoPath

{ hash_lut: &mut HashMap>, ) -> bool { if let Some((start_depth, start_index, deltas, digests)) = inner_copath { - // verifier rejects if remaining deltas with empty digests + // verifier rejects if remaining deltas with empty digests if digests.is_empty() { return deltas.is_empty(); } @@ -616,7 +624,7 @@ impl CoPath

{ let mut cursor = 0usize; let mut prev_coord: Option<(usize, usize)> = None; - // Helper to insert digest into the LUT + // Helper to insert digest into the LUT let mut push_entry = |depth_i64: i64, index_i64: i64, digest: &P::InnerDigest| -> bool { let depth_usize = match usize::try_from(depth_i64) { @@ -636,7 +644,7 @@ impl CoPath

{ return false; } } - // check for conflicting siblings at the same coordinate + // check for conflicting siblings at the same coordinate if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { if existing != digest { return false; @@ -655,7 +663,7 @@ impl CoPath

{ return false; } - // accumulate remaining digests + // accumulate remaining digests for digest in digests.iter().skip(1) { let depth_delta = match decode_delta(deltas, &mut cursor) { Some(delta) => delta, @@ -936,21 +944,21 @@ impl MerkleTree

{ }) } - /// Returns a CoPath struct (a compressed membership proof for a set of leaves), + /// Returns a CoPath struct (a compressed membership proof for a set of leaves), /// sufficient to verify each leaf up to the root. /// Indexes are internally sorted and emitted in this order. - /// + /// /// With the CoSet (minimal co-path) encoding, we do not store full per-leaf authentication paths. - /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. + /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. /// This yields a smaller proof than front-incremental prefix encoding in the typical case, /// while preserving the same verification interface. - /// + /// /// For sorted indexes, the CoSet proof carries: /// * `tree_height`; /// * `leaf_indexes` (ascending, unique); /// * `leaf_copath`: the leaf-layer co-path digests `B*_{d-1}`, in ascending sibling index order; /// * `inner_copath`: the inner co-path packed as `(start_depth, start_index, packed deltas, digests)`. - /// + /// /// When verifying the proof, leaves hashes should be supplied in order of `leaf_indexes`, that is: /// ```text /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); @@ -1006,10 +1014,9 @@ impl MerkleTree

{ let sibling_idx = path_idx ^ 1; if on_path[depth].binary_search(&sibling_idx).is_err() { let heap_idx = level_index(depth, sibling_idx); - let sibling_digest = self - .non_leaf_nodes - .get(heap_idx) - .ok_or_else(|| crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()))?; + let sibling_digest = self.non_leaf_nodes.get(heap_idx).ok_or_else(|| { + crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()) + })?; inner_copath_entries.push((depth, sibling_idx, sibling_digest.clone())); } } @@ -1236,7 +1243,7 @@ fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { /// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. /// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. -/// +/// /// Implementation detail: /// * Uses sorted `Vec` per level to keep the hot loops linear and cache-friendly. /// * Each leaf contributes one index per depth; we divide by 2 as we walk up and then sort+dedup. diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs index 6e81f3ce..999fc420 100644 --- a/crypto-primitives/src/merkle_tree/tests/bench_report.rs +++ b/crypto-primitives/src/merkle_tree/tests/bench_report.rs @@ -1,5 +1,6 @@ #![cfg(feature = "bench_harness")] +use super::super::{decode_delta, encode_varint}; use crate::merkle_tree::{ legacy, tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, LeafParam, MerkleTree, TwoToOneParam, @@ -51,6 +52,22 @@ type LegacyTwoToOneParam = legacy::TwoToOneParam; const TREE_EXPONENTS: &[u32] = &[12, 14, 16, 18, 20]; const BATCH_SIZES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; const LEAF_WIDTH: usize = 3; +const COORD_TREE_EXPONENTS: &[u32] = &[18]; + +#[derive(Clone, Copy)] +enum CoordinateEncoding { + Natural, + Leb128Index, +} + +impl CoordinateEncoding { + fn label(self) -> &'static str { + match self { + CoordinateEncoding::Natural => "natural", + CoordinateEncoding::Leb128Index => "leb128", + } + } +} #[derive(Clone, Copy)] enum IndexPattern { @@ -108,6 +125,22 @@ struct PlotMetric { value: fn(&ReportRow) -> Option, } +struct CoordinateSizeRow { + tree_size: usize, + log2_size: u32, + batch: usize, + pattern: &'static str, + strategy: &'static str, + proof_bytes: usize, + proof_nodes: usize, +} + +struct InnerEntry<'a, D> { + depth: usize, + index: usize, + digest: &'a D, +} + trait ProofStats { fn opened(&self) -> usize; fn total_nodes(&self) -> usize; @@ -119,7 +152,11 @@ impl ProofStats for CoPath

{ } fn total_nodes(&self) -> usize { - let inner = self.inner_copath.as_ref().map(|(_, _, _, digests)| digests.len()).unwrap_or(0); + let inner = self + .inner_copath + .as_ref() + .map(|(_, _, _, digests)| digests.len()) + .unwrap_or(0); self.leaf_copath.len() + inner } } @@ -130,11 +167,7 @@ impl ProofStats for legacy::MultiPath

{ } fn total_nodes(&self) -> usize { - let auth_len: usize = self - .auth_paths_suffixes - .iter() - .map(|path| path.len()) - .sum(); + let auth_len: usize = self.auth_paths_suffixes.iter().map(|path| path.len()).sum(); self.leaf_siblings_hashes.len() + auth_len } } @@ -166,6 +199,18 @@ fn multiproof_v2_benchmark_report() { run_report().expect("benchmark report must succeed"); } +#[test] +#[ignore] +fn coordinate_encoding_proof_size_report() { + run_coordinate_report().expect("coordinate encoding report must succeed"); +} + +#[test] +#[ignore] +fn leb128_vs_delta_proof_size_report() { + run_leb128_vs_delta_report().expect("leb128 vs delta report must succeed"); +} + fn run_report() -> Result<(), Box> { let mut fixtures = Vec::new(); for &exp in TREE_EXPONENTS { @@ -205,6 +250,115 @@ fn run_report() -> Result<(), Box> { Ok(()) } +fn run_coordinate_report() -> Result<(), Box> { + let mut fixtures = Vec::new(); + for &exp in COORD_TREE_EXPONENTS { + fixtures.push(build_fixture(exp)?); + } + + let patterns = [IndexPattern::Random, IndexPattern::Clustered]; + let encodings = [CoordinateEncoding::Natural, CoordinateEncoding::Leb128Index]; + + let mut rows = Vec::new(); + for fixture in fixtures.iter() { + for &batch in BATCH_SIZES { + if batch > fixture.leaves.len() { + continue; + } + for &pattern in &patterns { + let mut scenario_rng = StdRng::seed_from_u64( + 0xC0DE_1280_u64 + ^ ((fixture.leaves.len() as u64) << 16) + ^ ((batch as u64) << 2) + ^ pattern.id(), + ); + let indexes = + sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); + let proof = fixture.tree.generate_multi_proof(indexes.iter().copied())?; + let proof_nodes = proof.total_nodes(); + + for &encoding in &encodings { + rows.push(CoordinateSizeRow { + tree_size: fixture.leaves.len(), + log2_size: fixture.log2_size(), + batch, + pattern: pattern.label(), + strategy: encoding.label(), + proof_bytes: serialized_coordinate_proof_size(&proof, encoding), + proof_nodes, + }); + } + } + } + } + + let report_dir = PathBuf::from("target/merkle_tree_reports"); + fs::create_dir_all(&report_dir)?; + let plot_files = write_coordinate_plots(&rows, &report_dir)?; + write_coordinate_report(&rows, &report_dir, &plot_files)?; + write_coordinate_rows_csv(&rows, &report_dir)?; + Ok(()) +} + +fn run_leb128_vs_delta_report() -> Result<(), Box> { + let mut fixtures = Vec::new(); + for &exp in COORD_TREE_EXPONENTS { + fixtures.push(build_fixture(exp)?); + } + + let patterns = [IndexPattern::Random, IndexPattern::Clustered]; + let mut rows = Vec::new(); + + for fixture in fixtures.iter() { + for &batch in BATCH_SIZES { + if batch > fixture.leaves.len() { + continue; + } + for &pattern in &patterns { + let mut scenario_rng = StdRng::seed_from_u64( + 0xD37A_1280_u64 + ^ ((fixture.leaves.len() as u64) << 16) + ^ ((batch as u64) << 2) + ^ pattern.id(), + ); + let indexes = + sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); + let proof = fixture.tree.generate_multi_proof(indexes.iter().copied())?; + let proof_nodes = proof.total_nodes(); + + rows.push(CoordinateSizeRow { + tree_size: fixture.leaves.len(), + log2_size: fixture.log2_size(), + batch, + pattern: pattern.label(), + strategy: "leb128", + proof_bytes: serialized_coordinate_proof_size( + &proof, + CoordinateEncoding::Leb128Index, + ), + proof_nodes, + }); + rows.push(CoordinateSizeRow { + tree_size: fixture.leaves.len(), + log2_size: fixture.log2_size(), + batch, + pattern: pattern.label(), + strategy: "delta", + proof_bytes: serialized_size(&proof), + proof_nodes, + }); + } + } + } + + let report_dir = PathBuf::from("target/merkle_tree_reports"); + fs::create_dir_all(&report_dir)?; + let plot_files = write_leb128_vs_delta_plots(&rows, &report_dir)?; + write_leb128_vs_delta_report(&rows, &report_dir, &plot_files)?; + write_leb128_vs_delta_rows_csv(&rows, &report_dir)?; + Ok(()) +} + fn run_scenario( fixture: &TreeFixture, batch: usize, @@ -217,7 +371,11 @@ fn run_scenario( let legacy_row = benchmark_strategy( "prefix", - || fixture.legacy_tree.generate_multi_proof(indexes.iter().copied()), + || { + fixture + .legacy_tree + .generate_multi_proof(indexes.iter().copied()) + }, |proof: &legacy::MultiPath<_>, leaves| { proof.verify( &fixture.legacy_leaf_params, @@ -236,12 +394,15 @@ fn run_scenario( let coset_row = benchmark_strategy( "coset", || fixture.tree.generate_multi_proof(indexes.iter().copied()), - |proof: &CoPath<_>, leaves| proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - leaves, - ), + |proof: &CoPath<_>, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + fixture.tree.height(), + leaves, + ) + }, &opened_leaves, fixture.leaves.len(), fixture.log2_size(), @@ -419,13 +580,168 @@ fn write_report( } writeln!(file, "\n## {} Visualizations\n", metric)?; for plot in files { - writeln!(file, "![{}]({})", metric.replace(' ', "-").to_lowercase(), plot)?; + writeln!( + file, + "![{}]({})", + metric.replace(' ', "-").to_lowercase(), + plot + )?; } } Ok(()) } +fn write_coordinate_report( + rows: &[CoordinateSizeRow], + report_dir: &Path, + plot_files: &[String], +) -> Result<(), Box> { + let report_path = report_dir.join("coordinate_encoding_report.md"); + let mut file = File::create(&report_path)?; + + writeln!(file, "# Merkle Tree Proof Size Comparison")?; + writeln!( + file, + "\nGenerated: {:?}\n", + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? + )?; + writeln!( + file, + "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes |" + )?; + writeln!( + file, + "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- |" + )?; + + for row in rows { + writeln!( + file, + "| {} | {} | {} | {} | {} | {} | {} |", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + )?; + } + + if !plot_files.is_empty() { + writeln!(file, "\n## Figures\n")?; + for plot in plot_files { + writeln!(file, "![coordinate-proof-size]({})", plot)?; + } + } + + Ok(()) +} + +fn write_coordinate_rows_csv( + rows: &[CoordinateSizeRow], + report_dir: &Path, +) -> Result<(), Box> { + let csv_path = report_dir.join("coordinate_encoding_rows.csv"); + let mut file = File::create(&csv_path)?; + writeln!( + file, + "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes" + )?; + + for row in rows { + writeln!( + file, + "{},{},{},{},{},{},{}", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + )?; + } + + Ok(()) +} + +fn write_leb128_vs_delta_report( + rows: &[CoordinateSizeRow], + report_dir: &Path, + plot_files: &[String], +) -> Result<(), Box> { + let report_path = report_dir.join("leb128_vs_delta_report.md"); + let mut file = File::create(&report_path)?; + + writeln!(file, "# Merkle Tree Proof Size Comparison")?; + writeln!( + file, + "\nGenerated: {:?}\n", + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? + )?; + writeln!( + file, + "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes |" + )?; + writeln!( + file, + "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- |" + )?; + + for row in rows { + writeln!( + file, + "| {} | {} | {} | {} | {} | {} | {} |", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + )?; + } + + if !plot_files.is_empty() { + writeln!(file, "\n## Figures\n")?; + for plot in plot_files { + writeln!(file, "![leb128-vs-delta]({})", plot)?; + } + } + + Ok(()) +} + +fn write_leb128_vs_delta_rows_csv( + rows: &[CoordinateSizeRow], + report_dir: &Path, +) -> Result<(), Box> { + let csv_path = report_dir.join("leb128_vs_delta_rows.csv"); + let mut file = File::create(&csv_path)?; + writeln!( + file, + "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes" + )?; + + for row in rows { + writeln!( + file, + "{},{},{},{},{},{},{}", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + )?; + } + + Ok(()) +} + fn write_plots( rows: &[ReportRow], report_dir: &Path, @@ -460,8 +776,7 @@ fn write_plots( } } - if ordered_series.len() < 2 - || !ordered_series.iter().any(|(name, _)| *name == "prefix") + if ordered_series.len() < 2 || !ordered_series.iter().any(|(name, _)| *name == "prefix") { continue; } @@ -486,10 +801,7 @@ fn write_plots( let x_pad = ((max_x - min_x) * 0.05).max(1.0); let y_pad = ((max_y - min_y) * 0.05).max(1.0); - let filename = format!( - "{}_{}_{}.svg", - metric.filename_prefix, tree_size, pattern - ); + let filename = format!("{}_{}_{}.svg", metric.filename_prefix, tree_size, pattern); let filepath = report_dir.join(&filename); let filepath_str = filepath.to_string_lossy().to_string(); let drawing_area = SVGBackend::new(&filepath_str, (960, 540)).into_drawing_area(); @@ -497,8 +809,11 @@ fn write_plots( let mut chart = ChartBuilder::on(&drawing_area) .caption( - format!("{} vs k (n={}, pattern={})", metric.name, tree_size, pattern), - ("sans-serif", 26), + format!( + "{} vs k (n={}, pattern={})", + metric.name, tree_size, pattern + ), + ("Helvetica Neue", 26).into_font().style(FontStyle::Bold), ) .margin(20) .x_label_area_size(45) @@ -525,7 +840,10 @@ fn write_plots( }); } - chart.configure_series_labels().border_style(&BLACK).draw()?; + chart + .configure_series_labels() + .border_style(&BLACK) + .draw()?; generated.push(filename); } @@ -536,6 +854,220 @@ fn write_plots( Ok(outputs) } +fn write_coordinate_plots( + rows: &[CoordinateSizeRow], + report_dir: &Path, +) -> Result, Box> { + let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = + BTreeMap::new(); + + for row in rows { + grouped + .entry((row.tree_size, row.pattern)) + .or_default() + .entry(row.strategy) + .or_default() + .push((row.batch as f64, row.proof_bytes as f64)); + } + + let mut generated = Vec::new(); + for ((tree_size, pattern), strategies) in grouped { + let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); + for &name in &["natural", "leb128"] { + if let Some(mut series) = strategies.get(name).cloned() { + if series.is_empty() { + continue; + } + series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + ordered_series.push((name, series)); + } + } + + if ordered_series.len() < 2 { + continue; + } + + let mut min_x = f64::MAX; + let mut max_x = f64::MIN; + let mut min_y = f64::MAX; + let mut max_y = f64::MIN; + for (_, series) in &ordered_series { + for &(x, y) in series { + min_x = min_x.min(x); + max_x = max_x.max(x); + min_y = min_y.min(y); + max_y = max_y.max(y); + } + } + if min_x == f64::MAX || min_y == f64::MAX { + continue; + } + + let x_pad = ((max_x - min_x) * 0.06).max(8.0); + let y_pad = ((max_y - min_y) * 0.10).max(256.0); + + let filename = format!("coordinate_proof_size_{}_{}.svg", tree_size, pattern); + let filepath = report_dir.join(&filename); + let filepath_str = filepath.to_string_lossy().to_string(); + let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); + drawing_area.fill(&WHITE)?; + + let mut chart = ChartBuilder::on(&drawing_area) + .margin(28) + .x_label_area_size(64) + .y_label_area_size(96) + .build_cartesian_2d( + (min_x - x_pad)..(max_x + x_pad), + (min_y - y_pad)..(max_y + y_pad), + )?; + + chart + .configure_mesh() + .x_desc("input size k (opened leaves)") + .y_desc("proof size (bytes)") + .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) + .label_style(("Helvetica Neue", 18).into_font()) + .light_line_style(WHITE.mix(0.0)) + .draw()?; + + for (name, series) in &ordered_series { + let color = coordinate_strategy_color(name); + chart + .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? + .label(coordinate_strategy_label(name)) + .legend({ + let color = color.clone(); + move |(x, y)| PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) + }); + + chart.draw_series( + series + .iter() + .map(|point| Circle::new(*point, 5, color.filled())), + )?; + } + + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperLeft) + .background_style(WHITE.mix(0.85)) + .border_style(BLACK) + .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) + .draw()?; + + generated.push(filename); + } + + Ok(generated) +} + +fn write_leb128_vs_delta_plots( + rows: &[CoordinateSizeRow], + report_dir: &Path, +) -> Result, Box> { + let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = + BTreeMap::new(); + + for row in rows { + grouped + .entry((row.tree_size, row.pattern)) + .or_default() + .entry(row.strategy) + .or_default() + .push((row.batch as f64, row.proof_bytes as f64)); + } + + let mut generated = Vec::new(); + for ((tree_size, pattern), strategies) in grouped { + let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); + for &name in &["leb128", "delta"] { + if let Some(mut series) = strategies.get(name).cloned() { + if series.is_empty() { + continue; + } + series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + ordered_series.push((name, series)); + } + } + + if ordered_series.len() < 2 { + continue; + } + + let mut min_x = f64::MAX; + let mut max_x = f64::MIN; + let mut min_y = f64::MAX; + let mut max_y = f64::MIN; + for (_, series) in &ordered_series { + for &(x, y) in series { + min_x = min_x.min(x); + max_x = max_x.max(x); + min_y = min_y.min(y); + max_y = max_y.max(y); + } + } + if min_x == f64::MAX || min_y == f64::MAX { + continue; + } + + let x_pad = ((max_x - min_x) * 0.06).max(8.0); + let y_pad = ((max_y - min_y) * 0.10).max(256.0); + + let filename = format!("leb128_vs_delta_proof_size_{}_{}.svg", tree_size, pattern); + let filepath = report_dir.join(&filename); + let filepath_str = filepath.to_string_lossy().to_string(); + let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); + drawing_area.fill(&WHITE)?; + + let mut chart = ChartBuilder::on(&drawing_area) + .margin(28) + .x_label_area_size(64) + .y_label_area_size(96) + .build_cartesian_2d( + (min_x - x_pad)..(max_x + x_pad), + (min_y - y_pad)..(max_y + y_pad), + )?; + + chart + .configure_mesh() + .x_desc("input size k (opened leaves)") + .y_desc("proof size (bytes)") + .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) + .label_style(("Helvetica Neue", 18).into_font()) + .light_line_style(WHITE.mix(0.0)) + .draw()?; + + for (name, series) in &ordered_series { + let color = leb128_vs_delta_color(name); + chart + .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? + .label(leb128_vs_delta_label(name)) + .legend({ + let color = color.clone(); + move |(x, y)| PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) + }); + + chart.draw_series( + series + .iter() + .map(|point| Circle::new(*point, 5, color.filled())), + )?; + } + + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperLeft) + .background_style(WHITE.mix(0.85)) + .border_style(BLACK) + .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) + .draw()?; + + generated.push(filename); + } + + Ok(generated) +} + fn strategy_label(name: &str) -> &str { match name { "prefix" => "prefix", @@ -544,6 +1076,38 @@ fn strategy_label(name: &str) -> &str { } } +fn coordinate_strategy_label(name: &str) -> &str { + match name { + "natural" => "Unoptimized", + "leb128" => "Partially Optimized", + _ => name, + } +} + +fn coordinate_strategy_color(name: &str) -> RGBColor { + match name { + "natural" => RGBColor(217, 95, 2), + "leb128" => RGBColor(27, 158, 119), + _ => BLACK, + } +} + +fn leb128_vs_delta_label(name: &str) -> &str { + match name { + "leb128" => "Partially Optimized", + "delta" => "Optimized", + _ => name, + } +} + +fn leb128_vs_delta_color(name: &str) -> RGBColor { + match name { + "leb128" => RGBColor(27, 158, 119), + "delta" => RGBColor(117, 112, 179), + _ => BLACK, + } +} + fn strategy_color(name: &str) -> RGBColor { match name { "prefix" => RED, @@ -596,6 +1160,110 @@ fn serialized_size(value: &T) -> usize { buf.len() } +fn serialized_coordinate_proof_size( + proof: &CoPath

, + encoding: CoordinateEncoding, +) -> usize { + let mut buf = Vec::new(); + proof + .tree_height + .serialize_uncompressed(&mut buf) + .expect("tree height serialization must succeed"); + proof + .leaf_copath + .serialize_uncompressed(&mut buf) + .expect("leaf co-path serialization must succeed"); + serialize_inner_copath_absolute(&mut buf, proof, encoding); + proof + .leaf_indexes + .serialize_uncompressed(&mut buf) + .expect("leaf indexes serialization must succeed"); + buf.len() +} + +fn serialize_inner_copath_absolute( + buf: &mut Vec, + proof: &CoPath

, + encoding: CoordinateEncoding, +) { + let entries = unpack_inner_entries(proof); + (!entries.is_empty()) + .serialize_uncompressed(&mut *buf) + .expect("option tag serialization must succeed"); + if entries.is_empty() { + return; + } + + entries + .len() + .serialize_uncompressed(&mut *buf) + .expect("coordinate count serialization must succeed"); + for entry in &entries { + entry + .depth + .serialize_uncompressed(&mut *buf) + .expect("depth serialization must succeed"); + match encoding { + CoordinateEncoding::Natural => entry + .index + .serialize_uncompressed(&mut *buf) + .expect("index serialization must succeed"), + CoordinateEncoding::Leb128Index => { + encode_varint(buf, entry.index as u64); + } + } + } + + entries + .len() + .serialize_uncompressed(&mut *buf) + .expect("digest count serialization must succeed"); + for entry in entries { + entry + .digest + .serialize_uncompressed(&mut *buf) + .expect("digest serialization must succeed"); + } +} + +fn unpack_inner_entries<'a, P: Config>( + proof: &'a CoPath

, +) -> Vec> { + let Some((start_depth, start_index, deltas, digests)) = proof.inner_copath.as_ref() else { + return Vec::new(); + }; + if digests.is_empty() { + return Vec::new(); + } + + let mut entries = Vec::with_capacity(digests.len()); + let mut depth = *start_depth as i64; + let mut index = *start_index as i64; + entries.push(InnerEntry { + depth: *start_depth, + index: *start_index, + digest: &digests[0], + }); + + let mut cursor = 0usize; + for digest in digests.iter().skip(1) { + depth += decode_delta(deltas, &mut cursor).expect("packed depth delta must decode"); + index += decode_delta(deltas, &mut cursor).expect("packed index delta must decode"); + entries.push(InnerEntry { + depth: usize::try_from(depth).expect("depth must remain non-negative"), + index: usize::try_from(index).expect("index must remain non-negative"), + digest, + }); + } + + assert_eq!( + cursor, + deltas.len(), + "all packed coordinate bytes must be consumed" + ); + entries +} + fn duration_ms(duration: Duration) -> f64 { duration.as_secs_f64() * 1000.0 } diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index 270b531c..603d3986 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "constraints")] mod constraints; -mod test_utils; mod delta_encoding_tests; +mod test_utils; #[cfg(all(test, feature = "bench_harness"))] mod bench_report; @@ -68,7 +68,13 @@ mod bytes_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + leaves.clone() + ) .unwrap()); // test merkle tree update functionality @@ -93,7 +99,13 @@ mod bytes_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + leaves.clone() + ) .unwrap()); } @@ -186,7 +198,7 @@ mod field_mt_tests { tests::test_utils::poseidon_parameters, Config, IdentityDigestConverter, MerkleTree, }, }; - use ark_std::{test_rng, UniformRand, One}; + use ark_std::{test_rng, One, UniformRand}; type F = ark_ed_on_bls12_381::Fr; type H = poseidon::CRH; @@ -227,7 +239,13 @@ mod field_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + leaves.clone() + ) .unwrap()); { @@ -281,7 +299,13 @@ mod field_mt_tests { .unwrap(); assert!(multi_proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), leaves.clone()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + leaves.clone() + ) .unwrap()); } @@ -309,7 +333,9 @@ mod field_mt_tests { #[test] fn multiproof_empty_batch_verifies() { let mut rng = test_rng(); - let leaves: Vec> = (0..4).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaves: Vec> = (0..4) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); let leaf_crh_params = poseidon_parameters(); let two_to_one_params = leaf_crh_params.clone(); let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); @@ -318,7 +344,13 @@ mod field_mt_tests { let proof = tree.generate_multi_proof(Vec::::new()).unwrap(); assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), Vec::>::new()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + Vec::>::new() + ) .unwrap(), "empty batch proof should verify" ); @@ -328,7 +360,9 @@ mod field_mt_tests { #[test] fn multiproof_duplicate_indices_deduped() { let mut rng = test_rng(); - let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaves: Vec> = (0..8) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); let leaf_crh_params = poseidon_parameters(); let two_to_one_params = leaf_crh_params.clone(); let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); @@ -336,7 +370,11 @@ mod field_mt_tests { let indexes = vec![3usize, 1, 3, 1, 5]; let proof = tree.generate_multi_proof(indexes.clone()).unwrap(); - assert_eq!(proof.leaf_indexes, vec![1, 3, 5], "indexes should be sorted & deduped"); + assert_eq!( + proof.leaf_indexes, + vec![1, 3, 5], + "indexes should be sorted & deduped" + ); let opened: Vec<_> = proof .leaf_indexes @@ -346,7 +384,13 @@ mod field_mt_tests { assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + opened + ) .unwrap(), "proof with duplicate input indices should verify after deduplication" ); @@ -355,7 +399,9 @@ mod field_mt_tests { #[test] fn multiproof_wrong_leaf_copath_fails() { let mut rng = test_rng(); - let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaves: Vec> = (0..8) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); let leaf_crh_params = poseidon_parameters(); let two_to_one_params = leaf_crh_params.clone(); let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); @@ -373,7 +419,13 @@ mod field_mt_tests { .collect(); let ok = bad - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + opened, + ) .unwrap(); assert!(!ok, "tampered leaf_copath digest must fail verification"); } @@ -381,7 +433,9 @@ mod field_mt_tests { #[test] fn multiproof_missing_inner_entry_fails() { let mut rng = test_rng(); - let leaves: Vec> = (0..16).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaves: Vec> = (0..16) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); let leaf_crh_params = poseidon_parameters(); let two_to_one_params = leaf_crh_params.clone(); let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); @@ -400,7 +454,13 @@ mod field_mt_tests { .map(|&i| leaves[i].clone()) .collect(); let ok = bad - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), opened) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + opened, + ) .unwrap(); assert!(!ok, "missing inner copath entry must invalidate the proof"); } @@ -408,7 +468,9 @@ mod field_mt_tests { #[test] fn multiproof_open_order_robustness() { let mut rng = test_rng(); - let leaves: Vec> = (0..8).map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()).collect(); + let leaves: Vec> = (0..8) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); let leaf_crh_params = poseidon_parameters(); let two_to_one_params = leaf_crh_params.clone(); let tree = FieldMT::new(&leaf_crh_params, &two_to_one_params, &leaves).unwrap(); @@ -425,26 +487,36 @@ mod field_mt_tests { .collect(); assert!( proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), ordered_leaves.clone()) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + ordered_leaves.clone() + ) .unwrap(), "proof should verify when leaves follow proof.leaf_indexes order" ); // providing leaves in shuffled query order should fail - let shuffled_leaves: Vec<_> = indexes - .iter() - .map(|&i| leaves[i].clone()) - .collect(); + let shuffled_leaves: Vec<_> = indexes.iter().map(|&i| leaves[i].clone()).collect(); let ok = proof - .verify(&leaf_crh_params, &two_to_one_params, &root, tree.height(), shuffled_leaves) + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + shuffled_leaves, + ) .unwrap(); assert!(!ok, "mismatched leaf ordering must fail verification"); } - } mod delta_encoding_spacing_tests { - use super::super::{decode_delta, CoPath, Config, IdentityDigestConverter, CRHScheme, TwoToOneCRHScheme}; + use super::super::{ + decode_delta, CRHScheme, CoPath, Config, IdentityDigestConverter, TwoToOneCRHScheme, + }; use ark_std::borrow::Borrow; struct DummyCfg; @@ -463,7 +535,9 @@ mod delta_encoding_spacing_tests { type Output = u8; type Parameters = (); - fn setup(_rng: &mut R) -> Result { + fn setup( + _rng: &mut R, + ) -> Result { Ok(()) } @@ -481,7 +555,9 @@ mod delta_encoding_spacing_tests { type Output = u8; type Parameters = (); - fn setup(_rng: &mut R) -> Result { + fn setup( + _rng: &mut R, + ) -> Result { Ok(()) } @@ -508,7 +584,7 @@ mod delta_encoding_spacing_tests { // between consecutive coordinates in this order (not relative to a global heap index). // // This test demonstrates the worst case spaced openings scenario at the leaf level, plus a - // higher-layer sibling at depth d-2. + // higher-layer sibling at depth d-2. let d: usize = 14; let depth_inner = d - 2; let depth_leaf = d - 1; diff --git a/scripts/build_results_pack.py b/scripts/build_results_pack.py new file mode 100644 index 00000000..6d99aa7d --- /dev/null +++ b/scripts/build_results_pack.py @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 +"""Build a 7-slide results replacement pack (PPTX) from benchmark figure PNGs. + +Usage: + python3 scripts/build_results_pack.py \ + --fig-dir figures/presentation \ + --out /mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack.pptx \ + --title Results +""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, List, Sequence, Tuple + +from PIL import Image +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.enum.shapes import MSO_SHAPE +from pptx.enum.text import PP_ALIGN +from pptx.util import Emu, Pt + +EMU_PER_INCH = 914400 + +# 16:9 widescreen +SLIDE_W = Emu(int(13.333 * EMU_PER_INCH)) +SLIDE_H = Emu(int(7.5 * EMU_PER_INCH)) + +TOKENS = { + "accent": RGBColor(48, 93, 255), + "bg_dark": RGBColor(5, 14, 42), + "bg": RGBColor(248, 250, 252), + "text_dark": RGBColor(18, 25, 38), + "text_muted": RGBColor(88, 96, 112), + "card": RGBColor(241, 244, 249), + "card_border": RGBColor(219, 225, 236), + "sticky": RGBColor(255, 247, 214), + "sticky_border": RGBColor(234, 222, 172), + "progress_bg": RGBColor(234, 240, 255), +} + + +@dataclass(frozen=True) +class FigureSlide: + filename: str + title: str + subtitle: str + callout_header: str + callout_body: str + progress: str + + +FIGURE_SLIDES: Tuple[FigureSlide, ...] = ( + FigureSlide( + filename="proof_size_vs_k_clustered.png", + title="Proof Size - Clustered", + subtitle="One graph per slide. Shared y-scale with random for clean comparison.", + callout_header="🎯 Clustered wins clearly", + callout_body="Coset pruning removes duplicated siblings, giving the largest byte savings at high k.", + progress="Clustered 1/3", + ), + FigureSlide( + filename="prover_time_vs_k_clustered.png", + title="Prover Time - Clustered", + subtitle="Same visual frame to support narrative pacing.", + callout_header="⏱️ Prover overhead is controlled", + callout_body="Extraction/reconstruction costs stay modest versus structural proof-size gains.", + progress="Clustered 2/3", + ), + FigureSlide( + filename="verifier_time_vs_k_clustered.png", + title="Verifier Time - Clustered", + subtitle="Consistent scale and typography to reduce audience load.", + callout_header="⚖️ Hashing still dominates", + callout_body="Verifier trend remains hash-heavy; metadata optimizations have a smaller effect.", + progress="Clustered 3/3", + ), + FigureSlide( + filename="proof_size_vs_k_random.png", + title="Proof Size - Random", + subtitle="Now random queries under the same axis policy.", + callout_header="🧭 Savings persist in random", + callout_body="Overlap is lower than clustered, but coset still reduces proof bytes materially.", + progress="Random 1/3", + ), + FigureSlide( + filename="prover_time_vs_k_random.png", + title="Prover Time - Random", + subtitle="Directly comparable to clustered due to shared y-ranges.", + callout_header="🔎 Runtime tradeoff is visible", + callout_body="Prover-time gap remains moderate relative to proof-size improvements.", + progress="Random 2/3", + ), + FigureSlide( + filename="verifier_time_vs_k_random.png", + title="Verifier Time - Random", + subtitle="Final evidence slide in the same visual grammar.", + callout_header="✅ End-to-end story closes", + callout_body="Verifier cost tracks hashing work; optimization impact is stable across query styles.", + progress="Random 3/3", + ), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--fig-dir", + type=Path, + default=Path("figures/presentation"), + help="Directory containing required graph PNGs.", + ) + parser.add_argument( + "--out", + type=Path, + default=Path("/mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack.pptx"), + help="Output PPTX path.", + ) + parser.add_argument( + "--title", + type=str, + default="Results", + help="Section divider title word.", + ) + parser.add_argument( + "--readme", + type=Path, + default=Path("/mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack_README.txt"), + help="Companion README/TXT path.", + ) + return parser.parse_args() + + +def ensure_parent(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + +def preflight_figure_assets(fig_dir: Path, names: Sequence[str]) -> List[Path]: + resolved: List[Path] = [] + missing: List[str] = [] + sizes: List[Tuple[int, int]] = [] + + for name in names: + candidate = fig_dir / name + if not candidate.exists(): + missing.append(name) + continue + resolved.append(candidate) + with Image.open(candidate) as im: + sizes.append((im.width, im.height)) + + if missing: + raise FileNotFoundError(f"Missing figure files in {fig_dir}: {', '.join(missing)}") + + heights = {h for _, h in sizes} + if len(heights) > 1: + raise ValueError(f"Figure heights are inconsistent: {sorted(heights)}") + + widths = [w for w, _ in sizes] + w_min = min(widths) + w_max = max(widths) + if w_max > 1.35 * w_min: + raise ValueError( + "Figure widths are too inconsistent for a single visual family " + f"(min={w_min}, max={w_max}, ratio={w_max / w_min:.2f})." + ) + + return resolved + + +def add_textbox( + slide, + left: Emu, + top: Emu, + width: Emu, + height: Emu, + text: str, + *, + size: int, + bold: bool = False, + color: RGBColor | None = None, + align: PP_ALIGN = PP_ALIGN.LEFT, +) -> None: + tb = slide.shapes.add_textbox(left, top, width, height) + tf = tb.text_frame + tf.clear() + p = tf.paragraphs[0] + p.text = text + p.alignment = align + run = p.runs[0] + run.font.size = Pt(size) + run.font.bold = bold + if color is not None: + run.font.color.rgb = color + + +def add_rounded_box( + slide, + left: Emu, + top: Emu, + width: Emu, + height: Emu, + *, + fill: RGBColor, + line: RGBColor | None = None, + radius_adjust: float = 0.12, +): + shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) + shape.fill.solid() + shape.fill.fore_color.rgb = fill + if line is None: + shape.line.fill.background() + else: + shape.line.color.rgb = line + shape.line.width = Pt(1.0) + # Smaller corner radius for a sleek card look + shape.adjustments[0] = radius_adjust + return shape + + +def place_picture_contain(slide, image_path: Path, left: Emu, top: Emu, width: Emu, height: Emu) -> None: + with Image.open(image_path) as im: + img_w, img_h = im.size + scale = min(float(width) / img_w, float(height) / img_h) + pic_w = Emu(int(img_w * scale)) + pic_h = Emu(int(img_h * scale)) + pic_left = Emu(int(left + (width - pic_w) / 2)) + pic_top = Emu(int(top + (height - pic_h) / 2)) + slide.shapes.add_picture(str(image_path), pic_left, pic_top, width=pic_w, height=pic_h) + + +def build_divider_slide(prs: Presentation, title_word: str) -> None: + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Emu(0), Emu(0), SLIDE_W, SLIDE_H) + bg.fill.solid() + bg.fill.fore_color.rgb = TOKENS["bg_dark"] + bg.line.fill.background() + + band = slide.shapes.add_shape( + MSO_SHAPE.RECTANGLE, + Emu(0), + Emu(int(5.6 * EMU_PER_INCH)), + SLIDE_W, + Emu(int(0.16 * EMU_PER_INCH)), + ) + band.fill.solid() + band.fill.fore_color.rgb = TOKENS["accent"] + band.line.fill.background() + + add_textbox( + slide, + Emu(int(1.1 * EMU_PER_INCH)), + Emu(int(2.4 * EMU_PER_INCH)), + Emu(int(11.2 * EMU_PER_INCH)), + Emu(int(1.6 * EMU_PER_INCH)), + title_word, + size=66, + bold=True, + color=RGBColor(248, 250, 255), + ) + + add_textbox( + slide, + Emu(int(1.1 * EMU_PER_INCH)), + Emu(int(4.15 * EMU_PER_INCH)), + Emu(int(8.2 * EMU_PER_INCH)), + Emu(int(0.6 * EMU_PER_INCH)), + "Measured with one graph per slide for cleaner storytelling.", + size=20, + color=RGBColor(193, 203, 226), + ) + + chip = add_rounded_box( + slide, + Emu(int(12.35 * EMU_PER_INCH)), + Emu(int(0.35 * EMU_PER_INCH)), + Emu(int(0.28 * EMU_PER_INCH)), + Emu(int(0.28 * EMU_PER_INCH)), + fill=TOKENS["accent"], + line=None, + radius_adjust=0.35, + ) + chip.line.fill.background() + + +def build_metric_slide(prs: Presentation, spec: FigureSlide, image_path: Path) -> None: + slide = prs.slides.add_slide(prs.slide_layouts[6]) + + bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Emu(0), Emu(0), SLIDE_W, SLIDE_H) + bg.fill.solid() + bg.fill.fore_color.rgb = TOKENS["bg"] + bg.line.fill.background() + + accent_bar = slide.shapes.add_shape( + MSO_SHAPE.RECTANGLE, + Emu(int(0.08 * EMU_PER_INCH)), + Emu(int(0.5 * EMU_PER_INCH)), + Emu(int(0.05 * EMU_PER_INCH)), + Emu(int(6.5 * EMU_PER_INCH)), + ) + accent_bar.fill.solid() + accent_bar.fill.fore_color.rgb = TOKENS["accent"] + accent_bar.line.fill.background() + + add_textbox( + slide, + Emu(int(0.5 * EMU_PER_INCH)), + Emu(int(0.35 * EMU_PER_INCH)), + Emu(int(8.6 * EMU_PER_INCH)), + Emu(int(0.72 * EMU_PER_INCH)), + spec.title, + size=34, + bold=True, + color=TOKENS["text_dark"], + ) + add_textbox( + slide, + Emu(int(0.5 * EMU_PER_INCH)), + Emu(int(0.96 * EMU_PER_INCH)), + Emu(int(8.9 * EMU_PER_INCH)), + Emu(int(0.52 * EMU_PER_INCH)), + spec.subtitle, + size=16, + color=TOKENS["text_muted"], + ) + + card_left = Emu(int(0.48 * EMU_PER_INCH)) + card_top = Emu(int(1.42 * EMU_PER_INCH)) + card_w = Emu(int(9.65 * EMU_PER_INCH)) + card_h = Emu(int(5.55 * EMU_PER_INCH)) + + add_rounded_box( + slide, + card_left, + card_top, + card_w, + card_h, + fill=TOKENS["card"], + line=TOKENS["card_border"], + radius_adjust=0.06, + ) + + pic_margin = Emu(int(0.18 * EMU_PER_INCH)) + place_picture_contain( + slide, + image_path, + Emu(int(card_left + pic_margin)), + Emu(int(card_top + pic_margin)), + Emu(int(card_w - 2 * pic_margin)), + Emu(int(card_h - 2 * pic_margin)), + ) + + sticky_left = Emu(int(10.45 * EMU_PER_INCH)) + sticky_top = Emu(int(1.65 * EMU_PER_INCH)) + sticky_w = Emu(int(2.62 * EMU_PER_INCH)) + sticky_h = Emu(int(2.76 * EMU_PER_INCH)) + + add_rounded_box( + slide, + sticky_left, + sticky_top, + sticky_w, + sticky_h, + fill=TOKENS["sticky"], + line=TOKENS["sticky_border"], + radius_adjust=0.09, + ) + + add_textbox( + slide, + Emu(int(sticky_left + 0.18 * EMU_PER_INCH)), + Emu(int(sticky_top + 0.16 * EMU_PER_INCH)), + Emu(int(sticky_w - 0.35 * EMU_PER_INCH)), + Emu(int(0.7 * EMU_PER_INCH)), + spec.callout_header, + size=16, + bold=True, + color=TOKENS["text_dark"], + ) + add_textbox( + slide, + Emu(int(sticky_left + 0.18 * EMU_PER_INCH)), + Emu(int(sticky_top + 0.74 * EMU_PER_INCH)), + Emu(int(sticky_w - 0.35 * EMU_PER_INCH)), + Emu(int(1.9 * EMU_PER_INCH)), + spec.callout_body, + size=14, + color=RGBColor(61, 67, 79), + ) + + tag = add_rounded_box( + slide, + Emu(int(10.52 * EMU_PER_INCH)), + Emu(int(4.88 * EMU_PER_INCH)), + Emu(int(2.5 * EMU_PER_INCH)), + Emu(int(0.54 * EMU_PER_INCH)), + fill=TOKENS["progress_bg"], + line=TOKENS["accent"], + radius_adjust=0.22, + ) + tag.line.width = Pt(1.1) + add_textbox( + slide, + Emu(int(10.62 * EMU_PER_INCH)), + Emu(int(5.01 * EMU_PER_INCH)), + Emu(int(2.3 * EMU_PER_INCH)), + Emu(int(0.34 * EMU_PER_INCH)), + spec.progress, + size=12, + bold=True, + color=TOKENS["accent"], + align=PP_ALIGN.CENTER, + ) + + add_textbox( + slide, + Emu(int(10.45 * EMU_PER_INCH)), + Emu(int(6.43 * EMU_PER_INCH)), + Emu(int(2.65 * EMU_PER_INCH)), + Emu(int(0.4 * EMU_PER_INCH)), + "Results chapter redesign", + size=11, + color=RGBColor(114, 121, 136), + align=PP_ALIGN.RIGHT, + ) + + +def build_readme(readme_path: Path, pptx_path: Path, fig_dir: Path) -> None: + text = f"""Results Replacement Pack - Quick Insert Guide + +Output deck: +{pptx_path} + +Source figures: +{fig_dir} + +Insertion steps: +1) Open the original Keynote deck and this PPTX side-by-side. +2) In the original deck, remove the existing Results block (old divider + old metric slides). +3) Import all 7 slides from the PPTX in order: + - Results divider + - Proof Size - Clustered + - Prover Time - Clustered + - Verifier Time - Clustered + - Proof Size - Random + - Prover Time - Random + - Verifier Time - Random +4) Keep downstream slides (Migration/Next/Questions) as-is. + +Style conformance checklist: +- Section architecture: full-bleed divider with a single large chapter word. +- Visual system: one accent color, neutral palette, fixed typography hierarchy, repeated rounded cards. +- Controlled density: one graph per slide with generous whitespace. +- Progressive disclosure: clustered trio first, then random trio in an identical frame. +- Embedded annotations: one modern sticky-note insight callout per metric slide. +- Tasteful informality: subtle semantic emoji in callout headers. + +Validation: +The redesigned chapter matches the requested benchmark-slide style principles and keeps comparisons clean via one-graph-per-slide pacing. Next step: paste this 7-slide block into the main Keynote deck. +""" + readme_path.write_text(text, encoding="utf-8") + + +def build_pack(fig_dir: Path, out_path: Path, title: str, readme_path: Path) -> None: + required = [s.filename for s in FIGURE_SLIDES] + resolved = preflight_figure_assets(fig_dir, required) + figure_map = {p.name: p for p in resolved} + + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + + build_divider_slide(prs, title) + for spec in FIGURE_SLIDES: + build_metric_slide(prs, spec, figure_map[spec.filename]) + + ensure_parent(out_path) + prs.save(out_path) + + ensure_parent(readme_path) + build_readme(readme_path, out_path, fig_dir) + + +def main() -> None: + args = parse_args() + build_pack(args.fig_dir, args.out, args.title, args.readme) + print(f"Wrote {args.out}") + print(f"Wrote {args.readme}") + + +if __name__ == "__main__": + main() diff --git a/scripts/presentation_coord_encoding_plots.py b/scripts/presentation_coord_encoding_plots.py new file mode 100644 index 00000000..09306351 --- /dev/null +++ b/scripts/presentation_coord_encoding_plots.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +"""Generate presentation-style proof-size figures for coordinate encoding comparisons. + +Usage: + python3 scripts/presentation_coord_encoding_plots.py --input target/merkle_tree_reports/coordinate_encoding_rows.csv --output figures/presentation + python3 scripts/presentation_coord_encoding_plots.py --input target/merkle_tree_reports/leb128_vs_delta_rows.csv --output figures/presentation +""" + +from __future__ import annotations + +import argparse +import csv +import math +import os +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Sequence, Tuple + +os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") + +import matplotlib.pyplot as plt + + +@dataclass(frozen=True) +class Row: + tree_size: int + log2_size: int + batch: int + pattern: str + strategy: str + proof_bytes: int + proof_nodes: int + + +DEFAULT_LABELS = { + "natural": "Unoptimized", + "leb128": "Partially Optimized", + "delta": "Optimized", +} + +DEFAULT_COLORS = { + "natural": "#d95f02", + "leb128": "#1b9e77", + "delta": "#1f77b4", +} + +DEFAULT_MARKERS = { + "natural": "s", + "leb128": "o", + "delta": "^", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--input", + type=Path, + default=None, + help="Optional input file (.csv rows or .md report table). Auto-detected if omitted.", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("figures/presentation"), + help="Directory where presentation figures are written", + ) + parser.add_argument( + "--patterns", + nargs="+", + default=["clustered", "random"], + help="Patterns to render (default: clustered random)", + ) + parser.add_argument( + "--tree-size", + type=int, + default=None, + help="Specific tree size n to plot. Default: max n in dataset", + ) + parser.add_argument( + "--points", + type=int, + default=5, + help="Representative k points per plot", + ) + return parser.parse_args() + + +def resolve_input_path(explicit: Path | None) -> Path: + if explicit is not None: + return explicit + + candidates = [ + Path("target/merkle_tree_reports/coordinate_encoding_rows.csv"), + Path("target/merkle_tree_reports/coordinate_encoding_report.md"), + Path("target/merkle_tree_reports/leb128_vs_delta_rows.csv"), + Path("target/merkle_tree_reports/leb128_vs_delta_report.md"), + Path("crypto-primitives/target/merkle_tree_reports/coordinate_encoding_rows.csv"), + Path("crypto-primitives/target/merkle_tree_reports/coordinate_encoding_report.md"), + Path("crypto-primitives/target/merkle_tree_reports/leb128_vs_delta_rows.csv"), + Path("crypto-primitives/target/merkle_tree_reports/leb128_vs_delta_report.md"), + ] + for candidate in candidates: + if candidate.exists(): + return candidate + raise FileNotFoundError( + "No coordinate encoding input found under target/merkle_tree_reports " + "or crypto-primitives/target/merkle_tree_reports." + ) + + +def read_rows_csv(path: Path) -> List[Row]: + rows: List[Row] = [] + with path.open(newline="", encoding="utf-8") as handle: + reader = csv.DictReader(handle) + for row in reader: + rows.append( + Row( + tree_size=int(row["tree_size"]), + log2_size=int(row["log2_size"]), + batch=int(row["batch"]), + pattern=row["pattern"], + strategy=row["strategy"], + proof_bytes=int(row["proof_bytes"]), + proof_nodes=int(row["proof_nodes"]), + ) + ) + return rows + + +def read_rows_markdown(path: Path) -> List[Row]: + rows: List[Row] = [] + headers: List[str] = [] + + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if not line.startswith("|"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if not cells: + continue + if cells[0] == "tree_n": + headers = cells + continue + if cells[0].startswith("------") or not headers: + continue + if len(cells) != len(headers): + continue + + row = dict(zip(headers, cells)) + rows.append( + Row( + tree_size=int(row["tree_n"]), + log2_size=int(row["log2(n)"]), + batch=int(row["batch_k"]), + pattern=row["pattern"], + strategy=row["strategy"], + proof_bytes=int(row["proof_bytes"]), + proof_nodes=int(row["proof_nodes"]), + ) + ) + + return rows + + +def representative_values(values: Sequence[int], count: int) -> Tuple[List[int], List[Tuple[float, int]]]: + unique = sorted(set(values)) + if not unique: + return [], [] + if len(unique) <= count: + return unique, [(float(v), v) for v in unique] + + vmin = unique[0] + vmax = unique[-1] + log_min = math.log(vmin) + log_max = math.log(vmax) + + targets = [] + for i in range(count): + t = math.exp(log_min + (log_max - log_min) * (i / (count - 1))) + targets.append(t) + + selected: List[int] = [] + selections: List[Tuple[float, int]] = [] + + for target in targets: + remaining = [v for v in unique if v not in selected] + if not remaining: + break + choice = min(remaining, key=lambda v: abs(math.log(v) - math.log(target))) + selected.append(choice) + selections.append((target, choice)) + + if unique[0] not in selected: + selected[0] = unique[0] + if unique[-1] not in selected: + selected[-1] = unique[-1] + + selected = sorted(set(selected)) + if len(selected) < count: + for v in unique: + if v not in selected: + selected.append(v) + if len(selected) == count: + break + selected = sorted(selected) + + mapped_targets = [] + for t in targets: + mapped_targets.append((t, min(selected, key=lambda v: abs(math.log(v) - math.log(t))))) + + return selected, mapped_targets + + +def ensure_slide_style() -> None: + plt.rcParams.update( + { + "font.family": "Helvetica Neue", + "svg.fonttype": "none", + "figure.titlesize": 30, + "axes.titlesize": 28, + "axes.labelsize": 24, + "xtick.labelsize": 18, + "ytick.labelsize": 18, + "legend.fontsize": 17, + } + ) + + +def choose_byte_unit(values: Sequence[float]) -> Tuple[float, str]: + if not values: + return 1.0, "" + + max_abs = max(abs(v) for v in values) + if max_abs >= 1_000_000_000: + return 1_000_000_000.0, "G" + if max_abs >= 1_000_000: + return 1_000_000.0, "M" + if max_abs >= 1_000: + return 1_000.0, "k" + return 1.0, "" + + +def proof_size_label(prefix: str) -> str: + units = { + "": "B", + "k": "kB", + "M": "MB", + "G": "GB", + } + return f"Proof size ({units[prefix]})" + + +def infer_strategy_order(rows: Sequence[Row]) -> List[str]: + strategies = sorted({row.strategy for row in rows}) + if strategies == ["leb128", "natural"]: + return ["natural", "leb128"] + if strategies == ["delta", "leb128"]: + return ["leb128", "delta"] + preferred = [name for name in ["natural", "leb128", "delta"] if name in strategies] + return preferred + [name for name in strategies if name not in preferred] + + +def label_for(strategy: str) -> str: + return DEFAULT_LABELS.get(strategy, strategy) + + +def color_for(strategy: str) -> str: + return DEFAULT_COLORS.get(strategy, "#000000") + + +def marker_for(strategy: str) -> str: + return DEFAULT_MARKERS.get(strategy, "o") + + +def filename_prefix_for(strategies: Sequence[str]) -> str: + ordered = list(strategies) + if ordered == ["natural", "leb128"]: + return "coordinate_proof_size" + if ordered == ["leb128", "delta"]: + return "leb128_vs_delta_proof_size" + return "coord_comparison_proof_size" + + +def generate_plot( + rows: Sequence[Row], + pattern: str, + tree_size: int, + selected_k: Sequence[int], + output_dir: Path, + strategy_order: Sequence[str], +) -> List[Path]: + by_strategy: Dict[str, Dict[int, float]] = defaultdict(dict) + for row in rows: + if row.pattern != pattern or row.tree_size != tree_size: + continue + by_strategy[row.strategy][row.batch] = float(row.proof_bytes) + + fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) + + raw_values = [value for points in by_strategy.values() for value in points.values()] + unit_scale, unit_prefix = choose_byte_unit(raw_values) + + all_points: List[Tuple[int, float, str]] = [] + for strategy in strategy_order: + points = by_strategy.get(strategy, {}) + xs = [k for k in selected_k if k in points] + ys = [points[k] / unit_scale for k in xs] + if not xs: + continue + ax.plot( + xs, + ys, + label=label_for(strategy), + color=color_for(strategy), + marker=marker_for(strategy), + linewidth=3.0, + markersize=9, + ) + for x, y in zip(xs, ys): + all_points.append((x, y, strategy)) + + if not all_points: + plt.close(fig) + return [] + + y_values = [p[1] for p in all_points] + ymin = min(y_values) + ymax = max(y_values) + yrange = max(ymax - ymin, ymax * 0.08, 1e-9) + + ax.set_xscale("log", base=2) + ax.set_xticks(list(selected_k)) + ax.set_xticklabels([str(x) for x in selected_k]) + ax.set_xlim(min(selected_k) * 0.9, max(selected_k) * 1.15) + ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) + ax.set_xlabel("Number of leaves opened", labelpad=10, fontname="Helvetica Neue", fontweight="bold") + ax.set_ylabel( + proof_size_label(unit_prefix), + labelpad=10, + fontname="Helvetica Neue", + fontweight="bold", + ) + ax.grid(True, which="major", linestyle="--", alpha=0.28) + + legend = ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0), frameon=False) + if legend is not None: + for legend_text in legend.get_texts(): + legend_text.set_fontfamily("Helvetica Neue") + legend_text.set_fontweight("bold") + + for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): + tick_label.set_fontfamily("Helvetica Neue") + + fig.tight_layout() + + basename = f"{filename_prefix_for(strategy_order)}_vs_k_{pattern}" + outputs = [] + for ext in ("png", "pdf", "svg"): + out_path = output_dir / f"{basename}.{ext}" + save_kwargs = {"bbox_inches": "tight"} + if ext == "png": + save_kwargs["dpi"] = 300 + fig.savefig(out_path, **save_kwargs) + outputs.append(out_path) + + plt.close(fig) + return outputs + + +def main() -> None: + args = parse_args() + ensure_slide_style() + + input_path = resolve_input_path(args.input) + if input_path.suffix.lower() == ".csv": + rows = read_rows_csv(input_path) + elif input_path.suffix.lower() == ".md": + rows = read_rows_markdown(input_path) + else: + raise ValueError(f"Unsupported input format: {input_path}") + + if not rows: + raise RuntimeError(f"No benchmark rows found in {input_path}") + + available_tree_sizes = sorted({row.tree_size for row in rows}) + tree_size = args.tree_size if args.tree_size is not None else available_tree_sizes[-1] + if tree_size not in available_tree_sizes: + raise ValueError(f"tree_size {tree_size} not present. Available: {available_tree_sizes}") + + output_dir = args.output + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"input={input_path}") + + strategy_order = infer_strategy_order(rows) + pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]] = {} + + for pattern in args.patterns: + strategy_batches: Dict[str, List[int]] = defaultdict(list) + for row in rows: + if row.tree_size == tree_size and row.pattern == pattern: + strategy_batches[row.strategy].append(row.batch) + + if not strategy_batches: + print(f"Skipping pattern={pattern}: no rows for tree_size={tree_size}") + continue + + shared = sorted(set.intersection(*(set(v) for v in strategy_batches.values()))) + if not shared: + print(f"Skipping pattern={pattern}: no shared k across strategies") + continue + + selected_k, target_map = representative_values(shared, args.points) + pattern_to_selection[pattern] = (selected_k, target_map, shared) + + print(f"pattern={pattern} tree_size={tree_size} available_k={shared}") + print(f"pattern={pattern} selected_k={selected_k}") + + for pattern, (selected_k, _, _) in pattern_to_selection.items(): + scoped_rows = [row for row in rows if row.tree_size == tree_size and row.pattern == pattern] + files = generate_plot( + rows=scoped_rows, + pattern=pattern, + tree_size=tree_size, + selected_k=selected_k, + output_dir=output_dir, + strategy_order=strategy_order, + ) + if files: + print(f"generated={ [str(path) for path in files] }") + + +if __name__ == "__main__": + main() diff --git a/scripts/presentation_plots.py b/scripts/presentation_plots.py new file mode 100644 index 00000000..14dd9dd2 --- /dev/null +++ b/scripts/presentation_plots.py @@ -0,0 +1,560 @@ +#!/usr/bin/env python3 +"""Generate presentation-friendly figures from multiproof benchmark rows. + +Data flow (existing benchmark path): +1) Rust benchmark test builds trees, samples (k, pattern), and benchmarks prefix/coset. +2) The test writes either raw rows CSV (multiproof_v2_rows.csv) and/or markdown report table + (multiproof_v2_report.md) under target/merkle_tree_reports/. +3) This script reads those rows and renders one figure per metric/pattern in + figures/presentation/, using a representative subset of k values. + +Usage: + python3 scripts/presentation_plots.py + python3 scripts/presentation_plots.py \ + --input target/merkle_tree_reports/multiproof_v2_report.md \ + --output figures/presentation +""" + +from __future__ import annotations + +import argparse +import csv +import math +import os +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Sequence, Tuple + +# Matplotlib needs a writable config dir in some sandboxed environments. +os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") + +import matplotlib.pyplot as plt + + +@dataclass(frozen=True) +class Row: + tree_size: int + log2_size: int + batch: int + pattern: str + strategy: str + proof_bytes: int + proof_nodes: int + hashes_per_opening: float + prove_ms: float + verify_ms: float + rss_delta_kb: float | None + + +@dataclass(frozen=True) +class MetricSpec: + key: str + title: str + y_label: str + filename_prefix: str + + +METRICS: Tuple[MetricSpec, ...] = ( + MetricSpec("proof_bytes", "Proof Size vs Input Size", "Proof size (bytes)", "proof_size"), + MetricSpec("prove_ms", "Prover Time vs Input Size", "Time (ms)", "prover_time"), + MetricSpec("verify_ms", "Verifier Time vs Input Size", "Time (ms)", "verifier_time"), + MetricSpec( + "hashes_per_opening", + "Hashes per Opening (m Proxy) vs Input Size", + "Hashes per opening", + "hashes_per_opening", + ), +) + +STRATEGY_STYLE = { + "prefix": {"label": "Old", "color": "#d62728", "marker": "s"}, + "coset": {"label": "New", "color": "#1f77b4", "marker": "o"}, +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--input", + type=Path, + default=None, + help="Optional input file (.csv rows or .md report table). Auto-detected if omitted.", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("figures/presentation"), + help="Directory where presentation figures are written", + ) + parser.add_argument( + "--patterns", + nargs="+", + default=["clustered", "random"], + help="Patterns to render (default: clustered random)", + ) + parser.add_argument( + "--tree-size", + type=int, + default=None, + help="Specific tree size n to plot. Default: max n in dataset", + ) + parser.add_argument( + "--points", + type=int, + default=5, + help="Representative k points per plot", + ) + return parser.parse_args() + + +def read_rows_csv(path: Path) -> List[Row]: + rows: List[Row] = [] + with path.open(newline="", encoding="utf-8") as handle: + reader = csv.DictReader(handle) + for r in reader: + rss_raw = (r.get("rss_delta_kb") or "").strip() + rows.append( + Row( + tree_size=int(r["tree_size"]), + log2_size=int(r["log2_size"]), + batch=int(r["batch"]), + pattern=r["pattern"], + strategy=r["strategy"], + proof_bytes=int(r["proof_bytes"]), + proof_nodes=int(r["proof_nodes"]), + hashes_per_opening=float(r["hashes_per_opening"]), + prove_ms=float(r["prove_ms"]), + verify_ms=float(r["verify_ms"]), + rss_delta_kb=float(rss_raw) if rss_raw else None, + ) + ) + return rows + + +def read_rows_markdown(path: Path) -> List[Row]: + rows: List[Row] = [] + headers: List[str] = [] + + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if not line.startswith("|"): + continue + cells = [cell.strip() for cell in line.strip().strip("|").split("|")] + if not cells: + continue + if cells[0] == "tree_n": + headers = cells + continue + if cells[0].startswith("------") or not headers: + continue + if len(cells) != len(headers): + continue + + row = dict(zip(headers, cells)) + rows.append( + Row( + tree_size=int(row["tree_n"]), + log2_size=int(row["log2(n)"]), + batch=int(row["batch_k"]), + pattern=row["pattern"], + strategy=row["strategy"], + proof_bytes=int(row["proof_bytes"]), + proof_nodes=int(row["proof_nodes"]), + hashes_per_opening=float(row["hashes/leaf"]), + prove_ms=float(row["prove_ms"]), + verify_ms=float(row["verify_ms"]), + rss_delta_kb=float(row["rss_delta_kb"]) if row["rss_delta_kb"] != "-" else None, + ) + ) + + return rows + + +def resolve_input_path(explicit: Path | None) -> Path: + if explicit is not None: + return explicit + + candidates = [ + Path("target/merkle_tree_reports/multiproof_v2_rows.csv"), + Path("target/merkle_tree_reports/multiproof_v2_report.md"), + Path("crypto-primitives/target/merkle_tree_reports/multiproof_v2_rows.csv"), + Path("crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md"), + ] + for candidate in candidates: + if candidate.exists(): + return candidate + raise FileNotFoundError( + "No benchmark input found. Looked for multiproof_v2_rows.csv or multiproof_v2_report.md " + "under target/merkle_tree_reports and crypto-primitives/target/merkle_tree_reports." + ) + + +def metric_value(row: Row, metric_key: str) -> float: + return float(getattr(row, metric_key)) + + +def representative_values(values: Sequence[int], count: int) -> Tuple[List[int], List[Tuple[float, int]]]: + unique = sorted(set(values)) + if not unique: + return [], [] + if len(unique) <= count: + return unique, [(float(v), v) for v in unique] + + vmin = unique[0] + vmax = unique[-1] + log_min = math.log(vmin) + log_max = math.log(vmax) + + targets = [] + for i in range(count): + t = math.exp(log_min + (log_max - log_min) * (i / (count - 1))) + targets.append(t) + + selected: List[int] = [] + selections: List[Tuple[float, int]] = [] + + for target in targets: + remaining = [v for v in unique if v not in selected] + if not remaining: + break + choice = min(remaining, key=lambda v: abs(math.log(v) - math.log(target))) + selected.append(choice) + selections.append((target, choice)) + + if unique[0] not in selected: + selected[0] = unique[0] + if unique[-1] not in selected: + selected[-1] = unique[-1] + + selected = sorted(set(selected)) + if len(selected) < count: + for v in unique: + if v not in selected: + selected.append(v) + if len(selected) == count: + break + selected = sorted(selected) + + mapped_targets = [] + for t in targets: + mapped_targets.append((t, min(selected, key=lambda v: abs(math.log(v) - math.log(t))))) + + return selected, mapped_targets + + +def ensure_slide_style() -> None: + plt.rcParams.update( + { + "font.family": "Helvetica Neue", + "svg.fonttype": "none", + "figure.titlesize": 30, + "axes.titlesize": 28, + "axes.labelsize": 24, + "xtick.labelsize": 18, + "ytick.labelsize": 18, + "legend.fontsize": 17, + } + ) + + +def generate_plot( + rows: Sequence[Row], + metric: MetricSpec, + pattern: str, + tree_size: int, + selected_k: Sequence[int], + output_dir: Path, + shared_y_range: Tuple[float, float] | None = None, +) -> List[Path]: + by_strategy: Dict[str, Dict[int, float]] = defaultdict(dict) + for row in rows: + if row.pattern != pattern or row.tree_size != tree_size: + continue + by_strategy[row.strategy][row.batch] = metric_value(row, metric.key) + + fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) + + all_points: List[Tuple[int, float, str]] = [] + unit_scale = 1.0 + unit_prefix = "" + if metric.key == "proof_bytes": + raw_values = list(by_strategy.get("prefix", {}).values()) + list(by_strategy.get("coset", {}).values()) + unit_scale, unit_prefix = choose_byte_unit(raw_values) + + for strategy in ("prefix", "coset"): + points = by_strategy.get(strategy, {}) + xs = [k for k in selected_k if k in points] + ys = [points[k] / unit_scale for k in xs] + if not xs: + continue + style = STRATEGY_STYLE.get(strategy, {"label": strategy, "color": "#000000", "marker": "o"}) + ax.plot( + xs, + ys, + label=style["label"], + color=style["color"], + marker=style["marker"], + linewidth=3.0, + markersize=9, + ) + for x, y in zip(xs, ys): + all_points.append((x, y, strategy)) + + if not all_points: + plt.close(fig) + return [] + + if shared_y_range is None: + y_values = [p[1] for p in all_points] + ymin = min(y_values) + ymax = max(y_values) + else: + ymin, ymax = shared_y_range + ymin /= unit_scale + ymax /= unit_scale + yrange = max(ymax - ymin, ymax * 0.08, 1e-9) + + ax.set_xscale("log", base=2) + ax.set_xticks(list(selected_k)) + ax.set_xticklabels([str(x) for x in selected_k]) + ax.set_xlim(min(selected_k) * 0.9, max(selected_k) * 1.15) + ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) + ax.set_xlabel("Number of leaves opened", labelpad=10, fontname="Helvetica Neue", fontweight="bold") + ax.set_ylabel( + proof_size_label(unit_prefix) if metric.key == "proof_bytes" else metric.y_label, + labelpad=10, + fontname="Helvetica Neue", + fontweight="bold", + ) + ax.grid(True, which="major", linestyle="--", alpha=0.28) + + legend = ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0), frameon=False) + if legend is not None: + for legend_text in legend.get_texts(): + legend_text.set_fontfamily("Helvetica Neue") + legend_text.set_fontweight("bold") + + for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): + tick_label.set_fontfamily("Helvetica Neue") + + fig.tight_layout() + + basename = f"{metric.filename_prefix}_vs_k_{pattern}" + outputs = [] + for ext in ("png", "pdf", "svg"): + out_path = output_dir / f"{basename}.{ext}" + save_kwargs = {"bbox_inches": "tight"} + if ext == "png": + save_kwargs["dpi"] = 300 + fig.savefig(out_path, **save_kwargs) + outputs.append(out_path) + + plt.close(fig) + return outputs + + +def compute_shared_y_ranges( + rows: Sequence[Row], + tree_size: int, + pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]], + metrics: Sequence[MetricSpec], +) -> Dict[str, Tuple[float, float]]: + ranges: Dict[str, Tuple[float, float]] = {} + patterns = set(pattern_to_selection.keys()) + + for metric in metrics: + values: List[float] = [] + for row in rows: + if row.tree_size != tree_size or row.pattern not in patterns: + continue + selected_k = pattern_to_selection[row.pattern][0] + if row.batch not in selected_k: + continue + values.append(metric_value(row, metric.key)) + + if values: + ranges[metric.key] = (min(values), max(values)) + + return ranges + + +def choose_byte_unit(values: Sequence[float]) -> Tuple[float, str]: + if not values: + return 1.0, "" + + max_abs = max(abs(v) for v in values) + if max_abs >= 1_000_000_000: + return 1_000_000_000.0, "G" + if max_abs >= 1_000_000: + return 1_000_000.0, "M" + if max_abs >= 1_000: + return 1_000.0, "k" + return 1.0, "" + + +def proof_size_label(prefix: str) -> str: + units = { + "": "B", + "k": "kB", + "M": "MB", + "G": "GB", + } + return f"Proof size ({units[prefix]})" + + +def write_selection_summary( + output_dir: Path, + tree_size: int, + pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]], +) -> None: + summary_path = output_dir / "selection_summary.txt" + with summary_path.open("w", encoding="utf-8") as handle: + handle.write(f"tree_size={tree_size}\n") + for pattern, (selected, mapping, available) in pattern_to_selection.items(): + handle.write(f"\npattern={pattern}\n") + handle.write(f"available_k={available}\n") + handle.write(f"selected_k={selected}\n") + handle.write("targets_to_selected=\n") + for target, chosen in mapping: + handle.write(f" target~{target:.2f} -> {chosen}\n") + + +def average(values: Sequence[float]) -> float: + if not values: + return 0.0 + return sum(values) / float(len(values)) + + +def write_highest_k_differences( + output_dir: Path, + rows: Sequence[Row], + tree_size: int, + patterns: Sequence[str], +) -> None: + summary_path = output_dir / "largest_k_percent_change.txt" + lines: List[str] = [f"tree_size={tree_size}"] + + metric_specs = [ + ("proof_bytes", "proof size"), + ("prove_ms", "prover time"), + ("verify_ms", "verifier time"), + ] + + for pattern in patterns: + scoped = [r for r in rows if r.tree_size == tree_size and r.pattern == pattern] + if not scoped: + continue + + by_strategy: Dict[str, Dict[int, List[Row]]] = defaultdict(lambda: defaultdict(list)) + for row in scoped: + by_strategy[row.strategy][row.batch].append(row) + + if "prefix" not in by_strategy or "coset" not in by_strategy: + continue + + shared_k = sorted(set(by_strategy["prefix"].keys()) & set(by_strategy["coset"].keys())) + if not shared_k: + continue + + highest_k = shared_k[-1] + lines.append(f"") + lines.append(f"pattern={pattern}") + lines.append(f"largest_k={highest_k}") + + for metric_key, label in metric_specs: + prefix_values = [metric_value(r, metric_key) for r in by_strategy["prefix"][highest_k]] + coset_values = [metric_value(r, metric_key) for r in by_strategy["coset"][highest_k]] + if not prefix_values or not coset_values: + continue + + old_avg = average(prefix_values) + new_avg = average(coset_values) + pct = 0.0 if old_avg == 0 else ((new_avg - old_avg) / old_avg) * 100.0 + lines.append(f"{label}: {pct:+.2f}% (Old={old_avg:.3f}, New={new_avg:.3f})") + + with summary_path.open("w", encoding="utf-8") as handle: + handle.write("\n".join(lines) + "\n") + + +def main() -> None: + args = parse_args() + ensure_slide_style() + + input_path = resolve_input_path(args.input) + if input_path.suffix.lower() == ".csv": + rows = read_rows_csv(input_path) + elif input_path.suffix.lower() == ".md": + rows = read_rows_markdown(input_path) + else: + raise ValueError(f"Unsupported input format: {input_path}") + + if not rows: + raise RuntimeError(f"No benchmark rows found in {input_path}") + + available_tree_sizes = sorted({r.tree_size for r in rows}) + tree_size = args.tree_size if args.tree_size is not None else available_tree_sizes[-1] + if tree_size not in available_tree_sizes: + raise ValueError(f"tree_size {tree_size} not present. Available: {available_tree_sizes}") + + output_dir = args.output + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"input={input_path}") + + pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]] = {} + + for pattern in args.patterns: + strategy_batches: Dict[str, List[int]] = defaultdict(list) + for row in rows: + if row.tree_size == tree_size and row.pattern == pattern: + strategy_batches[row.strategy].append(row.batch) + + if not strategy_batches: + print(f"Skipping pattern={pattern}: no rows for tree_size={tree_size}") + continue + + shared = sorted(set.intersection(*(set(v) for v in strategy_batches.values()))) + if not shared: + print(f"Skipping pattern={pattern}: no shared k across strategies") + continue + + selected_k, target_map = representative_values(shared, args.points) + pattern_to_selection[pattern] = (selected_k, target_map, shared) + + print(f"pattern={pattern} tree_size={tree_size} available_k={shared}") + print(f"pattern={pattern} selected_k={selected_k}") + + shared_y_ranges = compute_shared_y_ranges( + rows=rows, + tree_size=tree_size, + pattern_to_selection=pattern_to_selection, + metrics=METRICS, + ) + + for pattern, (selected_k, _, _) in pattern_to_selection.items(): + scoped_rows = [r for r in rows if r.tree_size == tree_size and r.pattern == pattern] + for metric in METRICS: + files = generate_plot( + scoped_rows, + metric, + pattern, + tree_size, + selected_k, + output_dir, + shared_y_range=None if metric.key in {"proof_bytes", "prove_ms", "verify_ms"} else shared_y_ranges.get(metric.key), + ) + if files: + print(f"regenerated(shared-y): {[str(f) for f in files]}") + + write_selection_summary(output_dir, tree_size, pattern_to_selection) + write_highest_k_differences( + output_dir=output_dir, + rows=rows, + tree_size=tree_size, + patterns=list(pattern_to_selection.keys()), + ) + + +if __name__ == "__main__": + main() From 46e5c1450ae6016478449f7af70e404286d4fc51 Mon Sep 17 00:00:00 2001 From: ajhavlin <116843349+ajhavlin@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:15:07 +0200 Subject: [PATCH 16/22] merkle_tree: add coordinate-free ImplicitCoPath batch proof Introduce ImplicitCoPath as a leaner alternative to CoPath that drops all coordinate metadata (start_depth, start_index, deltas). Both prover and verifier derive copath positions from leaf_indexes and tree_height, so only the digests are transmitted in canonical depth-then-index order. Adds generate_implicit_multi_proof to MerkleTree and a full test suite covering single-leaf, full-batch, tampering, and edge cases. Co-Authored-By: Claude Opus 4.6 --- .../scripts/presentation_implicit_plots.py | 320 ++++++++++++++ crypto-primitives/src/merkle_tree/implicit.rs | 205 +++++++++ crypto-primitives/src/merkle_tree/mod.rs | 89 +++- .../src/merkle_tree/tests/bench_report.rs | 414 ++++++++++++++++++ .../tests/implicit_copath_tests.rs | 198 +++++++++ .../src/merkle_tree/tests/mod.rs | 1 + 6 files changed, 1218 insertions(+), 9 deletions(-) create mode 100644 crypto-primitives/scripts/presentation_implicit_plots.py create mode 100644 crypto-primitives/src/merkle_tree/implicit.rs create mode 100644 crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs diff --git a/crypto-primitives/scripts/presentation_implicit_plots.py b/crypto-primitives/scripts/presentation_implicit_plots.py new file mode 100644 index 00000000..f88769c4 --- /dev/null +++ b/crypto-primitives/scripts/presentation_implicit_plots.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""Generate presentation-style figures comparing CoSet (with coordinates) vs Implicit (coord-free). + +Data flow: +1) Run the Rust benchmark test: + cargo test --features bench_harness,merkle_tree -- --ignored --nocapture implicit_vs_coset_report +2) This writes target/merkle_tree_reports/implicit_vs_coset_rows.csv +3) Run this script: + python3 scripts/presentation_implicit_plots.py + Output goes to figures/presentation/ as PNG, PDF, and SVG. + +The script generates one figure per metric (proof_bytes, prove_ms, verify_ms) per pattern +(clustered, random), with both strategies on the same axes in different colours. +""" + +from __future__ import annotations + +import argparse +import csv +import os +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Tuple + +os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") + +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class Row: + tree_size: int + log2_size: int + batch: int + pattern: str + strategy: str + proof_bytes: int + proof_nodes: int + hashes_per_opening: float + prove_ms: float + verify_ms: float + + +@dataclass(frozen=True) +class MetricSpec: + key: str + title: str + y_label: str + filename_prefix: str + + +METRICS: Tuple[MetricSpec, ...] = ( + MetricSpec("proof_bytes", "Proof Size vs Input Size", "Proof size (kB)", "proof_size"), + MetricSpec("prove_ms", "Prover Time vs Input Size", "Time (ms)", "prover_time"), + MetricSpec("verify_ms", "Verifier Time vs Input Size", "Time (ms)", "verifier_time"), +) + +# Colour and marker choices: +# coset — blue (same shade used in presentation_plots.py for "New/coset") +# implicit — purple (distinct; dark enough to remain legible in greyscale) +STRATEGY_STYLE: Dict[str, dict] = { + "coset": { + "label": "CoSet (delta coords)", + "color": "#1f77b4", + "marker": "o", + "zorder": 3, + }, + "implicit": { + "label": "Implicit (coord-free)", + "color": "#7b35c1", + "marker": "^", + "zorder": 4, + }, +} + +DRAW_ORDER = ["coset", "implicit"] +PRESENTATION_X_TICKS = [1, 8, 64, 512, 4096] + + +def ensure_slide_style() -> None: + plt.rcParams.update( + { + "font.family": "Helvetica Neue", + "svg.fonttype": "none", + "figure.titlesize": 30, + "axes.titlesize": 28, + "axes.labelsize": 24, + "xtick.labelsize": 18, + "ytick.labelsize": 18, + "legend.fontsize": 17, + } + ) + + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--input", + type=Path, + default=Path("target/merkle_tree_reports/implicit_vs_coset_rows.csv"), + help="CSV produced by the implicit_vs_coset_report bench test", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("figures/presentation"), + help="Directory where figures are written", + ) + parser.add_argument( + "--patterns", + nargs="+", + default=["clustered", "random"], + help="Query patterns to render (default: clustered random)", + ) + parser.add_argument( + "--tree-size", + type=int, + default=None, + help="Specific tree size n to plot. Default: largest n in the dataset", + ) + return parser.parse_args() + + +# --------------------------------------------------------------------------- +# CSV loading +# --------------------------------------------------------------------------- + +def load_csv(path: Path) -> List[Row]: + rows: List[Row] = [] + with path.open(newline="") as f: + reader = csv.DictReader(f) + for raw in reader: + rows.append( + Row( + tree_size=int(raw["tree_size"]), + log2_size=int(raw["log2_size"]), + batch=int(raw["batch"]), + pattern=raw["pattern"], + strategy=raw["strategy"], + proof_bytes=int(raw["proof_bytes"]), + proof_nodes=int(raw["proof_nodes"]), + hashes_per_opening=float(raw["hashes_per_opening"]), + prove_ms=float(raw["prove_ms"]), + verify_ms=float(raw["verify_ms"]), + ) + ) + return rows + + +# --------------------------------------------------------------------------- +# Plotting +# --------------------------------------------------------------------------- + +def _get_metric_value(row: Row, metric: MetricSpec) -> float: + value = float(getattr(row, metric.key)) + if metric.key == "proof_bytes": + return value / 1024.0 + return value + + +def _format_y_tick(metric: MetricSpec) -> ticker.Formatter: + if metric.key == "proof_bytes": + return ticker.FuncFormatter( + lambda v, _: f"{v:,.0f}" if v >= 100 else f"{v:,.1f}".rstrip("0").rstrip(".") + ) + return ticker.FuncFormatter( + lambda v, _: f"{v:,.2f}".rstrip("0").rstrip(".") if abs(v) < 100 else f"{v:,.0f}" + ) + + +def plot_metric_pattern( + rows: List[Row], + metric: MetricSpec, + pattern: str, + tree_size: int, + out_dir: Path, +) -> List[Path]: + """Render one figure for (metric, pattern) and write PNG/PDF/SVG.""" + ensure_slide_style() + + subset = [ + r for r in rows if r.pattern == pattern and r.tree_size == tree_size + ] + if not subset: + return [] + + # Group by strategy + series: Dict[str, List[Tuple[int, float]]] = defaultdict(list) + for row in subset: + if row.batch in PRESENTATION_X_TICKS: + series[row.strategy].append((row.batch, _get_metric_value(row, metric))) + + for strat in series: + series[strat].sort() + + fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) + available_x_values = sorted({batch for points in series.values() for batch, _ in points}) + x_ticks = [x for x in PRESENTATION_X_TICKS if x in available_x_values] + + if not x_ticks: + plt.close(fig) + return [] + + for strat in DRAW_ORDER: + if strat not in series: + continue + style = STRATEGY_STYLE[strat] + xs = [p[0] for p in series[strat]] + ys = [p[1] for p in series[strat]] + ax.plot( + xs, + ys, + color=style["color"], + marker=style["marker"], + label=style["label"], + linewidth=3.0, + markersize=9, + zorder=style["zorder"], + ) + + all_points = [(x, y) for points in series.values() for x, y in points] + y_values = [y for _, y in all_points] + ymin = min(y_values) + ymax = max(y_values) + yrange = max(ymax - ymin, ymax * 0.08, 1e-9) + + ax.set_xlabel( + "Number of leaves opened", + labelpad=10, + fontname="Helvetica Neue", + fontweight="bold", + ) + ax.set_ylabel( + metric.y_label, + labelpad=10, + fontname="Helvetica Neue", + fontweight="bold", + ) + ax.set_xscale("log", base=2) + ax.set_xticks(x_ticks) + ax.set_xticklabels([str(x) for x in x_ticks]) + ax.set_xlim(min(x_ticks) * 0.9, max(x_ticks) * 1.15) + ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) + ax.xaxis.set_minor_locator(ticker.NullLocator()) + ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=6)) + ax.yaxis.set_major_formatter(_format_y_tick(metric)) + legend = ax.legend( + loc="upper left", + bbox_to_anchor=(1.02, 1.0), + borderaxespad=0.0, + frameon=False, + ) + if legend is not None: + for legend_text in legend.get_texts(): + legend_text.set_fontfamily("Helvetica Neue") + legend_text.set_fontweight("bold") + + for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): + tick_label.set_fontfamily("Helvetica Neue") + + ax.grid(True, which="major", linestyle="--", alpha=0.28) + fig.tight_layout() + + stem = f"implicit_vs_coset_{metric.filename_prefix}_vs_k_{pattern}" + written: List[Path] = [] + for ext in ("png", "pdf", "svg"): + dest = out_dir / f"{stem}.{ext}" + save_kwargs = {"bbox_inches": "tight"} + if ext == "png": + save_kwargs["dpi"] = 300 + fig.savefig(dest, **save_kwargs) + written.append(dest) + + plt.close(fig) + return written + + +def main() -> None: + args = parse_args() + + if not args.input.exists(): + raise SystemExit( + f"Input file not found: {args.input}\n" + "Run the Rust benchmark first:\n" + " cargo test --features bench_harness,merkle_tree -- " + "--ignored --nocapture implicit_vs_coset_report" + ) + + rows = load_csv(args.input) + if not rows: + raise SystemExit(f"No rows found in {args.input}") + + tree_size = args.tree_size or max(r.tree_size for r in rows) + args.output.mkdir(parents=True, exist_ok=True) + + all_written: List[Path] = [] + for metric in METRICS: + for pattern in args.patterns: + written = plot_metric_pattern(rows, metric, pattern, tree_size, args.output) + all_written.extend(written) + + for p in all_written: + print(f"Wrote {p}") + + if not all_written: + print("No figures generated — check that the CSV contains matching rows.") + + +if __name__ == "__main__": + main() diff --git a/crypto-primitives/src/merkle_tree/implicit.rs b/crypto-primitives/src/merkle_tree/implicit.rs new file mode 100644 index 00000000..fe6f2626 --- /dev/null +++ b/crypto-primitives/src/merkle_tree/implicit.rs @@ -0,0 +1,205 @@ +//! Coordinate-free batch Merkle membership proof. +//! +//! Both prover and verifier independently derive the positions of all required copath nodes +//! from `leaf_indexes` and `tree_height` by running [`compute_on_path`]. Only the digests are +//! transmitted, in canonical depth-then-index order. No coordinate metadata is included in the +//! proof, so the inner copath is a plain `Vec` rather than a packed delta stream. +//! +//! # Wire format vs [`super::CoPath`] +//! +//! | Field | CoPath | ImplicitCoPath | +//! |-------|--------|----------------| +//! | `tree_height` | ✓ | ✓ | +//! | `leaf_copath` | `Vec` | `Vec` (identical) | +//! | `inner_copath` | `(start_depth, start_index, deltas, digests)` | `Vec` | +//! | `leaf_indexes` | `Vec` | `Vec` | +//! +//! The saved bytes come entirely from dropping the coordinate metadata (`start_depth`, +//! `start_index`, `deltas`). For a tree with `h` inner layers and `n_c` copath entries the +//! saving is `2 * sizeof(usize) + (n_c - 1) * 2 * avg_delta_bytes`. + +use crate::Error; +use ark_serialize::{ + CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, +}; +#[cfg(not(feature = "std"))] +use ark_std::vec::Vec; +use ark_std::{ + borrow::Borrow, + collections::{BTreeMap, BTreeSet}, + hash::BuildHasherDefault, +}; +use hashbrown::HashMap; + +use super::{ + compute_on_path, level_index, CoPath, Config, DefaultHasher, LeafParam, TwoToOneParam, +}; + +/// Coordinate-free batch Merkle membership proof. +/// +/// See the [module-level documentation](self) for a description of the protocol and a comparison +/// with [`CoPath`]. +#[derive(Derivative, CanonicalSerialize)] +#[derivative( + Clone(bound = "P: Config"), + Debug(bound = "P: Config"), + Default(bound = "P: Config") +)] +pub struct ImplicitCoPath { + /// Height of the tree this proof was generated from (>= 2). + pub tree_height: usize, + /// Leaf-layer copath digests (`B*_{d-1}`), ascending sibling index order. + pub leaf_copath: Vec, + /// Inner copath digests in canonical order: depth 1 ascending, depth 2 ascending, … + /// No coordinates are stored; the verifier derives them from `leaf_indexes`. + pub inner_copath: Vec, + /// Leaf indexes that were opened, in ascending order. + pub leaf_indexes: Vec, +} + +impl Valid for ImplicitCoPath

{ + fn check(&self) -> Result<(), SerializationError> { + if self.tree_height < 2 { + return Err(SerializationError::InvalidData); + } + self.leaf_copath.check()?; + self.inner_copath.check()?; + self.leaf_indexes.check() + } +} + +impl CanonicalDeserialize for ImplicitCoPath

{ + fn deserialize_with_mode( + mut reader: R, + compress: Compress, + validate: Validate, + ) -> Result { + let tree_height = usize::deserialize_with_mode(&mut reader, compress, validate)?; + let leaf_copath = + Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let inner_copath = + Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + let leaf_indexes = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; + if tree_height < 2 { + return Err(SerializationError::InvalidData); + } + Ok(ImplicitCoPath { + tree_height, + leaf_copath, + inner_copath, + leaf_indexes, + }) + } +} + +impl ImplicitCoPath

{ + /// Verify that the leaves (supplied in `leaf_indexes` order) are at the claimed positions in + /// the tree with root `root_hash` and height `expected_tree_height`. + /// + /// The verifier independently reconstructs the canonical copath order from `leaf_indexes` and + /// `tree_height`, then consumes `inner_copath` in that order. If the digest count does not + /// match what the verifier derives, verification returns `Ok(false)`. + pub fn verify + Clone>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + expected_tree_height: usize, + leaves: impl IntoIterator, + ) -> Result { + if self.leaf_indexes.is_empty() { + return Err(Error::GenericError(ark_std::boxed::Box::new( + ark_std::io::Error::new( + ark_std::io::ErrorKind::InvalidInput, + "batch proof must contain at least one leaf index", + ), + ))); + } + + if self.tree_height < 2 { + return Err(Error::GenericError(ark_std::boxed::Box::new( + ark_std::io::Error::new( + ark_std::io::ErrorKind::InvalidInput, + "tree_height must be >= 2", + ), + ))); + } + + if self.tree_height != expected_tree_height { + return Ok(false); + } + + let d = self.tree_height; + let leaf_depth = d - 1; + + let mut leaves_iter = leaves.into_iter(); + let mut leaf_level = + CoPath::

::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params)?; + + let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); + let on_path = compute_on_path(leaf_depth, &index_set); + + let expected_leaf_coset = CoPath::

::expected_leaf_coset(leaf_depth, &on_path); + if !CoPath::

::validate_leaf_copath( + &expected_leaf_coset, + &self.leaf_copath, + &mut leaf_level, + ) { + return Ok(false); + } + + let mut inner_levels: Vec> = + (0..d).map(|_| BTreeMap::new()).collect(); + let mut hash_lut: HashMap = + HashMap::with_hasher(BuildHasherDefault::::default()); + + // Consume inner_copath in canonical order: depths 1..leaf_depth, ascending index. + let mut cursor = 0usize; + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[depth].binary_search(&sibling_idx).is_err() { + let digest = match self.inner_copath.get(cursor) { + Some(d) => d, + None => return Ok(false), // prover sent fewer digests than expected + }; + cursor += 1; + inner_levels[depth].insert(sibling_idx, digest.clone()); + let heap_idx = level_index(depth, sibling_idx); + hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); + } + } + } + + // Reject if prover sent more digests than we expected. + if cursor != self.inner_copath.len() { + return Ok(false); + } + + if !CoPath::

::recompute_bottom_parents( + leaf_depth, + &on_path, + &leaf_level, + two_to_one_params, + &mut hash_lut, + &mut inner_levels, + )? { + return Ok(false); + } + + if !CoPath::

::recompute_inner_layers( + leaf_depth, + &on_path, + two_to_one_params, + &mut hash_lut, + &mut inner_levels, + )? { + return Ok(false); + } + + match inner_levels[0].get(&0) { + Some(h) => Ok(h == root_hash), + None => Ok(false), + } + } +} diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 56aff343..e1c40624 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -39,7 +39,7 @@ pub mod legacy; target_has_atomic = "64", target_has_atomic = "ptr" ))] -type DefaultHasher = ahash::AHasher; +pub(super) type DefaultHasher = ahash::AHasher; #[cfg(not(all( target_has_atomic = "8", @@ -48,7 +48,9 @@ type DefaultHasher = ahash::AHasher; target_has_atomic = "64", target_has_atomic = "ptr" )))] -type DefaultHasher = fnv::FnvHasher; +pub(super) type DefaultHasher = fnv::FnvHasher; + +pub mod implicit; type PackedInnerCopath

= (usize, usize, Vec, Vec<

::InnerDigest>); @@ -485,7 +487,7 @@ impl CoPath

{ } /// Hashes provided leaves (ordered by `leaf_indexes`) and returns a map from leaf index to digest. - fn ingest_leaves( + pub(super) fn ingest_leaves( leaf_indexes: &[usize], leaves: &mut I, leaf_hash_params: &LeafParam

, @@ -509,7 +511,7 @@ impl CoPath

{ } /// Computes the minimal leaf-layer copath indices `B*_{d-1}` (siblings of on-path nodes not on-path). - fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { + pub(super) fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { let mut expected_leaf_coset: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; @@ -522,7 +524,7 @@ impl CoPath

{ } /// Confirms provided leaf copath matches the expected indices and augments `leaf_level` with them. - fn validate_leaf_copath( + pub(super) fn validate_leaf_copath( expected_leaf_coset: &[usize], provided_leaf_copath: &[P::LeafDigest], leaf_level: &mut BTreeMap, @@ -543,7 +545,7 @@ impl CoPath

{ } /// Recomputes parents at depth `d-2` (immediately above leaves) using the leaf digests and LUT. - fn recompute_bottom_parents( + pub(super) fn recompute_bottom_parents( leaf_depth: usize, on_path: &[Vec], leaf_level: &BTreeMap, @@ -572,7 +574,7 @@ impl CoPath

{ } /// Recomputes inner layers up to the root using cached inner digests and stores results in LUT. - fn recompute_inner_layers( + pub(super) fn recompute_inner_layers( leaf_depth: usize, on_path: &[Vec], two_to_one_params: &TwoToOneParam

, @@ -1033,6 +1035,75 @@ impl MerkleTree

{ }) } + /// Returns an [`ImplicitCoPath`] for the given leaf indexes. + /// + /// This is a coordinate-free variant of [`Self::generate_multi_proof`]: both prover and + /// verifier independently derive the positions of required copath nodes from `leaf_indexes` + /// and `tree_height`, so only the digests are transmitted (in canonical depth-then-index + /// order). The proof is strictly smaller than a `CoPath` for any tree with more than one + /// inner layer, since no coordinate metadata is included. + pub fn generate_implicit_multi_proof( + &self, + indexes: impl IntoIterator, + ) -> Result, crate::Error> { + use ark_std::collections::BTreeSet; + let indexes: BTreeSet = indexes.into_iter().collect(); + let d = self.height(); + + if indexes.is_empty() { + return Ok(implicit::ImplicitCoPath { + tree_height: d, + leaf_copath: Vec::new(), + inner_copath: Vec::new(), + leaf_indexes: Vec::new(), + }); + } + + let leaf_depth = d - 1; + let on_path = compute_on_path(leaf_depth, &indexes); + + // leaf layer — identical protocol to generate_multi_proof + let mut leaf_coset_ids: Vec = Vec::new(); + for &path_idx in on_path[leaf_depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { + leaf_coset_ids.push(sibling_idx); + } + } + leaf_coset_ids.sort_unstable(); + + let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); + for sibling_idx in leaf_coset_ids { + let digest = self + .leaf_nodes + .get(sibling_idx) + .ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_nodes.len()))?; + leaf_copath.push(digest.clone()); + } + + // inner layers: canonical order = depths 1..leaf_depth, ascending index within each depth + let mut inner_copath: Vec = Vec::new(); + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[depth].binary_search(&sibling_idx).is_err() { + let heap_idx = level_index(depth, sibling_idx); + let digest = self.non_leaf_nodes.get(heap_idx).ok_or_else(|| { + crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()) + })?; + inner_copath.push(digest.clone()); + } + } + } + + Ok(implicit::ImplicitCoPath { + tree_height: d, + leaf_copath, + inner_copath, + leaf_indexes: Vec::from_iter(indexes), + }) + } + /// Given the index and new leaf, return the hash of leaf and an updated path in order from root to bottom non-leaf level. /// This does not mutate the underlying tree. fn updated_path>( @@ -1146,7 +1217,7 @@ fn tree_height(num_leaves: usize) -> usize { /// Return level-order index encoded in global heap. /// Node at `depth` (root=0) and position `pos` (0-based at that depth) -> heap index `(1< usize { +pub(super) fn level_index(depth: usize, pos: usize) -> usize { ((1usize << depth) - 1) + pos } /// Returns true iff the index represents the root. @@ -1247,7 +1318,7 @@ fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { /// Implementation detail: /// * Uses sorted `Vec` per level to keep the hot loops linear and cache-friendly. /// * Each leaf contributes one index per depth; we divide by 2 as we walk up and then sort+dedup. -fn compute_on_path( +pub(super) fn compute_on_path( depth_leaves: usize, indexes: &ark_std::collections::BTreeSet, ) -> Vec> { diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs index 999fc420..d585b9c7 100644 --- a/crypto-primitives/src/merkle_tree/tests/bench_report.rs +++ b/crypto-primitives/src/merkle_tree/tests/bench_report.rs @@ -2,6 +2,7 @@ use super::super::{decode_delta, encode_varint}; use crate::merkle_tree::{ + implicit::ImplicitCoPath, legacy, tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, LeafParam, MerkleTree, TwoToOneParam, }; @@ -53,6 +54,8 @@ const TREE_EXPONENTS: &[u32] = &[12, 14, 16, 18, 20]; const BATCH_SIZES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; const LEAF_WIDTH: usize = 3; const COORD_TREE_EXPONENTS: &[u32] = &[18]; +const IMPLICIT_COMPARE_TREE_EXPONENTS: &[u32] = &[20]; +const PRESENTATION_BATCH_SIZES: &[usize] = &[1, 8, 64, 512, 4096]; #[derive(Clone, Copy)] enum CoordinateEncoding { @@ -161,6 +164,16 @@ impl ProofStats for CoPath

{ } } +impl ProofStats for ImplicitCoPath

{ + fn opened(&self) -> usize { + self.leaf_indexes.len() + } + + fn total_nodes(&self) -> usize { + self.leaf_copath.len() + self.inner_copath.len() + } +} + impl ProofStats for legacy::MultiPath

{ fn opened(&self) -> usize { self.leaf_indexes.len() @@ -1273,3 +1286,404 @@ impl TreeFixture { (self.leaves.len() as f64).log2().round() as u32 } } + +// --------------------------------------------------------------------------- +// Implicit vs CoSet benchmark +// --------------------------------------------------------------------------- + +#[test] +#[ignore] +fn implicit_vs_coset_report() { + run_implicit_vs_coset_report().expect("implicit vs coset report must succeed"); +} + +fn run_implicit_vs_coset_report() -> Result<(), Box> { + let mut fixtures = Vec::new(); + for &exp in IMPLICIT_COMPARE_TREE_EXPONENTS { + fixtures.push(build_fixture(exp)?); + } + + let patterns = [IndexPattern::Random, IndexPattern::Clustered]; + let mut rows = Vec::new(); + + for fixture in fixtures.iter() { + for &batch in PRESENTATION_BATCH_SIZES { + if batch > fixture.leaves.len() { + continue; + } + for &pattern in &patterns { + let mut scenario_rng = StdRng::seed_from_u64( + 0x1A1B_1C17_u64 + ^ ((fixture.leaves.len() as u64) << 16) + ^ ((batch as u64) << 2) + ^ pattern.id(), + ); + let indexes = + sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); + rows.extend(run_implicit_scenario(fixture, batch, pattern, &indexes)?); + } + } + } + + let report_dir = PathBuf::from("target/merkle_tree_reports"); + fs::create_dir_all(&report_dir)?; + let plot_files = write_implicit_vs_coset_plots(&rows, &report_dir)?; + write_implicit_vs_coset_report(&rows, &report_dir, &plot_files)?; + write_implicit_vs_coset_rows_csv(&rows, &report_dir)?; + Ok(()) +} + +fn run_implicit_scenario( + fixture: &TreeFixture, + batch: usize, + pattern: IndexPattern, + indexes: &[usize], +) -> Result<[ReportRow; 2], Box> { + let root = fixture.tree.root(); + let opened_leaves: Vec> = indexes.iter().map(|&i| fixture.leaves[i].clone()).collect(); + let repeats = implicit_compare_repetitions(batch); + + let coset_row = benchmark_strategy_repeated( + "coset", + || fixture.tree.generate_multi_proof(indexes.iter().copied()), + |proof: &CoPath<_>, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + fixture.tree.height(), + leaves, + ) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + repeats, + )?; + + let implicit_row = benchmark_strategy_repeated( + "implicit", + || { + fixture + .tree + .generate_implicit_multi_proof(indexes.iter().copied()) + }, + |proof: &ImplicitCoPath<_>, leaves| { + proof.verify( + &fixture.leaf_params, + &fixture.two_to_one_params, + &root, + fixture.tree.height(), + leaves, + ) + }, + &opened_leaves, + fixture.leaves.len(), + fixture.log2_size(), + batch, + pattern, + repeats, + )?; + + Ok([coset_row, implicit_row]) +} + +fn implicit_compare_repetitions(batch: usize) -> usize { + match batch { + 0..=8 => 200, + 9..=64 => 100, + 65..=512 => 20, + _ => 3, + } +} + +fn benchmark_strategy_repeated( + strategy: &'static str, + mut generator: Gen, + mut verifier: Verify, + opened_leaves: &[Vec], + tree_size: usize, + log2_size: u32, + batch: usize, + pattern: IndexPattern, + repetitions: usize, +) -> Result> +where + Proof: CanonicalSerialize + ProofStats, + Gen: FnMut() -> Result, + Verify: FnMut(&Proof, Vec>) -> Result, +{ + let repetitions = repetitions.max(1); + + let rss_before = rss_bytes(); + let prove_start = std::time::Instant::now(); + let mut proof = None; + for _ in 0..repetitions { + proof = Some(generator()?); + } + let prove_elapsed = prove_start.elapsed(); + let prove_rss = rss_delta_kb(rss_before, rss_bytes()); + let proof = proof.expect("repeated benchmark must generate at least one proof"); + + let proof_bytes = serialized_size(&proof); + let proof_nodes = proof.total_nodes(); + let opened = proof.opened().max(1); + let hashes_per_opening = proof_nodes as f64 / opened as f64; + + let rss_before_verify = rss_bytes(); + let verify_start = std::time::Instant::now(); + for _ in 0..repetitions { + let verify_ok = verifier(&proof, opened_leaves.to_vec())?; + assert!( + verify_ok, + "verification must succeed for {} (tree_n={}, batch={}, pattern={})", + strategy, + tree_size, + batch, + pattern.label() + ); + } + let verify_elapsed = verify_start.elapsed(); + let verify_rss = rss_delta_kb(rss_before_verify, rss_bytes()); + + Ok(ReportRow { + tree_size, + log2_size, + batch, + pattern: pattern.label(), + strategy, + proof_bytes, + proof_nodes, + hashes_per_opening, + prove_ms: duration_ms(prove_elapsed) / repetitions as f64, + verify_ms: duration_ms(verify_elapsed) / repetitions as f64, + rss_delta_kb: combine_rss(prove_rss, verify_rss), + }) +} + +fn write_implicit_vs_coset_plots( + rows: &[ReportRow], + report_dir: &Path, +) -> Result>, Box> { + let mut outputs: BTreeMap<&'static str, Vec> = BTreeMap::new(); + + for metric in PLOT_METRICS { + let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = + BTreeMap::new(); + + for row in rows { + if let Some(value) = (metric.value)(row) { + grouped + .entry((row.tree_size, row.pattern)) + .or_default() + .entry(row.strategy) + .or_default() + .push((row.batch as f64, value)); + } + } + + let mut generated = Vec::new(); + for ((tree_size, pattern), strategies) in grouped { + let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); + for &name in &["coset", "implicit"] { + if let Some(mut series) = strategies.get(name).cloned() { + if series.is_empty() { + continue; + } + series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + ordered_series.push((name, series)); + } + } + + if ordered_series.len() < 2 { + continue; + } + + let mut min_x = f64::MAX; + let mut max_x = f64::MIN; + let mut min_y = f64::MAX; + let mut max_y = f64::MIN; + for (_, series) in &ordered_series { + for &(x, y) in series { + min_x = min_x.min(x); + max_x = max_x.max(x); + min_y = min_y.min(y); + max_y = max_y.max(y); + } + } + if min_x == f64::MAX || min_y == f64::MAX { + continue; + } + + let x_pad = ((max_x - min_x) * 0.06).max(8.0); + let y_pad = ((max_y - min_y) * 0.10).max(256.0); + + let filename = format!( + "implicit_vs_coset_{}_{}_{}.svg", + metric.filename_prefix, tree_size, pattern + ); + let filepath = report_dir.join(&filename); + let filepath_str = filepath.to_string_lossy().to_string(); + let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); + drawing_area.fill(&WHITE)?; + + let mut chart = ChartBuilder::on(&drawing_area) + .margin(28) + .x_label_area_size(64) + .y_label_area_size(96) + .build_cartesian_2d( + (min_x - x_pad)..(max_x + x_pad), + (min_y - y_pad)..(max_y + y_pad), + )?; + + chart + .configure_mesh() + .x_desc("input size k (opened leaves)") + .y_desc(metric.y_label) + .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) + .label_style(("Helvetica Neue", 18).into_font()) + .light_line_style(WHITE.mix(0.0)) + .draw()?; + + for (name, series) in &ordered_series { + let color = implicit_strategy_color(name); + chart + .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? + .label(implicit_strategy_label(name)) + .legend({ + let color = color.clone(); + move |(x, y)| { + PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) + } + }); + + chart.draw_series( + series + .iter() + .map(|point| Circle::new(*point, 5, color.filled())), + )?; + } + + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperLeft) + .background_style(WHITE.mix(0.85)) + .border_style(BLACK) + .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) + .draw()?; + + generated.push(filename); + } + + outputs.insert(metric.name, generated); + } + + Ok(outputs) +} + +fn write_implicit_vs_coset_report( + rows: &[ReportRow], + report_dir: &Path, + plot_files: &BTreeMap<&'static str, Vec>, +) -> Result<(), Box> { + let report_path = report_dir.join("implicit_vs_coset_report.md"); + let mut file = File::create(&report_path)?; + + writeln!(file, "# Implicit vs CoSet Benchmark Report")?; + writeln!( + file, + "\nGenerated: {:?}\n", + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? + )?; + writeln!( + file, + "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms |" + )?; + writeln!( + file, + "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- |" + )?; + + for row in rows { + writeln!( + file, + "| {} | {} | {} | {} | {} | {} | {} | {:.2} | {:.2} | {:.2} |", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + row.hashes_per_opening, + row.prove_ms, + row.verify_ms, + )?; + } + + for (metric, files) in plot_files { + if files.is_empty() { + continue; + } + writeln!(file, "\n## {} Visualizations\n", metric)?; + for plot in files { + writeln!( + file, + "![{}]({})", + metric.replace(' ', "-").to_lowercase(), + plot + )?; + } + } + + Ok(()) +} + +fn write_implicit_vs_coset_rows_csv( + rows: &[ReportRow], + report_dir: &Path, +) -> Result<(), Box> { + let csv_path = report_dir.join("implicit_vs_coset_rows.csv"); + let mut file = File::create(&csv_path)?; + writeln!( + file, + "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes,hashes_per_opening,prove_ms,verify_ms" + )?; + + for row in rows { + writeln!( + file, + "{},{},{},{},{},{},{},{:.6},{:.6},{:.6}", + row.tree_size, + row.log2_size, + row.batch, + row.pattern, + row.strategy, + row.proof_bytes, + row.proof_nodes, + row.hashes_per_opening, + row.prove_ms, + row.verify_ms, + )?; + } + + Ok(()) +} + +fn implicit_strategy_label(name: &str) -> &str { + match name { + "coset" => "CoSet (with coords)", + "implicit" => "Implicit (coord-free)", + _ => name, + } +} + +fn implicit_strategy_color(name: &str) -> RGBColor { + match name { + "coset" => RGBColor(31, 119, 180), // blue — same as STRATEGY_STYLE["coset"] in Python + "implicit" => RGBColor(123, 53, 193), // purple + _ => BLACK, + } +} diff --git a/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs b/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs new file mode 100644 index 00000000..78d45a7a --- /dev/null +++ b/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs @@ -0,0 +1,198 @@ +use crate::{ + crh::poseidon, + merkle_tree::{ + tests::test_utils::poseidon_parameters, + Config, IdentityDigestConverter, MerkleTree, + }, +}; +use ark_std::{test_rng, One, UniformRand}; + +type F = ark_ed_on_bls12_381::Fr; +type H = poseidon::CRH; +type TwoToOneH = poseidon::TwoToOneCRH; + +struct FieldMTConfig; +impl Config for FieldMTConfig { + type Leaf = [F]; + type LeafDigest = F; + type LeafInnerDigestConverter = IdentityDigestConverter; + type InnerDigest = F; + type LeafHash = H; + type TwoToOneHash = TwoToOneH; +} +type FieldMT = MerkleTree; + +fn make_tree(num_leaves: usize) -> (FieldMT, Vec>) { + let mut rng = test_rng(); + let params = poseidon_parameters(); + let leaves: Vec> = (0..num_leaves) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); + let tree = FieldMT::new(¶ms, ¶ms, &leaves).unwrap(); + (tree, leaves) +} + +#[test] +fn implicit_proof_verifies_single_leaf() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + for i in 0..leaves.len() { + let proof = tree.generate_implicit_multi_proof([i]).unwrap(); + let ok = proof + .verify(¶ms, ¶ms, &root, tree.height(), [leaves[i].clone()]) + .unwrap(); + assert!(ok, "single-leaf implicit proof must verify for index {i}"); + } +} + +#[test] +fn implicit_proof_verifies_full_batch() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree + .generate_implicit_multi_proof(0..leaves.len()) + .unwrap(); + assert!( + proof + .verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()) + .unwrap(), + "full-batch implicit proof must verify" + ); + // full batch: no inner copath elements needed — every sibling is on-path + assert_eq!( + proof.inner_copath.len(), + 0, + "full-batch inner copath must be empty" + ); +} + +#[test] +fn implicit_proof_matches_coset_proof_node_count() { + // The number of digests transmitted should be identical to the CoPath counterpart, + // since both encode the same copath set — just without coordinates in ImplicitCoPath. + let (tree, _leaves) = make_tree(32); + for idxs in &[ + vec![0usize, 1, 2, 3], + vec![0, 7, 15, 31], + vec![5, 11, 23], + ] { + let implicit = tree.generate_implicit_multi_proof(idxs.iter().copied()).unwrap(); + let coset = tree.generate_multi_proof(idxs.iter().copied()).unwrap(); + + let coset_inner_nodes = coset + .inner_copath + .as_ref() + .map(|(_, _, _, d)| d.len()) + .unwrap_or(0); + + assert_eq!( + implicit.inner_copath.len(), + coset_inner_nodes, + "inner node count must match for indexes {:?}", + idxs + ); + assert_eq!( + implicit.leaf_copath.len(), + coset.leaf_copath.len(), + "leaf copath length must match for indexes {:?}", + idxs + ); + } +} + +#[test] +fn implicit_proof_wrong_root_fails() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let wrong_root = tree.root() + F::one(); + + let proof = tree.generate_implicit_multi_proof([2usize, 5]).unwrap(); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = proof + .verify(¶ms, ¶ms, &wrong_root, tree.height(), opened) + .unwrap(); + assert!(!ok, "wrong root must fail verification"); +} + +#[test] +fn implicit_proof_tampered_inner_digest_fails() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_implicit_multi_proof([1usize, 6, 10]).unwrap(); + let mut bad = proof.clone(); + if let Some(d) = bad.inner_copath.get_mut(0) { + *d += F::one(); + } + let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); + assert!(!ok, "tampered inner digest must fail verification"); +} + +#[test] +fn implicit_proof_extra_digest_fails() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_implicit_multi_proof([3usize, 9]).unwrap(); + let mut bad = proof.clone(); + bad.inner_copath.push(F::one()); // one spurious digest + let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); + assert!(!ok, "extra inner digest must fail verification"); +} + +#[test] +fn implicit_proof_missing_inner_digest_fails() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_implicit_multi_proof([2usize, 5, 9]).unwrap(); + let mut bad = proof.clone(); + if !bad.inner_copath.is_empty() { + bad.inner_copath.pop(); + } + let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); + assert!(!ok, "missing inner digest must fail verification"); +} + +#[test] +fn implicit_proof_wrong_tree_height_fails() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_implicit_multi_proof([0usize, 3]).unwrap(); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + // supply wrong expected height + let ok = proof + .verify(¶ms, ¶ms, &root, tree.height() + 1, opened) + .unwrap(); + assert!(!ok, "mismatched tree height must fail verification"); +} + +#[test] +fn implicit_proof_duplicate_indices_deduped() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree + .generate_implicit_multi_proof([2usize, 2, 5, 5, 5]) + .unwrap(); + assert_eq!(proof.leaf_indexes, vec![2, 5], "duplicates must be deduped"); + + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "deduped implicit proof must verify" + ); +} diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index 603d3986..efc6d618 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "constraints")] mod constraints; mod delta_encoding_tests; +mod implicit_copath_tests; mod test_utils; #[cfg(all(test, feature = "bench_harness"))] From ad0dabe00b944c69dbffcde8bdb880e6097adcd4 Mon Sep 17 00:00:00 2001 From: ajhavlin <116843349+ajhavlin@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:18:20 +0200 Subject: [PATCH 17/22] chore: remove benchmark harness, legacy MultiPath, and presentation scripts Strip all benchmark-specific code from the merkle_tree module: - Remove legacy.rs (MultiPath front-incremental encoding, bench_harness only) - Remove tests/bench_report.rs (encoding comparison benchmarks) - Remove scripts/ (presentation plot generators) - Remove bench_harness feature flag and plotters dev-dependency - Fix multiproof_empty_batch test to expect Err (caller error per spec) The coordinate-based CoPath and coordinate-free ImplicitCoPath remain intact for production use. Co-Authored-By: Claude Opus 4.6 --- crypto-primitives/Cargo.toml | 2 - .../scripts/presentation_implicit_plots.py | 320 ---- crypto-primitives/src/merkle_tree/legacy.rs | 814 -------- crypto-primitives/src/merkle_tree/mod.rs | 3 - .../src/merkle_tree/tests/bench_report.rs | 1689 ----------------- .../src/merkle_tree/tests/mod.rs | 11 +- scripts/build_results_pack.py | 495 ----- scripts/linkify_changelog.py | 30 - scripts/presentation_coord_encoding_plots.py | 436 ----- scripts/presentation_plots.py | 560 ------ 10 files changed, 4 insertions(+), 4356 deletions(-) delete mode 100644 crypto-primitives/scripts/presentation_implicit_plots.py delete mode 100644 crypto-primitives/src/merkle_tree/legacy.rs delete mode 100644 crypto-primitives/src/merkle_tree/tests/bench_report.rs delete mode 100644 scripts/build_results_pack.py delete mode 100644 scripts/linkify_changelog.py delete mode 100644 scripts/presentation_coord_encoding_plots.py delete mode 100644 scripts/presentation_plots.py diff --git a/crypto-primitives/Cargo.toml b/crypto-primitives/Cargo.toml index 70036e4a..87c4046c 100644 --- a/crypto-primitives/Cargo.toml +++ b/crypto-primitives/Cargo.toml @@ -54,7 +54,6 @@ crh = ["sponge"] sponge = ["merlin"] commitment = ["crh"] merkle_tree = ["crh", "hashbrown"] -bench_harness = [] encryption = [] prf = [] snark = [] @@ -75,7 +74,6 @@ ark-bls12-381 = { git = "https://github.com/arkworks-rs/algebra", default-featur ark-mnt4-298 = { git = "https://github.com/arkworks-rs/algebra", default-features = false, features = [ "curve", "r1cs" ] } ark-mnt6-298 = { git = "https://github.com/arkworks-rs/algebra", default-features = false, features = [ "r1cs" ] } criterion = { version = "0.6" } -plotters = { version = "0.3", default-features = false, features = ["svg_backend", "line_series", "ttf"] } ################################# Benchmarks ################################## diff --git a/crypto-primitives/scripts/presentation_implicit_plots.py b/crypto-primitives/scripts/presentation_implicit_plots.py deleted file mode 100644 index f88769c4..00000000 --- a/crypto-primitives/scripts/presentation_implicit_plots.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/env python3 -"""Generate presentation-style figures comparing CoSet (with coordinates) vs Implicit (coord-free). - -Data flow: -1) Run the Rust benchmark test: - cargo test --features bench_harness,merkle_tree -- --ignored --nocapture implicit_vs_coset_report -2) This writes target/merkle_tree_reports/implicit_vs_coset_rows.csv -3) Run this script: - python3 scripts/presentation_implicit_plots.py - Output goes to figures/presentation/ as PNG, PDF, and SVG. - -The script generates one figure per metric (proof_bytes, prove_ms, verify_ms) per pattern -(clustered, random), with both strategies on the same axes in different colours. -""" - -from __future__ import annotations - -import argparse -import csv -import os -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Tuple - -os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") - -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker - - -# --------------------------------------------------------------------------- -# Data model -# --------------------------------------------------------------------------- - -@dataclass(frozen=True) -class Row: - tree_size: int - log2_size: int - batch: int - pattern: str - strategy: str - proof_bytes: int - proof_nodes: int - hashes_per_opening: float - prove_ms: float - verify_ms: float - - -@dataclass(frozen=True) -class MetricSpec: - key: str - title: str - y_label: str - filename_prefix: str - - -METRICS: Tuple[MetricSpec, ...] = ( - MetricSpec("proof_bytes", "Proof Size vs Input Size", "Proof size (kB)", "proof_size"), - MetricSpec("prove_ms", "Prover Time vs Input Size", "Time (ms)", "prover_time"), - MetricSpec("verify_ms", "Verifier Time vs Input Size", "Time (ms)", "verifier_time"), -) - -# Colour and marker choices: -# coset — blue (same shade used in presentation_plots.py for "New/coset") -# implicit — purple (distinct; dark enough to remain legible in greyscale) -STRATEGY_STYLE: Dict[str, dict] = { - "coset": { - "label": "CoSet (delta coords)", - "color": "#1f77b4", - "marker": "o", - "zorder": 3, - }, - "implicit": { - "label": "Implicit (coord-free)", - "color": "#7b35c1", - "marker": "^", - "zorder": 4, - }, -} - -DRAW_ORDER = ["coset", "implicit"] -PRESENTATION_X_TICKS = [1, 8, 64, 512, 4096] - - -def ensure_slide_style() -> None: - plt.rcParams.update( - { - "font.family": "Helvetica Neue", - "svg.fonttype": "none", - "figure.titlesize": 30, - "axes.titlesize": 28, - "axes.labelsize": 24, - "xtick.labelsize": 18, - "ytick.labelsize": 18, - "legend.fontsize": 17, - } - ) - - -# --------------------------------------------------------------------------- -# Argument parsing -# --------------------------------------------------------------------------- - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--input", - type=Path, - default=Path("target/merkle_tree_reports/implicit_vs_coset_rows.csv"), - help="CSV produced by the implicit_vs_coset_report bench test", - ) - parser.add_argument( - "--output", - type=Path, - default=Path("figures/presentation"), - help="Directory where figures are written", - ) - parser.add_argument( - "--patterns", - nargs="+", - default=["clustered", "random"], - help="Query patterns to render (default: clustered random)", - ) - parser.add_argument( - "--tree-size", - type=int, - default=None, - help="Specific tree size n to plot. Default: largest n in the dataset", - ) - return parser.parse_args() - - -# --------------------------------------------------------------------------- -# CSV loading -# --------------------------------------------------------------------------- - -def load_csv(path: Path) -> List[Row]: - rows: List[Row] = [] - with path.open(newline="") as f: - reader = csv.DictReader(f) - for raw in reader: - rows.append( - Row( - tree_size=int(raw["tree_size"]), - log2_size=int(raw["log2_size"]), - batch=int(raw["batch"]), - pattern=raw["pattern"], - strategy=raw["strategy"], - proof_bytes=int(raw["proof_bytes"]), - proof_nodes=int(raw["proof_nodes"]), - hashes_per_opening=float(raw["hashes_per_opening"]), - prove_ms=float(raw["prove_ms"]), - verify_ms=float(raw["verify_ms"]), - ) - ) - return rows - - -# --------------------------------------------------------------------------- -# Plotting -# --------------------------------------------------------------------------- - -def _get_metric_value(row: Row, metric: MetricSpec) -> float: - value = float(getattr(row, metric.key)) - if metric.key == "proof_bytes": - return value / 1024.0 - return value - - -def _format_y_tick(metric: MetricSpec) -> ticker.Formatter: - if metric.key == "proof_bytes": - return ticker.FuncFormatter( - lambda v, _: f"{v:,.0f}" if v >= 100 else f"{v:,.1f}".rstrip("0").rstrip(".") - ) - return ticker.FuncFormatter( - lambda v, _: f"{v:,.2f}".rstrip("0").rstrip(".") if abs(v) < 100 else f"{v:,.0f}" - ) - - -def plot_metric_pattern( - rows: List[Row], - metric: MetricSpec, - pattern: str, - tree_size: int, - out_dir: Path, -) -> List[Path]: - """Render one figure for (metric, pattern) and write PNG/PDF/SVG.""" - ensure_slide_style() - - subset = [ - r for r in rows if r.pattern == pattern and r.tree_size == tree_size - ] - if not subset: - return [] - - # Group by strategy - series: Dict[str, List[Tuple[int, float]]] = defaultdict(list) - for row in subset: - if row.batch in PRESENTATION_X_TICKS: - series[row.strategy].append((row.batch, _get_metric_value(row, metric))) - - for strat in series: - series[strat].sort() - - fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) - available_x_values = sorted({batch for points in series.values() for batch, _ in points}) - x_ticks = [x for x in PRESENTATION_X_TICKS if x in available_x_values] - - if not x_ticks: - plt.close(fig) - return [] - - for strat in DRAW_ORDER: - if strat not in series: - continue - style = STRATEGY_STYLE[strat] - xs = [p[0] for p in series[strat]] - ys = [p[1] for p in series[strat]] - ax.plot( - xs, - ys, - color=style["color"], - marker=style["marker"], - label=style["label"], - linewidth=3.0, - markersize=9, - zorder=style["zorder"], - ) - - all_points = [(x, y) for points in series.values() for x, y in points] - y_values = [y for _, y in all_points] - ymin = min(y_values) - ymax = max(y_values) - yrange = max(ymax - ymin, ymax * 0.08, 1e-9) - - ax.set_xlabel( - "Number of leaves opened", - labelpad=10, - fontname="Helvetica Neue", - fontweight="bold", - ) - ax.set_ylabel( - metric.y_label, - labelpad=10, - fontname="Helvetica Neue", - fontweight="bold", - ) - ax.set_xscale("log", base=2) - ax.set_xticks(x_ticks) - ax.set_xticklabels([str(x) for x in x_ticks]) - ax.set_xlim(min(x_ticks) * 0.9, max(x_ticks) * 1.15) - ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) - ax.xaxis.set_minor_locator(ticker.NullLocator()) - ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins=6)) - ax.yaxis.set_major_formatter(_format_y_tick(metric)) - legend = ax.legend( - loc="upper left", - bbox_to_anchor=(1.02, 1.0), - borderaxespad=0.0, - frameon=False, - ) - if legend is not None: - for legend_text in legend.get_texts(): - legend_text.set_fontfamily("Helvetica Neue") - legend_text.set_fontweight("bold") - - for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): - tick_label.set_fontfamily("Helvetica Neue") - - ax.grid(True, which="major", linestyle="--", alpha=0.28) - fig.tight_layout() - - stem = f"implicit_vs_coset_{metric.filename_prefix}_vs_k_{pattern}" - written: List[Path] = [] - for ext in ("png", "pdf", "svg"): - dest = out_dir / f"{stem}.{ext}" - save_kwargs = {"bbox_inches": "tight"} - if ext == "png": - save_kwargs["dpi"] = 300 - fig.savefig(dest, **save_kwargs) - written.append(dest) - - plt.close(fig) - return written - - -def main() -> None: - args = parse_args() - - if not args.input.exists(): - raise SystemExit( - f"Input file not found: {args.input}\n" - "Run the Rust benchmark first:\n" - " cargo test --features bench_harness,merkle_tree -- " - "--ignored --nocapture implicit_vs_coset_report" - ) - - rows = load_csv(args.input) - if not rows: - raise SystemExit(f"No rows found in {args.input}") - - tree_size = args.tree_size or max(r.tree_size for r in rows) - args.output.mkdir(parents=True, exist_ok=True) - - all_written: List[Path] = [] - for metric in METRICS: - for pattern in args.patterns: - written = plot_metric_pattern(rows, metric, pattern, tree_size, args.output) - all_written.extend(written) - - for p in all_written: - print(f"Wrote {p}") - - if not all_written: - print("No figures generated — check that the CSV contains matching rows.") - - -if __name__ == "__main__": - main() diff --git a/crypto-primitives/src/merkle_tree/legacy.rs b/crypto-primitives/src/merkle_tree/legacy.rs deleted file mode 100644 index 000eb4bc..00000000 --- a/crypto-primitives/src/merkle_tree/legacy.rs +++ /dev/null @@ -1,814 +0,0 @@ -#![allow(clippy::needless_range_loop)] - -/// Defines a trait to chain two types of CRHs. -use crate::{ - crh::{CRHScheme, TwoToOneCRHScheme}, - sponge::Absorb, - Error, -}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -#[cfg(not(feature = "std"))] -use ark_std::vec::Vec; -use ark_std::{ - borrow::Borrow, - collections::BTreeSet, - fmt::Debug, - hash::{BuildHasherDefault, Hash}, -}; -use hashbrown::HashMap; -#[cfg(feature = "parallel")] -use rayon::prelude::*; - -#[cfg(feature = "constraints")] -pub mod constraints; - -#[cfg(all( - target_has_atomic = "8", - target_has_atomic = "16", - target_has_atomic = "32", - target_has_atomic = "64", - target_has_atomic = "ptr" -))] -type DefaultHasher = ahash::AHasher; - -#[cfg(not(all( - target_has_atomic = "8", - target_has_atomic = "16", - target_has_atomic = "32", - target_has_atomic = "64", - target_has_atomic = "ptr" -)))] -type DefaultHasher = fnv::FnvHasher; - -/// Convert the hash digest in different layers by converting previous layer's output to -/// `TargetType`, which is a `Borrow` to next layer's input. -pub trait DigestConverter { - type TargetType: Borrow; - fn convert(item: From) -> Result; -} - -/// A trivial converter where digest of previous layer's hash is the same as next layer's input. -pub struct IdentityDigestConverter { - _prev_layer_digest: T, -} - -impl DigestConverter for IdentityDigestConverter { - type TargetType = T; - fn convert(item: T) -> Result { - Ok(item) - } -} - -/// Convert previous layer's digest to bytes and use bytes as input for next layer's digest. -/// TODO: `ToBytes` trait will be deprecated in future versions. -pub struct ByteDigestConverter { - _prev_layer_digest: T, -} - -impl DigestConverter for ByteDigestConverter { - type TargetType = Vec; - - fn convert(item: T) -> Result { - // TODO: In some tests, `serialize` is not consistent with constraints. Try fix those. - Ok(crate::to_uncompressed_bytes!(item)?) - } -} - -/// Merkle tree has two types of hashes. -/// * `LeafHash`: Convert leaf to leaf digest -/// * `TwoToOneHash`: Compress two inner digests to one inner digest -pub trait Config { - type Leaf: ?Sized + Send; // merkle tree does not store the leaf - // leaf layer - type LeafDigest: Clone - + Eq - + Debug - + Hash - + Default - + CanonicalSerialize - + CanonicalDeserialize - + Send - + Sync; - - // transition between leaf layer to inner layer - type LeafInnerDigestConverter: DigestConverter< - Self::LeafDigest, - ::Input, - >; - // inner layer - type InnerDigest: Clone - + Eq - + Debug - + Hash - + Default - + CanonicalSerialize - + CanonicalDeserialize - + Send - + Sync - + Absorb; - - // 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. - - /// leaf -> leaf digest - /// If leaf hash digest and inner hash digest are different, we can create a new - /// leaf hash which wraps the original leaf hash and convert its output to `Digest`. - type LeafHash: CRHScheme; - /// 2 inner digest -> inner digest - type TwoToOneHash: TwoToOneCRHScheme; -} - -pub type TwoToOneParam

= <

::TwoToOneHash as TwoToOneCRHScheme>::Parameters; -pub type LeafParam

= <

::LeafHash as CRHScheme>::Parameters; - -/// Stores the hashes of a particular path (in order) from root to leaf. -/// For example: -/// ```tree_diagram -/// [A] -/// / \ -/// [B] C -/// / \ / \ -/// D [E] F H -/// .. / \ .... -/// [I] J -/// ``` -/// Suppose we want to prove I, then `leaf_sibling_hash` is J, `auth_path` is `[C,D]` -#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] -#[derivative( - PartialEq(bound = "P: Config"), - Clone(bound = "P: Config"), - Debug(bound = "P: Config"), - Default(bound = "P: Config") -)] -pub struct Path { - pub leaf_sibling_hash: P::LeafDigest, - /// The sibling of path node ordered from higher layer to lower layer (does not include root node). - pub auth_path: Vec, - /// stores the leaf index of the node - pub leaf_index: usize, -} - -impl Path

{ - /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. - /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. - /// - /// This function simply converts `self.leaf_index` to boolean array in big endian form. - #[allow(unused)] // this function is actually used when r1cs feature is on - fn position_list(&'_ self) -> impl '_ + Iterator { - (0..self.auth_path.len() + 1) - .map(move |i| ((self.leaf_index >> i) & 1) != 0) - .rev() - } -} - -impl Path

{ - /// Verify that a leaf is at `self.index` of the merkle tree. - /// * `leaf_size`: leaf size in number of bytes - /// - /// `verify` infers the tree height by setting `tree_height = self.auth_path.len() + 2` - pub fn verify>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - leaf: L, - ) -> Result { - // calculate leaf hash - let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf)?; - // check hash along the path from bottom to root - let (left_child, right_child) = - select_left_right_child(self.leaf_index, &claimed_leaf_hash, &self.leaf_sibling_hash)?; - - // leaf layer to inner layer conversion - let left_child = P::LeafInnerDigestConverter::convert(left_child)?; - let right_child = P::LeafInnerDigestConverter::convert(right_child)?; - - let mut curr_path_node = - P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child)?; - - // we will use `index` variable to track the position of path - let mut index = self.leaf_index; - index >>= 1; - - // Check levels between leaf level and root - for level in (0..self.auth_path.len()).rev() { - // check if path node at this level is left or right - let (left, right) = - select_left_right_child(index, &curr_path_node, &self.auth_path[level])?; - // update curr_path_node - curr_path_node = P::TwoToOneHash::compress(&two_to_one_params, &left, &right)?; - index >>= 1; - } - - // check if final hash is root - if &curr_path_node != root_hash { - return Ok(false); - } - - Ok(true) - } -} - -/// Optimized data structure to store multiple nodes proofs. -/// For example: -/// ```tree_diagram -/// [A] -/// / \ -/// [B] C -/// / \ / \ -/// D [E] F H -/// ... / \ / \ .... -/// [I] J L M -/// ``` -/// Suppose we want to prove I and J, then: -/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) -/// `leaf_siblings_hashes`: `[J,I]` -/// `auth_paths_prefix_lenghts`: `[0,2]` -/// `auth_paths_suffixes`: `[ [C,D], []]` -/// We can reconstruct the paths incrementally: -/// First, we reconstruct the first path. The prefix length is 0, hence we do not have any prefix encoding. -/// The path is thus `[C,D]`. -/// Once the first path is verified, we can reconstruct the second path. -/// The prefix length of 2 means that the path prefix will be `previous_path[:2] -> [C,D]`. -/// Since the Merkle Tree branch is the same, the authentication path is the same (which means in this case that there is no suffix). -/// The second path is hence `[C,D] + []` (i.e., plus the empty suffix). We can verify the second path as the first one. - -#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] -#[derivative( - Clone(bound = "P: Config"), - Debug(bound = "P: Config"), - Default(bound = "P: Config") -)] -pub struct MultiPath { - /// For node i, stores the hash of node i's sibling - pub leaf_siblings_hashes: Vec, - /// For node i path, stores at index i the prefix length of the path, for Incremental encoding - pub auth_paths_prefix_lenghts: Vec, - /// For node i path, stores at index i the suffix of the path for Incremental Encoding (as vector of symbols to be resolved with self.lut). Order is from higher layer to lower layer (does not include root node). - pub auth_paths_suffixes: Vec>, - /// stores the leaf indexes of the nodes to prove - pub leaf_indexes: Vec, -} - -impl MultiPath

{ - /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. - /// Note that the order of the leaves hashes should match the leaves respective indexes - /// * `leaf_size`: leaf size in number of bytes - /// - /// `verify` infers the tree height by setting `tree_height = self.auth_paths_suffixes[0].len() + 2` - pub fn verify + Clone>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - leaves: impl IntoIterator, - ) -> Result { - let tree_height = self.auth_paths_suffixes[0].len() + 2; - let mut leaves = leaves.into_iter(); - - // LookUp table to speedup computation avoid redundant hash computations - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); - - // init prev path for decoding - let mut prev_path: Vec<_> = self.auth_paths_suffixes[0].clone(); - - for i in 0..self.leaf_indexes.len() { - let leaf_index = self.leaf_indexes[i]; - let leaf = leaves.next().unwrap(); - let leaf_sibling_hash = &self.leaf_siblings_hashes[i]; - - // decode i-th auth path - let auth_path = prefix_decode_path( - &prev_path, - self.auth_paths_prefix_lenghts[i], - &self.auth_paths_suffixes[i], - ); - // update prev path for decoding next one - prev_path = auth_path.clone(); - - let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf.clone())?; - let (left_child, right_child) = - select_left_right_child(leaf_index, &claimed_leaf_hash, &leaf_sibling_hash)?; - // check hash along the path from bottom to root - - // leaf layer to inner layer conversion - let left_child = P::LeafInnerDigestConverter::convert(left_child)?; - let right_child = P::LeafInnerDigestConverter::convert(right_child)?; - - // we will use `index` variable to track the position of path - let mut index = leaf_index; - let mut index_in_tree = convert_index_to_last_level(leaf_index, tree_height); - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - - let mut curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap() - }); - - // Check levels between leaf level and root - for level in (0..auth_path.len()).rev() { - // check if path node at this level is left or right - let (left, right) = - select_left_right_child(index, curr_path_node, &auth_path[level])?; - // update curr_path_node - index >>= 1; - index_in_tree = parent(index_in_tree).unwrap(); - curr_path_node = hash_lut.entry(index_in_tree).or_insert_with(|| { - P::TwoToOneHash::compress(&two_to_one_params, left, right).unwrap() - }); - } - - // check if final hash is root - if curr_path_node != root_hash { - return Ok(false); - } - } - Ok(true) - } - - /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. - /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. - /// - /// This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. - #[allow(unused)] // this function is actually used when r1cs feature is on - fn position_list(&'_ self) -> impl '_ + Iterator> { - let path_len = self.auth_paths_suffixes[0].len(); - - cfg_into_iter!(self.leaf_indexes.clone()) - .map(move |i| { - (0..path_len + 1) - .map(move |j| ((i >> j) & 1) != 0) - .rev() - .collect() - }) - .collect::>() - .into_iter() - } -} - -/// `index` is the first `path.len()` bits of -/// the position of tree. -/// -/// If the least significant bit of `index` is 0, then `sibling` will be left and `computed` will be right. -/// Otherwise, `sibling` will be right and `computed` will be left. -/// -/// Returns: (left, right) -fn select_left_right_child( - index: usize, - computed_hash: &L, - sibling_hash: &L, -) -> Result<(L, L), crate::Error> { - let is_left = index & 1 == 0; - let mut left_child = computed_hash; - let mut right_child = sibling_hash; - if !is_left { - core::mem::swap(&mut left_child, &mut right_child); - } - Ok((left_child.clone(), right_child.clone())) -} - -/// Defines a merkle tree data structure. -/// This merkle tree has runtime fixed height, and assumes number of leaves is 2^height. -/// -/// TODO: add RFC-6962 compatible merkle tree in the future. -/// For this release, padding will not be supported because of security concerns: if the leaf hash and two to one hash uses same underlying -/// CRH, a malicious prover can prove a leaf while the actual node is an inner node. In the future, we can prefix leaf hashes in different layers to -/// solve the problem. -#[derive(Derivative)] -#[derivative(Clone(bound = "P: Config"))] -pub struct MerkleTree { - /// stores the non-leaf nodes in level order. The first element is the root node. - /// The ith nodes (starting at 1st) children are at indices `2*i`, `2*i+1` - non_leaf_nodes: Vec, - /// store the hash of leaf nodes from left to right - leaf_nodes: Vec, - /// Store the inner hash parameters - two_to_one_hash_param: TwoToOneParam

, - /// Store the leaf hash parameters - leaf_hash_param: LeafParam

, - /// Stores the height of the MerkleTree - height: usize, -} - -impl MerkleTree

{ - /// Create an empty merkle tree such that all leaves are zero-filled. - /// Consider using a sparse merkle tree if you need the tree to be low memory - pub fn blank( - leaf_hash_param: &LeafParam

, - two_to_one_hash_param: &TwoToOneParam

, - height: usize, - ) -> Result { - // use empty leaf digest - let leaf_digests = vec![P::LeafDigest::default(); 1 << (height - 1)]; - Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaf_digests) - } - - /// Returns a new merkle tree. `leaves.len()` should be power of two. - pub fn new + Send>( - leaf_hash_param: &LeafParam

, - two_to_one_hash_param: &TwoToOneParam

, - #[cfg(not(feature = "parallel"))] leaves: impl IntoIterator, - #[cfg(feature = "parallel")] leaves: impl IntoParallelIterator, - ) -> Result { - let leaf_digests: Vec<_> = cfg_into_iter!(leaves) - .map(|input| P::LeafHash::evaluate(leaf_hash_param, input.as_ref())) - .collect::, _>>()?; - - Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaf_digests) - } - - pub fn new_with_leaf_digest( - leaf_hash_param: &LeafParam

, - two_to_one_hash_param: &TwoToOneParam

, - leaf_digests: Vec, - ) -> Result { - let leaf_nodes_size = leaf_digests.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 = cfg_into_iter!(0..non_leaf_nodes_size) - .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 the hash values for the non-leaf bottom layer - { - let start_index = level_indices.pop().unwrap(); - let upper_bound = left_child(start_index); - - cfg_iter_mut!(non_leaf_nodes[start_index..upper_bound]) - .enumerate() - .try_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; - - *n = P::TwoToOneHash::evaluate( - two_to_one_hash_param, - P::LeafInnerDigestConverter::convert( - leaf_digests[left_leaf_index].clone(), - )?, - P::LeafInnerDigestConverter::convert( - leaf_digests[right_leaf_index].clone(), - )?, - )?; - Ok::<(), crate::Error>(()) - })?; - } - - // 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); - // Iterate over the nodes at the current level, and compute the hash of each node - cfg_iter_mut!(nodes_at_level[start_index..]) - .enumerate() - .try_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; - - // need for unwrap as Box does not implement trait Send - *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(), - )?; - Ok::<_, crate::Error>(()) - })?; - } - Ok(MerkleTree { - leaf_nodes: leaf_digests, - non_leaf_nodes, - height: tree_height, - leaf_hash_param: leaf_hash_param.clone(), - two_to_one_hash_param: two_to_one_hash_param.clone(), - }) - } - - /// Returns the root of the Merkle tree. - pub fn root(&self) -> P::InnerDigest { - self.non_leaf_nodes[0].clone() - } - - /// Returns the height of the Merkle tree. - pub fn height(&self) -> usize { - self.height - } - - /// Given the `index` of a leaf, returns the digest of its leaf sibling - pub fn get_leaf_sibling_hash(&self, index: usize) -> P::LeafDigest { - if index & 1 == 0 { - // leaf is left child - self.leaf_nodes[index + 1].clone() - } else { - // leaf is right child - self.leaf_nodes[index - 1].clone() - } - } - - /// Returns the authentication path from leaf at `index` to root, as a Vec of digests - fn compute_auth_path(&self, index: usize) -> Vec { - // gather basic tree information - let tree_height = tree_height(self.leaf_nodes.len()); - - // Get Leaf hash, and leaf sibling hash, - let leaf_index_in_tree = convert_index_to_last_level(index, tree_height); - - // path.len() = `tree height - 2`, the two missing elements being the leaf sibling hash and the root - let mut path = Vec::with_capacity(tree_height - 2); - // Iterate from the bottom layer after the leaves, to the top, storing all sibling node's hash values. - let mut current_node = parent(leaf_index_in_tree).unwrap(); - while !is_root(current_node) { - let sibling_node = sibling(current_node).unwrap(); - path.push(self.non_leaf_nodes[sibling_node].clone()); - current_node = parent(current_node).unwrap(); - } - - debug_assert_eq!(path.len(), tree_height - 2); - - // we want to make path from root to bottom - path.reverse(); - path - } - - /// Returns the authentication path from leaf at `index` to root. - pub fn generate_proof(&self, index: usize) -> Result, crate::Error> { - let path = self.compute_auth_path(index); - Ok(Path { - leaf_index: index, - auth_path: path, - leaf_sibling_hash: self.get_leaf_sibling_hash(index), - }) - } - - /// Returns a MultiPath (multiple authentication paths in compressed form, with Front Incremental Encoding), - /// from every leaf to root. - /// Note that for compression efficiency, the indexes are internally sorted. - /// For sorted indexes, MultiPath contains: - /// `2*( (num_leaves.log2()-1).pow(2) - (num_leaves.log2()-2) )` - /// instead of - /// `num_leaves*(num_leaves.log2()-1)` - /// When verifying the proof, leaves hashes should be supplied in order, that is: - /// ```ignore - /// let ordered_leaves: Vec<_> = self.leaf_indexes.into_iter().map(|i| leaves[i]).collect(); - /// ``` - pub fn generate_multi_proof( - &self, - indexes: impl IntoIterator, - ) -> Result, crate::Error> { - // pruned and sorted for encoding efficiency - let indexes: BTreeSet = indexes.into_iter().collect(); - - //let auth_paths = Vec::with_capacity(indexes.len()); - let mut auth_paths_prefix_lenghts: Vec = Vec::with_capacity(indexes.len()); - let mut auth_paths_suffixes: Vec> = Vec::with_capacity(indexes.len()); - - let mut leaf_siblings_hashes = Vec::with_capacity(indexes.len()); - - let mut prev_path = Vec::new(); - - for index in &indexes { - leaf_siblings_hashes.push(self.get_leaf_sibling_hash(*index)); - - let path = self.compute_auth_path(*index); - - // incremental encoding - let (prefix_len, suffix) = prefix_encode_path(&prev_path, &path); - auth_paths_prefix_lenghts.push(prefix_len); - auth_paths_suffixes.push(suffix); - prev_path = path; - } - - Ok(MultiPath { - leaf_indexes: Vec::from_iter(indexes), - auth_paths_prefix_lenghts, - auth_paths_suffixes, - leaf_siblings_hashes, - }) - } - - /// Given the index and new leaf, return the hash of leaf and an updated path in order from root to bottom non-leaf level. - /// This does not mutate the underlying tree. - fn updated_path>( - &self, - index: usize, - new_leaf: T, - ) -> Result<(P::LeafDigest, Vec), crate::Error> { - // calculate the hash of leaf - let new_leaf_hash: P::LeafDigest = P::LeafHash::evaluate(&self.leaf_hash_param, new_leaf)?; - - // calculate leaf sibling hash and locate its position (left or right) - let (leaf_left, leaf_right) = if index & 1 == 0 { - // leaf on left - (&new_leaf_hash, &self.leaf_nodes[index + 1]) - } else { - (&self.leaf_nodes[index - 1], &new_leaf_hash) - }; - - // calculate the updated hash at bottom non-leaf-level - let mut path_bottom_to_top = Vec::with_capacity(self.height - 1); - { - path_bottom_to_top.push(P::TwoToOneHash::evaluate( - &self.two_to_one_hash_param, - P::LeafInnerDigestConverter::convert(leaf_left.clone())?, - P::LeafInnerDigestConverter::convert(leaf_right.clone())?, - )?); - } - - // then calculate the updated hash from bottom to root - let leaf_index_in_tree = convert_index_to_last_level(index, self.height); - let mut prev_index = parent(leaf_index_in_tree).unwrap(); - while !is_root(prev_index) { - let (left_child, right_child) = if is_left_child(prev_index) { - ( - path_bottom_to_top.last().unwrap(), - &self.non_leaf_nodes[sibling(prev_index).unwrap()], - ) - } else { - ( - &self.non_leaf_nodes[sibling(prev_index).unwrap()], - path_bottom_to_top.last().unwrap(), - ) - }; - let evaluated = - P::TwoToOneHash::compress(&self.two_to_one_hash_param, left_child, right_child)?; - path_bottom_to_top.push(evaluated); - prev_index = parent(prev_index).unwrap(); - } - - debug_assert_eq!(path_bottom_to_top.len(), self.height - 1); - let path_top_to_bottom: Vec<_> = path_bottom_to_top.into_iter().rev().collect(); - Ok((new_leaf_hash, path_top_to_bottom)) - } - - /// Update the leaf at `index` to updated leaf. - /// ```tree_diagram - /// [A] - /// / \ - /// [B] C - /// / \ / \ - /// D [E] F H - /// .. / \ .... - /// [I] J - /// ``` - /// update(3, {new leaf}) would swap the leaf value at `[I]` and cause a recomputation of `[A]`, `[B]`, and `[E]`. - pub fn update(&mut self, index: usize, new_leaf: &P::Leaf) -> Result<(), crate::Error> { - assert!(index < self.leaf_nodes.len(), "index out of range"); - let (updated_leaf_hash, mut updated_path) = self.updated_path(index, new_leaf)?; - self.leaf_nodes[index] = updated_leaf_hash; - let mut curr_index = convert_index_to_last_level(index, self.height); - for _ in 0..self.height - 1 { - curr_index = parent(curr_index).unwrap(); - self.non_leaf_nodes[curr_index] = updated_path.pop().unwrap(); - } - Ok(()) - } - - /// Update the leaf and check if the updated root is equal to `asserted_new_root`. - /// - /// Tree will not be modified if the check fails. - pub fn check_update>( - &mut self, - index: usize, - new_leaf: &P::Leaf, - asserted_new_root: &P::InnerDigest, - ) -> Result { - assert!(index < self.leaf_nodes.len(), "index out of range"); - let (updated_leaf_hash, mut updated_path) = self.updated_path(index, new_leaf)?; - if &updated_path[0] != asserted_new_root { - return Ok(false); - } - self.leaf_nodes[index] = updated_leaf_hash; - let mut curr_index = convert_index_to_last_level(index, self.height); - for _ in 0..self.height - 1 { - curr_index = parent(curr_index).unwrap(); - self.non_leaf_nodes[curr_index] = updated_path.pop().unwrap(); - } - Ok(true) - } -} - -/// Returns the height of the tree, given the number of leaves. -#[inline] -fn tree_height(num_leaves: usize) -> usize { - if num_leaves == 1 { - return 1; - } - - (ark_std::log2(num_leaves) as usize) + 1 -} -/// Returns true iff the index represents the root. -#[inline] -fn is_root(index: usize) -> bool { - index == 0 -} - -/// Returns the index of the left child, given an index. -#[inline] -fn left_child(index: usize) -> usize { - 2 * index + 1 -} - -/// Returns the index of the right child, given an index. -#[inline] -fn right_child(index: usize) -> usize { - 2 * index + 2 -} - -/// Returns the index of the sibling, given an index. -#[inline] -fn sibling(index: usize) -> Option { - if index == 0 { - None - } else if is_left_child(index) { - Some(index + 1) - } else { - Some(index - 1) - } -} - -/// Returns true iff the given index represents a left child. -#[inline] -fn is_left_child(index: usize) -> bool { - index % 2 == 1 -} - -/// Returns the index of the parent, given an index. -#[inline] -fn parent(index: usize) -> Option { - if index > 0 { - Some((index - 1) >> 1) - } else { - None - } -} - -#[inline] -fn convert_index_to_last_level(index: usize, tree_height: usize) -> usize { - index + (1 << (tree_height - 1)) - 1 -} - -/// Encodes path with Incremental Encoding by comparing with prev_path -/// Returns the prefix length and the suffix to append during decoding -/// Example: -/// If `prev_path` is vec![C,D] and `path` is vec![C,E] (where C,D,E are hashes) -/// `prefix_encode_path` returns 1,vec![E] - -#[inline] -fn prefix_encode_path(prev_path: &Vec, path: &Vec) -> (usize, Vec) -where - T: Eq + Clone, -{ - let prefix_length = prev_path - .iter() - .zip(path.iter()) - .take_while(|(a, b)| a == b) - .count(); - - (prefix_length, path[prefix_length..].to_vec()) -} - -fn prefix_decode_path(prev_path: &Vec, prefix_len: usize, suffix: &Vec) -> Vec -where - T: Eq + Clone, -{ - if prefix_len == 0 { - suffix.clone() - } else { - vec![prev_path[0..prefix_len].to_vec(), suffix.clone()].concat() - } -} diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index e1c40624..ecca8ed3 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -29,9 +29,6 @@ pub mod constraints; #[cfg(test)] mod tests; -#[cfg(any(test, feature = "bench_harness"))] -pub mod legacy; - #[cfg(all( target_has_atomic = "8", target_has_atomic = "16", diff --git a/crypto-primitives/src/merkle_tree/tests/bench_report.rs b/crypto-primitives/src/merkle_tree/tests/bench_report.rs deleted file mode 100644 index d585b9c7..00000000 --- a/crypto-primitives/src/merkle_tree/tests/bench_report.rs +++ /dev/null @@ -1,1689 +0,0 @@ -#![cfg(feature = "bench_harness")] - -use super::super::{decode_delta, encode_varint}; -use crate::merkle_tree::{ - implicit::ImplicitCoPath, - legacy, tests::test_utils::poseidon_parameters, CoPath, Config, IdentityDigestConverter, - LeafParam, MerkleTree, TwoToOneParam, -}; -use ark_ed_on_bls12_381::Fr; -use ark_serialize::CanonicalSerialize; -use ark_std::{ - rand::{rngs::StdRng, Rng, SeedableRng}, - UniformRand, -}; -use plotters::prelude::*; -use std::{ - collections::{BTreeMap, BTreeSet}, - fs::{self, File}, - io::Write, - path::{Path, PathBuf}, - time::{Duration, SystemTime}, -}; - -type F = Fr; -type H = crate::crh::poseidon::CRH; -type TwoToOneH = crate::crh::poseidon::TwoToOneCRH; - -struct FieldMTConfig; -impl Config for FieldMTConfig { - type Leaf = [F]; - type LeafDigest = F; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = F; - type LeafHash = H; - type TwoToOneHash = TwoToOneH; -} - -struct LegacyFieldMTConfig; -impl legacy::Config for LegacyFieldMTConfig { - type Leaf = [F]; - type LeafDigest = F; - type LeafInnerDigestConverter = legacy::IdentityDigestConverter; - type InnerDigest = F; - type LeafHash = H; - type TwoToOneHash = TwoToOneH; -} - -type FieldMT = MerkleTree; -type LegacyFieldMT = legacy::MerkleTree; -type LegacyLeafParam = legacy::LeafParam; -type LegacyTwoToOneParam = legacy::TwoToOneParam; - -const TREE_EXPONENTS: &[u32] = &[12, 14, 16, 18, 20]; -const BATCH_SIZES: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; -const LEAF_WIDTH: usize = 3; -const COORD_TREE_EXPONENTS: &[u32] = &[18]; -const IMPLICIT_COMPARE_TREE_EXPONENTS: &[u32] = &[20]; -const PRESENTATION_BATCH_SIZES: &[usize] = &[1, 8, 64, 512, 4096]; - -#[derive(Clone, Copy)] -enum CoordinateEncoding { - Natural, - Leb128Index, -} - -impl CoordinateEncoding { - fn label(self) -> &'static str { - match self { - CoordinateEncoding::Natural => "natural", - CoordinateEncoding::Leb128Index => "leb128", - } - } -} - -#[derive(Clone, Copy)] -enum IndexPattern { - Random, - Clustered, - Adversarial, -} - -impl IndexPattern { - fn label(self) -> &'static str { - match self { - IndexPattern::Random => "random", - IndexPattern::Clustered => "clustered", - IndexPattern::Adversarial => "adversarial", - } - } - - fn id(self) -> u64 { - match self { - IndexPattern::Random => 0, - IndexPattern::Clustered => 1, - IndexPattern::Adversarial => 2, - } - } -} - -struct TreeFixture { - leaves: Vec>, - tree: FieldMT, - legacy_tree: LegacyFieldMT, - leaf_params: LeafParam, - legacy_leaf_params: LegacyLeafParam, - two_to_one_params: TwoToOneParam, - legacy_two_to_one_params: LegacyTwoToOneParam, -} - -struct ReportRow { - tree_size: usize, - log2_size: u32, - batch: usize, - pattern: &'static str, - strategy: &'static str, - proof_bytes: usize, - proof_nodes: usize, - hashes_per_opening: f64, - prove_ms: f64, - verify_ms: f64, - rss_delta_kb: Option, -} - -struct PlotMetric { - name: &'static str, - filename_prefix: &'static str, - y_label: &'static str, - value: fn(&ReportRow) -> Option, -} - -struct CoordinateSizeRow { - tree_size: usize, - log2_size: u32, - batch: usize, - pattern: &'static str, - strategy: &'static str, - proof_bytes: usize, - proof_nodes: usize, -} - -struct InnerEntry<'a, D> { - depth: usize, - index: usize, - digest: &'a D, -} - -trait ProofStats { - fn opened(&self) -> usize; - fn total_nodes(&self) -> usize; -} - -impl ProofStats for CoPath

{ - fn opened(&self) -> usize { - self.leaf_indexes.len() - } - - fn total_nodes(&self) -> usize { - let inner = self - .inner_copath - .as_ref() - .map(|(_, _, _, digests)| digests.len()) - .unwrap_or(0); - self.leaf_copath.len() + inner - } -} - -impl ProofStats for ImplicitCoPath

{ - fn opened(&self) -> usize { - self.leaf_indexes.len() - } - - fn total_nodes(&self) -> usize { - self.leaf_copath.len() + self.inner_copath.len() - } -} - -impl ProofStats for legacy::MultiPath

{ - fn opened(&self) -> usize { - self.leaf_indexes.len() - } - - fn total_nodes(&self) -> usize { - let auth_len: usize = self.auth_paths_suffixes.iter().map(|path| path.len()).sum(); - self.leaf_siblings_hashes.len() + auth_len - } -} - -const PLOT_METRICS: &[PlotMetric] = &[ - PlotMetric { - name: "Proof Size", - filename_prefix: "proof_size", - y_label: "proof size (bytes)", - value: |row: &ReportRow| Some(row.proof_bytes as f64), - }, - PlotMetric { - name: "Proving Time", - filename_prefix: "prove_ms", - y_label: "prove time (ms)", - value: |row: &ReportRow| Some(row.prove_ms), - }, - PlotMetric { - name: "Verification Time", - filename_prefix: "verify_ms", - y_label: "verify time (ms)", - value: |row: &ReportRow| Some(row.verify_ms), - }, -]; - -#[test] -#[ignore] -fn multiproof_v2_benchmark_report() { - run_report().expect("benchmark report must succeed"); -} - -#[test] -#[ignore] -fn coordinate_encoding_proof_size_report() { - run_coordinate_report().expect("coordinate encoding report must succeed"); -} - -#[test] -#[ignore] -fn leb128_vs_delta_proof_size_report() { - run_leb128_vs_delta_report().expect("leb128 vs delta report must succeed"); -} - -fn run_report() -> Result<(), Box> { - let mut fixtures = Vec::new(); - for &exp in TREE_EXPONENTS { - fixtures.push(build_fixture(exp)?); - } - - let patterns = [ - IndexPattern::Random, - IndexPattern::Clustered, - IndexPattern::Adversarial, - ]; - - let mut rows = Vec::new(); - for fixture in fixtures.iter() { - for &batch in BATCH_SIZES { - if batch > fixture.leaves.len() { - continue; - } - for &pattern in &patterns { - let mut scenario_rng = StdRng::seed_from_u64( - 0xC057_E771_u64 - ^ ((fixture.leaves.len() as u64) << 16) - ^ ((batch as u64) << 2) - ^ pattern.id(), - ); - let indexes = - sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); - rows.extend(run_scenario(fixture, batch, pattern, &indexes)?); - } - } - } - - let report_dir = PathBuf::from("target/merkle_tree_reports"); - fs::create_dir_all(&report_dir)?; - let plot_files = write_plots(&rows, &report_dir)?; - write_report(&rows, &report_dir, &plot_files)?; - Ok(()) -} - -fn run_coordinate_report() -> Result<(), Box> { - let mut fixtures = Vec::new(); - for &exp in COORD_TREE_EXPONENTS { - fixtures.push(build_fixture(exp)?); - } - - let patterns = [IndexPattern::Random, IndexPattern::Clustered]; - let encodings = [CoordinateEncoding::Natural, CoordinateEncoding::Leb128Index]; - - let mut rows = Vec::new(); - for fixture in fixtures.iter() { - for &batch in BATCH_SIZES { - if batch > fixture.leaves.len() { - continue; - } - for &pattern in &patterns { - let mut scenario_rng = StdRng::seed_from_u64( - 0xC0DE_1280_u64 - ^ ((fixture.leaves.len() as u64) << 16) - ^ ((batch as u64) << 2) - ^ pattern.id(), - ); - let indexes = - sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); - let proof = fixture.tree.generate_multi_proof(indexes.iter().copied())?; - let proof_nodes = proof.total_nodes(); - - for &encoding in &encodings { - rows.push(CoordinateSizeRow { - tree_size: fixture.leaves.len(), - log2_size: fixture.log2_size(), - batch, - pattern: pattern.label(), - strategy: encoding.label(), - proof_bytes: serialized_coordinate_proof_size(&proof, encoding), - proof_nodes, - }); - } - } - } - } - - let report_dir = PathBuf::from("target/merkle_tree_reports"); - fs::create_dir_all(&report_dir)?; - let plot_files = write_coordinate_plots(&rows, &report_dir)?; - write_coordinate_report(&rows, &report_dir, &plot_files)?; - write_coordinate_rows_csv(&rows, &report_dir)?; - Ok(()) -} - -fn run_leb128_vs_delta_report() -> Result<(), Box> { - let mut fixtures = Vec::new(); - for &exp in COORD_TREE_EXPONENTS { - fixtures.push(build_fixture(exp)?); - } - - let patterns = [IndexPattern::Random, IndexPattern::Clustered]; - let mut rows = Vec::new(); - - for fixture in fixtures.iter() { - for &batch in BATCH_SIZES { - if batch > fixture.leaves.len() { - continue; - } - for &pattern in &patterns { - let mut scenario_rng = StdRng::seed_from_u64( - 0xD37A_1280_u64 - ^ ((fixture.leaves.len() as u64) << 16) - ^ ((batch as u64) << 2) - ^ pattern.id(), - ); - let indexes = - sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); - let proof = fixture.tree.generate_multi_proof(indexes.iter().copied())?; - let proof_nodes = proof.total_nodes(); - - rows.push(CoordinateSizeRow { - tree_size: fixture.leaves.len(), - log2_size: fixture.log2_size(), - batch, - pattern: pattern.label(), - strategy: "leb128", - proof_bytes: serialized_coordinate_proof_size( - &proof, - CoordinateEncoding::Leb128Index, - ), - proof_nodes, - }); - rows.push(CoordinateSizeRow { - tree_size: fixture.leaves.len(), - log2_size: fixture.log2_size(), - batch, - pattern: pattern.label(), - strategy: "delta", - proof_bytes: serialized_size(&proof), - proof_nodes, - }); - } - } - } - - let report_dir = PathBuf::from("target/merkle_tree_reports"); - fs::create_dir_all(&report_dir)?; - let plot_files = write_leb128_vs_delta_plots(&rows, &report_dir)?; - write_leb128_vs_delta_report(&rows, &report_dir, &plot_files)?; - write_leb128_vs_delta_rows_csv(&rows, &report_dir)?; - Ok(()) -} - -fn run_scenario( - fixture: &TreeFixture, - batch: usize, - pattern: IndexPattern, - indexes: &[usize], -) -> Result<[ReportRow; 2], Box> { - let root = fixture.tree.root(); - let legacy_root = fixture.legacy_tree.root(); - let opened_leaves: Vec> = indexes.iter().map(|&i| fixture.leaves[i].clone()).collect(); - - let legacy_row = benchmark_strategy( - "prefix", - || { - fixture - .legacy_tree - .generate_multi_proof(indexes.iter().copied()) - }, - |proof: &legacy::MultiPath<_>, leaves| { - proof.verify( - &fixture.legacy_leaf_params, - &fixture.legacy_two_to_one_params, - &legacy_root, - leaves, - ) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - )?; - - let coset_row = benchmark_strategy( - "coset", - || fixture.tree.generate_multi_proof(indexes.iter().copied()), - |proof: &CoPath<_>, leaves| { - proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - fixture.tree.height(), - leaves, - ) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - )?; - Ok([legacy_row, coset_row]) -} - -fn benchmark_strategy( - strategy: &'static str, - mut generator: Gen, - mut verifier: Verify, - opened_leaves: &[Vec], - tree_size: usize, - log2_size: u32, - batch: usize, - pattern: IndexPattern, -) -> Result> -where - Proof: CanonicalSerialize + ProofStats, - Gen: FnMut() -> Result, - Verify: FnMut(&Proof, Vec>) -> Result, -{ - let rss_before = rss_bytes(); - let prove_start = std::time::Instant::now(); - let proof = generator()?; - let prove_time = prove_start.elapsed(); - let prove_rss = rss_delta_kb(rss_before, rss_bytes()); - - let proof_bytes = serialized_size(&proof); - let proof_nodes = proof.total_nodes(); - let opened = proof.opened().max(1); - let hashes_per_opening = proof_nodes as f64 / opened as f64; - - let verify_input = opened_leaves.to_vec(); - let rss_before_verify = rss_bytes(); - let verify_start = std::time::Instant::now(); - let verify_ok = verifier(&proof, verify_input.clone())?; - let verify_time = verify_start.elapsed(); - let verify_rss = rss_delta_kb(rss_before_verify, rss_bytes()); - assert!( - verify_ok, - "verification must succeed for {} (tree_n={}, batch={}, pattern={})", - strategy, - tree_size, - batch, - pattern.label() - ); - - let row = ReportRow { - tree_size, - log2_size, - batch, - pattern: pattern.label(), - strategy, - proof_bytes, - proof_nodes, - hashes_per_opening, - prove_ms: duration_ms(prove_time), - verify_ms: duration_ms(verify_time), - rss_delta_kb: combine_rss(prove_rss, verify_rss), - }; - - Ok(row) -} - -fn sample_indexes( - pattern: IndexPattern, - batch: usize, - num_leaves: usize, - rng: &mut StdRng, -) -> Vec { - match pattern { - IndexPattern::Random => { - let mut set = BTreeSet::new(); - while set.len() < batch { - let idx = rng.gen_range(0..num_leaves); - set.insert(idx); - } - set.into_iter().collect() - } - IndexPattern::Clustered => { - let max_start = num_leaves.saturating_sub(batch); - let start = rng.gen_range(0..=max_start); - (start..start + batch).collect() - } - IndexPattern::Adversarial => { - if batch >= num_leaves { - return (0..num_leaves).collect(); - } - let step = num_leaves / batch; - (0..batch).map(|i| (i * step) % num_leaves).collect() - } - } -} - -fn build_fixture(exp: u32) -> Result> { - let leaf_params = poseidon_parameters(); - let legacy_leaf_params: LegacyLeafParam = leaf_params.clone(); - let two_to_one_params = leaf_params.clone(); - let legacy_two_to_one_params: LegacyTwoToOneParam = two_to_one_params.clone(); - - let num_leaves = 1usize << exp; - let mut rng = StdRng::seed_from_u64(0x5EED_C0DE_u64 ^ (exp as u64)); - let leaves = sample_leaves(num_leaves, &mut rng); - - let tree = FieldMT::new(&leaf_params, &two_to_one_params, &leaves).unwrap(); - let legacy_tree = - LegacyFieldMT::new(&legacy_leaf_params, &legacy_two_to_one_params, &leaves).unwrap(); - - Ok(TreeFixture { - leaves, - tree, - legacy_tree, - leaf_params, - legacy_leaf_params, - two_to_one_params, - legacy_two_to_one_params, - }) -} - -fn sample_leaves(count: usize, rng: &mut StdRng) -> Vec> { - (0..count) - .map(|_| (0..LEAF_WIDTH).map(|_| F::rand(rng)).collect()) - .collect() -} - -fn write_report( - rows: &[ReportRow], - report_dir: &Path, - plot_files: &BTreeMap<&'static str, Vec>, -) -> Result<(), Box> { - let report_path = report_dir.join("multiproof_v2_report.md"); - let mut file = File::create(&report_path)?; - - writeln!(file, "# Merkle Tree Multiproof Benchmark Report")?; - writeln!( - file, - "\nGenerated: {:?}\n", - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? - )?; - writeln!( - file, - "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms | rss_delta_kb |" - )?; - writeln!( - file, - "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- | ------------ |" - )?; - - for row in rows { - writeln!( - file, - "| {} | {} | {} | {} | {} | {} | {} | {:.2} | {:.2} | {:.2} | {} |", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - row.hashes_per_opening, - row.prove_ms, - row.verify_ms, - row.rss_delta_kb - .map(|kb| kb.to_string()) - .unwrap_or_else(|| "-".into()) - )?; - } - - for (metric, files) in plot_files { - if files.is_empty() { - continue; - } - writeln!(file, "\n## {} Visualizations\n", metric)?; - for plot in files { - writeln!( - file, - "![{}]({})", - metric.replace(' ', "-").to_lowercase(), - plot - )?; - } - } - - Ok(()) -} - -fn write_coordinate_report( - rows: &[CoordinateSizeRow], - report_dir: &Path, - plot_files: &[String], -) -> Result<(), Box> { - let report_path = report_dir.join("coordinate_encoding_report.md"); - let mut file = File::create(&report_path)?; - - writeln!(file, "# Merkle Tree Proof Size Comparison")?; - writeln!( - file, - "\nGenerated: {:?}\n", - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? - )?; - writeln!( - file, - "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes |" - )?; - writeln!( - file, - "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- |" - )?; - - for row in rows { - writeln!( - file, - "| {} | {} | {} | {} | {} | {} | {} |", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - )?; - } - - if !plot_files.is_empty() { - writeln!(file, "\n## Figures\n")?; - for plot in plot_files { - writeln!(file, "![coordinate-proof-size]({})", plot)?; - } - } - - Ok(()) -} - -fn write_coordinate_rows_csv( - rows: &[CoordinateSizeRow], - report_dir: &Path, -) -> Result<(), Box> { - let csv_path = report_dir.join("coordinate_encoding_rows.csv"); - let mut file = File::create(&csv_path)?; - writeln!( - file, - "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes" - )?; - - for row in rows { - writeln!( - file, - "{},{},{},{},{},{},{}", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - )?; - } - - Ok(()) -} - -fn write_leb128_vs_delta_report( - rows: &[CoordinateSizeRow], - report_dir: &Path, - plot_files: &[String], -) -> Result<(), Box> { - let report_path = report_dir.join("leb128_vs_delta_report.md"); - let mut file = File::create(&report_path)?; - - writeln!(file, "# Merkle Tree Proof Size Comparison")?; - writeln!( - file, - "\nGenerated: {:?}\n", - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? - )?; - writeln!( - file, - "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes |" - )?; - writeln!( - file, - "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- |" - )?; - - for row in rows { - writeln!( - file, - "| {} | {} | {} | {} | {} | {} | {} |", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - )?; - } - - if !plot_files.is_empty() { - writeln!(file, "\n## Figures\n")?; - for plot in plot_files { - writeln!(file, "![leb128-vs-delta]({})", plot)?; - } - } - - Ok(()) -} - -fn write_leb128_vs_delta_rows_csv( - rows: &[CoordinateSizeRow], - report_dir: &Path, -) -> Result<(), Box> { - let csv_path = report_dir.join("leb128_vs_delta_rows.csv"); - let mut file = File::create(&csv_path)?; - writeln!( - file, - "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes" - )?; - - for row in rows { - writeln!( - file, - "{},{},{},{},{},{},{}", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - )?; - } - - Ok(()) -} - -fn write_plots( - rows: &[ReportRow], - report_dir: &Path, -) -> Result>, Box> { - let mut outputs: BTreeMap<&'static str, Vec> = BTreeMap::new(); - - for metric in PLOT_METRICS { - let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = - BTreeMap::new(); - - for row in rows { - if let Some(value) = (metric.value)(row) { - grouped - .entry((row.tree_size, row.pattern)) - .or_default() - .entry(row.strategy) - .or_default() - .push((row.batch as f64, value)); - } - } - - let mut generated = Vec::new(); - for ((tree_size, pattern), strategies) in grouped { - let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); - for &name in &["prefix", "coset"] { - if let Some(mut series) = strategies.get(name).cloned() { - if series.is_empty() { - continue; - } - series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - ordered_series.push((name, series)); - } - } - - if ordered_series.len() < 2 || !ordered_series.iter().any(|(name, _)| *name == "prefix") - { - continue; - } - - let mut min_x = f64::MAX; - let mut max_x = f64::MIN; - let mut min_y = f64::MAX; - let mut max_y = f64::MIN; - - for (_, series) in &ordered_series { - for &(x, y) in series { - min_x = min_x.min(x); - max_x = max_x.max(x); - min_y = min_y.min(y); - max_y = max_y.max(y); - } - } - if min_x == f64::MAX || min_y == f64::MAX { - continue; - } - - let x_pad = ((max_x - min_x) * 0.05).max(1.0); - let y_pad = ((max_y - min_y) * 0.05).max(1.0); - - let filename = format!("{}_{}_{}.svg", metric.filename_prefix, tree_size, pattern); - let filepath = report_dir.join(&filename); - let filepath_str = filepath.to_string_lossy().to_string(); - let drawing_area = SVGBackend::new(&filepath_str, (960, 540)).into_drawing_area(); - drawing_area.fill(&WHITE)?; - - let mut chart = ChartBuilder::on(&drawing_area) - .caption( - format!( - "{} vs k (n={}, pattern={})", - metric.name, tree_size, pattern - ), - ("Helvetica Neue", 26).into_font().style(FontStyle::Bold), - ) - .margin(20) - .x_label_area_size(45) - .y_label_area_size(70) - .build_cartesian_2d( - (min_x - x_pad)..(max_x + x_pad), - (min_y - y_pad)..(max_y + y_pad), - )?; - - chart - .configure_mesh() - .x_desc("batch size (k)") - .y_desc(metric.y_label) - .draw()?; - - for (name, series) in &ordered_series { - let color = strategy_color(name); - chart - .draw_series(LineSeries::new(series.clone(), color.clone()))? - .label(strategy_label(name)) - .legend({ - let color = color.clone(); - move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color.clone()) - }); - } - - chart - .configure_series_labels() - .border_style(&BLACK) - .draw()?; - - generated.push(filename); - } - - outputs.insert(metric.name, generated); - } - - Ok(outputs) -} - -fn write_coordinate_plots( - rows: &[CoordinateSizeRow], - report_dir: &Path, -) -> Result, Box> { - let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = - BTreeMap::new(); - - for row in rows { - grouped - .entry((row.tree_size, row.pattern)) - .or_default() - .entry(row.strategy) - .or_default() - .push((row.batch as f64, row.proof_bytes as f64)); - } - - let mut generated = Vec::new(); - for ((tree_size, pattern), strategies) in grouped { - let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); - for &name in &["natural", "leb128"] { - if let Some(mut series) = strategies.get(name).cloned() { - if series.is_empty() { - continue; - } - series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - ordered_series.push((name, series)); - } - } - - if ordered_series.len() < 2 { - continue; - } - - let mut min_x = f64::MAX; - let mut max_x = f64::MIN; - let mut min_y = f64::MAX; - let mut max_y = f64::MIN; - for (_, series) in &ordered_series { - for &(x, y) in series { - min_x = min_x.min(x); - max_x = max_x.max(x); - min_y = min_y.min(y); - max_y = max_y.max(y); - } - } - if min_x == f64::MAX || min_y == f64::MAX { - continue; - } - - let x_pad = ((max_x - min_x) * 0.06).max(8.0); - let y_pad = ((max_y - min_y) * 0.10).max(256.0); - - let filename = format!("coordinate_proof_size_{}_{}.svg", tree_size, pattern); - let filepath = report_dir.join(&filename); - let filepath_str = filepath.to_string_lossy().to_string(); - let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); - drawing_area.fill(&WHITE)?; - - let mut chart = ChartBuilder::on(&drawing_area) - .margin(28) - .x_label_area_size(64) - .y_label_area_size(96) - .build_cartesian_2d( - (min_x - x_pad)..(max_x + x_pad), - (min_y - y_pad)..(max_y + y_pad), - )?; - - chart - .configure_mesh() - .x_desc("input size k (opened leaves)") - .y_desc("proof size (bytes)") - .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) - .label_style(("Helvetica Neue", 18).into_font()) - .light_line_style(WHITE.mix(0.0)) - .draw()?; - - for (name, series) in &ordered_series { - let color = coordinate_strategy_color(name); - chart - .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? - .label(coordinate_strategy_label(name)) - .legend({ - let color = color.clone(); - move |(x, y)| PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) - }); - - chart.draw_series( - series - .iter() - .map(|point| Circle::new(*point, 5, color.filled())), - )?; - } - - chart - .configure_series_labels() - .position(SeriesLabelPosition::UpperLeft) - .background_style(WHITE.mix(0.85)) - .border_style(BLACK) - .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) - .draw()?; - - generated.push(filename); - } - - Ok(generated) -} - -fn write_leb128_vs_delta_plots( - rows: &[CoordinateSizeRow], - report_dir: &Path, -) -> Result, Box> { - let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = - BTreeMap::new(); - - for row in rows { - grouped - .entry((row.tree_size, row.pattern)) - .or_default() - .entry(row.strategy) - .or_default() - .push((row.batch as f64, row.proof_bytes as f64)); - } - - let mut generated = Vec::new(); - for ((tree_size, pattern), strategies) in grouped { - let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); - for &name in &["leb128", "delta"] { - if let Some(mut series) = strategies.get(name).cloned() { - if series.is_empty() { - continue; - } - series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - ordered_series.push((name, series)); - } - } - - if ordered_series.len() < 2 { - continue; - } - - let mut min_x = f64::MAX; - let mut max_x = f64::MIN; - let mut min_y = f64::MAX; - let mut max_y = f64::MIN; - for (_, series) in &ordered_series { - for &(x, y) in series { - min_x = min_x.min(x); - max_x = max_x.max(x); - min_y = min_y.min(y); - max_y = max_y.max(y); - } - } - if min_x == f64::MAX || min_y == f64::MAX { - continue; - } - - let x_pad = ((max_x - min_x) * 0.06).max(8.0); - let y_pad = ((max_y - min_y) * 0.10).max(256.0); - - let filename = format!("leb128_vs_delta_proof_size_{}_{}.svg", tree_size, pattern); - let filepath = report_dir.join(&filename); - let filepath_str = filepath.to_string_lossy().to_string(); - let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); - drawing_area.fill(&WHITE)?; - - let mut chart = ChartBuilder::on(&drawing_area) - .margin(28) - .x_label_area_size(64) - .y_label_area_size(96) - .build_cartesian_2d( - (min_x - x_pad)..(max_x + x_pad), - (min_y - y_pad)..(max_y + y_pad), - )?; - - chart - .configure_mesh() - .x_desc("input size k (opened leaves)") - .y_desc("proof size (bytes)") - .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) - .label_style(("Helvetica Neue", 18).into_font()) - .light_line_style(WHITE.mix(0.0)) - .draw()?; - - for (name, series) in &ordered_series { - let color = leb128_vs_delta_color(name); - chart - .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? - .label(leb128_vs_delta_label(name)) - .legend({ - let color = color.clone(); - move |(x, y)| PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) - }); - - chart.draw_series( - series - .iter() - .map(|point| Circle::new(*point, 5, color.filled())), - )?; - } - - chart - .configure_series_labels() - .position(SeriesLabelPosition::UpperLeft) - .background_style(WHITE.mix(0.85)) - .border_style(BLACK) - .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) - .draw()?; - - generated.push(filename); - } - - Ok(generated) -} - -fn strategy_label(name: &str) -> &str { - match name { - "prefix" => "prefix", - "coset" => "coset", - _ => name, - } -} - -fn coordinate_strategy_label(name: &str) -> &str { - match name { - "natural" => "Unoptimized", - "leb128" => "Partially Optimized", - _ => name, - } -} - -fn coordinate_strategy_color(name: &str) -> RGBColor { - match name { - "natural" => RGBColor(217, 95, 2), - "leb128" => RGBColor(27, 158, 119), - _ => BLACK, - } -} - -fn leb128_vs_delta_label(name: &str) -> &str { - match name { - "leb128" => "Partially Optimized", - "delta" => "Optimized", - _ => name, - } -} - -fn leb128_vs_delta_color(name: &str) -> RGBColor { - match name { - "leb128" => RGBColor(27, 158, 119), - "delta" => RGBColor(117, 112, 179), - _ => BLACK, - } -} - -fn strategy_color(name: &str) -> RGBColor { - match name { - "prefix" => RED, - "coset" => BLUE, - _ => BLACK, - } -} - -fn rss_bytes() -> Option { - #[cfg(target_os = "linux")] - { - let data = fs::read_to_string("/proc/self/status").ok()?; - for line in data.lines() { - if let Some(rest) = line.strip_prefix("VmRSS:") { - let kb: u64 = rest - .split_whitespace() - .nth(1) - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - return Some(kb * 1024); - } - } - None - } - #[cfg(not(target_os = "linux"))] - { - None - } -} - -fn rss_delta_kb(before: Option, after: Option) -> Option { - match (before, after) { - (Some(b), Some(a)) => Some(((a as i64) - (b as i64)) / 1024), - _ => None, - } -} - -fn combine_rss(a: Option, b: Option) -> Option { - match (a, b) { - (Some(x), Some(y)) => Some(x + y), - _ => a.or(b), - } -} - -fn serialized_size(value: &T) -> usize { - let mut buf = Vec::new(); - value - .serialize_uncompressed(&mut buf) - .expect("serialization must succeed"); - buf.len() -} - -fn serialized_coordinate_proof_size( - proof: &CoPath

, - encoding: CoordinateEncoding, -) -> usize { - let mut buf = Vec::new(); - proof - .tree_height - .serialize_uncompressed(&mut buf) - .expect("tree height serialization must succeed"); - proof - .leaf_copath - .serialize_uncompressed(&mut buf) - .expect("leaf co-path serialization must succeed"); - serialize_inner_copath_absolute(&mut buf, proof, encoding); - proof - .leaf_indexes - .serialize_uncompressed(&mut buf) - .expect("leaf indexes serialization must succeed"); - buf.len() -} - -fn serialize_inner_copath_absolute( - buf: &mut Vec, - proof: &CoPath

, - encoding: CoordinateEncoding, -) { - let entries = unpack_inner_entries(proof); - (!entries.is_empty()) - .serialize_uncompressed(&mut *buf) - .expect("option tag serialization must succeed"); - if entries.is_empty() { - return; - } - - entries - .len() - .serialize_uncompressed(&mut *buf) - .expect("coordinate count serialization must succeed"); - for entry in &entries { - entry - .depth - .serialize_uncompressed(&mut *buf) - .expect("depth serialization must succeed"); - match encoding { - CoordinateEncoding::Natural => entry - .index - .serialize_uncompressed(&mut *buf) - .expect("index serialization must succeed"), - CoordinateEncoding::Leb128Index => { - encode_varint(buf, entry.index as u64); - } - } - } - - entries - .len() - .serialize_uncompressed(&mut *buf) - .expect("digest count serialization must succeed"); - for entry in entries { - entry - .digest - .serialize_uncompressed(&mut *buf) - .expect("digest serialization must succeed"); - } -} - -fn unpack_inner_entries<'a, P: Config>( - proof: &'a CoPath

, -) -> Vec> { - let Some((start_depth, start_index, deltas, digests)) = proof.inner_copath.as_ref() else { - return Vec::new(); - }; - if digests.is_empty() { - return Vec::new(); - } - - let mut entries = Vec::with_capacity(digests.len()); - let mut depth = *start_depth as i64; - let mut index = *start_index as i64; - entries.push(InnerEntry { - depth: *start_depth, - index: *start_index, - digest: &digests[0], - }); - - let mut cursor = 0usize; - for digest in digests.iter().skip(1) { - depth += decode_delta(deltas, &mut cursor).expect("packed depth delta must decode"); - index += decode_delta(deltas, &mut cursor).expect("packed index delta must decode"); - entries.push(InnerEntry { - depth: usize::try_from(depth).expect("depth must remain non-negative"), - index: usize::try_from(index).expect("index must remain non-negative"), - digest, - }); - } - - assert_eq!( - cursor, - deltas.len(), - "all packed coordinate bytes must be consumed" - ); - entries -} - -fn duration_ms(duration: Duration) -> f64 { - duration.as_secs_f64() * 1000.0 -} - -impl TreeFixture { - fn log2_size(&self) -> u32 { - (self.leaves.len() as f64).log2().round() as u32 - } -} - -// --------------------------------------------------------------------------- -// Implicit vs CoSet benchmark -// --------------------------------------------------------------------------- - -#[test] -#[ignore] -fn implicit_vs_coset_report() { - run_implicit_vs_coset_report().expect("implicit vs coset report must succeed"); -} - -fn run_implicit_vs_coset_report() -> Result<(), Box> { - let mut fixtures = Vec::new(); - for &exp in IMPLICIT_COMPARE_TREE_EXPONENTS { - fixtures.push(build_fixture(exp)?); - } - - let patterns = [IndexPattern::Random, IndexPattern::Clustered]; - let mut rows = Vec::new(); - - for fixture in fixtures.iter() { - for &batch in PRESENTATION_BATCH_SIZES { - if batch > fixture.leaves.len() { - continue; - } - for &pattern in &patterns { - let mut scenario_rng = StdRng::seed_from_u64( - 0x1A1B_1C17_u64 - ^ ((fixture.leaves.len() as u64) << 16) - ^ ((batch as u64) << 2) - ^ pattern.id(), - ); - let indexes = - sample_indexes(pattern, batch, fixture.leaves.len(), &mut scenario_rng); - rows.extend(run_implicit_scenario(fixture, batch, pattern, &indexes)?); - } - } - } - - let report_dir = PathBuf::from("target/merkle_tree_reports"); - fs::create_dir_all(&report_dir)?; - let plot_files = write_implicit_vs_coset_plots(&rows, &report_dir)?; - write_implicit_vs_coset_report(&rows, &report_dir, &plot_files)?; - write_implicit_vs_coset_rows_csv(&rows, &report_dir)?; - Ok(()) -} - -fn run_implicit_scenario( - fixture: &TreeFixture, - batch: usize, - pattern: IndexPattern, - indexes: &[usize], -) -> Result<[ReportRow; 2], Box> { - let root = fixture.tree.root(); - let opened_leaves: Vec> = indexes.iter().map(|&i| fixture.leaves[i].clone()).collect(); - let repeats = implicit_compare_repetitions(batch); - - let coset_row = benchmark_strategy_repeated( - "coset", - || fixture.tree.generate_multi_proof(indexes.iter().copied()), - |proof: &CoPath<_>, leaves| { - proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - fixture.tree.height(), - leaves, - ) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - repeats, - )?; - - let implicit_row = benchmark_strategy_repeated( - "implicit", - || { - fixture - .tree - .generate_implicit_multi_proof(indexes.iter().copied()) - }, - |proof: &ImplicitCoPath<_>, leaves| { - proof.verify( - &fixture.leaf_params, - &fixture.two_to_one_params, - &root, - fixture.tree.height(), - leaves, - ) - }, - &opened_leaves, - fixture.leaves.len(), - fixture.log2_size(), - batch, - pattern, - repeats, - )?; - - Ok([coset_row, implicit_row]) -} - -fn implicit_compare_repetitions(batch: usize) -> usize { - match batch { - 0..=8 => 200, - 9..=64 => 100, - 65..=512 => 20, - _ => 3, - } -} - -fn benchmark_strategy_repeated( - strategy: &'static str, - mut generator: Gen, - mut verifier: Verify, - opened_leaves: &[Vec], - tree_size: usize, - log2_size: u32, - batch: usize, - pattern: IndexPattern, - repetitions: usize, -) -> Result> -where - Proof: CanonicalSerialize + ProofStats, - Gen: FnMut() -> Result, - Verify: FnMut(&Proof, Vec>) -> Result, -{ - let repetitions = repetitions.max(1); - - let rss_before = rss_bytes(); - let prove_start = std::time::Instant::now(); - let mut proof = None; - for _ in 0..repetitions { - proof = Some(generator()?); - } - let prove_elapsed = prove_start.elapsed(); - let prove_rss = rss_delta_kb(rss_before, rss_bytes()); - let proof = proof.expect("repeated benchmark must generate at least one proof"); - - let proof_bytes = serialized_size(&proof); - let proof_nodes = proof.total_nodes(); - let opened = proof.opened().max(1); - let hashes_per_opening = proof_nodes as f64 / opened as f64; - - let rss_before_verify = rss_bytes(); - let verify_start = std::time::Instant::now(); - for _ in 0..repetitions { - let verify_ok = verifier(&proof, opened_leaves.to_vec())?; - assert!( - verify_ok, - "verification must succeed for {} (tree_n={}, batch={}, pattern={})", - strategy, - tree_size, - batch, - pattern.label() - ); - } - let verify_elapsed = verify_start.elapsed(); - let verify_rss = rss_delta_kb(rss_before_verify, rss_bytes()); - - Ok(ReportRow { - tree_size, - log2_size, - batch, - pattern: pattern.label(), - strategy, - proof_bytes, - proof_nodes, - hashes_per_opening, - prove_ms: duration_ms(prove_elapsed) / repetitions as f64, - verify_ms: duration_ms(verify_elapsed) / repetitions as f64, - rss_delta_kb: combine_rss(prove_rss, verify_rss), - }) -} - -fn write_implicit_vs_coset_plots( - rows: &[ReportRow], - report_dir: &Path, -) -> Result>, Box> { - let mut outputs: BTreeMap<&'static str, Vec> = BTreeMap::new(); - - for metric in PLOT_METRICS { - let mut grouped: BTreeMap<(usize, &'static str), BTreeMap<&'static str, Vec<(f64, f64)>>> = - BTreeMap::new(); - - for row in rows { - if let Some(value) = (metric.value)(row) { - grouped - .entry((row.tree_size, row.pattern)) - .or_default() - .entry(row.strategy) - .or_default() - .push((row.batch as f64, value)); - } - } - - let mut generated = Vec::new(); - for ((tree_size, pattern), strategies) in grouped { - let mut ordered_series: Vec<(&'static str, Vec<(f64, f64)>)> = Vec::new(); - for &name in &["coset", "implicit"] { - if let Some(mut series) = strategies.get(name).cloned() { - if series.is_empty() { - continue; - } - series.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - ordered_series.push((name, series)); - } - } - - if ordered_series.len() < 2 { - continue; - } - - let mut min_x = f64::MAX; - let mut max_x = f64::MIN; - let mut min_y = f64::MAX; - let mut max_y = f64::MIN; - for (_, series) in &ordered_series { - for &(x, y) in series { - min_x = min_x.min(x); - max_x = max_x.max(x); - min_y = min_y.min(y); - max_y = max_y.max(y); - } - } - if min_x == f64::MAX || min_y == f64::MAX { - continue; - } - - let x_pad = ((max_x - min_x) * 0.06).max(8.0); - let y_pad = ((max_y - min_y) * 0.10).max(256.0); - - let filename = format!( - "implicit_vs_coset_{}_{}_{}.svg", - metric.filename_prefix, tree_size, pattern - ); - let filepath = report_dir.join(&filename); - let filepath_str = filepath.to_string_lossy().to_string(); - let drawing_area = SVGBackend::new(&filepath_str, (1280, 720)).into_drawing_area(); - drawing_area.fill(&WHITE)?; - - let mut chart = ChartBuilder::on(&drawing_area) - .margin(28) - .x_label_area_size(64) - .y_label_area_size(96) - .build_cartesian_2d( - (min_x - x_pad)..(max_x + x_pad), - (min_y - y_pad)..(max_y + y_pad), - )?; - - chart - .configure_mesh() - .x_desc("input size k (opened leaves)") - .y_desc(metric.y_label) - .axis_desc_style(("Helvetica Neue", 24).into_font().style(FontStyle::Bold)) - .label_style(("Helvetica Neue", 18).into_font()) - .light_line_style(WHITE.mix(0.0)) - .draw()?; - - for (name, series) in &ordered_series { - let color = implicit_strategy_color(name); - chart - .draw_series(LineSeries::new(series.clone(), color.stroke_width(4)))? - .label(implicit_strategy_label(name)) - .legend({ - let color = color.clone(); - move |(x, y)| { - PathElement::new(vec![(x, y), (x + 28, y)], color.stroke_width(4)) - } - }); - - chart.draw_series( - series - .iter() - .map(|point| Circle::new(*point, 5, color.filled())), - )?; - } - - chart - .configure_series_labels() - .position(SeriesLabelPosition::UpperLeft) - .background_style(WHITE.mix(0.85)) - .border_style(BLACK) - .label_font(("Helvetica Neue", 22).into_font().style(FontStyle::Bold)) - .draw()?; - - generated.push(filename); - } - - outputs.insert(metric.name, generated); - } - - Ok(outputs) -} - -fn write_implicit_vs_coset_report( - rows: &[ReportRow], - report_dir: &Path, - plot_files: &BTreeMap<&'static str, Vec>, -) -> Result<(), Box> { - let report_path = report_dir.join("implicit_vs_coset_report.md"); - let mut file = File::create(&report_path)?; - - writeln!(file, "# Implicit vs CoSet Benchmark Report")?; - writeln!( - file, - "\nGenerated: {:?}\n", - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)? - )?; - writeln!( - file, - "| tree_n | log2(n) | batch_k | pattern | strategy | proof_bytes | proof_nodes | hashes/leaf | prove_ms | verify_ms |" - )?; - writeln!( - file, - "| ------ | ------- | ------- | ------- | -------- | ----------- | ----------- | ------------ | -------- | --------- |" - )?; - - for row in rows { - writeln!( - file, - "| {} | {} | {} | {} | {} | {} | {} | {:.2} | {:.2} | {:.2} |", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - row.hashes_per_opening, - row.prove_ms, - row.verify_ms, - )?; - } - - for (metric, files) in plot_files { - if files.is_empty() { - continue; - } - writeln!(file, "\n## {} Visualizations\n", metric)?; - for plot in files { - writeln!( - file, - "![{}]({})", - metric.replace(' ', "-").to_lowercase(), - plot - )?; - } - } - - Ok(()) -} - -fn write_implicit_vs_coset_rows_csv( - rows: &[ReportRow], - report_dir: &Path, -) -> Result<(), Box> { - let csv_path = report_dir.join("implicit_vs_coset_rows.csv"); - let mut file = File::create(&csv_path)?; - writeln!( - file, - "tree_size,log2_size,batch,pattern,strategy,proof_bytes,proof_nodes,hashes_per_opening,prove_ms,verify_ms" - )?; - - for row in rows { - writeln!( - file, - "{},{},{},{},{},{},{},{:.6},{:.6},{:.6}", - row.tree_size, - row.log2_size, - row.batch, - row.pattern, - row.strategy, - row.proof_bytes, - row.proof_nodes, - row.hashes_per_opening, - row.prove_ms, - row.verify_ms, - )?; - } - - Ok(()) -} - -fn implicit_strategy_label(name: &str) -> &str { - match name { - "coset" => "CoSet (with coords)", - "implicit" => "Implicit (coord-free)", - _ => name, - } -} - -fn implicit_strategy_color(name: &str) -> RGBColor { - match name { - "coset" => RGBColor(31, 119, 180), // blue — same as STRATEGY_STYLE["coset"] in Python - "implicit" => RGBColor(123, 53, 193), // purple - _ => BLACK, - } -} diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index efc6d618..d4043a2d 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -4,9 +4,6 @@ mod delta_encoding_tests; mod implicit_copath_tests; mod test_utils; -#[cfg(all(test, feature = "bench_harness"))] -mod bench_report; - mod bytes_mt_tests { use crate::{crh::*, merkle_tree::*}; @@ -332,7 +329,7 @@ mod field_mt_tests { } #[test] - fn multiproof_empty_batch_verifies() { + fn multiproof_empty_batch_is_caller_error() { let mut rng = test_rng(); let leaves: Vec> = (0..4) .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) @@ -343,6 +340,7 @@ mod field_mt_tests { let root = tree.root(); let proof = tree.generate_multi_proof(Vec::::new()).unwrap(); + assert_eq!(proof.leaf_indexes.len(), 0); assert!( proof .verify( @@ -352,10 +350,9 @@ mod field_mt_tests { tree.height(), Vec::>::new() ) - .unwrap(), - "empty batch proof should verify" + .is_err(), + "verifying an empty batch proof is a caller error" ); - assert_eq!(proof.leaf_indexes.len(), 0); } #[test] diff --git a/scripts/build_results_pack.py b/scripts/build_results_pack.py deleted file mode 100644 index 6d99aa7d..00000000 --- a/scripts/build_results_pack.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -"""Build a 7-slide results replacement pack (PPTX) from benchmark figure PNGs. - -Usage: - python3 scripts/build_results_pack.py \ - --fig-dir figures/presentation \ - --out /mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack.pptx \ - --title Results -""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, List, Sequence, Tuple - -from PIL import Image -from pptx import Presentation -from pptx.dml.color import RGBColor -from pptx.enum.shapes import MSO_SHAPE -from pptx.enum.text import PP_ALIGN -from pptx.util import Emu, Pt - -EMU_PER_INCH = 914400 - -# 16:9 widescreen -SLIDE_W = Emu(int(13.333 * EMU_PER_INCH)) -SLIDE_H = Emu(int(7.5 * EMU_PER_INCH)) - -TOKENS = { - "accent": RGBColor(48, 93, 255), - "bg_dark": RGBColor(5, 14, 42), - "bg": RGBColor(248, 250, 252), - "text_dark": RGBColor(18, 25, 38), - "text_muted": RGBColor(88, 96, 112), - "card": RGBColor(241, 244, 249), - "card_border": RGBColor(219, 225, 236), - "sticky": RGBColor(255, 247, 214), - "sticky_border": RGBColor(234, 222, 172), - "progress_bg": RGBColor(234, 240, 255), -} - - -@dataclass(frozen=True) -class FigureSlide: - filename: str - title: str - subtitle: str - callout_header: str - callout_body: str - progress: str - - -FIGURE_SLIDES: Tuple[FigureSlide, ...] = ( - FigureSlide( - filename="proof_size_vs_k_clustered.png", - title="Proof Size - Clustered", - subtitle="One graph per slide. Shared y-scale with random for clean comparison.", - callout_header="🎯 Clustered wins clearly", - callout_body="Coset pruning removes duplicated siblings, giving the largest byte savings at high k.", - progress="Clustered 1/3", - ), - FigureSlide( - filename="prover_time_vs_k_clustered.png", - title="Prover Time - Clustered", - subtitle="Same visual frame to support narrative pacing.", - callout_header="⏱️ Prover overhead is controlled", - callout_body="Extraction/reconstruction costs stay modest versus structural proof-size gains.", - progress="Clustered 2/3", - ), - FigureSlide( - filename="verifier_time_vs_k_clustered.png", - title="Verifier Time - Clustered", - subtitle="Consistent scale and typography to reduce audience load.", - callout_header="⚖️ Hashing still dominates", - callout_body="Verifier trend remains hash-heavy; metadata optimizations have a smaller effect.", - progress="Clustered 3/3", - ), - FigureSlide( - filename="proof_size_vs_k_random.png", - title="Proof Size - Random", - subtitle="Now random queries under the same axis policy.", - callout_header="🧭 Savings persist in random", - callout_body="Overlap is lower than clustered, but coset still reduces proof bytes materially.", - progress="Random 1/3", - ), - FigureSlide( - filename="prover_time_vs_k_random.png", - title="Prover Time - Random", - subtitle="Directly comparable to clustered due to shared y-ranges.", - callout_header="🔎 Runtime tradeoff is visible", - callout_body="Prover-time gap remains moderate relative to proof-size improvements.", - progress="Random 2/3", - ), - FigureSlide( - filename="verifier_time_vs_k_random.png", - title="Verifier Time - Random", - subtitle="Final evidence slide in the same visual grammar.", - callout_header="✅ End-to-end story closes", - callout_body="Verifier cost tracks hashing work; optimization impact is stable across query styles.", - progress="Random 3/3", - ), -) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--fig-dir", - type=Path, - default=Path("figures/presentation"), - help="Directory containing required graph PNGs.", - ) - parser.add_argument( - "--out", - type=Path, - default=Path("/mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack.pptx"), - help="Output PPTX path.", - ) - parser.add_argument( - "--title", - type=str, - default="Results", - help="Section divider title word.", - ) - parser.add_argument( - "--readme", - type=Path, - default=Path("/mnt/c/Users/ajhav/Downloads/revised_batch_proofs_strategy_results_pack_README.txt"), - help="Companion README/TXT path.", - ) - return parser.parse_args() - - -def ensure_parent(path: Path) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - - -def preflight_figure_assets(fig_dir: Path, names: Sequence[str]) -> List[Path]: - resolved: List[Path] = [] - missing: List[str] = [] - sizes: List[Tuple[int, int]] = [] - - for name in names: - candidate = fig_dir / name - if not candidate.exists(): - missing.append(name) - continue - resolved.append(candidate) - with Image.open(candidate) as im: - sizes.append((im.width, im.height)) - - if missing: - raise FileNotFoundError(f"Missing figure files in {fig_dir}: {', '.join(missing)}") - - heights = {h for _, h in sizes} - if len(heights) > 1: - raise ValueError(f"Figure heights are inconsistent: {sorted(heights)}") - - widths = [w for w, _ in sizes] - w_min = min(widths) - w_max = max(widths) - if w_max > 1.35 * w_min: - raise ValueError( - "Figure widths are too inconsistent for a single visual family " - f"(min={w_min}, max={w_max}, ratio={w_max / w_min:.2f})." - ) - - return resolved - - -def add_textbox( - slide, - left: Emu, - top: Emu, - width: Emu, - height: Emu, - text: str, - *, - size: int, - bold: bool = False, - color: RGBColor | None = None, - align: PP_ALIGN = PP_ALIGN.LEFT, -) -> None: - tb = slide.shapes.add_textbox(left, top, width, height) - tf = tb.text_frame - tf.clear() - p = tf.paragraphs[0] - p.text = text - p.alignment = align - run = p.runs[0] - run.font.size = Pt(size) - run.font.bold = bold - if color is not None: - run.font.color.rgb = color - - -def add_rounded_box( - slide, - left: Emu, - top: Emu, - width: Emu, - height: Emu, - *, - fill: RGBColor, - line: RGBColor | None = None, - radius_adjust: float = 0.12, -): - shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) - shape.fill.solid() - shape.fill.fore_color.rgb = fill - if line is None: - shape.line.fill.background() - else: - shape.line.color.rgb = line - shape.line.width = Pt(1.0) - # Smaller corner radius for a sleek card look - shape.adjustments[0] = radius_adjust - return shape - - -def place_picture_contain(slide, image_path: Path, left: Emu, top: Emu, width: Emu, height: Emu) -> None: - with Image.open(image_path) as im: - img_w, img_h = im.size - scale = min(float(width) / img_w, float(height) / img_h) - pic_w = Emu(int(img_w * scale)) - pic_h = Emu(int(img_h * scale)) - pic_left = Emu(int(left + (width - pic_w) / 2)) - pic_top = Emu(int(top + (height - pic_h) / 2)) - slide.shapes.add_picture(str(image_path), pic_left, pic_top, width=pic_w, height=pic_h) - - -def build_divider_slide(prs: Presentation, title_word: str) -> None: - slide = prs.slides.add_slide(prs.slide_layouts[6]) - - bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Emu(0), Emu(0), SLIDE_W, SLIDE_H) - bg.fill.solid() - bg.fill.fore_color.rgb = TOKENS["bg_dark"] - bg.line.fill.background() - - band = slide.shapes.add_shape( - MSO_SHAPE.RECTANGLE, - Emu(0), - Emu(int(5.6 * EMU_PER_INCH)), - SLIDE_W, - Emu(int(0.16 * EMU_PER_INCH)), - ) - band.fill.solid() - band.fill.fore_color.rgb = TOKENS["accent"] - band.line.fill.background() - - add_textbox( - slide, - Emu(int(1.1 * EMU_PER_INCH)), - Emu(int(2.4 * EMU_PER_INCH)), - Emu(int(11.2 * EMU_PER_INCH)), - Emu(int(1.6 * EMU_PER_INCH)), - title_word, - size=66, - bold=True, - color=RGBColor(248, 250, 255), - ) - - add_textbox( - slide, - Emu(int(1.1 * EMU_PER_INCH)), - Emu(int(4.15 * EMU_PER_INCH)), - Emu(int(8.2 * EMU_PER_INCH)), - Emu(int(0.6 * EMU_PER_INCH)), - "Measured with one graph per slide for cleaner storytelling.", - size=20, - color=RGBColor(193, 203, 226), - ) - - chip = add_rounded_box( - slide, - Emu(int(12.35 * EMU_PER_INCH)), - Emu(int(0.35 * EMU_PER_INCH)), - Emu(int(0.28 * EMU_PER_INCH)), - Emu(int(0.28 * EMU_PER_INCH)), - fill=TOKENS["accent"], - line=None, - radius_adjust=0.35, - ) - chip.line.fill.background() - - -def build_metric_slide(prs: Presentation, spec: FigureSlide, image_path: Path) -> None: - slide = prs.slides.add_slide(prs.slide_layouts[6]) - - bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Emu(0), Emu(0), SLIDE_W, SLIDE_H) - bg.fill.solid() - bg.fill.fore_color.rgb = TOKENS["bg"] - bg.line.fill.background() - - accent_bar = slide.shapes.add_shape( - MSO_SHAPE.RECTANGLE, - Emu(int(0.08 * EMU_PER_INCH)), - Emu(int(0.5 * EMU_PER_INCH)), - Emu(int(0.05 * EMU_PER_INCH)), - Emu(int(6.5 * EMU_PER_INCH)), - ) - accent_bar.fill.solid() - accent_bar.fill.fore_color.rgb = TOKENS["accent"] - accent_bar.line.fill.background() - - add_textbox( - slide, - Emu(int(0.5 * EMU_PER_INCH)), - Emu(int(0.35 * EMU_PER_INCH)), - Emu(int(8.6 * EMU_PER_INCH)), - Emu(int(0.72 * EMU_PER_INCH)), - spec.title, - size=34, - bold=True, - color=TOKENS["text_dark"], - ) - add_textbox( - slide, - Emu(int(0.5 * EMU_PER_INCH)), - Emu(int(0.96 * EMU_PER_INCH)), - Emu(int(8.9 * EMU_PER_INCH)), - Emu(int(0.52 * EMU_PER_INCH)), - spec.subtitle, - size=16, - color=TOKENS["text_muted"], - ) - - card_left = Emu(int(0.48 * EMU_PER_INCH)) - card_top = Emu(int(1.42 * EMU_PER_INCH)) - card_w = Emu(int(9.65 * EMU_PER_INCH)) - card_h = Emu(int(5.55 * EMU_PER_INCH)) - - add_rounded_box( - slide, - card_left, - card_top, - card_w, - card_h, - fill=TOKENS["card"], - line=TOKENS["card_border"], - radius_adjust=0.06, - ) - - pic_margin = Emu(int(0.18 * EMU_PER_INCH)) - place_picture_contain( - slide, - image_path, - Emu(int(card_left + pic_margin)), - Emu(int(card_top + pic_margin)), - Emu(int(card_w - 2 * pic_margin)), - Emu(int(card_h - 2 * pic_margin)), - ) - - sticky_left = Emu(int(10.45 * EMU_PER_INCH)) - sticky_top = Emu(int(1.65 * EMU_PER_INCH)) - sticky_w = Emu(int(2.62 * EMU_PER_INCH)) - sticky_h = Emu(int(2.76 * EMU_PER_INCH)) - - add_rounded_box( - slide, - sticky_left, - sticky_top, - sticky_w, - sticky_h, - fill=TOKENS["sticky"], - line=TOKENS["sticky_border"], - radius_adjust=0.09, - ) - - add_textbox( - slide, - Emu(int(sticky_left + 0.18 * EMU_PER_INCH)), - Emu(int(sticky_top + 0.16 * EMU_PER_INCH)), - Emu(int(sticky_w - 0.35 * EMU_PER_INCH)), - Emu(int(0.7 * EMU_PER_INCH)), - spec.callout_header, - size=16, - bold=True, - color=TOKENS["text_dark"], - ) - add_textbox( - slide, - Emu(int(sticky_left + 0.18 * EMU_PER_INCH)), - Emu(int(sticky_top + 0.74 * EMU_PER_INCH)), - Emu(int(sticky_w - 0.35 * EMU_PER_INCH)), - Emu(int(1.9 * EMU_PER_INCH)), - spec.callout_body, - size=14, - color=RGBColor(61, 67, 79), - ) - - tag = add_rounded_box( - slide, - Emu(int(10.52 * EMU_PER_INCH)), - Emu(int(4.88 * EMU_PER_INCH)), - Emu(int(2.5 * EMU_PER_INCH)), - Emu(int(0.54 * EMU_PER_INCH)), - fill=TOKENS["progress_bg"], - line=TOKENS["accent"], - radius_adjust=0.22, - ) - tag.line.width = Pt(1.1) - add_textbox( - slide, - Emu(int(10.62 * EMU_PER_INCH)), - Emu(int(5.01 * EMU_PER_INCH)), - Emu(int(2.3 * EMU_PER_INCH)), - Emu(int(0.34 * EMU_PER_INCH)), - spec.progress, - size=12, - bold=True, - color=TOKENS["accent"], - align=PP_ALIGN.CENTER, - ) - - add_textbox( - slide, - Emu(int(10.45 * EMU_PER_INCH)), - Emu(int(6.43 * EMU_PER_INCH)), - Emu(int(2.65 * EMU_PER_INCH)), - Emu(int(0.4 * EMU_PER_INCH)), - "Results chapter redesign", - size=11, - color=RGBColor(114, 121, 136), - align=PP_ALIGN.RIGHT, - ) - - -def build_readme(readme_path: Path, pptx_path: Path, fig_dir: Path) -> None: - text = f"""Results Replacement Pack - Quick Insert Guide - -Output deck: -{pptx_path} - -Source figures: -{fig_dir} - -Insertion steps: -1) Open the original Keynote deck and this PPTX side-by-side. -2) In the original deck, remove the existing Results block (old divider + old metric slides). -3) Import all 7 slides from the PPTX in order: - - Results divider - - Proof Size - Clustered - - Prover Time - Clustered - - Verifier Time - Clustered - - Proof Size - Random - - Prover Time - Random - - Verifier Time - Random -4) Keep downstream slides (Migration/Next/Questions) as-is. - -Style conformance checklist: -- Section architecture: full-bleed divider with a single large chapter word. -- Visual system: one accent color, neutral palette, fixed typography hierarchy, repeated rounded cards. -- Controlled density: one graph per slide with generous whitespace. -- Progressive disclosure: clustered trio first, then random trio in an identical frame. -- Embedded annotations: one modern sticky-note insight callout per metric slide. -- Tasteful informality: subtle semantic emoji in callout headers. - -Validation: -The redesigned chapter matches the requested benchmark-slide style principles and keeps comparisons clean via one-graph-per-slide pacing. Next step: paste this 7-slide block into the main Keynote deck. -""" - readme_path.write_text(text, encoding="utf-8") - - -def build_pack(fig_dir: Path, out_path: Path, title: str, readme_path: Path) -> None: - required = [s.filename for s in FIGURE_SLIDES] - resolved = preflight_figure_assets(fig_dir, required) - figure_map = {p.name: p for p in resolved} - - prs = Presentation() - prs.slide_width = SLIDE_W - prs.slide_height = SLIDE_H - - build_divider_slide(prs, title) - for spec in FIGURE_SLIDES: - build_metric_slide(prs, spec, figure_map[spec.filename]) - - ensure_parent(out_path) - prs.save(out_path) - - ensure_parent(readme_path) - build_readme(readme_path, out_path, fig_dir) - - -def main() -> None: - args = parse_args() - build_pack(args.fig_dir, args.out, args.title, args.readme) - print(f"Wrote {args.out}") - print(f"Wrote {args.readme}") - - -if __name__ == "__main__": - main() diff --git a/scripts/linkify_changelog.py b/scripts/linkify_changelog.py deleted file mode 100644 index 1d85f290..00000000 --- a/scripts/linkify_changelog.py +++ /dev/null @@ -1,30 +0,0 @@ -import fileinput -import os -import re -import sys - -# Set this to the name of the repo, if you don't want it to be read from the filesystem. -# It assumes the changelog file is in the root of the repo. -repo_name = "" - -# This script goes through the provided file, and replaces any " \#", -# with the valid mark down formatted link to it. e.g. -# " [\#number](https://github.com/arkworks-rs/template/pull/) -# Note that if the number is for a an issue, github will auto-redirect you when you click the link. -# It is safe to run the script multiple times in succession. -# -# Example usage $ python3 linkify_changelog.py ../CHANGELOG.md -changelog_path = sys.argv[1] -if repo_name == "": - path = os.path.abspath(changelog_path) - components = path.split(os.path.sep) - repo_name = components[-2] - -for line in fileinput.input(inplace=True): - line = re.sub( - r"\- #([0-9]*)", - r"- [\#\1](https://github.com/arkworks-rs/" + repo_name + r"/pull/\1)", - line.rstrip(), - ) - # edits the current file - print(line) \ No newline at end of file diff --git a/scripts/presentation_coord_encoding_plots.py b/scripts/presentation_coord_encoding_plots.py deleted file mode 100644 index 09306351..00000000 --- a/scripts/presentation_coord_encoding_plots.py +++ /dev/null @@ -1,436 +0,0 @@ -#!/usr/bin/env python3 -"""Generate presentation-style proof-size figures for coordinate encoding comparisons. - -Usage: - python3 scripts/presentation_coord_encoding_plots.py --input target/merkle_tree_reports/coordinate_encoding_rows.csv --output figures/presentation - python3 scripts/presentation_coord_encoding_plots.py --input target/merkle_tree_reports/leb128_vs_delta_rows.csv --output figures/presentation -""" - -from __future__ import annotations - -import argparse -import csv -import math -import os -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Sequence, Tuple - -os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") - -import matplotlib.pyplot as plt - - -@dataclass(frozen=True) -class Row: - tree_size: int - log2_size: int - batch: int - pattern: str - strategy: str - proof_bytes: int - proof_nodes: int - - -DEFAULT_LABELS = { - "natural": "Unoptimized", - "leb128": "Partially Optimized", - "delta": "Optimized", -} - -DEFAULT_COLORS = { - "natural": "#d95f02", - "leb128": "#1b9e77", - "delta": "#1f77b4", -} - -DEFAULT_MARKERS = { - "natural": "s", - "leb128": "o", - "delta": "^", -} - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--input", - type=Path, - default=None, - help="Optional input file (.csv rows or .md report table). Auto-detected if omitted.", - ) - parser.add_argument( - "--output", - type=Path, - default=Path("figures/presentation"), - help="Directory where presentation figures are written", - ) - parser.add_argument( - "--patterns", - nargs="+", - default=["clustered", "random"], - help="Patterns to render (default: clustered random)", - ) - parser.add_argument( - "--tree-size", - type=int, - default=None, - help="Specific tree size n to plot. Default: max n in dataset", - ) - parser.add_argument( - "--points", - type=int, - default=5, - help="Representative k points per plot", - ) - return parser.parse_args() - - -def resolve_input_path(explicit: Path | None) -> Path: - if explicit is not None: - return explicit - - candidates = [ - Path("target/merkle_tree_reports/coordinate_encoding_rows.csv"), - Path("target/merkle_tree_reports/coordinate_encoding_report.md"), - Path("target/merkle_tree_reports/leb128_vs_delta_rows.csv"), - Path("target/merkle_tree_reports/leb128_vs_delta_report.md"), - Path("crypto-primitives/target/merkle_tree_reports/coordinate_encoding_rows.csv"), - Path("crypto-primitives/target/merkle_tree_reports/coordinate_encoding_report.md"), - Path("crypto-primitives/target/merkle_tree_reports/leb128_vs_delta_rows.csv"), - Path("crypto-primitives/target/merkle_tree_reports/leb128_vs_delta_report.md"), - ] - for candidate in candidates: - if candidate.exists(): - return candidate - raise FileNotFoundError( - "No coordinate encoding input found under target/merkle_tree_reports " - "or crypto-primitives/target/merkle_tree_reports." - ) - - -def read_rows_csv(path: Path) -> List[Row]: - rows: List[Row] = [] - with path.open(newline="", encoding="utf-8") as handle: - reader = csv.DictReader(handle) - for row in reader: - rows.append( - Row( - tree_size=int(row["tree_size"]), - log2_size=int(row["log2_size"]), - batch=int(row["batch"]), - pattern=row["pattern"], - strategy=row["strategy"], - proof_bytes=int(row["proof_bytes"]), - proof_nodes=int(row["proof_nodes"]), - ) - ) - return rows - - -def read_rows_markdown(path: Path) -> List[Row]: - rows: List[Row] = [] - headers: List[str] = [] - - with path.open("r", encoding="utf-8") as handle: - for line in handle: - if not line.startswith("|"): - continue - cells = [cell.strip() for cell in line.strip().strip("|").split("|")] - if not cells: - continue - if cells[0] == "tree_n": - headers = cells - continue - if cells[0].startswith("------") or not headers: - continue - if len(cells) != len(headers): - continue - - row = dict(zip(headers, cells)) - rows.append( - Row( - tree_size=int(row["tree_n"]), - log2_size=int(row["log2(n)"]), - batch=int(row["batch_k"]), - pattern=row["pattern"], - strategy=row["strategy"], - proof_bytes=int(row["proof_bytes"]), - proof_nodes=int(row["proof_nodes"]), - ) - ) - - return rows - - -def representative_values(values: Sequence[int], count: int) -> Tuple[List[int], List[Tuple[float, int]]]: - unique = sorted(set(values)) - if not unique: - return [], [] - if len(unique) <= count: - return unique, [(float(v), v) for v in unique] - - vmin = unique[0] - vmax = unique[-1] - log_min = math.log(vmin) - log_max = math.log(vmax) - - targets = [] - for i in range(count): - t = math.exp(log_min + (log_max - log_min) * (i / (count - 1))) - targets.append(t) - - selected: List[int] = [] - selections: List[Tuple[float, int]] = [] - - for target in targets: - remaining = [v for v in unique if v not in selected] - if not remaining: - break - choice = min(remaining, key=lambda v: abs(math.log(v) - math.log(target))) - selected.append(choice) - selections.append((target, choice)) - - if unique[0] not in selected: - selected[0] = unique[0] - if unique[-1] not in selected: - selected[-1] = unique[-1] - - selected = sorted(set(selected)) - if len(selected) < count: - for v in unique: - if v not in selected: - selected.append(v) - if len(selected) == count: - break - selected = sorted(selected) - - mapped_targets = [] - for t in targets: - mapped_targets.append((t, min(selected, key=lambda v: abs(math.log(v) - math.log(t))))) - - return selected, mapped_targets - - -def ensure_slide_style() -> None: - plt.rcParams.update( - { - "font.family": "Helvetica Neue", - "svg.fonttype": "none", - "figure.titlesize": 30, - "axes.titlesize": 28, - "axes.labelsize": 24, - "xtick.labelsize": 18, - "ytick.labelsize": 18, - "legend.fontsize": 17, - } - ) - - -def choose_byte_unit(values: Sequence[float]) -> Tuple[float, str]: - if not values: - return 1.0, "" - - max_abs = max(abs(v) for v in values) - if max_abs >= 1_000_000_000: - return 1_000_000_000.0, "G" - if max_abs >= 1_000_000: - return 1_000_000.0, "M" - if max_abs >= 1_000: - return 1_000.0, "k" - return 1.0, "" - - -def proof_size_label(prefix: str) -> str: - units = { - "": "B", - "k": "kB", - "M": "MB", - "G": "GB", - } - return f"Proof size ({units[prefix]})" - - -def infer_strategy_order(rows: Sequence[Row]) -> List[str]: - strategies = sorted({row.strategy for row in rows}) - if strategies == ["leb128", "natural"]: - return ["natural", "leb128"] - if strategies == ["delta", "leb128"]: - return ["leb128", "delta"] - preferred = [name for name in ["natural", "leb128", "delta"] if name in strategies] - return preferred + [name for name in strategies if name not in preferred] - - -def label_for(strategy: str) -> str: - return DEFAULT_LABELS.get(strategy, strategy) - - -def color_for(strategy: str) -> str: - return DEFAULT_COLORS.get(strategy, "#000000") - - -def marker_for(strategy: str) -> str: - return DEFAULT_MARKERS.get(strategy, "o") - - -def filename_prefix_for(strategies: Sequence[str]) -> str: - ordered = list(strategies) - if ordered == ["natural", "leb128"]: - return "coordinate_proof_size" - if ordered == ["leb128", "delta"]: - return "leb128_vs_delta_proof_size" - return "coord_comparison_proof_size" - - -def generate_plot( - rows: Sequence[Row], - pattern: str, - tree_size: int, - selected_k: Sequence[int], - output_dir: Path, - strategy_order: Sequence[str], -) -> List[Path]: - by_strategy: Dict[str, Dict[int, float]] = defaultdict(dict) - for row in rows: - if row.pattern != pattern or row.tree_size != tree_size: - continue - by_strategy[row.strategy][row.batch] = float(row.proof_bytes) - - fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) - - raw_values = [value for points in by_strategy.values() for value in points.values()] - unit_scale, unit_prefix = choose_byte_unit(raw_values) - - all_points: List[Tuple[int, float, str]] = [] - for strategy in strategy_order: - points = by_strategy.get(strategy, {}) - xs = [k for k in selected_k if k in points] - ys = [points[k] / unit_scale for k in xs] - if not xs: - continue - ax.plot( - xs, - ys, - label=label_for(strategy), - color=color_for(strategy), - marker=marker_for(strategy), - linewidth=3.0, - markersize=9, - ) - for x, y in zip(xs, ys): - all_points.append((x, y, strategy)) - - if not all_points: - plt.close(fig) - return [] - - y_values = [p[1] for p in all_points] - ymin = min(y_values) - ymax = max(y_values) - yrange = max(ymax - ymin, ymax * 0.08, 1e-9) - - ax.set_xscale("log", base=2) - ax.set_xticks(list(selected_k)) - ax.set_xticklabels([str(x) for x in selected_k]) - ax.set_xlim(min(selected_k) * 0.9, max(selected_k) * 1.15) - ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) - ax.set_xlabel("Number of leaves opened", labelpad=10, fontname="Helvetica Neue", fontweight="bold") - ax.set_ylabel( - proof_size_label(unit_prefix), - labelpad=10, - fontname="Helvetica Neue", - fontweight="bold", - ) - ax.grid(True, which="major", linestyle="--", alpha=0.28) - - legend = ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0), frameon=False) - if legend is not None: - for legend_text in legend.get_texts(): - legend_text.set_fontfamily("Helvetica Neue") - legend_text.set_fontweight("bold") - - for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): - tick_label.set_fontfamily("Helvetica Neue") - - fig.tight_layout() - - basename = f"{filename_prefix_for(strategy_order)}_vs_k_{pattern}" - outputs = [] - for ext in ("png", "pdf", "svg"): - out_path = output_dir / f"{basename}.{ext}" - save_kwargs = {"bbox_inches": "tight"} - if ext == "png": - save_kwargs["dpi"] = 300 - fig.savefig(out_path, **save_kwargs) - outputs.append(out_path) - - plt.close(fig) - return outputs - - -def main() -> None: - args = parse_args() - ensure_slide_style() - - input_path = resolve_input_path(args.input) - if input_path.suffix.lower() == ".csv": - rows = read_rows_csv(input_path) - elif input_path.suffix.lower() == ".md": - rows = read_rows_markdown(input_path) - else: - raise ValueError(f"Unsupported input format: {input_path}") - - if not rows: - raise RuntimeError(f"No benchmark rows found in {input_path}") - - available_tree_sizes = sorted({row.tree_size for row in rows}) - tree_size = args.tree_size if args.tree_size is not None else available_tree_sizes[-1] - if tree_size not in available_tree_sizes: - raise ValueError(f"tree_size {tree_size} not present. Available: {available_tree_sizes}") - - output_dir = args.output - output_dir.mkdir(parents=True, exist_ok=True) - - print(f"input={input_path}") - - strategy_order = infer_strategy_order(rows) - pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]] = {} - - for pattern in args.patterns: - strategy_batches: Dict[str, List[int]] = defaultdict(list) - for row in rows: - if row.tree_size == tree_size and row.pattern == pattern: - strategy_batches[row.strategy].append(row.batch) - - if not strategy_batches: - print(f"Skipping pattern={pattern}: no rows for tree_size={tree_size}") - continue - - shared = sorted(set.intersection(*(set(v) for v in strategy_batches.values()))) - if not shared: - print(f"Skipping pattern={pattern}: no shared k across strategies") - continue - - selected_k, target_map = representative_values(shared, args.points) - pattern_to_selection[pattern] = (selected_k, target_map, shared) - - print(f"pattern={pattern} tree_size={tree_size} available_k={shared}") - print(f"pattern={pattern} selected_k={selected_k}") - - for pattern, (selected_k, _, _) in pattern_to_selection.items(): - scoped_rows = [row for row in rows if row.tree_size == tree_size and row.pattern == pattern] - files = generate_plot( - rows=scoped_rows, - pattern=pattern, - tree_size=tree_size, - selected_k=selected_k, - output_dir=output_dir, - strategy_order=strategy_order, - ) - if files: - print(f"generated={ [str(path) for path in files] }") - - -if __name__ == "__main__": - main() diff --git a/scripts/presentation_plots.py b/scripts/presentation_plots.py deleted file mode 100644 index 14dd9dd2..00000000 --- a/scripts/presentation_plots.py +++ /dev/null @@ -1,560 +0,0 @@ -#!/usr/bin/env python3 -"""Generate presentation-friendly figures from multiproof benchmark rows. - -Data flow (existing benchmark path): -1) Rust benchmark test builds trees, samples (k, pattern), and benchmarks prefix/coset. -2) The test writes either raw rows CSV (multiproof_v2_rows.csv) and/or markdown report table - (multiproof_v2_report.md) under target/merkle_tree_reports/. -3) This script reads those rows and renders one figure per metric/pattern in - figures/presentation/, using a representative subset of k values. - -Usage: - python3 scripts/presentation_plots.py - python3 scripts/presentation_plots.py \ - --input target/merkle_tree_reports/multiproof_v2_report.md \ - --output figures/presentation -""" - -from __future__ import annotations - -import argparse -import csv -import math -import os -from collections import defaultdict -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, List, Sequence, Tuple - -# Matplotlib needs a writable config dir in some sandboxed environments. -os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") - -import matplotlib.pyplot as plt - - -@dataclass(frozen=True) -class Row: - tree_size: int - log2_size: int - batch: int - pattern: str - strategy: str - proof_bytes: int - proof_nodes: int - hashes_per_opening: float - prove_ms: float - verify_ms: float - rss_delta_kb: float | None - - -@dataclass(frozen=True) -class MetricSpec: - key: str - title: str - y_label: str - filename_prefix: str - - -METRICS: Tuple[MetricSpec, ...] = ( - MetricSpec("proof_bytes", "Proof Size vs Input Size", "Proof size (bytes)", "proof_size"), - MetricSpec("prove_ms", "Prover Time vs Input Size", "Time (ms)", "prover_time"), - MetricSpec("verify_ms", "Verifier Time vs Input Size", "Time (ms)", "verifier_time"), - MetricSpec( - "hashes_per_opening", - "Hashes per Opening (m Proxy) vs Input Size", - "Hashes per opening", - "hashes_per_opening", - ), -) - -STRATEGY_STYLE = { - "prefix": {"label": "Old", "color": "#d62728", "marker": "s"}, - "coset": {"label": "New", "color": "#1f77b4", "marker": "o"}, -} - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--input", - type=Path, - default=None, - help="Optional input file (.csv rows or .md report table). Auto-detected if omitted.", - ) - parser.add_argument( - "--output", - type=Path, - default=Path("figures/presentation"), - help="Directory where presentation figures are written", - ) - parser.add_argument( - "--patterns", - nargs="+", - default=["clustered", "random"], - help="Patterns to render (default: clustered random)", - ) - parser.add_argument( - "--tree-size", - type=int, - default=None, - help="Specific tree size n to plot. Default: max n in dataset", - ) - parser.add_argument( - "--points", - type=int, - default=5, - help="Representative k points per plot", - ) - return parser.parse_args() - - -def read_rows_csv(path: Path) -> List[Row]: - rows: List[Row] = [] - with path.open(newline="", encoding="utf-8") as handle: - reader = csv.DictReader(handle) - for r in reader: - rss_raw = (r.get("rss_delta_kb") or "").strip() - rows.append( - Row( - tree_size=int(r["tree_size"]), - log2_size=int(r["log2_size"]), - batch=int(r["batch"]), - pattern=r["pattern"], - strategy=r["strategy"], - proof_bytes=int(r["proof_bytes"]), - proof_nodes=int(r["proof_nodes"]), - hashes_per_opening=float(r["hashes_per_opening"]), - prove_ms=float(r["prove_ms"]), - verify_ms=float(r["verify_ms"]), - rss_delta_kb=float(rss_raw) if rss_raw else None, - ) - ) - return rows - - -def read_rows_markdown(path: Path) -> List[Row]: - rows: List[Row] = [] - headers: List[str] = [] - - with path.open("r", encoding="utf-8") as handle: - for line in handle: - if not line.startswith("|"): - continue - cells = [cell.strip() for cell in line.strip().strip("|").split("|")] - if not cells: - continue - if cells[0] == "tree_n": - headers = cells - continue - if cells[0].startswith("------") or not headers: - continue - if len(cells) != len(headers): - continue - - row = dict(zip(headers, cells)) - rows.append( - Row( - tree_size=int(row["tree_n"]), - log2_size=int(row["log2(n)"]), - batch=int(row["batch_k"]), - pattern=row["pattern"], - strategy=row["strategy"], - proof_bytes=int(row["proof_bytes"]), - proof_nodes=int(row["proof_nodes"]), - hashes_per_opening=float(row["hashes/leaf"]), - prove_ms=float(row["prove_ms"]), - verify_ms=float(row["verify_ms"]), - rss_delta_kb=float(row["rss_delta_kb"]) if row["rss_delta_kb"] != "-" else None, - ) - ) - - return rows - - -def resolve_input_path(explicit: Path | None) -> Path: - if explicit is not None: - return explicit - - candidates = [ - Path("target/merkle_tree_reports/multiproof_v2_rows.csv"), - Path("target/merkle_tree_reports/multiproof_v2_report.md"), - Path("crypto-primitives/target/merkle_tree_reports/multiproof_v2_rows.csv"), - Path("crypto-primitives/target/merkle_tree_reports/multiproof_v2_report.md"), - ] - for candidate in candidates: - if candidate.exists(): - return candidate - raise FileNotFoundError( - "No benchmark input found. Looked for multiproof_v2_rows.csv or multiproof_v2_report.md " - "under target/merkle_tree_reports and crypto-primitives/target/merkle_tree_reports." - ) - - -def metric_value(row: Row, metric_key: str) -> float: - return float(getattr(row, metric_key)) - - -def representative_values(values: Sequence[int], count: int) -> Tuple[List[int], List[Tuple[float, int]]]: - unique = sorted(set(values)) - if not unique: - return [], [] - if len(unique) <= count: - return unique, [(float(v), v) for v in unique] - - vmin = unique[0] - vmax = unique[-1] - log_min = math.log(vmin) - log_max = math.log(vmax) - - targets = [] - for i in range(count): - t = math.exp(log_min + (log_max - log_min) * (i / (count - 1))) - targets.append(t) - - selected: List[int] = [] - selections: List[Tuple[float, int]] = [] - - for target in targets: - remaining = [v for v in unique if v not in selected] - if not remaining: - break - choice = min(remaining, key=lambda v: abs(math.log(v) - math.log(target))) - selected.append(choice) - selections.append((target, choice)) - - if unique[0] not in selected: - selected[0] = unique[0] - if unique[-1] not in selected: - selected[-1] = unique[-1] - - selected = sorted(set(selected)) - if len(selected) < count: - for v in unique: - if v not in selected: - selected.append(v) - if len(selected) == count: - break - selected = sorted(selected) - - mapped_targets = [] - for t in targets: - mapped_targets.append((t, min(selected, key=lambda v: abs(math.log(v) - math.log(t))))) - - return selected, mapped_targets - - -def ensure_slide_style() -> None: - plt.rcParams.update( - { - "font.family": "Helvetica Neue", - "svg.fonttype": "none", - "figure.titlesize": 30, - "axes.titlesize": 28, - "axes.labelsize": 24, - "xtick.labelsize": 18, - "ytick.labelsize": 18, - "legend.fontsize": 17, - } - ) - - -def generate_plot( - rows: Sequence[Row], - metric: MetricSpec, - pattern: str, - tree_size: int, - selected_k: Sequence[int], - output_dir: Path, - shared_y_range: Tuple[float, float] | None = None, -) -> List[Path]: - by_strategy: Dict[str, Dict[int, float]] = defaultdict(dict) - for row in rows: - if row.pattern != pattern or row.tree_size != tree_size: - continue - by_strategy[row.strategy][row.batch] = metric_value(row, metric.key) - - fig, ax = plt.subplots(figsize=(13.33, 7.5), dpi=300) - - all_points: List[Tuple[int, float, str]] = [] - unit_scale = 1.0 - unit_prefix = "" - if metric.key == "proof_bytes": - raw_values = list(by_strategy.get("prefix", {}).values()) + list(by_strategy.get("coset", {}).values()) - unit_scale, unit_prefix = choose_byte_unit(raw_values) - - for strategy in ("prefix", "coset"): - points = by_strategy.get(strategy, {}) - xs = [k for k in selected_k if k in points] - ys = [points[k] / unit_scale for k in xs] - if not xs: - continue - style = STRATEGY_STYLE.get(strategy, {"label": strategy, "color": "#000000", "marker": "o"}) - ax.plot( - xs, - ys, - label=style["label"], - color=style["color"], - marker=style["marker"], - linewidth=3.0, - markersize=9, - ) - for x, y in zip(xs, ys): - all_points.append((x, y, strategy)) - - if not all_points: - plt.close(fig) - return [] - - if shared_y_range is None: - y_values = [p[1] for p in all_points] - ymin = min(y_values) - ymax = max(y_values) - else: - ymin, ymax = shared_y_range - ymin /= unit_scale - ymax /= unit_scale - yrange = max(ymax - ymin, ymax * 0.08, 1e-9) - - ax.set_xscale("log", base=2) - ax.set_xticks(list(selected_k)) - ax.set_xticklabels([str(x) for x in selected_k]) - ax.set_xlim(min(selected_k) * 0.9, max(selected_k) * 1.15) - ax.set_ylim(max(0.0, ymin - 0.10 * yrange), ymax + 0.14 * yrange) - ax.set_xlabel("Number of leaves opened", labelpad=10, fontname="Helvetica Neue", fontweight="bold") - ax.set_ylabel( - proof_size_label(unit_prefix) if metric.key == "proof_bytes" else metric.y_label, - labelpad=10, - fontname="Helvetica Neue", - fontweight="bold", - ) - ax.grid(True, which="major", linestyle="--", alpha=0.28) - - legend = ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0), frameon=False) - if legend is not None: - for legend_text in legend.get_texts(): - legend_text.set_fontfamily("Helvetica Neue") - legend_text.set_fontweight("bold") - - for tick_label in ax.get_xticklabels() + ax.get_yticklabels(): - tick_label.set_fontfamily("Helvetica Neue") - - fig.tight_layout() - - basename = f"{metric.filename_prefix}_vs_k_{pattern}" - outputs = [] - for ext in ("png", "pdf", "svg"): - out_path = output_dir / f"{basename}.{ext}" - save_kwargs = {"bbox_inches": "tight"} - if ext == "png": - save_kwargs["dpi"] = 300 - fig.savefig(out_path, **save_kwargs) - outputs.append(out_path) - - plt.close(fig) - return outputs - - -def compute_shared_y_ranges( - rows: Sequence[Row], - tree_size: int, - pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]], - metrics: Sequence[MetricSpec], -) -> Dict[str, Tuple[float, float]]: - ranges: Dict[str, Tuple[float, float]] = {} - patterns = set(pattern_to_selection.keys()) - - for metric in metrics: - values: List[float] = [] - for row in rows: - if row.tree_size != tree_size or row.pattern not in patterns: - continue - selected_k = pattern_to_selection[row.pattern][0] - if row.batch not in selected_k: - continue - values.append(metric_value(row, metric.key)) - - if values: - ranges[metric.key] = (min(values), max(values)) - - return ranges - - -def choose_byte_unit(values: Sequence[float]) -> Tuple[float, str]: - if not values: - return 1.0, "" - - max_abs = max(abs(v) for v in values) - if max_abs >= 1_000_000_000: - return 1_000_000_000.0, "G" - if max_abs >= 1_000_000: - return 1_000_000.0, "M" - if max_abs >= 1_000: - return 1_000.0, "k" - return 1.0, "" - - -def proof_size_label(prefix: str) -> str: - units = { - "": "B", - "k": "kB", - "M": "MB", - "G": "GB", - } - return f"Proof size ({units[prefix]})" - - -def write_selection_summary( - output_dir: Path, - tree_size: int, - pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]], -) -> None: - summary_path = output_dir / "selection_summary.txt" - with summary_path.open("w", encoding="utf-8") as handle: - handle.write(f"tree_size={tree_size}\n") - for pattern, (selected, mapping, available) in pattern_to_selection.items(): - handle.write(f"\npattern={pattern}\n") - handle.write(f"available_k={available}\n") - handle.write(f"selected_k={selected}\n") - handle.write("targets_to_selected=\n") - for target, chosen in mapping: - handle.write(f" target~{target:.2f} -> {chosen}\n") - - -def average(values: Sequence[float]) -> float: - if not values: - return 0.0 - return sum(values) / float(len(values)) - - -def write_highest_k_differences( - output_dir: Path, - rows: Sequence[Row], - tree_size: int, - patterns: Sequence[str], -) -> None: - summary_path = output_dir / "largest_k_percent_change.txt" - lines: List[str] = [f"tree_size={tree_size}"] - - metric_specs = [ - ("proof_bytes", "proof size"), - ("prove_ms", "prover time"), - ("verify_ms", "verifier time"), - ] - - for pattern in patterns: - scoped = [r for r in rows if r.tree_size == tree_size and r.pattern == pattern] - if not scoped: - continue - - by_strategy: Dict[str, Dict[int, List[Row]]] = defaultdict(lambda: defaultdict(list)) - for row in scoped: - by_strategy[row.strategy][row.batch].append(row) - - if "prefix" not in by_strategy or "coset" not in by_strategy: - continue - - shared_k = sorted(set(by_strategy["prefix"].keys()) & set(by_strategy["coset"].keys())) - if not shared_k: - continue - - highest_k = shared_k[-1] - lines.append(f"") - lines.append(f"pattern={pattern}") - lines.append(f"largest_k={highest_k}") - - for metric_key, label in metric_specs: - prefix_values = [metric_value(r, metric_key) for r in by_strategy["prefix"][highest_k]] - coset_values = [metric_value(r, metric_key) for r in by_strategy["coset"][highest_k]] - if not prefix_values or not coset_values: - continue - - old_avg = average(prefix_values) - new_avg = average(coset_values) - pct = 0.0 if old_avg == 0 else ((new_avg - old_avg) / old_avg) * 100.0 - lines.append(f"{label}: {pct:+.2f}% (Old={old_avg:.3f}, New={new_avg:.3f})") - - with summary_path.open("w", encoding="utf-8") as handle: - handle.write("\n".join(lines) + "\n") - - -def main() -> None: - args = parse_args() - ensure_slide_style() - - input_path = resolve_input_path(args.input) - if input_path.suffix.lower() == ".csv": - rows = read_rows_csv(input_path) - elif input_path.suffix.lower() == ".md": - rows = read_rows_markdown(input_path) - else: - raise ValueError(f"Unsupported input format: {input_path}") - - if not rows: - raise RuntimeError(f"No benchmark rows found in {input_path}") - - available_tree_sizes = sorted({r.tree_size for r in rows}) - tree_size = args.tree_size if args.tree_size is not None else available_tree_sizes[-1] - if tree_size not in available_tree_sizes: - raise ValueError(f"tree_size {tree_size} not present. Available: {available_tree_sizes}") - - output_dir = args.output - output_dir.mkdir(parents=True, exist_ok=True) - - print(f"input={input_path}") - - pattern_to_selection: Dict[str, Tuple[List[int], List[Tuple[float, int]], List[int]]] = {} - - for pattern in args.patterns: - strategy_batches: Dict[str, List[int]] = defaultdict(list) - for row in rows: - if row.tree_size == tree_size and row.pattern == pattern: - strategy_batches[row.strategy].append(row.batch) - - if not strategy_batches: - print(f"Skipping pattern={pattern}: no rows for tree_size={tree_size}") - continue - - shared = sorted(set.intersection(*(set(v) for v in strategy_batches.values()))) - if not shared: - print(f"Skipping pattern={pattern}: no shared k across strategies") - continue - - selected_k, target_map = representative_values(shared, args.points) - pattern_to_selection[pattern] = (selected_k, target_map, shared) - - print(f"pattern={pattern} tree_size={tree_size} available_k={shared}") - print(f"pattern={pattern} selected_k={selected_k}") - - shared_y_ranges = compute_shared_y_ranges( - rows=rows, - tree_size=tree_size, - pattern_to_selection=pattern_to_selection, - metrics=METRICS, - ) - - for pattern, (selected_k, _, _) in pattern_to_selection.items(): - scoped_rows = [r for r in rows if r.tree_size == tree_size and r.pattern == pattern] - for metric in METRICS: - files = generate_plot( - scoped_rows, - metric, - pattern, - tree_size, - selected_k, - output_dir, - shared_y_range=None if metric.key in {"proof_bytes", "prove_ms", "verify_ms"} else shared_y_ranges.get(metric.key), - ) - if files: - print(f"regenerated(shared-y): {[str(f) for f in files]}") - - write_selection_summary(output_dir, tree_size, pattern_to_selection) - write_highest_k_differences( - output_dir=output_dir, - rows=rows, - tree_size=tree_size, - patterns=list(pattern_to_selection.keys()), - ) - - -if __name__ == "__main__": - main() From fbea336b9cdc1ef61df4ba11d3920c107cf98dbb Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Wed, 1 Apr 2026 17:21:17 +0100 Subject: [PATCH 18/22] crypto-primitives/src/merkle_tree/mod.rs - Remove PackedInnerCopath, delta/varint encoding, pack_inner_copath, decode_inner_copath - Merge generate_implicit_multi_proof into generate_multi_proof - Delete implicit.rs (logic absorbed into CoPath) - Change helper visibility from pub(super) to private - Consolidate test suite; add six structural copath-count tests --- .gitignore | 24 +- CHANGELOG.md | 183 ++++--- crypto-primitives/src/merkle_tree/implicit.rs | 205 ------- crypto-primitives/src/merkle_tree/mod.rs | 499 ++++-------------- .../merkle_tree/tests/delta_encoding_tests.rs | 151 ------ .../tests/implicit_copath_tests.rs | 198 ------- .../src/merkle_tree/tests/mod.rs | 295 +++++++---- 7 files changed, 391 insertions(+), 1164 deletions(-) delete mode 100644 crypto-primitives/src/merkle_tree/implicit.rs delete mode 100644 crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs delete mode 100644 crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs diff --git a/.gitignore b/.gitignore index 1ce769b8..c314c989 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,13 @@ -target -Cargo.lock -.DS_Store -.idea -*.iml -*.ipynb_checkpoints -*.pyc -*.sage.py -params -*.swp -*.swo -.vscode +target +Cargo.lock +.DS_Store +.idea +*.iml +*.ipynb_checkpoints +*.pyc +*.sage.py +params +*.swp +*.swo +.vscode figures/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b73da58..edfe9d08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,90 @@ -# CHANGELOG - -## Pending - -### Breaking changes - -- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/) Replace the prefix-encoded `MultiPath` Merkle multiproof with a -CoSet-based `CoPath` representation and update `MerkleTree::generate_multi_proof` to return `CoPath`. This changes the proof -encoding for batch openings and removes the old `MultiPath` type from the public API. - -### Features - -### Improvements - -- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/) Implement CoSet (minimal copath) pruning and delta-encoding for Merkle -multiproofs, reducing proof size and redundant hashing in batched openings. - -### Bugfixes - -## v0.5.0 - -- [\#120](https://github.com/arkworks-rs/crypto-primitives/pull/120) Add input size check to `bowe_hopwood::CRHGadget::evaluate`. - -### Breaking changes - -### Features - -- [\#107](https://github.com/arkworks-rs/crypto-primitives/pull/107) Impl `CanonicalSerialize` and `CanonicalDeserialize` for `ark_crypto_primitives::crh::pedersen::Parameters` - -### Improvements - -### Bugfixes - -## v0.4.0 - -### Breaking changes - -- [\#56](https://github.com/arkworks-rs/crypto-primitives/pull/56) Compress the output of the Bowe-Hopwood-Pedersen CRH to a single field element, in line with the Zcash specification. -- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Merkle tree's `Config` requires a user-defined converter to turn leaf hash output to inner hash output. -- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Rename the CRH trait as `CRHScheme` and the CRHGadget trait to `CRHSchemeGadget`. -- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Use `ark-sponge` to instantiate Poseidon. -- [\#76](https://github.com/arkworks-rs/crypto-primitives/pull/79) Fix Pedersen padding bug. -- [\#77](https://github.com/arkworks-rs/crypto-primitives/pull/77) Implement SHA-256 CRH. -- [\#86](https://github.com/arkworks-rs/crypto-primitives/pull/86) - - Moves `ark-sponge` here. - - Updates dependencies and version number to `0.4`. - - Adds feature flags to enable downstream users to select exactly those components that they're interested in. -- [\#103](https://github.com/arkworks-rs/crypto-primitives/pull/103) Removes `cp-benches` and moves contents to `benches` -- [\#104](https://github.com/arkworks-rs/crypto-primitives/pull/104) Updates `digest`, `blake2`, `sha2` to `0.10`. Changes API for `Blake2sWithParameterBlock`. - -### Features - -- [\#59](https://github.com/arkworks-rs/crypto-primitives/pull/59) Implement `TwoToOneCRHScheme` for Bowe-Hopwood CRH. -- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Merkle tree no longer requires CRH to input and output bytes. Leaf can be any raw input of CRH, such as field elements. -- [\#67](https://github.com/arkworks-rs/crypto-primitives/pull/67) User can access or replace leaf index variable in `PathVar`. - -### Improvements - -### Bugfixes - -## v0.3.0 - -### Breaking changes - -- [\#30](https://github.com/arkworks-rs/crypto-primitives/pull/30) Refactor the Merkle tree to separate the leaf hash and two-to-one hash. - -### Features - -- [\#38](https://github.com/arkworks-rs/crypto-primitives/pull/38) Add a signature verification trait `SigVerifyGadget`. -- [\#44](https://github.com/arkworks-rs/crypto-primitives/pull/44) Add basic ElGamal encryption gadgets. -- [\#48](https://github.com/arkworks-rs/crypto-primitives/pull/48) Add `CanonicalSerialize` and `CanonicalDeserialize` to `Path` and `CRH` outputs. - -### Improvements - -### Bugfixes - -## v0.2.0 - -### Breaking changes - -### Features - -- [\#2](https://github.com/arkworks-rs/crypto-primitives/pull/2) Add the `SNARK` gadget traits. -- [\#3](https://github.com/arkworks-rs/crypto-primitives/pull/3) Add unchecked allocation for `ProofVar` and `VerifyingKeyVar`. -- [\#4](https://github.com/arkworks-rs/crypto-primitives/pull/4) Add `verifier_size` to `SNARKGadget`. -- [\#6](https://github.com/arkworks-rs/crypto-primitives/pull/6) Add `IntoIterator` for SNARK input gadgets. -- [\#28](https://github.com/arkworks-rs/crypto-primitives/pull/28) Adds Poseidon CRH w/ constraints. - -### Improvements - -### Bugfixes - -## v0.1.0 (Initial release of arkworks/crypto-primitives) +# CHANGELOG + +## Pending + +### Breaking changes + +- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/X) Replace the prefix-encoded `MultiPath` Merkle multiproof with a CoSet-based `CoPath` representation and update `MerkleTree::generate_multi_proof` to return `CoPath`. This changes the proof encoding for batch openings and removes the old `MultiPath` type from the public API. + +### Features + +### Improvements + +- [\#](https://github.com/arkworks-rs/crypto-primitives/pull/X) Implement CoSet (minimal copath) pruning for Merkle multiproofs, reducing proof size and redundant hashing in batched openings. + +### Bugfixes + +## v0.5.0 + +- [\#120](https://github.com/arkworks-rs/crypto-primitives/pull/120) Add input size check to `bowe_hopwood::CRHGadget::evaluate`. + +### Breaking changes + +### Features + +- [\#107](https://github.com/arkworks-rs/crypto-primitives/pull/107) Impl `CanonicalSerialize` and `CanonicalDeserialize` for `ark_crypto_primitives::crh::pedersen::Parameters` + +### Improvements + +### Bugfixes + +## v0.4.0 + +### Breaking changes + +- [\#56](https://github.com/arkworks-rs/crypto-primitives/pull/56) Compress the output of the Bowe-Hopwood-Pedersen CRH to a single field element, in line with the Zcash specification. +- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Merkle tree's `Config` requires a user-defined converter to turn leaf hash output to inner hash output. +- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Rename the CRH trait as `CRHScheme` and the CRHGadget trait to `CRHSchemeGadget`. +- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Use `ark-sponge` to instantiate Poseidon. +- [\#76](https://github.com/arkworks-rs/crypto-primitives/pull/79) Fix Pedersen padding bug. +- [\#77](https://github.com/arkworks-rs/crypto-primitives/pull/77) Implement SHA-256 CRH. +- [\#86](https://github.com/arkworks-rs/crypto-primitives/pull/86) + - Moves `ark-sponge` here. + - Updates dependencies and version number to `0.4`. + - Adds feature flags to enable downstream users to select exactly those components that they're interested in. +- [\#103](https://github.com/arkworks-rs/crypto-primitives/pull/103) Removes `cp-benches` and moves contents to `benches` +- [\#104](https://github.com/arkworks-rs/crypto-primitives/pull/104) Updates `digest`, `blake2`, `sha2` to `0.10`. Changes API for `Blake2sWithParameterBlock`. + +### Features + +- [\#59](https://github.com/arkworks-rs/crypto-primitives/pull/59) Implement `TwoToOneCRHScheme` for Bowe-Hopwood CRH. +- [\#60](https://github.com/arkworks-rs/crypto-primitives/pull/60) Merkle tree no longer requires CRH to input and output bytes. Leaf can be any raw input of CRH, such as field elements. +- [\#67](https://github.com/arkworks-rs/crypto-primitives/pull/67) User can access or replace leaf index variable in `PathVar`. + +### Improvements + +### Bugfixes + +## v0.3.0 + +### Breaking changes + +- [\#30](https://github.com/arkworks-rs/crypto-primitives/pull/30) Refactor the Merkle tree to separate the leaf hash and two-to-one hash. + +### Features + +- [\#38](https://github.com/arkworks-rs/crypto-primitives/pull/38) Add a signature verification trait `SigVerifyGadget`. +- [\#44](https://github.com/arkworks-rs/crypto-primitives/pull/44) Add basic ElGamal encryption gadgets. +- [\#48](https://github.com/arkworks-rs/crypto-primitives/pull/48) Add `CanonicalSerialize` and `CanonicalDeserialize` to `Path` and `CRH` outputs. + +### Improvements + +### Bugfixes + +## v0.2.0 + +### Breaking changes + +### Features + +- [\#2](https://github.com/arkworks-rs/crypto-primitives/pull/2) Add the `SNARK` gadget traits. +- [\#3](https://github.com/arkworks-rs/crypto-primitives/pull/3) Add unchecked allocation for `ProofVar` and `VerifyingKeyVar`. +- [\#4](https://github.com/arkworks-rs/crypto-primitives/pull/4) Add `verifier_size` to `SNARKGadget`. +- [\#6](https://github.com/arkworks-rs/crypto-primitives/pull/6) Add `IntoIterator` for SNARK input gadgets. +- [\#28](https://github.com/arkworks-rs/crypto-primitives/pull/28) Adds Poseidon CRH w/ constraints. + +### Improvements + +### Bugfixes + +## v0.1.0 (Initial release of arkworks/crypto-primitives) diff --git a/crypto-primitives/src/merkle_tree/implicit.rs b/crypto-primitives/src/merkle_tree/implicit.rs deleted file mode 100644 index fe6f2626..00000000 --- a/crypto-primitives/src/merkle_tree/implicit.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Coordinate-free batch Merkle membership proof. -//! -//! Both prover and verifier independently derive the positions of all required copath nodes -//! from `leaf_indexes` and `tree_height` by running [`compute_on_path`]. Only the digests are -//! transmitted, in canonical depth-then-index order. No coordinate metadata is included in the -//! proof, so the inner copath is a plain `Vec` rather than a packed delta stream. -//! -//! # Wire format vs [`super::CoPath`] -//! -//! | Field | CoPath | ImplicitCoPath | -//! |-------|--------|----------------| -//! | `tree_height` | ✓ | ✓ | -//! | `leaf_copath` | `Vec` | `Vec` (identical) | -//! | `inner_copath` | `(start_depth, start_index, deltas, digests)` | `Vec` | -//! | `leaf_indexes` | `Vec` | `Vec` | -//! -//! The saved bytes come entirely from dropping the coordinate metadata (`start_depth`, -//! `start_index`, `deltas`). For a tree with `h` inner layers and `n_c` copath entries the -//! saving is `2 * sizeof(usize) + (n_c - 1) * 2 * avg_delta_bytes`. - -use crate::Error; -use ark_serialize::{ - CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, -}; -#[cfg(not(feature = "std"))] -use ark_std::vec::Vec; -use ark_std::{ - borrow::Borrow, - collections::{BTreeMap, BTreeSet}, - hash::BuildHasherDefault, -}; -use hashbrown::HashMap; - -use super::{ - compute_on_path, level_index, CoPath, Config, DefaultHasher, LeafParam, TwoToOneParam, -}; - -/// Coordinate-free batch Merkle membership proof. -/// -/// See the [module-level documentation](self) for a description of the protocol and a comparison -/// with [`CoPath`]. -#[derive(Derivative, CanonicalSerialize)] -#[derivative( - Clone(bound = "P: Config"), - Debug(bound = "P: Config"), - Default(bound = "P: Config") -)] -pub struct ImplicitCoPath { - /// Height of the tree this proof was generated from (>= 2). - pub tree_height: usize, - /// Leaf-layer copath digests (`B*_{d-1}`), ascending sibling index order. - pub leaf_copath: Vec, - /// Inner copath digests in canonical order: depth 1 ascending, depth 2 ascending, … - /// No coordinates are stored; the verifier derives them from `leaf_indexes`. - pub inner_copath: Vec, - /// Leaf indexes that were opened, in ascending order. - pub leaf_indexes: Vec, -} - -impl Valid for ImplicitCoPath

{ - fn check(&self) -> Result<(), SerializationError> { - if self.tree_height < 2 { - return Err(SerializationError::InvalidData); - } - self.leaf_copath.check()?; - self.inner_copath.check()?; - self.leaf_indexes.check() - } -} - -impl CanonicalDeserialize for ImplicitCoPath

{ - fn deserialize_with_mode( - mut reader: R, - compress: Compress, - validate: Validate, - ) -> Result { - let tree_height = usize::deserialize_with_mode(&mut reader, compress, validate)?; - let leaf_copath = - Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - let inner_copath = - Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - let leaf_indexes = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - if tree_height < 2 { - return Err(SerializationError::InvalidData); - } - Ok(ImplicitCoPath { - tree_height, - leaf_copath, - inner_copath, - leaf_indexes, - }) - } -} - -impl ImplicitCoPath

{ - /// Verify that the leaves (supplied in `leaf_indexes` order) are at the claimed positions in - /// the tree with root `root_hash` and height `expected_tree_height`. - /// - /// The verifier independently reconstructs the canonical copath order from `leaf_indexes` and - /// `tree_height`, then consumes `inner_copath` in that order. If the digest count does not - /// match what the verifier derives, verification returns `Ok(false)`. - pub fn verify + Clone>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - expected_tree_height: usize, - leaves: impl IntoIterator, - ) -> Result { - if self.leaf_indexes.is_empty() { - return Err(Error::GenericError(ark_std::boxed::Box::new( - ark_std::io::Error::new( - ark_std::io::ErrorKind::InvalidInput, - "batch proof must contain at least one leaf index", - ), - ))); - } - - if self.tree_height < 2 { - return Err(Error::GenericError(ark_std::boxed::Box::new( - ark_std::io::Error::new( - ark_std::io::ErrorKind::InvalidInput, - "tree_height must be >= 2", - ), - ))); - } - - if self.tree_height != expected_tree_height { - return Ok(false); - } - - let d = self.tree_height; - let leaf_depth = d - 1; - - let mut leaves_iter = leaves.into_iter(); - let mut leaf_level = - CoPath::

::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params)?; - - let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); - let on_path = compute_on_path(leaf_depth, &index_set); - - let expected_leaf_coset = CoPath::

::expected_leaf_coset(leaf_depth, &on_path); - if !CoPath::

::validate_leaf_copath( - &expected_leaf_coset, - &self.leaf_copath, - &mut leaf_level, - ) { - return Ok(false); - } - - let mut inner_levels: Vec> = - (0..d).map(|_| BTreeMap::new()).collect(); - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); - - // Consume inner_copath in canonical order: depths 1..leaf_depth, ascending index. - let mut cursor = 0usize; - for depth in 1..leaf_depth { - for &path_idx in on_path[depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[depth].binary_search(&sibling_idx).is_err() { - let digest = match self.inner_copath.get(cursor) { - Some(d) => d, - None => return Ok(false), // prover sent fewer digests than expected - }; - cursor += 1; - inner_levels[depth].insert(sibling_idx, digest.clone()); - let heap_idx = level_index(depth, sibling_idx); - hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); - } - } - } - - // Reject if prover sent more digests than we expected. - if cursor != self.inner_copath.len() { - return Ok(false); - } - - if !CoPath::

::recompute_bottom_parents( - leaf_depth, - &on_path, - &leaf_level, - two_to_one_params, - &mut hash_lut, - &mut inner_levels, - )? { - return Ok(false); - } - - if !CoPath::

::recompute_inner_layers( - leaf_depth, - &on_path, - two_to_one_params, - &mut hash_lut, - &mut inner_levels, - )? { - return Ok(false); - } - - match inner_levels[0].get(&0) { - Some(h) => Ok(h == root_hash), - None => Ok(false), - } - } -} diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index ecca8ed3..77de2f4b 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -1,7 +1,5 @@ #![allow(clippy::needless_range_loop)] -use core::convert::TryFrom; - /// Defines a trait to chain two types of CRHs. use crate::{ crh::{CRHScheme, TwoToOneCRHScheme}, @@ -17,9 +15,8 @@ use ark_std::{ borrow::Borrow, collections::{BTreeMap, BTreeSet}, fmt::Debug, - hash::{BuildHasherDefault, Hash}, + hash::Hash, }; -use hashbrown::HashMap; #[cfg(feature = "parallel")] use rayon::prelude::*; @@ -29,28 +26,6 @@ pub mod constraints; #[cfg(test)] mod tests; -#[cfg(all( - target_has_atomic = "8", - target_has_atomic = "16", - target_has_atomic = "32", - target_has_atomic = "64", - target_has_atomic = "ptr" -))] -pub(super) type DefaultHasher = ahash::AHasher; - -#[cfg(not(all( - target_has_atomic = "8", - target_has_atomic = "16", - target_has_atomic = "32", - target_has_atomic = "64", - target_has_atomic = "ptr" -)))] -pub(super) type DefaultHasher = fnv::FnvHasher; - -pub mod implicit; - -type PackedInnerCopath

= (usize, usize, Vec, Vec<

::InnerDigest>); - /// Convert the hash digest in different layers by converting previous layer's output to /// `TargetType`, which is a `Borrow` to next layer's input. pub trait DigestConverter { @@ -220,7 +195,8 @@ impl Path

{ } } -/// Optimized data structure to store multiple nodes proofs. +/// Batch Merkle membership proof. +/// /// For example: /// ```tree_diagram /// [A] d = 0 @@ -231,21 +207,23 @@ impl Path

{ /// ... / \ / \ .... /// [I] J L M d = 3 /// ``` -/// Suppose we want to prove I and J, then: -/// `tree_height`: `4` -/// `leaf_copath`: `[]` -/// `inner_copath`: `[(2,0,D), (1,1,C)]` (store packed as `(2,0,[1,+1],[D,C])`) -/// `leaf_indexes` is: `[2,3]` (indexes in Merkle Tree leaves vector) +/// Suppose we want to prove I and J (leaf indexes 2 and 3), then: +/// - `tree_height`: `4` +/// - `leaf_copath`: `[]` (I and J are siblings — no leaf copath needed) +/// - `inner_copath`: `[D, C]` (depths 1..3, ascending index within each depth) +/// - `leaf_indexes`: `[2, 3]` /// -/// We can reconstruct upfront the minimal copath needed for the proof: -/// First, we reconstruct the minimal copath at the leaf layer (`depth = tree_height-1`). -/// This is only those sibling leaf digests that are required to complete parents of on-path leaves but are not themselves on-path. -/// The leaf copath is thus `[J,I]/[I,J]=[]`. -/// We then repeat this for each inner layer, computing only the non-on-path siblings needed to complete parents of the union of all single paths. -/// The inner copath digests are stored as (depth, index, digest) tuples ordered by (depth, index). -/// Thus, inner copath is `[(2,0,D), (1,1,C)]`. -/// Intuitively, CoSet transmits only what's missing to recompute every parent on the shared union-of-paths. - +/// Both prover and verifier independently derive the positions of all required copath nodes +/// from `leaf_indexes` and `tree_height` by running [`compute_on_path`]. Only the digests are +/// transmitted, in canonical depth-then-index order. No coordinate metadata is stored. +/// +/// At verification time: +/// 1. Reconstruct the on-path sets A_j from `leaf_indexes` via [`compute_on_path`]. +/// 2. For each depth 1..leaf_depth (ascending index within each depth), consume one digest from +/// `inner_copath` for each on-path node whose sibling is NOT on-path. +/// 3. Recompute all parent hashes bottom-up and compare the root against `root_hash`. +/// +/// CoSet transmits only what is missing to recompute every parent on the shared union-of-paths. #[derive(Derivative, CanonicalSerialize)] #[derivative( Clone(bound = "P: Config"), @@ -253,17 +231,21 @@ impl Path

{ Default(bound = "P: Config") )] pub struct CoPath { - /// stores the height of the tree (>= 2) to drive CoSet decoding + /// Height of the tree this proof was generated from (>= 2). pub(crate) tree_height: usize, - /// For leaf layer, stores co-path digests (B*_{d-1}) in ascending sibling index order + /// Leaf-layer copath digests (`B*_{d-1}`), ascending sibling index order. pub leaf_copath: Vec, - /// For inner layers, stores co-path entries packed as (start_depth, start_index, packed deltas, digests) - pub inner_copath: Option>, - /// stores the leaf indexes of the nodes to prove + /// Inner copath digests in canonical order: depth 1 ascending, depth 2 ascending, … + /// The verifier derives positions from `leaf_indexes` and `tree_height`. + pub inner_copath: Vec, + /// Leaf indexes that were opened, in ascending order. pub leaf_indexes: Vec, } -/// Supertrait for CanonicalDeserialize: +/// `CanonicalDeserialize` is implemented manually (rather than derived) so that the +/// `tree_height >= 2` invariant can be enforced during deserialization, before the struct +/// is handed to the caller. A derived impl would not check this, leaving callers that +/// skip the `Valid::check` step open to panics in `verify`. impl Valid for CoPath

{ fn check(&self) -> Result<(), SerializationError> { if self.tree_height < 2 { @@ -286,7 +268,7 @@ impl CanonicalDeserialize for CoPath

{ let leaf_copath = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; let inner_copath = - Option::>::deserialize_with_mode(&mut reader, compress, validate)?; + Vec::::deserialize_with_mode(&mut reader, compress, validate)?; let leaf_indexes = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; if tree_height < 2 { return Err(SerializationError::InvalidData); @@ -302,8 +284,15 @@ impl CanonicalDeserialize for CoPath

{ impl CoPath

{ /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. - /// Note that the order of the leaves hashes should match the leaves respective indexes - /// * `leaf_size`: leaf size in number of bytes + /// + /// The verifier independently reconstructs the canonical copath order from `leaf_indexes` and + /// `tree_height`, then consumes `inner_copath` in that order. If the digest count does not + /// match what the verifier derives, verification returns `Ok(false)`. + /// + /// Leaves must be supplied in `leaf_indexes` order: + /// ```text + /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + /// ``` /// /// `expected_tree_height` must equal the height of the tree the proof was generated from; /// the verifier supplies this value — it is not taken from the (prover-controlled) proof. @@ -340,31 +329,41 @@ impl CoPath

{ let d = self.tree_height; let leaf_depth = d - 1; - // hash opened leaves and build map containing all leaf digests needed at bottom layer + // Hash opened leaves and build map containing all leaf digests needed at the bottom layer. let mut leaves_iter = leaves.into_iter(); let mut leaf_level = Self::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params)?; - // Compute on-path sets A_j and reconstruct expected B*_j = siblings(A_j) \ A_j + // Compute on-path sets A_j and the expected leaf coset B*_{d-1} = siblings(A_{d-1}) \ A_{d-1}. let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); - let on_path = compute_on_path(leaf_depth, &index_set); // holds indices of on-path nodes at depth d + let on_path = compute_on_path(leaf_depth, &index_set); - // compute minimal copath at leaf layer (B*_{d-1}) let expected_leaf_coset = Self::expected_leaf_coset(leaf_depth, &on_path); if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { return Ok(false); } - // prepare inner-level maps for non-on-path siblings and computed parents + // Prepare inner-level maps for copath siblings and computed parents. let mut inner_levels: Vec> = (0..d).map(|_| BTreeMap::new()).collect(); - // LookUp table to speedup computation avoid redundant hash computations - let mut hash_lut: HashMap = - HashMap::with_hasher(BuildHasherDefault::::default()); + // Consume inner_copath in canonical order: depths 1..leaf_depth, ascending index. + let mut copath_iter = self.inner_copath.iter(); + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[depth].binary_search(&sibling_idx).is_err() { + let digest = match copath_iter.next() { + Some(d) => d, + None => return Ok(false), // prover sent fewer digests than expected + }; + inner_levels[depth].insert(sibling_idx, digest.clone()); + } + } + } - // Decode received inner copath and add digests to LUT - if !Self::decode_inner_copath(d, &self.inner_copath, &mut inner_levels, &mut hash_lut) { + // Reject if prover sent more digests than expected. + if copath_iter.next().is_some() { return Ok(false); } @@ -373,7 +372,6 @@ impl CoPath

{ &on_path, &leaf_level, two_to_one_params, - &mut hash_lut, &mut inner_levels, )? { return Ok(false); @@ -383,108 +381,20 @@ impl CoPath

{ leaf_depth, &on_path, two_to_one_params, - &mut hash_lut, &mut inner_levels, )? { return Ok(false); } - // check root + // Check root. match inner_levels[0].get(&0) { - Some(h) => Ok(h == root_hash), // valid: Ok(true) + Some(h) => Ok(h == root_hash), None => Ok(false), } } - /// Encodes the inner co-path entries [(depth, index, digest), ...] as compact delta encodings. - /// Keeps the first (depth, index) entry, then zigzag encodes signed deltas of subsequent paris, - /// collecting the corresponding digests in order. - /// Result is a tuple (start_depth: usize, start_index: usize, deltas: Vec, digests: Vec<

::InnerDigest>) - /// - /// For example: - /// ```tree_diagram - /// [A] d = 0 - /// / \ - /// [B] [C] d = 1 - /// / \ / \ - /// [D] E F [G] d = 2 - /// / \ / \ / \ / \ - /// H I [J] K L [M] N O d = 3 - /// / \ / \ / \ / \ / \ / \ / \ / \ - /// .... 4 5 6 7 8 9 10 11 .... d = 4 - /// ``` - /// - /// Suppose we want to prove the following openings: - /// ```text - /// I = {6, 8} - /// ``` - /// With the CoSet strategy: - /// * we take the union of all single paths and compute - /// `B*_j = siblings(A_j) \ A_j` for each depth `j`, - /// * at the leaf layer we compute `B*_{4} = {(4, 7), (4, 9)}`, - /// * and across inner layers (depths `1..3`) we compute: - /// `B*_{1} = {(1, 0), (1, 1)}`, `B*_{2} = {(2, 0), (2, 3)}`, `B*_{3} = {(3, 2), (3, 5)}`, - /// So the CoPath carries: - /// * `2` leaf-layer digests (`leaf_copath`), - /// * `6` inner-layer digests (`inner_copath`), - /// - /// We keep `leaf_copath` as-is but instead of storing all 6 `(depth, index)` pairs explicitly, we store: - /// - /// * a starting coordinate: - /// ```text - /// start_depth = 1 - /// start_index = 0 - /// ``` - /// - /// * followed by signed deltas between consecutive coordinates: - /// ```text - /// (Δd, Δi) sequence: - /// (0, +1), - /// (+1, -1), - /// (0, +3), - /// (+1, -1), - /// (0, +3) - /// ``` - /// - /// Each `(Δd, Δi)` is encoded to unsigned and then varint-encoded. All of these - /// deltas are very small (−1, 0, +1, +3), so each encoded value fits in a - /// single byte. On a 64-bit platform: - /// - /// * naive coordinate encoding for 6 entries as `(depth: usize, index: usize)` - /// uses roughly `6 × 2 × 8 = 96` bytes, - /// * the packed representation uses: - /// * one `(start_depth, start_index)` pair (16 bytes), - /// * plus `5 × 2` varints (10 bytes/20 bytes for index in a large tree), - /// * for ~ 26/36 bytes of coordinate data. - fn pack_inner_copath( - entries: &[(usize, usize, P::InnerDigest)], - ) -> Option> { - if entries.is_empty() { - return None; - } - - let first = &entries[0]; - let mut deltas = Vec::new(); - let mut digests = Vec::with_capacity(entries.len()); - let mut prev_depth = i64::try_from(first.0).ok()?; - let mut prev_index = i64::try_from(first.1).ok()?; - digests.push(first.2.clone()); - - for &(depth, index, ref digest) in entries.iter().skip(1) { - let depth_i64 = i64::try_from(depth).ok()?; - let index_i64 = i64::try_from(index).ok()?; - encode_delta(&mut deltas, depth_i64 - prev_depth); - encode_delta(&mut deltas, index_i64 - prev_index); - digests.push(digest.clone()); - prev_depth = depth_i64; - prev_index = index_i64; - } - - Some((first.0, first.1, deltas, digests)) - } - /// Hashes provided leaves (ordered by `leaf_indexes`) and returns a map from leaf index to digest. - pub(super) fn ingest_leaves( + fn ingest_leaves( leaf_indexes: &[usize], leaves: &mut I, leaf_hash_params: &LeafParam

, @@ -508,20 +418,20 @@ impl CoPath

{ } /// Computes the minimal leaf-layer copath indices `B*_{d-1}` (siblings of on-path nodes not on-path). - pub(super) fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { + fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { let mut expected_leaf_coset: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { - expected_leaf_coset.push(sibling_idx); // copath element needed for proof + expected_leaf_coset.push(sibling_idx); } } - expected_leaf_coset.sort_unstable(); // canonical order + expected_leaf_coset.sort_unstable(); expected_leaf_coset } /// Confirms provided leaf copath matches the expected indices and augments `leaf_level` with them. - pub(super) fn validate_leaf_copath( + fn validate_leaf_copath( expected_leaf_coset: &[usize], provided_leaf_copath: &[P::LeafDigest], leaf_level: &mut BTreeMap, @@ -532,7 +442,7 @@ impl CoPath

{ for (sibling_idx, sibling_digest) in expected_leaf_coset.iter().zip(provided_leaf_copath) { match leaf_level.get(sibling_idx) { - Some(existing) if existing != sibling_digest => return false, // digest must match new one + Some(existing) if existing != sibling_digest => return false, _ => { leaf_level.insert(*sibling_idx, sibling_digest.clone()); } @@ -541,13 +451,12 @@ impl CoPath

{ true } - /// Recomputes parents at depth `d-2` (immediately above leaves) using the leaf digests and LUT. - pub(super) fn recompute_bottom_parents( + /// Recomputes parents at depth `leaf_depth - 1` using the leaf digests. + fn recompute_bottom_parents( leaf_depth: usize, on_path: &[Vec], leaf_level: &BTreeMap, two_to_one_params: &TwoToOneParam

, - hash_lut: &mut HashMap>, inner_levels: &mut [BTreeMap], ) -> Result { for &parent_index in on_path[leaf_depth - 1].iter() { @@ -559,23 +468,19 @@ impl CoPath

{ }; let parent = P::TwoToOneHash::evaluate( two_to_one_params, - P::LeafInnerDigestConverter::convert(left)?, // convert to inner-hash input type + P::LeafInnerDigestConverter::convert(left)?, P::LeafInnerDigestConverter::convert(right)?, )?; - inner_levels[leaf_depth - 1].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(leaf_depth - 1, parent_index); - hash_lut.insert(heap_idx, parent); + inner_levels[leaf_depth - 1].insert(parent_index, parent); } Ok(true) } - /// Recomputes inner layers up to the root using cached inner digests and stores results in LUT. - pub(super) fn recompute_inner_layers( + /// Recomputes inner layers up to the root using cached inner digests. + fn recompute_inner_layers( leaf_depth: usize, on_path: &[Vec], two_to_one_params: &TwoToOneParam

, - hash_lut: &mut HashMap>, inner_levels: &mut [BTreeMap], ) -> Result { for depth in (1..=leaf_depth - 1).rev() { @@ -588,113 +493,12 @@ impl CoPath

{ _ => return Ok(false), }; let parent = P::TwoToOneHash::compress(two_to_one_params, &left, &right)?; - inner_levels[parent_depth].insert(parent_index, parent.clone()); - // add parent to LUT at heap index - let heap_idx = level_index(parent_depth, parent_index); - hash_lut.insert(heap_idx, parent); + inner_levels[parent_depth].insert(parent_index, parent); } } Ok(true) } - /// Decodes inner co-path entries back into their usize equivalents - /// Inserts corresponding digest into the LUT for memoisation. - fn decode_inner_copath( - tree_height: usize, - inner_copath: &Option>, - inner_levels: &mut [BTreeMap], - hash_lut: &mut HashMap>, - ) -> bool { - if let Some((start_depth, start_index, deltas, digests)) = inner_copath { - // verifier rejects if remaining deltas with empty digests - if digests.is_empty() { - return deltas.is_empty(); - } - - // init (depth, index) accumulation as i64 - let mut depth = match i64::try_from(*start_depth) { - Ok(value) => value, - Err(_) => return false, - }; - let mut index = match i64::try_from(*start_index) { - Ok(value) => value, - Err(_) => return false, - }; - let mut cursor = 0usize; - let mut prev_coord: Option<(usize, usize)> = None; - - // Helper to insert digest into the LUT - let mut push_entry = - |depth_i64: i64, index_i64: i64, digest: &P::InnerDigest| -> bool { - let depth_usize = match usize::try_from(depth_i64) { - Ok(v) => v, - Err(_) => return false, - }; - let index_usize = match usize::try_from(index_i64) { - Ok(v) => v, - Err(_) => return false, - }; - if depth_usize == 0 || depth_usize >= tree_height { - return false; - } - // ensure provers ordering is consistent - if let Some((pd, pi)) = prev_coord { - if (depth_usize, index_usize) < (pd, pi) { - return false; - } - } - // check for conflicting siblings at the same coordinate - if let Some(existing) = inner_levels[depth_usize].get(&index_usize) { - if existing != digest { - return false; - } - } else { - inner_levels[depth_usize].insert(index_usize, digest.clone()); - } - // seed LUT with known siblings - let heap_idx = level_index(depth_usize, index_usize); - hash_lut.entry(heap_idx).or_insert_with(|| digest.clone()); - prev_coord = Some((depth_usize, index_usize)); - true - }; - - if !push_entry(depth, index, &digests[0]) { - return false; - } - - // accumulate remaining digests - for digest in digests.iter().skip(1) { - let depth_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return false, - }; - let index_delta = match decode_delta(deltas, &mut cursor) { - Some(delta) => delta, - None => return false, - }; - // next (depth, index) - depth = match depth.checked_add(depth_delta) { - Some(value) => value, - None => return false, - }; - index = match index.checked_add(index_delta) { - Some(value) => value, - None => return false, - }; - // attempt push - if !push_entry(depth, index, digest) { - return false; - } - } - - if cursor != deltas.len() { - return false; - } - } - - true - } - // The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. // `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. // @@ -943,51 +747,45 @@ impl MerkleTree

{ }) } - /// Returns a CoPath struct (a compressed membership proof for a set of leaves), + /// Returns a [`CoPath`] (coordinate-free batch membership proof) for the given leaf indexes, /// sufficient to verify each leaf up to the root. - /// Indexes are internally sorted and emitted in this order. + /// Indexes are internally deduplicated and sorted; the proof emits digests in that order. /// - /// With the CoSet (minimal co-path) encoding, we do not store full per-leaf authentication paths. - /// Instead we collect, for each tree level, only those siblings of on-path nodes that are not themselves on-path. - /// This yields a smaller proof than front-incremental prefix encoding in the typical case, - /// while preserving the same verification interface. + /// With the CoSet encoding we do not store full per-leaf authentication paths. + /// Instead, for each tree level, only the siblings of on-path nodes that are not themselves + /// on-path are transmitted — in canonical depth-then-index order. The verifier reconstructs + /// the ordering independently from `leaf_indexes` and `tree_height`, so no coordinate + /// metadata is included. /// - /// For sorted indexes, the CoSet proof carries: - /// * `tree_height`; - /// * `leaf_indexes` (ascending, unique); - /// * `leaf_copath`: the leaf-layer co-path digests `B*_{d-1}`, in ascending sibling index order; - /// * `inner_copath`: the inner co-path packed as `(start_depth, start_index, packed deltas, digests)`. - /// - /// When verifying the proof, leaves hashes should be supplied in order of `leaf_indexes`, that is: + /// When verifying the proof, leaves must be supplied in `leaf_indexes` order: /// ```text /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); /// ``` - /// Notes: - /// * Empty input (`indexes` is empty) returns a structurally valid empty proof carrying `tree_height`, - /// and verification succeeds vacuously against the claimed root. + /// + /// An empty query produces a structurally valid empty proof; calling `verify` on it is a + /// caller error (`leaf_indexes.is_empty()` → `Err`) per the security invariant. pub fn generate_multi_proof( &self, indexes: impl IntoIterator, ) -> Result, crate::Error> { - // pruned and sorted for encoding efficiency + // Deduplicate and sort for canonical ordering. let indexes: BTreeSet = indexes.into_iter().collect(); let d = self.height(); - // TODO: should empty query return structurally valid empty proof if indexes.is_empty() { return Ok(CoPath { tree_height: d, leaf_copath: Vec::new(), - inner_copath: None, + inner_copath: Vec::new(), leaf_indexes: Vec::new(), }); } let leaf_depth = d - 1; - // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j + // Compute on-path sets A_j and then minimal co-path B*_j = siblings(A_j) \ A_j. let on_path = compute_on_path(leaf_depth, &indexes); - // leaf layer (depth = d-1) + // Leaf layer (depth = d-1): collect sibling indices not already on-path. let mut leaf_coset_ids: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; @@ -1006,79 +804,7 @@ impl MerkleTree

{ leaf_copath.push(sibling_digest.clone()); } - // inner layers (depth 1..d-2) - let mut inner_copath_entries: Vec<(usize, usize, P::InnerDigest)> = Vec::new(); - for depth in 1..leaf_depth { - for &path_idx in on_path[depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[depth].binary_search(&sibling_idx).is_err() { - let heap_idx = level_index(depth, sibling_idx); - let sibling_digest = self.non_leaf_nodes.get(heap_idx).ok_or_else(|| { - crate::Error::IncorrectInputLength(self.non_leaf_nodes.len()) - })?; - inner_copath_entries.push((depth, sibling_idx, sibling_digest.clone())); - } - } - } - // canonicalise order - inner_copath_entries.sort_by_key(|(dpt, idx, _)| (*dpt, *idx)); - let inner_copath = CoPath::

::pack_inner_copath(&inner_copath_entries); - - Ok(CoPath { - tree_height: d, - leaf_copath, - inner_copath, - leaf_indexes: Vec::from_iter(indexes), - }) - } - - /// Returns an [`ImplicitCoPath`] for the given leaf indexes. - /// - /// This is a coordinate-free variant of [`Self::generate_multi_proof`]: both prover and - /// verifier independently derive the positions of required copath nodes from `leaf_indexes` - /// and `tree_height`, so only the digests are transmitted (in canonical depth-then-index - /// order). The proof is strictly smaller than a `CoPath` for any tree with more than one - /// inner layer, since no coordinate metadata is included. - pub fn generate_implicit_multi_proof( - &self, - indexes: impl IntoIterator, - ) -> Result, crate::Error> { - use ark_std::collections::BTreeSet; - let indexes: BTreeSet = indexes.into_iter().collect(); - let d = self.height(); - - if indexes.is_empty() { - return Ok(implicit::ImplicitCoPath { - tree_height: d, - leaf_copath: Vec::new(), - inner_copath: Vec::new(), - leaf_indexes: Vec::new(), - }); - } - - let leaf_depth = d - 1; - let on_path = compute_on_path(leaf_depth, &indexes); - - // leaf layer — identical protocol to generate_multi_proof - let mut leaf_coset_ids: Vec = Vec::new(); - for &path_idx in on_path[leaf_depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[leaf_depth].binary_search(&sibling_idx).is_err() { - leaf_coset_ids.push(sibling_idx); - } - } - leaf_coset_ids.sort_unstable(); - - let mut leaf_copath = Vec::with_capacity(leaf_coset_ids.len()); - for sibling_idx in leaf_coset_ids { - let digest = self - .leaf_nodes - .get(sibling_idx) - .ok_or_else(|| crate::Error::IncorrectInputLength(self.leaf_nodes.len()))?; - leaf_copath.push(digest.clone()); - } - - // inner layers: canonical order = depths 1..leaf_depth, ascending index within each depth + // Inner layers: canonical order = depths 1..leaf_depth, ascending index within each depth. let mut inner_copath: Vec = Vec::new(); for depth in 1..leaf_depth { for &path_idx in on_path[depth].iter() { @@ -1093,7 +819,7 @@ impl MerkleTree

{ } } - Ok(implicit::ImplicitCoPath { + Ok(CoPath { tree_height: d, leaf_copath, inner_copath, @@ -1211,12 +937,14 @@ fn tree_height(num_leaves: usize) -> usize { (ark_std::log2(num_leaves) as usize) + 1 } + /// Return level-order index encoded in global heap. /// Node at `depth` (root=0) and position `pos` (0-based at that depth) -> heap index `(1< usize { ((1usize << depth) - 1) + pos } + /// Returns true iff the index represents the root. #[inline] fn is_root(index: usize) -> bool { @@ -1268,47 +996,6 @@ fn convert_index_to_last_level(index: usize, tree_height: usize) -> usize { index + (1 << (tree_height - 1)) - 1 } -/// Encodes indexes into the packed delta format -#[inline] -fn encode_delta(buffer: &mut Vec, value: i64) { - let zigzag = ((value << 1) ^ (value >> 63)) as u64; - encode_varint(buffer, zigzag); -} - -fn decode_delta(bytes: &[u8], cursor: &mut usize) -> Option { - let raw = decode_varint(bytes, cursor)?; - Some(((raw >> 1) as i64) ^ (-((raw & 1) as i64))) -} - -#[inline] -fn encode_varint(buffer: &mut Vec, mut value: u64) { - while value >= 0x80 { - buffer.push(((value as u8) & 0x7F) | 0x80); // MSB = 1 => more bytes follow - value >>= 7; - } - buffer.push(value as u8); // last byte, MSB = 0 -} - -fn decode_varint(bytes: &[u8], cursor: &mut usize) -> Option { - let mut value = 0u64; - let mut shift = 0u32; - - while *cursor < bytes.len() { - let byte = bytes[*cursor]; - *cursor += 1; - value |= ((byte & 0x7F) as u64) << shift; - if byte & 0x80 == 0 { - return Some(value); - } - shift += 7; - if shift >= 64 { - return None; - } - } - - None -} - /// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. /// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. /// diff --git a/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs b/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs deleted file mode 100644 index 23fe320d..00000000 --- a/crypto-primitives/src/merkle_tree/tests/delta_encoding_tests.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::crh::{CRHScheme, TwoToOneCRHScheme}; -use crate::merkle_tree::{ - decode_delta, decode_varint, encode_delta, encode_varint, CoPath, Config, DefaultHasher, - IdentityDigestConverter, -}; -use ark_std::{borrow::Borrow, collections::BTreeMap, hash::BuildHasherDefault}; -use hashbrown::HashMap; - -struct DummyCfg; -impl Config for DummyCfg { - type Leaf = (); - type LeafDigest = u8; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = u8; - type LeafHash = DummyLeafHash; - type TwoToOneHash = DummyTwoToOne; -} - -struct DummyLeafHash; -impl CRHScheme for DummyLeafHash { - type Input = (); - type Output = u8; - type Parameters = (); - - fn setup(_rng: &mut R) -> Result { - Ok(()) - } - - fn evaluate>( - _parameters: &Self::Parameters, - _input: T, - ) -> Result { - Ok(0) - } -} - -struct DummyTwoToOne; -impl TwoToOneCRHScheme for DummyTwoToOne { - type Input = u8; - type Output = u8; - type Parameters = (); - - fn setup(_rng: &mut R) -> Result { - Ok(()) - } - - fn evaluate>( - _parameters: &Self::Parameters, - left: T, - right: T, - ) -> Result { - Ok(*left.borrow() ^ *right.borrow()) - } - - fn compress>( - _parameters: &Self::Parameters, - left: T, - right: T, - ) -> Result { - Ok(*left.borrow() ^ *right.borrow()) - } -} - -#[test] -fn varint_roundtrips() { - let samples = [ - 0u64, - 1, - 2, - 42, - 127, - 128, - 255, - 256, - 10_000, - u32::MAX as u64, - u64::MAX / 2, - ]; - - for &value in &samples { - let mut buf = Vec::new(); - encode_varint(&mut buf, value); - let mut cursor = 0usize; - let decoded = decode_varint(&buf, &mut cursor).expect("must decode"); - assert_eq!(decoded, value); - assert_eq!(cursor, buf.len(), "cursor should advance to end"); - } -} - -#[test] -fn delta_roundtrips() { - let samples = [ - 0i64, - 1, - -1, - 5, - -7, - 127, - -128, - 256, - -256, - i32::MAX as i64, - i32::MIN as i64, - ]; - - for &value in &samples { - let mut buf = Vec::new(); - encode_delta(&mut buf, value); - let mut cursor = 0usize; - let decoded = decode_delta(&buf, &mut cursor).expect("must decode"); - assert_eq!(decoded, value); - assert_eq!(cursor, buf.len(), "cursor should advance to end"); - } -} - -#[test] -fn decode_rejects_tampered_deltas() { - let entries: Vec<(usize, usize, u8)> = vec![(1, 0, 10), (1, 1, 20), (2, 2, 30)]; - let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); - - // Tamper with deltas: flip a bit. - let mut tampered = packed.clone(); - tampered.2[0] ^= 0b0000_0001; - - let mut inner_levels: Vec> = (0..4).map(|_| BTreeMap::new()).collect(); - let mut lut = HashMap::with_hasher(BuildHasherDefault::::default()); - let ok = - CoPath::::decode_inner_copath(4, &Some(tampered), &mut inner_levels, &mut lut); - assert!(!ok, "tampered deltas should be rejected"); -} - -#[test] -fn pack_decode_roundtrip_preserves_entries() { - let entries: Vec<(usize, usize, u8)> = vec![(1, 0, 10), (1, 1, 20), (2, 2, 30)]; - let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); - - let mut inner_levels: Vec> = (0..4).map(|_| BTreeMap::new()).collect(); - let mut lut = HashMap::with_hasher(BuildHasherDefault::::default()); - let ok = CoPath::::decode_inner_copath(4, &Some(packed), &mut inner_levels, &mut lut); - assert!(ok, "packed/decoded should succeed"); - - for &(depth, idx, ref digest) in &entries { - assert_eq!( - inner_levels[depth].get(&idx), - Some(digest), - "entry at depth {}, idx {} should roundtrip", - depth, - idx - ); - } -} diff --git a/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs b/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs deleted file mode 100644 index 78d45a7a..00000000 --- a/crypto-primitives/src/merkle_tree/tests/implicit_copath_tests.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::{ - crh::poseidon, - merkle_tree::{ - tests::test_utils::poseidon_parameters, - Config, IdentityDigestConverter, MerkleTree, - }, -}; -use ark_std::{test_rng, One, UniformRand}; - -type F = ark_ed_on_bls12_381::Fr; -type H = poseidon::CRH; -type TwoToOneH = poseidon::TwoToOneCRH; - -struct FieldMTConfig; -impl Config for FieldMTConfig { - type Leaf = [F]; - type LeafDigest = F; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = F; - type LeafHash = H; - type TwoToOneHash = TwoToOneH; -} -type FieldMT = MerkleTree; - -fn make_tree(num_leaves: usize) -> (FieldMT, Vec>) { - let mut rng = test_rng(); - let params = poseidon_parameters(); - let leaves: Vec> = (0..num_leaves) - .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) - .collect(); - let tree = FieldMT::new(¶ms, ¶ms, &leaves).unwrap(); - (tree, leaves) -} - -#[test] -fn implicit_proof_verifies_single_leaf() { - let (tree, leaves) = make_tree(8); - let params = poseidon_parameters(); - let root = tree.root(); - - for i in 0..leaves.len() { - let proof = tree.generate_implicit_multi_proof([i]).unwrap(); - let ok = proof - .verify(¶ms, ¶ms, &root, tree.height(), [leaves[i].clone()]) - .unwrap(); - assert!(ok, "single-leaf implicit proof must verify for index {i}"); - } -} - -#[test] -fn implicit_proof_verifies_full_batch() { - let (tree, leaves) = make_tree(16); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree - .generate_implicit_multi_proof(0..leaves.len()) - .unwrap(); - assert!( - proof - .verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()) - .unwrap(), - "full-batch implicit proof must verify" - ); - // full batch: no inner copath elements needed — every sibling is on-path - assert_eq!( - proof.inner_copath.len(), - 0, - "full-batch inner copath must be empty" - ); -} - -#[test] -fn implicit_proof_matches_coset_proof_node_count() { - // The number of digests transmitted should be identical to the CoPath counterpart, - // since both encode the same copath set — just without coordinates in ImplicitCoPath. - let (tree, _leaves) = make_tree(32); - for idxs in &[ - vec![0usize, 1, 2, 3], - vec![0, 7, 15, 31], - vec![5, 11, 23], - ] { - let implicit = tree.generate_implicit_multi_proof(idxs.iter().copied()).unwrap(); - let coset = tree.generate_multi_proof(idxs.iter().copied()).unwrap(); - - let coset_inner_nodes = coset - .inner_copath - .as_ref() - .map(|(_, _, _, d)| d.len()) - .unwrap_or(0); - - assert_eq!( - implicit.inner_copath.len(), - coset_inner_nodes, - "inner node count must match for indexes {:?}", - idxs - ); - assert_eq!( - implicit.leaf_copath.len(), - coset.leaf_copath.len(), - "leaf copath length must match for indexes {:?}", - idxs - ); - } -} - -#[test] -fn implicit_proof_wrong_root_fails() { - let (tree, leaves) = make_tree(8); - let params = poseidon_parameters(); - let wrong_root = tree.root() + F::one(); - - let proof = tree.generate_implicit_multi_proof([2usize, 5]).unwrap(); - let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - let ok = proof - .verify(¶ms, ¶ms, &wrong_root, tree.height(), opened) - .unwrap(); - assert!(!ok, "wrong root must fail verification"); -} - -#[test] -fn implicit_proof_tampered_inner_digest_fails() { - let (tree, leaves) = make_tree(16); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree.generate_implicit_multi_proof([1usize, 6, 10]).unwrap(); - let mut bad = proof.clone(); - if let Some(d) = bad.inner_copath.get_mut(0) { - *d += F::one(); - } - let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); - assert!(!ok, "tampered inner digest must fail verification"); -} - -#[test] -fn implicit_proof_extra_digest_fails() { - let (tree, leaves) = make_tree(16); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree.generate_implicit_multi_proof([3usize, 9]).unwrap(); - let mut bad = proof.clone(); - bad.inner_copath.push(F::one()); // one spurious digest - let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); - assert!(!ok, "extra inner digest must fail verification"); -} - -#[test] -fn implicit_proof_missing_inner_digest_fails() { - let (tree, leaves) = make_tree(16); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree.generate_implicit_multi_proof([2usize, 5, 9]).unwrap(); - let mut bad = proof.clone(); - if !bad.inner_copath.is_empty() { - bad.inner_copath.pop(); - } - let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - let ok = bad.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(); - assert!(!ok, "missing inner digest must fail verification"); -} - -#[test] -fn implicit_proof_wrong_tree_height_fails() { - let (tree, leaves) = make_tree(8); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree.generate_implicit_multi_proof([0usize, 3]).unwrap(); - let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - // supply wrong expected height - let ok = proof - .verify(¶ms, ¶ms, &root, tree.height() + 1, opened) - .unwrap(); - assert!(!ok, "mismatched tree height must fail verification"); -} - -#[test] -fn implicit_proof_duplicate_indices_deduped() { - let (tree, leaves) = make_tree(8); - let params = poseidon_parameters(); - let root = tree.root(); - - let proof = tree - .generate_implicit_multi_proof([2usize, 2, 5, 5, 5]) - .unwrap(); - assert_eq!(proof.leaf_indexes, vec![2, 5], "duplicates must be deduped"); - - let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), - "deduped implicit proof must verify" - ); -} diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index d4043a2d..c62408ac 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -1,7 +1,5 @@ #[cfg(feature = "constraints")] mod constraints; -mod delta_encoding_tests; -mod implicit_copath_tests; mod test_utils; mod bytes_mt_tests { @@ -214,6 +212,16 @@ mod field_mt_tests { type FieldMT = MerkleTree; + fn make_tree(num_leaves: usize) -> (FieldMT, Vec>) { + let mut rng = test_rng(); + let params = poseidon_parameters(); + let leaves: Vec> = (0..num_leaves) + .map(|_| (0..3).map(|_| F::rand(&mut rng)).collect()) + .collect(); + let tree = FieldMT::new(¶ms, ¶ms, &leaves).unwrap(); + (tree, leaves) + } + fn merkle_tree_test(leaves: &[Vec], update_query: &[(usize, Vec)]) -> () { let mut leaves = leaves.to_vec(); let leaf_crh_params = poseidon_parameters(); @@ -441,10 +449,8 @@ mod field_mt_tests { let proof = tree.generate_multi_proof(vec![2usize, 5, 9]).unwrap(); let mut bad = proof.clone(); - if let Some((_, _, _, digests)) = bad.inner_copath.as_mut() { - if !digests.is_empty() { - digests.pop(); // drop one inner sibling digest - } + if !bad.inner_copath.is_empty() { + bad.inner_copath.pop(); // drop one inner sibling digest } let opened: Vec<_> = bad .leaf_indexes @@ -509,113 +515,204 @@ mod field_mt_tests { .unwrap(); assert!(!ok, "mismatched leaf ordering must fail verification"); } -} -mod delta_encoding_spacing_tests { - use super::super::{ - decode_delta, CRHScheme, CoPath, Config, IdentityDigestConverter, TwoToOneCRHScheme, - }; - use ark_std::borrow::Borrow; - - struct DummyCfg; - impl Config for DummyCfg { - type Leaf = (); - type LeafDigest = u8; - type LeafInnerDigestConverter = IdentityDigestConverter; - type InnerDigest = u8; - type LeafHash = DummyLeafHash; - type TwoToOneHash = DummyTwoToOne; - } + // --- Tests ported from implicit_copath_tests --- - struct DummyLeafHash; - impl CRHScheme for DummyLeafHash { - type Input = (); - type Output = u8; - type Parameters = (); + #[test] + fn multiproof_verifies_single_leaf() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); - fn setup( - _rng: &mut R, - ) -> Result { - Ok(()) + for i in 0..leaves.len() { + let proof = tree.generate_multi_proof([i]).unwrap(); + let ok = proof + .verify(¶ms, ¶ms, &root, tree.height(), [leaves[i].clone()]) + .unwrap(); + assert!(ok, "single-leaf proof must verify for index {i}"); } + } - fn evaluate>( - _parameters: &Self::Parameters, - _input: T, - ) -> Result { - Ok(0) - } + #[test] + fn multiproof_verifies_full_batch() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof(0..leaves.len()).unwrap(); + assert!( + proof + .verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()) + .unwrap(), + "full-batch proof must verify" + ); + // full batch: every sibling is on-path, so no inner copath elements needed + assert_eq!( + proof.inner_copath.len(), + 0, + "full-batch inner copath must be empty" + ); } - struct DummyTwoToOne; - impl TwoToOneCRHScheme for DummyTwoToOne { - type Input = u8; - type Output = u8; - type Parameters = (); + #[test] + fn multiproof_extra_inner_digest_fails() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); - fn setup( - _rng: &mut R, - ) -> Result { - Ok(()) - } + let proof = tree.generate_multi_proof([3usize, 9]).unwrap(); + let mut bad = proof.clone(); + bad.inner_copath.push(F::one()); // one spurious digest + let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = bad + .verify(¶ms, ¶ms, &root, tree.height(), opened) + .unwrap(); + assert!(!ok, "extra inner digest must fail verification"); + } - fn evaluate>( - _parameters: &Self::Parameters, - left: T, - right: T, - ) -> Result { - Ok(*left.borrow() ^ *right.borrow()) - } + #[test] + fn multiproof_wrong_tree_height_fails() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); - fn compress>( - _parameters: &Self::Parameters, - left: T, - right: T, - ) -> Result { - Ok(*left.borrow() ^ *right.borrow()) - } + let proof = tree.generate_multi_proof([0usize, 3]).unwrap(); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + let ok = proof + .verify(¶ms, ¶ms, &root, tree.height() + 1, opened) + .unwrap(); + assert!(!ok, "mismatched tree height must fail verification"); } + // --- CoSet structural tests: verify that path sharing reduces copath size --- + + /// I = {5, 6} on an 8-leaf tree (T3, height=4). + /// Leaves 5 and 6 are siblings, so their parent is on-path from both; only one inner node + /// (the parent's sibling at depth 1) is needed. #[test] - fn packed_deltas_save_with_large_index_gaps() { - // Coordinate entries are sorted lexicographically by (depth, index), and deltas are taken - // between consecutive coordinates in this order (not relative to a global heap index). - // - // This test demonstrates the worst case spaced openings scenario at the leaf level, plus a - // higher-layer sibling at depth d-2. - let d: usize = 14; - let depth_inner = d - 2; - let depth_leaf = d - 1; - - let mid = 1usize << (d - 2); - let end = (1usize << (d - 1)) - 1; - - let entries: Vec<(usize, usize, u8)> = vec![ - (depth_inner, 0, 10), - (depth_leaf, 0, 20), - (depth_leaf, mid, 30), - (depth_leaf, end, 40), - ]; - - let packed = CoPath::::pack_inner_copath(&entries).expect("packs"); - let (_start_depth, _start_index, deltas, _digests) = packed; // returns `deltas` which is a Vec. - - // The first step is from (d-2,0) -> (d-1,0): depth delta is +1, index delta is 0. - let mut cursor = 0usize; - let depth_delta = decode_delta(&deltas, &mut cursor).expect("depth delta decodes"); - let index_delta = decode_delta(&deltas, &mut cursor).expect("index delta decodes"); - assert_eq!(depth_delta, 1); - assert_eq!(index_delta, 0); - - // Sanity check that the packed coordinate encoding is smaller than storing every (depth,index) - // as a fixed-width pair of `usize`s. - let naive_coord_bytes = entries.len() * 2 * core::mem::size_of::(); - let packed_coord_bytes = 2 * core::mem::size_of::() + deltas.len(); + fn multiproof_duplicate_commitments() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof([5usize, 6]).unwrap(); + assert_eq!( + proof.inner_copath.len(), + 1, + "siblings {{5,6}} share a parent; only one inner copath node needed" + ); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "proof must verify" + ); + } + + /// I = {3, 6} on an 8-leaf tree (T3, height=4). + /// The two paths diverge immediately but their inner nodes at depth 1 are both on-path + /// (each is the other's sibling — no, wait: 3>>2=0 and 6>>2=1 at depth 1, so they ARE + /// each other's sibling and both on-path, needing zero depth-1 copath entries). + /// At depth 2: 3>>1=1 and 6>>1=3; sibling of 1 is 0 (not on-path) and sibling of 3 is 2 + /// (not on-path) — two copath entries. + #[test] + fn multiproof_derivable_commitments() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof([3usize, 6]).unwrap(); + assert_eq!( + proof.inner_copath.len(), + 2, + "paths {{3,6}} share depth-1 nodes; two inner copath nodes needed at depth 2" + ); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "proof must verify" + ); + } + + /// I = {1, 3, 5, 6} on an 8-leaf tree (T3, height=4). + /// All depth-2 and depth-1 nodes are on-path (every sibling is accounted for), + /// so the inner copath is empty. + #[test] + fn multiproof_duplicate_and_derivable() { + let (tree, leaves) = make_tree(8); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof([1usize, 3, 5, 6]).unwrap(); + assert_eq!( + proof.inner_copath.len(), + 0, + "I={{1,3,5,6}} covers all inner nodes; inner copath must be empty" + ); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "proof must verify" + ); + } + + /// I = {1, 2, 3, 4} on a 16-leaf tree (T4, height=5). + /// These four leaves form a contiguous subtree; only 2 inner copath nodes are needed + /// (the subtree's sibling at depth 1 and one stray at depth 3). + #[test] + fn multiproof_full_subtree_batch() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof([1usize, 2, 3, 4]).unwrap(); + assert_eq!( + proof.inner_copath.len(), + 2, + "contiguous subtree I={{1,2,3,4}} on T4 needs exactly 2 inner copath nodes" + ); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "proof must verify" + ); + } + + /// I = {2, 7, 12, 14} on a 16-leaf tree (T4, height=5) — spread-out leaves. + /// Paths share few nodes; 3 inner copath nodes are needed. + #[test] + fn multiproof_spread_batch() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof([2usize, 7, 12, 14]).unwrap(); + assert_eq!( + proof.inner_copath.len(), + 3, + "spread I={{2,7,12,14}} on T4 needs exactly 3 inner copath nodes" + ); + let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + assert!( + proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + "proof must verify" + ); + } + + /// Opening all leaves: every sibling is on-path, so the inner copath is empty. + #[test] + fn multiproof_all_leaves() { + let (tree, leaves) = make_tree(16); + let params = poseidon_parameters(); + let root = tree.root(); + + let proof = tree.generate_multi_proof(0..leaves.len()).unwrap(); + assert!( + proof.inner_copath.is_empty(), + "opening all leaves: inner copath must be empty" + ); assert!( - packed_coord_bytes < naive_coord_bytes, - "expected packed coordinates to be smaller (packed={}, naive={})", - packed_coord_bytes, - naive_coord_bytes + proof.verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()).unwrap(), + "proof must verify" ); } } From 159eac7e2e550e0e0e65c211fcee0682f3643bd1 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Thu, 2 Apr 2026 12:14:25 +0100 Subject: [PATCH 19/22] serialization: remove manual serialization to keep consistency in panics --- crypto-primitives/src/merkle_tree/mod.rs | 67 +++---------------- .../src/merkle_tree/tests/mod.rs | 22 +++--- 2 files changed, 18 insertions(+), 71 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index 77de2f4b..a0b05922 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -7,7 +7,7 @@ use crate::{ Error, }; use ark_serialize::{ - CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate, + CanonicalDeserialize, CanonicalSerialize, }; #[cfg(not(feature = "std"))] use ark_std::vec::Vec; @@ -224,7 +224,7 @@ impl Path

{ /// 3. Recompute all parent hashes bottom-up and compare the root against `root_hash`. /// /// CoSet transmits only what is missing to recompute every parent on the shared union-of-paths. -#[derive(Derivative, CanonicalSerialize)] +#[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] #[derivative( Clone(bound = "P: Config"), Debug(bound = "P: Config"), @@ -242,45 +242,6 @@ pub struct CoPath { pub leaf_indexes: Vec, } -/// `CanonicalDeserialize` is implemented manually (rather than derived) so that the -/// `tree_height >= 2` invariant can be enforced during deserialization, before the struct -/// is handed to the caller. A derived impl would not check this, leaving callers that -/// skip the `Valid::check` step open to panics in `verify`. -impl Valid for CoPath

{ - fn check(&self) -> Result<(), SerializationError> { - if self.tree_height < 2 { - return Err(SerializationError::InvalidData); - } - // Propagate field checks to ensure the whole structure is valid. - self.leaf_copath.check()?; - self.inner_copath.check()?; - self.leaf_indexes.check() - } -} - -impl CanonicalDeserialize for CoPath

{ - fn deserialize_with_mode( - mut reader: R, - compress: Compress, - validate: Validate, - ) -> Result { - let tree_height = usize::deserialize_with_mode(&mut reader, compress, validate)?; - let leaf_copath = - Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - let inner_copath = - Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - let leaf_indexes = Vec::::deserialize_with_mode(&mut reader, compress, validate)?; - if tree_height < 2 { - return Err(SerializationError::InvalidData); - } - Ok(CoPath { - tree_height, - leaf_copath, - inner_copath, - leaf_indexes, - }) - } -} impl CoPath

{ /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. @@ -304,23 +265,11 @@ impl CoPath

{ expected_tree_height: usize, leaves: impl IntoIterator, ) -> Result { - if self.leaf_indexes.is_empty() { - return Err(crate::Error::GenericError(ark_std::boxed::Box::new( - ark_std::io::Error::new( - ark_std::io::ErrorKind::InvalidInput, - "batch proof must contain at least one leaf index", - ), - ))); - } - - if self.tree_height < 2 { - return Err(crate::Error::GenericError(ark_std::boxed::Box::new( - ark_std::io::Error::new( - ark_std::io::ErrorKind::InvalidInput, - "tree_height must be >= 2", - ), - ))); - } + assert!( + !self.leaf_indexes.is_empty(), + "batch proof must contain at least one leaf index" + ); + assert!(self.tree_height >= 2, "tree_height must be >= 2"); if self.tree_height != expected_tree_height { return Ok(false); @@ -763,7 +712,7 @@ impl MerkleTree

{ /// ``` /// /// An empty query produces a structurally valid empty proof; calling `verify` on it is a - /// caller error (`leaf_indexes.is_empty()` → `Err`) per the security invariant. + /// caller error (`leaf_indexes.is_empty()` → panic) per the security invariant. pub fn generate_multi_proof( &self, indexes: impl IntoIterator, diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index c62408ac..0240c632 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -337,6 +337,7 @@ mod field_mt_tests { } #[test] + #[should_panic(expected = "batch proof must contain at least one leaf index")] fn multiproof_empty_batch_is_caller_error() { let mut rng = test_rng(); let leaves: Vec> = (0..4) @@ -349,18 +350,15 @@ mod field_mt_tests { let proof = tree.generate_multi_proof(Vec::::new()).unwrap(); assert_eq!(proof.leaf_indexes.len(), 0); - assert!( - proof - .verify( - &leaf_crh_params, - &two_to_one_params, - &root, - tree.height(), - Vec::>::new() - ) - .is_err(), - "verifying an empty batch proof is a caller error" - ); + proof + .verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree.height(), + Vec::>::new() + ) + .unwrap(); } #[test] From 3787d8e9402f5fd3f6620729796c33a9edb5c79f Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Wed, 8 Apr 2026 15:13:37 +0100 Subject: [PATCH 20/22] add: linkify_changelog script --- scripts/linkify_changelog.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/linkify_changelog.py diff --git a/scripts/linkify_changelog.py b/scripts/linkify_changelog.py new file mode 100644 index 00000000..1d85f290 --- /dev/null +++ b/scripts/linkify_changelog.py @@ -0,0 +1,30 @@ +import fileinput +import os +import re +import sys + +# Set this to the name of the repo, if you don't want it to be read from the filesystem. +# It assumes the changelog file is in the root of the repo. +repo_name = "" + +# This script goes through the provided file, and replaces any " \#", +# with the valid mark down formatted link to it. e.g. +# " [\#number](https://github.com/arkworks-rs/template/pull/) +# Note that if the number is for a an issue, github will auto-redirect you when you click the link. +# It is safe to run the script multiple times in succession. +# +# Example usage $ python3 linkify_changelog.py ../CHANGELOG.md +changelog_path = sys.argv[1] +if repo_name == "": + path = os.path.abspath(changelog_path) + components = path.split(os.path.sep) + repo_name = components[-2] + +for line in fileinput.input(inplace=True): + line = re.sub( + r"\- #([0-9]*)", + r"- [\#\1](https://github.com/arkworks-rs/" + repo_name + r"/pull/\1)", + line.rstrip(), + ) + # edits the current file + print(line) \ No newline at end of file From c75ea58ed9739980d58ca497866cdcba8413a542 Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Wed, 15 Apr 2026 22:11:52 +0200 Subject: [PATCH 21/22] readability: change function names and reorder --- crypto-primitives/benches/merkle_tree.rs | 13 +- crypto-primitives/src/merkle_tree/mod.rs | 333 +++++++++--------- .../src/merkle_tree/tests/constraints.rs | 6 +- .../src/merkle_tree/tests/mod.rs | 75 ++-- 4 files changed, 200 insertions(+), 227 deletions(-) diff --git a/crypto-primitives/benches/merkle_tree.rs b/crypto-primitives/benches/merkle_tree.rs index 405cbbc5..f02fe308 100644 --- a/crypto-primitives/benches/merkle_tree.rs +++ b/crypto-primitives/benches/merkle_tree.rs @@ -116,9 +116,7 @@ mod bytes_mt_benches { c.bench_function("Merkle Tree Verify Proof (Leaves as [u8])", move |b| { b.iter(|| { for (proof, leaf) in zip(proofs.clone(), leaves.clone()) { - proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap(); + proof.verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()); } }) }); @@ -175,6 +173,7 @@ mod bytes_mt_benches { .unwrap(); let root = tree.root(); + let tree_height = tree.height(); let multi_proof = tree .generate_multi_proof((0..leaves.len()).collect::>()) @@ -184,7 +183,13 @@ mod bytes_mt_benches { "Merkle Tree Verify Multi Proof (Leaves as [u8])", move |b| { b.iter(|| { - multi_proof.verify(&leaf_crh_params, &two_to_one_params, &root, leaves.clone()) + multi_proof.verify( + &leaf_crh_params, + &two_to_one_params, + &root, + tree_height, + leaves.clone(), + ) }) }, ); diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index a0b05922..fbe5db3f 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -26,14 +26,14 @@ pub mod constraints; #[cfg(test)] mod tests; -/// Convert the hash digest in different layers by converting previous layer's output to -/// `TargetType`, which is a `Borrow` to next layer's input. +/// Convert a hash digest from one layer to the next by transforming the previous layer's output +/// into `TargetType`, which borrows into the next layer's input. pub trait DigestConverter { type TargetType: Borrow; fn convert(item: From) -> Result; } -/// A trivial converter where digest of previous layer's hash is the same as next layer's input. +/// A trivial converter where the previous layer's digest is identical to the next layer's input. pub struct IdentityDigestConverter { _prev_layer_digest: T, } @@ -128,7 +128,7 @@ pub type LeafParam

= <

::LeafHash as CRHScheme>::Parameters; )] pub struct Path { pub leaf_sibling_hash: P::LeafDigest, - /// The sibling of path node ordered from higher layer to lower layer (does not include root node). + /// Sibling hashes from root to leaf layer (does not include the root). pub auth_path: Vec, /// stores the leaf index of the node pub leaf_index: usize, @@ -138,7 +138,7 @@ impl Path

{ /// The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. /// `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. /// - /// This function simply converts `self.leaf_index` to boolean array in big endian form. + /// Converts `self.leaf_index` to a boolean array in big-endian form. #[allow(unused)] // this function is actually used when r1cs feature is on fn position_list(&'_ self) -> impl '_ + Iterator { (0..self.auth_path.len() + 1) @@ -158,19 +158,19 @@ impl Path

{ two_to_one_params: &TwoToOneParam

, root_hash: &P::InnerDigest, leaf: L, - ) -> Result { + ) -> bool { // calculate leaf hash - let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf)?; + let claimed_leaf_hash = P::LeafHash::evaluate(&leaf_hash_params, leaf).unwrap(); // check hash along the path from bottom to root let (left_child, right_child) = - select_left_right_child(self.leaf_index, &claimed_leaf_hash, &self.leaf_sibling_hash)?; + select_left_right_child(self.leaf_index, &claimed_leaf_hash, &self.leaf_sibling_hash); // leaf layer to inner layer conversion - let left_child = P::LeafInnerDigestConverter::convert(left_child)?; - let right_child = P::LeafInnerDigestConverter::convert(right_child)?; + let left_child = P::LeafInnerDigestConverter::convert(left_child).unwrap(); + let right_child = P::LeafInnerDigestConverter::convert(right_child).unwrap(); let mut curr_path_node = - P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child)?; + P::TwoToOneHash::evaluate(&two_to_one_params, left_child, right_child).unwrap(); // we will use `index` variable to track the position of path let mut index = self.leaf_index; @@ -180,18 +180,14 @@ impl Path

{ for level in (0..self.auth_path.len()).rev() { // check if path node at this level is left or right let (left, right) = - select_left_right_child(index, &curr_path_node, &self.auth_path[level])?; + select_left_right_child(index, &curr_path_node, &self.auth_path[level]); // update curr_path_node - curr_path_node = P::TwoToOneHash::compress(&two_to_one_params, &left, &right)?; + curr_path_node = P::TwoToOneHash::compress(&two_to_one_params, &left, &right).unwrap(); index >>= 1; } // check if final hash is root - if &curr_path_node != root_hash { - return Ok(false); - } - - Ok(true) + &curr_path_node == root_hash } } @@ -209,13 +205,13 @@ impl Path

{ /// ``` /// Suppose we want to prove I and J (leaf indexes 2 and 3), then: /// - `tree_height`: `4` -/// - `leaf_copath`: `[]` (I and J are siblings — no leaf copath needed) +/// - `leaf_copath`: `[]` (I and J are siblings, so no leaf copath is needed) /// - `inner_copath`: `[D, C]` (depths 1..3, ascending index within each depth) /// - `leaf_indexes`: `[2, 3]` /// /// Both prover and verifier independently derive the positions of all required copath nodes -/// from `leaf_indexes` and `tree_height` by running [`compute_on_path`]. Only the digests are -/// transmitted, in canonical depth-then-index order. No coordinate metadata is stored. +/// from `leaf_indexes` and `tree_height` via [`compute_on_path`]. The proof transmits only +/// digests in canonical depth-then-index order. /// /// At verification time: /// 1. Reconstruct the on-path sets A_j from `leaf_indexes` via [`compute_on_path`]. @@ -223,7 +219,7 @@ impl Path

{ /// `inner_copath` for each on-path node whose sibling is NOT on-path. /// 3. Recompute all parent hashes bottom-up and compare the root against `root_hash`. /// -/// CoSet transmits only what is missing to recompute every parent on the shared union-of-paths. +/// The proof contains only the siblings needed to reconstruct all parents on the union of paths. #[derive(Derivative, CanonicalSerialize, CanonicalDeserialize)] #[derivative( Clone(bound = "P: Config"), @@ -244,130 +240,30 @@ pub struct CoPath { impl CoPath

{ - /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. - /// - /// The verifier independently reconstructs the canonical copath order from `leaf_indexes` and - /// `tree_height`, then consumes `inner_copath` in that order. If the digest count does not - /// match what the verifier derives, verification returns `Ok(false)`. - /// - /// Leaves must be supplied in `leaf_indexes` order: - /// ```text - /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); - /// ``` - /// - /// `expected_tree_height` must equal the height of the tree the proof was generated from; - /// the verifier supplies this value — it is not taken from the (prover-controlled) proof. - pub fn verify + Clone>( - &self, - leaf_hash_params: &LeafParam

, - two_to_one_params: &TwoToOneParam

, - root_hash: &P::InnerDigest, - expected_tree_height: usize, - leaves: impl IntoIterator, - ) -> Result { - assert!( - !self.leaf_indexes.is_empty(), - "batch proof must contain at least one leaf index" - ); - assert!(self.tree_height >= 2, "tree_height must be >= 2"); - - if self.tree_height != expected_tree_height { - return Ok(false); - } - - let d = self.tree_height; - let leaf_depth = d - 1; - - // Hash opened leaves and build map containing all leaf digests needed at the bottom layer. - let mut leaves_iter = leaves.into_iter(); - let mut leaf_level = - Self::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params)?; - - // Compute on-path sets A_j and the expected leaf coset B*_{d-1} = siblings(A_{d-1}) \ A_{d-1}. - let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); - let on_path = compute_on_path(leaf_depth, &index_set); - - let expected_leaf_coset = Self::expected_leaf_coset(leaf_depth, &on_path); - if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { - return Ok(false); - } - - // Prepare inner-level maps for copath siblings and computed parents. - let mut inner_levels: Vec> = - (0..d).map(|_| BTreeMap::new()).collect(); - - // Consume inner_copath in canonical order: depths 1..leaf_depth, ascending index. - let mut copath_iter = self.inner_copath.iter(); - for depth in 1..leaf_depth { - for &path_idx in on_path[depth].iter() { - let sibling_idx = path_idx ^ 1; - if on_path[depth].binary_search(&sibling_idx).is_err() { - let digest = match copath_iter.next() { - Some(d) => d, - None => return Ok(false), // prover sent fewer digests than expected - }; - inner_levels[depth].insert(sibling_idx, digest.clone()); - } - } - } - - // Reject if prover sent more digests than expected. - if copath_iter.next().is_some() { - return Ok(false); - } - - if !Self::recompute_bottom_parents( - leaf_depth, - &on_path, - &leaf_level, - two_to_one_params, - &mut inner_levels, - )? { - return Ok(false); - } - - if !Self::recompute_inner_layers( - leaf_depth, - &on_path, - two_to_one_params, - &mut inner_levels, - )? { - return Ok(false); - } - - // Check root. - match inner_levels[0].get(&0) { - Some(h) => Ok(h == root_hash), - None => Ok(false), - } - } - /// Hashes provided leaves (ordered by `leaf_indexes`) and returns a map from leaf index to digest. fn ingest_leaves( leaf_indexes: &[usize], leaves: &mut I, leaf_hash_params: &LeafParam

, - ) -> Result, crate::Error> + ) -> Option> where L: Borrow, I: Iterator, { let mut leaf_level: BTreeMap = BTreeMap::new(); for &idx in leaf_indexes { - let leaf = leaves - .next() - .ok_or_else(|| crate::Error::IncorrectInputLength(leaf_indexes.len()))?; - let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow())?; + let leaf = leaves.next()?; + let leaf_hash = P::LeafHash::evaluate(leaf_hash_params, leaf.borrow()).unwrap(); leaf_level.insert(idx, leaf_hash); } if leaves.next().is_some() { - return Err(crate::Error::IncorrectInputLength(leaf_indexes.len())); + return None; } - Ok(leaf_level) + Some(leaf_level) } - /// Computes the minimal leaf-layer copath indices `B*_{d-1}` (siblings of on-path nodes not on-path). - fn expected_leaf_coset(leaf_depth: usize, on_path: &[Vec]) -> Vec { + /// Compute which leaf siblings are needed to verify the proof (those not already on-path). + fn compute_needed_leaf_siblings(leaf_depth: usize, on_path: &[Vec]) -> Vec { let mut expected_leaf_coset: Vec = Vec::new(); for &path_idx in on_path[leaf_depth].iter() { let sibling_idx = path_idx ^ 1; @@ -379,8 +275,8 @@ impl CoPath

{ expected_leaf_coset } - /// Confirms provided leaf copath matches the expected indices and augments `leaf_level` with them. - fn validate_leaf_copath( + /// Absorb the leaf copath digests into `leaf_level`, verifying counts and detecting conflicts. + fn absorb_leaf_copath( expected_leaf_coset: &[usize], provided_leaf_copath: &[P::LeafDigest], leaf_level: &mut BTreeMap, @@ -400,38 +296,39 @@ impl CoPath

{ true } - /// Recomputes parents at depth `leaf_depth - 1` using the leaf digests. - fn recompute_bottom_parents( + /// Verify and hash the transition from leaf digests to the first inner layer. + fn verify_and_hash_bottom_layer( leaf_depth: usize, on_path: &[Vec], leaf_level: &BTreeMap, two_to_one_params: &TwoToOneParam

, inner_levels: &mut [BTreeMap], - ) -> Result { + ) -> bool { for &parent_index in on_path[leaf_depth - 1].iter() { let left = leaf_level.get(&(parent_index * 2)).cloned(); let right = leaf_level.get(&(parent_index * 2 + 1)).cloned(); let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), - _ => return Ok(false), + _ => return false, }; let parent = P::TwoToOneHash::evaluate( two_to_one_params, - P::LeafInnerDigestConverter::convert(left)?, - P::LeafInnerDigestConverter::convert(right)?, - )?; + P::LeafInnerDigestConverter::convert(left).unwrap(), + P::LeafInnerDigestConverter::convert(right).unwrap(), + ) + .unwrap(); inner_levels[leaf_depth - 1].insert(parent_index, parent); } - Ok(true) + true } - /// Recomputes inner layers up to the root using cached inner digests. - fn recompute_inner_layers( + /// Verify and hash the inner layers from leaf depth up to the root. + fn verify_and_hash_inner_chain( leaf_depth: usize, on_path: &[Vec], two_to_one_params: &TwoToOneParam

, inner_levels: &mut [BTreeMap], - ) -> Result { + ) -> bool { for depth in (1..=leaf_depth - 1).rev() { let parent_depth = depth - 1; for &parent_index in on_path[parent_depth].iter() { @@ -439,19 +336,120 @@ impl CoPath

{ let right = inner_levels[depth].get(&(parent_index * 2 + 1)).cloned(); let (left, right) = match (left, right) { (Some(left), Some(right)) => (left, right), - _ => return Ok(false), + _ => return false, }; - let parent = P::TwoToOneHash::compress(two_to_one_params, &left, &right)?; + let parent = + P::TwoToOneHash::compress(two_to_one_params, &left, &right).unwrap(); inner_levels[parent_depth].insert(parent_index, parent); } } - Ok(true) + true + } + + /// Verify that leaves are at `self.leaf_indexes` of the merkle tree. + /// + /// The verifier reconstructs the canonical copath order from `leaf_indexes` and `tree_height`, + /// then consumes digests from `inner_copath`. If the count doesn't match, verification fails. + /// + /// Leaves must be supplied in `leaf_indexes` order: + /// ```text + /// let ordered_leaves: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); + /// ``` + /// + /// `expected_tree_height` must equal the height of the tree the proof was generated from. + /// The verifier supplies this value rather than taking it from the (prover-controlled) proof. + pub fn verify + Clone>( + &self, + leaf_hash_params: &LeafParam

, + two_to_one_params: &TwoToOneParam

, + root_hash: &P::InnerDigest, + expected_tree_height: usize, + leaves: impl IntoIterator, + ) -> bool { + assert!( + !self.leaf_indexes.is_empty(), + "batch proof must contain at least one leaf index" + ); + assert!(self.tree_height >= 2, "tree_height must be >= 2"); + + if self.tree_height != expected_tree_height { + return false; + } + + let d = self.tree_height; + let leaf_depth = d - 1; + + // Hash opened leaves and build map containing all leaf digests needed at the bottom layer. + let mut leaves_iter = leaves.into_iter(); + let mut leaf_level = + match Self::ingest_leaves(&self.leaf_indexes, &mut leaves_iter, leaf_hash_params) { + Some(m) => m, + None => return false, + }; + + // Compute on-path sets A_j and the expected leaf coset B*_{d-1} = siblings(A_{d-1}) \ A_{d-1}. + let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); + let on_path = compute_on_path(leaf_depth, &index_set); + + let expected_leaf_coset = Self::expected_leaf_coset(leaf_depth, &on_path); + if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { + return false; + } + + // Prepare inner-level maps for copath siblings and computed parents. + let mut inner_levels: Vec> = + (0..d).map(|_| BTreeMap::new()).collect(); + + // Consume inner_copath in canonical order: depths 1..leaf_depth, ascending index. + let mut copath_iter = self.inner_copath.iter(); + for depth in 1..leaf_depth { + for &path_idx in on_path[depth].iter() { + let sibling_idx = path_idx ^ 1; + if on_path[depth].binary_search(&sibling_idx).is_err() { + let digest = match copath_iter.next() { + Some(d) => d, + None => return false, // prover sent fewer digests than expected + }; + inner_levels[depth].insert(sibling_idx, digest.clone()); + } + } + } + + // Reject if prover sent more digests than expected. + if copath_iter.next().is_some() { + return false; + } + + if !Self::recompute_bottom_parents( + leaf_depth, + &on_path, + &leaf_level, + two_to_one_params, + &mut inner_levels, + ) { + return false; + } + + if !Self::recompute_inner_layers( + leaf_depth, + &on_path, + two_to_one_params, + &mut inner_levels, + ) { + return false; + } + + // Check root. + match inner_levels[0].get(&0) { + Some(h) => h == root_hash, + None => false, + } } // The position of on_path node in `leaf_and_sibling_hash` and `non_leaf_and_sibling_hash_path`. // `position[i]` is 0 (false) iff `i`th on-path node from top to bottom is on the left. // - // This function simply converts every index in `self.leaf_indexes` to boolean array in big endian form. + // Converts each index in `self.leaf_indexes` to a boolean array in big-endian form. #[allow(unused)] // this function is actually used when r1cs feature is on fn position_list(&'_ self) -> impl '_ + Iterator> { let path_len = self.tree_height.saturating_sub(2); @@ -479,28 +477,26 @@ fn select_left_right_child( index: usize, computed_hash: &L, sibling_hash: &L, -) -> Result<(L, L), crate::Error> { +) -> (L, L) { let is_left = index & 1 == 0; let mut left_child = computed_hash; let mut right_child = sibling_hash; if !is_left { core::mem::swap(&mut left_child, &mut right_child); } - Ok((left_child.clone(), right_child.clone())) + (left_child.clone(), right_child.clone()) } -/// Defines a merkle tree data structure. -/// This merkle tree has runtime fixed height, and assumes number of leaves is 2^height. +/// A merkle tree with fixed height and a leaf count of 2^height. /// /// TODO: add RFC-6962 compatible merkle tree in the future. -/// For this release, padding will not be supported because of security concerns: if the leaf hash and two to one hash uses same underlying -/// CRH, a malicious prover can prove a leaf while the actual node is an inner node. In the future, we can prefix leaf hashes in different layers to -/// solve the problem. +/// For this release, padding is not supported due to security: if leaf and inner hashes use +/// the same CRH, a malicious prover could prove a leaf that is actually an inner node. Future +/// versions can prefix hashes by layer to prevent this. #[derive(Derivative)] #[derivative(Clone(bound = "P: Config"))] pub struct MerkleTree { - /// stores the non-leaf nodes in level order. The first element is the root node. - /// The ith nodes (starting at 1st) children are at indices `2*i`, `2*i+1` + /// Non-leaf nodes in level order, with the root at index 0. For node i, children are at `2*i + 1` and `2*i + 2`. non_leaf_nodes: Vec, /// store the hash of leaf nodes from left to right leaf_nodes: Vec, @@ -513,8 +509,7 @@ pub struct MerkleTree { } impl MerkleTree

{ - /// Create an empty merkle tree such that all leaves are zero-filled. - /// Consider using a sparse merkle tree if you need the tree to be low memory + /// Create a merkle tree with zero-filled leaves. Use a sparse tree for memory efficiency. pub fn blank( leaf_hash_param: &LeafParam

, two_to_one_hash_param: &TwoToOneParam

, @@ -525,7 +520,7 @@ impl MerkleTree

{ Self::new_with_leaf_digest(leaf_hash_param, two_to_one_hash_param, leaf_digests) } - /// Returns a new merkle tree. `leaves.len()` should be power of two. + /// Create a merkle tree from leaves. The leaf count must be a power of two. pub fn new + Send>( leaf_hash_param: &LeafParam

, two_to_one_hash_param: &TwoToOneParam

, @@ -696,13 +691,13 @@ impl MerkleTree

{ }) } - /// Returns a [`CoPath`] (coordinate-free batch membership proof) for the given leaf indexes, + /// Returns a [`CoPath`] (batch membership proof) for the given leaf indexes, /// sufficient to verify each leaf up to the root. /// Indexes are internally deduplicated and sorted; the proof emits digests in that order. /// /// With the CoSet encoding we do not store full per-leaf authentication paths. /// Instead, for each tree level, only the siblings of on-path nodes that are not themselves - /// on-path are transmitted — in canonical depth-then-index order. The verifier reconstructs + /// on-path are transmitted in canonical depth-then-index order. The verifier reconstructs /// the ordering independently from `leaf_indexes` and `tree_height`, so no coordinate /// metadata is included. /// @@ -776,8 +771,7 @@ impl MerkleTree

{ }) } - /// Given the index and new leaf, return the hash of leaf and an updated path in order from root to bottom non-leaf level. - /// This does not mutate the underlying tree. + /// Compute the hash of a new leaf and the updated path from root to leaf, without modifying the tree. fn updated_path>( &self, index: usize, @@ -830,7 +824,7 @@ impl MerkleTree

{ Ok((new_leaf_hash, path_top_to_bottom)) } - /// Update the leaf at `index` to updated leaf. + /// Update the leaf at `index`. /// ```tree_diagram /// [A] /// / \ @@ -853,9 +847,7 @@ impl MerkleTree

{ Ok(()) } - /// Update the leaf and check if the updated root is equal to `asserted_new_root`. - /// - /// Tree will not be modified if the check fails. + /// Update the leaf and verify the root matches `asserted_new_root`. Does not modify the tree if verification fails. pub fn check_update>( &mut self, index: usize, @@ -887,8 +879,7 @@ fn tree_height(num_leaves: usize) -> usize { (ark_std::log2(num_leaves) as usize) + 1 } -/// Return level-order index encoded in global heap. -/// Node at `depth` (root=0) and position `pos` (0-based at that depth) -> heap index `(1< usize { ((1usize << depth) - 1) + pos @@ -945,12 +936,12 @@ fn convert_index_to_last_level(index: usize, tree_height: usize) -> usize { index + (1 << (tree_height - 1)) - 1 } -/// Build the on-path sets A_j from the (sorted, unique) leaf index set I and the leaf depth `d-1`. -/// A_j contains 0-based indices at depth j that lie on the union of all single paths from I to the root. +/// Compute the on-path sets A_j for a batch of leaf indexes. +/// A_j contains all indices at depth j that lie on at least one path from the leaves to the root. /// /// Implementation detail: /// * Uses sorted `Vec` per level to keep the hot loops linear and cache-friendly. -/// * Each leaf contributes one index per depth; we divide by 2 as we walk up and then sort+dedup. +/// * Each leaf contributes one index per depth. As we walk up, we divide by 2 then sort and dedup. pub(super) fn compute_on_path( depth_leaves: usize, indexes: &ark_std::collections::BTreeSet, diff --git a/crypto-primitives/src/merkle_tree/tests/constraints.rs b/crypto-primitives/src/merkle_tree/tests/constraints.rs index ffabb2ed..86868a1b 100644 --- a/crypto-primitives/src/merkle_tree/tests/constraints.rs +++ b/crypto-primitives/src/merkle_tree/tests/constraints.rs @@ -73,8 +73,7 @@ mod byte_mt_tests { &two_to_one_crh_params, &root, leaf.as_slice() - ) - .unwrap()); + )); // Allocate Merkle Tree Root let root = >::OutputVar::new_witness( @@ -291,8 +290,7 @@ mod field_mt_tests { let cs = ConstraintSystem::::new_ref(); let proof = tree.generate_proof(i).unwrap(); assert!(proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap()); + .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice())); // Allocate MT root let root = FpVar::new_witness(cs.clone(), || { if use_bad_root { diff --git a/crypto-primitives/src/merkle_tree/tests/mod.rs b/crypto-primitives/src/merkle_tree/tests/mod.rs index 0240c632..fb33b30f 100644 --- a/crypto-primitives/src/merkle_tree/tests/mod.rs +++ b/crypto-primitives/src/merkle_tree/tests/mod.rs @@ -54,8 +54,7 @@ mod bytes_mt_tests { for (i, leaf) in leaves.iter().enumerate() { let proof = tree.generate_proof(i).unwrap(); assert!(proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap()); + .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice())); } // test the merkle tree multi-proof functionality @@ -70,8 +69,7 @@ mod bytes_mt_tests { &root, tree.height(), leaves.clone() - ) - .unwrap()); + )); // test merkle tree update functionality for (i, v) in update_query { @@ -85,8 +83,7 @@ mod bytes_mt_tests { for (i, leaf) in leaves.iter().enumerate() { let proof = tree.generate_proof(i).unwrap(); assert!(proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap()); + .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice())); } // test the merkle tree multi-proof functionality again @@ -101,8 +98,7 @@ mod bytes_mt_tests { &root, tree.height(), leaves.clone() - ) - .unwrap()); + )); } #[test] @@ -182,8 +178,7 @@ mod bytes_mt_tests { &tree.root(), tree.height(), serialized_leaves.clone() - ) - .unwrap()); + )); } } @@ -235,8 +230,7 @@ mod field_mt_tests { for (i, leaf) in leaves.iter().enumerate() { let proof = tree.generate_proof(i).unwrap(); assert!(proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap()); + .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice())); } // test the merkle tree multi-proof functionality @@ -251,8 +245,7 @@ mod field_mt_tests { &root, tree.height(), leaves.clone() - ) - .unwrap()); + )); { // wrong root should lead to error but do not panic @@ -264,8 +257,7 @@ mod field_mt_tests { &two_to_one_params, &wrong_root, leaves[0].as_slice() - ) - .unwrap()); + )); // test the merkle tree multi-proof functionality let multi_proof = tree @@ -279,8 +271,7 @@ mod field_mt_tests { &wrong_root, tree.height(), leaves.clone() - ) - .unwrap()); + )); } // test merkle tree update functionality @@ -296,8 +287,7 @@ mod field_mt_tests { for (i, leaf) in leaves.iter().enumerate() { let proof = tree.generate_proof(i).unwrap(); assert!(proof - .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice()) - .unwrap()); + .verify(&leaf_crh_params, &two_to_one_params, &root, leaf.as_slice())); } multi_proof = tree @@ -311,8 +301,7 @@ mod field_mt_tests { &root, tree.height(), leaves.clone() - ) - .unwrap()); + )); } #[test] @@ -357,8 +346,7 @@ mod field_mt_tests { &root, tree.height(), Vec::>::new() - ) - .unwrap(); + ); } #[test] @@ -394,8 +382,7 @@ mod field_mt_tests { &root, tree.height(), opened - ) - .unwrap(), + ), "proof with duplicate input indices should verify after deduplication" ); } @@ -429,8 +416,7 @@ mod field_mt_tests { &root, tree.height(), opened, - ) - .unwrap(); + ); assert!(!ok, "tampered leaf_copath digest must fail verification"); } @@ -462,8 +448,7 @@ mod field_mt_tests { &root, tree.height(), opened, - ) - .unwrap(); + ); assert!(!ok, "missing inner copath entry must invalidate the proof"); } @@ -495,8 +480,7 @@ mod field_mt_tests { &root, tree.height(), ordered_leaves.clone() - ) - .unwrap(), + ), "proof should verify when leaves follow proof.leaf_indexes order" ); @@ -509,8 +493,7 @@ mod field_mt_tests { &root, tree.height(), shuffled_leaves, - ) - .unwrap(); + ); assert!(!ok, "mismatched leaf ordering must fail verification"); } @@ -525,8 +508,7 @@ mod field_mt_tests { for i in 0..leaves.len() { let proof = tree.generate_multi_proof([i]).unwrap(); let ok = proof - .verify(¶ms, ¶ms, &root, tree.height(), [leaves[i].clone()]) - .unwrap(); + .verify(¶ms, ¶ms, &root, tree.height(), [leaves[i].clone()]); assert!(ok, "single-leaf proof must verify for index {i}"); } } @@ -540,8 +522,7 @@ mod field_mt_tests { let proof = tree.generate_multi_proof(0..leaves.len()).unwrap(); assert!( proof - .verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()) - .unwrap(), + .verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()), "full-batch proof must verify" ); // full batch: every sibling is on-path, so no inner copath elements needed @@ -563,8 +544,7 @@ mod field_mt_tests { bad.inner_copath.push(F::one()); // one spurious digest let opened: Vec<_> = bad.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); let ok = bad - .verify(¶ms, ¶ms, &root, tree.height(), opened) - .unwrap(); + .verify(¶ms, ¶ms, &root, tree.height(), opened); assert!(!ok, "extra inner digest must fail verification"); } @@ -577,8 +557,7 @@ mod field_mt_tests { let proof = tree.generate_multi_proof([0usize, 3]).unwrap(); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); let ok = proof - .verify(¶ms, ¶ms, &root, tree.height() + 1, opened) - .unwrap(); + .verify(¶ms, ¶ms, &root, tree.height() + 1, opened); assert!(!ok, "mismatched tree height must fail verification"); } @@ -601,7 +580,7 @@ mod field_mt_tests { ); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), opened), "proof must verify" ); } @@ -626,7 +605,7 @@ mod field_mt_tests { ); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), opened), "proof must verify" ); } @@ -648,7 +627,7 @@ mod field_mt_tests { ); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), opened), "proof must verify" ); } @@ -670,7 +649,7 @@ mod field_mt_tests { ); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), opened), "proof must verify" ); } @@ -691,7 +670,7 @@ mod field_mt_tests { ); let opened: Vec<_> = proof.leaf_indexes.iter().map(|&i| leaves[i].clone()).collect(); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), opened).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), opened), "proof must verify" ); } @@ -709,7 +688,7 @@ mod field_mt_tests { "opening all leaves: inner copath must be empty" ); assert!( - proof.verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()).unwrap(), + proof.verify(¶ms, ¶ms, &root, tree.height(), leaves.clone()), "proof must verify" ); } From 0b9057cc81f5e0d18ea51244b2b76bf7e87f2ada Mon Sep 17 00:00:00 2001 From: ajhavlin Date: Thu, 16 Apr 2026 15:24:13 +0200 Subject: [PATCH 22/22] fix: call site naming must match updated function names --- crypto-primitives/src/merkle_tree/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto-primitives/src/merkle_tree/mod.rs b/crypto-primitives/src/merkle_tree/mod.rs index fbe5db3f..44768e46 100644 --- a/crypto-primitives/src/merkle_tree/mod.rs +++ b/crypto-primitives/src/merkle_tree/mod.rs @@ -391,8 +391,8 @@ impl CoPath

{ let index_set: BTreeSet = self.leaf_indexes.iter().copied().collect(); let on_path = compute_on_path(leaf_depth, &index_set); - let expected_leaf_coset = Self::expected_leaf_coset(leaf_depth, &on_path); - if !Self::validate_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { + let expected_leaf_coset = Self::compute_needed_leaf_siblings(leaf_depth, &on_path); + if !Self::absorb_leaf_copath(&expected_leaf_coset, &self.leaf_copath, &mut leaf_level) { return false; } @@ -420,7 +420,7 @@ impl CoPath

{ return false; } - if !Self::recompute_bottom_parents( + if !Self::verify_and_hash_bottom_layer( leaf_depth, &on_path, &leaf_level, @@ -430,7 +430,7 @@ impl CoPath

{ return false; } - if !Self::recompute_inner_layers( + if !Self::verify_and_hash_inner_chain( leaf_depth, &on_path, two_to_one_params,