#[cfg(test)]
mod tests {
use crate::merkle::{
self as merkle,
mmr::{
iterator::PeakIterator, mem::Mmr, Error, Family, Location, Position,
StandardHasher as Standard,
},
proof::{nodes_required_for_multi_proof, Blueprint},
Family as _, LocationRangeExt as _,
};
use commonware_cryptography::{sha256::Digest, Hasher, Sha256};
fn test_digest(v: u8) -> Digest {
Sha256::hash(&[v])
}
#[test]
fn test_proving_digests_from_range() {
let hasher: Standard<Sha256> = Standard::new();
let mut mmr = Mmr::new(&hasher);
let elements: Vec<_> = (0..49).map(test_digest).collect();
let batch = {
let mut batch = mmr.new_batch();
for element in &elements {
batch = batch.add(&hasher, element);
}
batch.merkleize(&mmr, &hasher)
};
mmr.apply_batch(&batch).unwrap();
let root = mmr.root();
let proof = mmr
.range_proof(&hasher, Location::new(0)..mmr.leaves())
.unwrap();
let mut node_digests = proof
.verify_range_inclusion_and_extract_digests(&hasher, &elements, Location::new(0), root)
.unwrap();
assert_eq!(node_digests.len() as u64, mmr.size());
node_digests.sort_by_key(|(pos, _)| *pos);
for (i, (pos, d)) in node_digests.into_iter().enumerate() {
assert_eq!(pos, i as u64);
assert_eq!(mmr.get_node(pos).unwrap(), d);
}
let wrong_root = elements[0]; assert!(matches!(
proof.verify_range_inclusion_and_extract_digests(
&hasher,
&elements,
Location::new(0),
&wrong_root
),
Err(Error::RootMismatch)
));
let range = Location::new(0)..Location::new(1);
let single_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
let range_start = range.start;
let single_digests = single_proof
.verify_range_inclusion_and_extract_digests(
&hasher,
&elements[range.to_usize_range()],
range_start,
root,
)
.unwrap();
assert!(single_digests.len() > 1);
let mid_idx = 24;
let range = Location::new(mid_idx)..Location::new(mid_idx + 1);
let range_start = range.start;
let mid_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
let mid_digests = mid_proof
.verify_range_inclusion_and_extract_digests(
&hasher,
&elements[range.to_usize_range()],
range_start,
root,
)
.unwrap();
assert!(mid_digests.len() > 1);
let last_idx = elements.len() as u64 - 1;
let range = Location::new(last_idx)..Location::new(last_idx + 1);
let range_start = range.start;
let last_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
let last_digests = last_proof
.verify_range_inclusion_and_extract_digests(
&hasher,
&elements[range.to_usize_range()],
range_start,
root,
)
.unwrap();
assert!(!last_digests.is_empty());
let range = Location::new(0)..Location::new(5);
let range_start = range.start;
let small_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
let small_digests = small_proof
.verify_range_inclusion_and_extract_digests(
&hasher,
&elements[range.to_usize_range()],
range_start,
root,
)
.unwrap();
assert!(small_digests.len() > 5);
let range = Location::new(10)..Location::new(31);
let range_start = range.start;
let mid_range_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
let mid_range_digests = mid_range_proof
.verify_range_inclusion_and_extract_digests(
&hasher,
&elements[range.to_usize_range()],
range_start,
root,
)
.unwrap();
let num_elements = range.end - range.start;
assert!(mid_range_digests.len() as u64 > num_elements);
}
#[test]
fn test_max_location_is_provable() {
let max_loc = Family::MAX_LEAVES;
let max_loc_plus_1 = Location::new(*max_loc + 1);
let result = Blueprint::new(max_loc, max_loc - 1..max_loc);
assert!(
result.is_ok(),
"Should be able to prove with MAX_LEAVES leaves"
);
let result_overflow = Blueprint::new(max_loc_plus_1, max_loc..max_loc_plus_1);
assert!(
result_overflow.is_err(),
"Should reject location > MAX_LEAVES"
);
assert!(matches!(
result_overflow,
Err(merkle::Error::LocationOverflow(_))
));
}
#[test]
fn test_max_location_multi_proof() {
let max_loc = Family::MAX_LEAVES;
let result = nodes_required_for_multi_proof(max_loc, &[max_loc - 1]);
assert!(
result.is_ok(),
"Should be able to generate multi-proof for MAX_LEAVES"
);
let invalid_loc = max_loc + 1;
let result_overflow = nodes_required_for_multi_proof(invalid_loc, &[max_loc]);
assert!(
result_overflow.is_err(),
"Should reject location > MAX_LEAVES in multi-proof"
);
}
#[test]
fn test_max_proof_digests_per_element_sufficient() {
const NUM_PEAKS: usize = 62;
const MAX_TREE_HEIGHT: usize = 61;
const EXPECTED_WORST_CASE: usize = MAX_TREE_HEIGHT + (NUM_PEAKS - 1);
let many_peaks_size = Position::new((1u64 << 63) - 64);
assert!(
many_peaks_size.is_valid_size(),
"Size {many_peaks_size} should be a valid MMR size",
);
let peak_count = PeakIterator::new(many_peaks_size).count();
assert_eq!(peak_count, NUM_PEAKS);
let peaks: Vec<_> = PeakIterator::new(many_peaks_size).collect();
for (i, &(_pos, height)) in peaks.iter().enumerate() {
let expected_height = (NUM_PEAKS - 1 - i) as u32;
assert_eq!(
height, expected_height,
"Peak {i} should have height {expected_height}, got {height}",
);
}
let leaves = Location::try_from(many_peaks_size).unwrap();
let loc = Location::new(0);
let bp =
Blueprint::new(leaves, loc..loc + 1).expect("should compute blueprint for location 0");
let total_nodes = bp.fold_prefix.len() + bp.fetch_nodes.len();
assert_eq!(
total_nodes,
EXPECTED_WORST_CASE,
"Location 0 proof should require exactly {EXPECTED_WORST_CASE} digests (61 path + 61 peaks)",
);
let last_leaf_loc = leaves - 1;
let bp = Blueprint::new(leaves, last_leaf_loc..last_leaf_loc + 1)
.expect("should compute blueprint for last leaf");
let total_nodes = bp.fold_prefix.len() + bp.fetch_nodes.len();
let expected_last_leaf = NUM_PEAKS - 1;
assert_eq!(
total_nodes,
expected_last_leaf,
"Last leaf proof should require exactly {expected_last_leaf} digests (0 path + 61 peaks)",
);
}
#[test]
fn test_max_proof_digests_per_element_is_maximum() {
let n_for_63_peaks = (1u128 << 63) - 1;
let size_for_63_peaks = 2 * n_for_63_peaks - 63; assert!(
size_for_63_peaks > *Family::MAX_NODES as u128,
"63 peaks requires size {size_for_63_peaks} > MAX_NODES",
);
let size_truncated = size_for_63_peaks as u64;
assert!(
!Position::new(size_truncated).is_valid_size(),
"Size for 63 peaks should fail is_valid_size()"
);
}
#[test]
fn test_verify_proof_and_pinned_nodes_sibling_case() {
let hasher: Standard<Sha256> = Standard::new();
let mut mmr = Mmr::new(&hasher);
let elements: Vec<Digest> = (0..3).map(test_digest).collect();
let batch = {
let mut batch = mmr.new_batch();
for e in &elements {
batch = batch.add(&hasher, e);
}
batch.merkleize(&mmr, &hasher)
};
mmr.apply_batch(&batch).unwrap();
let root = mmr.root();
let start_loc = Location::new(1);
let proof = mmr
.range_proof(&hasher, start_loc..Location::new(3))
.unwrap();
let pinned: Vec<Digest> = mmr.nodes_to_pin(start_loc).into_values().collect();
assert_eq!(pinned.len(), 1, "should have exactly one pinned node");
assert!(
proof.verify_proof_and_pinned_nodes(&hasher, &elements[1..], start_loc, &pinned, root,),
"valid pinned nodes should verify"
);
let bad_pinned = vec![test_digest(99)];
assert!(
!proof.verify_proof_and_pinned_nodes(
&hasher,
&elements[1..],
start_loc,
&bad_pinned,
root,
),
"wrong pinned digest should fail"
);
let extra_pinned = vec![pinned[0], test_digest(42)];
assert!(
!proof.verify_proof_and_pinned_nodes(
&hasher,
&elements[1..],
start_loc,
&extra_pinned,
root,
),
"extra pinned node should fail"
);
assert!(
!proof.verify_proof_and_pinned_nodes(&hasher, &elements[1..], start_loc, &[], root,),
"missing pinned nodes should fail"
);
}
#[test]
fn test_verify_proof_and_pinned_nodes_fold_prefix_case() {
let hasher: Standard<Sha256> = Standard::new();
let mut mmr = Mmr::new(&hasher);
let elements: Vec<Digest> = (0..10).map(test_digest).collect();
let batch = {
let mut batch = mmr.new_batch();
for e in &elements {
batch = batch.add(&hasher, e);
}
batch.merkleize(&mmr, &hasher)
};
mmr.apply_batch(&batch).unwrap();
let root = mmr.root();
let start_loc = Location::new(8);
let proof = mmr
.range_proof(&hasher, start_loc..Location::new(10))
.unwrap();
let pinned: Vec<Digest> = mmr.nodes_to_pin(start_loc).into_values().collect();
assert_eq!(pinned.len(), 1, "should have one fold-prefix peak");
assert!(
proof.verify_proof_and_pinned_nodes(&hasher, &elements[8..], start_loc, &pinned, root,),
"valid fold-prefix pinned nodes should verify"
);
assert!(
!proof.verify_proof_and_pinned_nodes(
&hasher,
&elements[8..],
start_loc,
&[test_digest(99)],
root,
),
"wrong fold-prefix digest should fail"
);
}
#[cfg(feature = "arbitrary")]
mod conformance {
use super::Family;
use crate::merkle::proof::Proof;
use commonware_codec::conformance::CodecConformance;
use commonware_cryptography::sha256::Digest as Sha256Digest;
commonware_conformance::conformance_tests! {
CodecConformance<Proof<Family, Sha256Digest>>,
}
}
}