#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::merkle::tree::{AppendOnlyTree, RawLeaf};
use crate::merkle::{raw_leaf_hash, verify_inclusion_raw};
const MAX_SIZE: u64 = 33;
fn leaf_bytes(index: u64) -> Vec<u8> {
format!("leaf-{index}").into_bytes()
}
fn tree_of(size: u64) -> AppendOnlyTree<RawLeaf> {
let mut tree = AppendOnlyTree::<RawLeaf>::new();
for index in 0..size {
tree.append_raw(&leaf_bytes(index));
}
tree
}
#[test]
fn reconstructed_root_matches_the_tree_at_every_size_and_index() {
for size in 1..=MAX_SIZE {
let tree = tree_of(size);
let root = tree.root();
let (expected_root_bytes, num_leaves) = root.to_parts();
assert_eq!(num_leaves, size);
for index in 0..size {
let proof = tree.prove_inclusion(index).unwrap();
let leaf_hash = raw_leaf_hash(&leaf_bytes(index));
let reconstructed = root_from_inclusion_path(&leaf_hash, index, size, proof.as_bytes())
.unwrap_or_else(|e| {
panic!("reconstruction failed at size {size} index {index}: {e}")
});
assert_eq!(
reconstructed, expected_root_bytes,
"reconstructed root differs from the tree's own root at size {size} index {index}"
);
}
}
}
#[test]
fn reconstruction_agrees_with_the_supplied_root_verifier() {
for size in [1u64, 2, 3, 5, 8, 13, 21] {
let tree = tree_of(size);
let root = tree.root();
for index in 0..size {
let proof = tree.prove_inclusion(index).unwrap();
let bytes = leaf_bytes(index);
verify_inclusion_raw(&root, &bytes, index, &proof).unwrap();
let reconstructed =
root_from_inclusion_path(&raw_leaf_hash(&bytes), index, size, proof.as_bytes())
.unwrap();
assert_eq!(reconstructed, root.to_parts().0);
}
}
}
#[test]
fn a_tampered_path_reconstructs_a_different_root() {
let size = 9;
let tree = tree_of(size);
let expected = tree.root().to_parts().0;
let index = 4;
let proof = tree.prove_inclusion(index).unwrap();
let leaf_hash = raw_leaf_hash(&leaf_bytes(index));
let mut tampered = proof.as_bytes().to_vec();
assert!(!tampered.is_empty(), "expected a non-empty path");
tampered[0] ^= 0x01;
let reconstructed = root_from_inclusion_path(&leaf_hash, index, size, &tampered).unwrap();
assert_ne!(
reconstructed, expected,
"a tampered path must not reconstruct the root the anchor signed"
);
}
#[test]
fn the_wrong_leaf_index_reconstructs_a_different_root() {
let size = 16;
let tree = tree_of(size);
let expected = tree.root().to_parts().0;
let proof = tree.prove_inclusion(5).unwrap();
let reconstructed =
root_from_inclusion_path(&raw_leaf_hash(&leaf_bytes(5)), 6, size, proof.as_bytes())
.unwrap();
assert_ne!(reconstructed, expected);
}
#[test]
fn a_single_leaf_tree_has_an_empty_path_and_reconstructs_its_leaf_hash() {
let tree = tree_of(1);
let proof = tree.prove_inclusion(0).unwrap();
assert!(proof.as_bytes().is_empty(), "a one-leaf tree needs no path");
let leaf_hash = raw_leaf_hash(&leaf_bytes(0));
let reconstructed = root_from_inclusion_path(&leaf_hash, 0, 1, proof.as_bytes()).unwrap();
assert_eq!(reconstructed, leaf_hash);
assert_eq!(reconstructed, tree.root().to_parts().0);
}
#[test]
fn structural_inputs_are_rejected_rather_than_guessed() {
let leaf_hash = raw_leaf_hash(b"leaf-0");
assert!(root_from_inclusion_path(&leaf_hash, 0, 0, &[]).is_err());
assert!(root_from_inclusion_path(&leaf_hash, 4, 4, &[]).is_err());
assert!(root_from_inclusion_path(&leaf_hash, 99, 4, &[]).is_err());
assert!(root_from_inclusion_path(&leaf_hash, 0, 4, &[0u8; 33]).is_err());
assert!(root_from_inclusion_path(&leaf_hash, 0, 4, &[0u8; 32]).is_err());
assert!(root_from_inclusion_path(&leaf_hash, 0, 4, &[0u8; 32 * 5]).is_err());
}
#[test]
fn only_the_exact_path_length_is_accepted() {
for size in 1..=MAX_SIZE {
let tree = tree_of(size);
for index in 0..size {
let proof = tree.prove_inclusion(index).unwrap();
let leaf_hash = raw_leaf_hash(&leaf_bytes(index));
let exact = proof.as_bytes().to_vec();
let mut padded = exact.clone();
padded.extend_from_slice(&[0u8; 32]);
assert!(
root_from_inclusion_path(&leaf_hash, index, size, &padded).is_err(),
"a padded path was accepted at size {size} index {index}"
);
if !exact.is_empty() {
let truncated = &exact[..exact.len() - 32];
assert!(
root_from_inclusion_path(&leaf_hash, index, size, truncated).is_err(),
"a truncated path was accepted at size {size} index {index}"
);
}
}
}
}