Skip to content

Commit 640b015

Browse files
committed
feat(merkle): add batch proof generation and verification (#37)
1 parent e882103 commit 640b015

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

src/merkle.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,31 @@ impl MerkleTree {
219219

220220
Ok(current.ct_eq(auth_root).into())
221221
}
222+
223+
/// Generate proofs for multiple leaf indices in one call.
224+
///
225+
/// Returns proofs in the same order as `indices`.
226+
/// Fails fast if any index is out of bounds.
227+
pub fn proofs(&self, indices: &[usize]) -> HcaResult<Vec<MerkleProof>> {
228+
indices.iter().map(|&i| self.proof(i)).collect()
229+
}
230+
231+
/// Verify multiple proofs against the same auth_root.
232+
///
233+
/// Returns `Ok(true)` only if every proof is valid.
234+
/// Returns `Ok(false)` on the first invalid proof.
235+
/// Returns `Err` if any proof exceeds `MAX_TREE_DEPTH`.
236+
pub fn verify_batch(
237+
items: &[(&[u8; 32], &MerkleProof)],
238+
auth_root: &[u8; 32],
239+
) -> HcaResult<bool> {
240+
for (leaf_hash, proof) in items {
241+
if !Self::verify(leaf_hash, proof, auth_root)? {
242+
return Ok(false);
243+
}
244+
}
245+
Ok(true)
246+
}
222247
}
223248

224249
/// Compute branch node hash
@@ -413,4 +438,85 @@ mod tests {
413438
assert!(result.is_err());
414439
assert!(matches!(result.unwrap_err(), HcaError::TreeTooDeep { .. }));
415440
}
441+
442+
fn make_tree(n: usize) -> (MerkleTree, Vec<Leaf>) {
443+
let leaves: Vec<Leaf> = (0..n)
444+
.map(|i| Leaf::new(0x01, vec![0x60, i as u8], &format!("leaf {}", i)).unwrap())
445+
.collect();
446+
let tree = MerkleTree::new(leaves.clone()).unwrap();
447+
(tree, leaves)
448+
}
449+
450+
#[test]
451+
fn test_batch_proofs_all_leaves() {
452+
let (tree, leaves) = make_tree(4);
453+
let indices: Vec<usize> = (0..leaves.len()).collect();
454+
let proofs = tree.proofs(&indices).unwrap();
455+
assert_eq!(proofs.len(), 4);
456+
let root = tree.auth_root();
457+
for (i, proof) in proofs.iter().enumerate() {
458+
assert!(MerkleTree::verify(&leaves[i].hash(), proof, &root).unwrap());
459+
}
460+
}
461+
462+
#[test]
463+
fn test_batch_proofs_partial() {
464+
let (tree, leaves) = make_tree(4);
465+
let proofs = tree.proofs(&[0, 2]).unwrap();
466+
let root = tree.auth_root();
467+
assert!(MerkleTree::verify(&leaves[0].hash(), &proofs[0], &root).unwrap());
468+
assert!(MerkleTree::verify(&leaves[2].hash(), &proofs[1], &root).unwrap());
469+
}
470+
471+
#[test]
472+
fn test_batch_proofs_out_of_bounds_fails() {
473+
let (tree, _) = make_tree(4);
474+
assert!(tree.proofs(&[0, 99]).is_err());
475+
}
476+
477+
#[test]
478+
fn test_verify_batch_all_valid() {
479+
let (tree, leaves) = make_tree(4);
480+
let indices: Vec<usize> = (0..leaves.len()).collect();
481+
let proofs = tree.proofs(&indices).unwrap();
482+
let root = tree.auth_root();
483+
let items: Vec<(&[u8; 32], &MerkleProof)> = leaves
484+
.iter()
485+
.map(|l| l.hash())
486+
.zip(proofs.iter())
487+
.map(|(h, p)| (Box::leak(Box::new(h)) as &[u8; 32], p))
488+
.collect();
489+
assert!(MerkleTree::verify_batch(&items, &root).unwrap());
490+
}
491+
492+
#[test]
493+
fn test_verify_batch_empty() {
494+
let (tree, _) = make_tree(2);
495+
let root = tree.auth_root();
496+
assert!(MerkleTree::verify_batch(&[], &root).unwrap());
497+
}
498+
499+
#[test]
500+
fn test_verify_batch_one_invalid_fails() {
501+
let (tree, leaves) = make_tree(4);
502+
let proofs = tree.proofs(&[0, 1]).unwrap();
503+
let root = tree.auth_root();
504+
// Swap leaf hashes — leaf 1's hash with proof for index 0 should fail
505+
let h0 = leaves[0].hash();
506+
let h1 = leaves[1].hash();
507+
let items: Vec<(&[u8; 32], &MerkleProof)> =
508+
vec![(&h0, &proofs[0]), (&h1, &proofs[0])];
509+
assert!(!MerkleTree::verify_batch(&items, &root).unwrap());
510+
}
511+
512+
#[test]
513+
fn test_verify_batch_order_preserved() {
514+
// Verify that proofs() returns proofs in the same order as the requested indices
515+
let (tree, leaves) = make_tree(4);
516+
let proofs = tree.proofs(&[3, 1, 0]).unwrap();
517+
let root = tree.auth_root();
518+
assert!(MerkleTree::verify(&leaves[3].hash(), &proofs[0], &root).unwrap());
519+
assert!(MerkleTree::verify(&leaves[1].hash(), &proofs[1], &root).unwrap());
520+
assert!(MerkleTree::verify(&leaves[0].hash(), &proofs[2], &root).unwrap());
521+
}
416522
}

0 commit comments

Comments
 (0)