use hush_core::canonical::canonicalize;
use hush_core::merkle::{leaf_hash, node_hash};
use hush_core::Hash;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum MerkleError {
#[error("leaf index {index} out of range for tree of size {tree_size}")]
LeafIndexOutOfRange { index: u64, tree_size: u64 },
#[error("old size {old_size} exceeds current tree size {tree_size}")]
OldSizeExceedsTree { old_size: u64, tree_size: u64 },
#[error("old size must be greater than zero")]
OldSizeZero,
#[error("empty tree has no root")]
EmptyTree,
#[error("canonical JSON error: {0}")]
Canonical(String),
#[error("inclusion proof verification failed")]
InclusionProofFailed,
#[error("consistency proof verification failed")]
ConsistencyProofFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LeafData {
pub package_name: String,
pub version: String,
pub content_hash: String,
pub publisher_key: String,
pub timestamp: String,
}
impl LeafData {
pub fn leaf_hash(&self) -> Result<Hash, MerkleError> {
let value =
serde_json::to_value(self).map_err(|e| MerkleError::Canonical(e.to_string()))?;
let canonical = canonicalize(&value).map_err(|e| MerkleError::Canonical(e.to_string()))?;
Ok(leaf_hash(canonical.as_bytes()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InclusionProof {
pub leaf_index: u64,
pub tree_size: u64,
pub proof_path: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsistencyProof {
pub old_size: u64,
pub new_size: u64,
pub proof_path: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleTree {
leaf_hashes: Vec<Hash>,
}
impl MerkleTree {
pub fn new() -> Self {
Self {
leaf_hashes: Vec::new(),
}
}
pub fn append(&mut self, leaf_data: &[u8]) -> u64 {
let hash = leaf_hash(leaf_data);
self.leaf_hashes.push(hash);
(self.leaf_hashes.len() - 1) as u64
}
pub fn append_hash(&mut self, hash: Hash) -> u64 {
self.leaf_hashes.push(hash);
(self.leaf_hashes.len() - 1) as u64
}
pub fn tree_size(&self) -> u64 {
self.leaf_hashes.len() as u64
}
pub fn root(&self) -> Result<String, MerkleError> {
if self.leaf_hashes.is_empty() {
return Err(MerkleError::EmptyTree);
}
Ok(merkle_tree_hash(&self.leaf_hashes).to_hex())
}
pub fn generate_inclusion_proof(&self, leaf_index: u64) -> Result<InclusionProof, MerkleError> {
let n = self.tree_size();
if n == 0 {
return Err(MerkleError::EmptyTree);
}
if leaf_index >= n {
return Err(MerkleError::LeafIndexOutOfRange {
index: leaf_index,
tree_size: n,
});
}
let mut path = Vec::new();
subproof(
leaf_index as usize,
0,
self.leaf_hashes.len(),
&self.leaf_hashes,
&mut path,
);
Ok(InclusionProof {
leaf_index,
tree_size: n,
proof_path: path.iter().map(|h| h.to_hex()).collect(),
})
}
pub fn generate_consistency_proof(
&self,
old_size: u64,
) -> Result<ConsistencyProof, MerkleError> {
let new_size = self.tree_size();
if old_size == 0 {
return Err(MerkleError::OldSizeZero);
}
if old_size > new_size {
return Err(MerkleError::OldSizeExceedsTree {
old_size,
tree_size: new_size,
});
}
let path = consistency_proof_path(old_size as usize, new_size as usize, &self.leaf_hashes);
Ok(ConsistencyProof {
old_size,
new_size,
proof_path: path.iter().map(|h| h.to_hex()).collect(),
})
}
}
impl Default for MerkleTree {
fn default() -> Self {
Self::new()
}
}
fn merkle_tree_hash(hashes: &[Hash]) -> Hash {
match hashes.len() {
0 => Hash::zero(),
1 => hashes[0],
n => {
let k = largest_power_of_two_less_than(n);
let left = merkle_tree_hash(&hashes[..k]);
let right = merkle_tree_hash(&hashes[k..]);
node_hash(&left, &right)
}
}
}
fn largest_power_of_two_less_than(n: usize) -> usize {
debug_assert!(n > 1);
let mut p = 1usize;
while (p << 1) < n {
p <<= 1;
}
p
}
fn subproof(m: usize, start: usize, end: usize, leaves: &[Hash], path: &mut Vec<Hash>) {
let n = end - start;
if n <= 1 {
return;
}
let k = largest_power_of_two_less_than(n);
if m < k {
subproof(m, start, start + k, leaves, path);
path.push(merkle_tree_hash(&leaves[start + k..end]));
} else {
subproof(m - k, start + k, end, leaves, path);
path.push(merkle_tree_hash(&leaves[start..start + k]));
}
}
fn consistency_proof_path(old_size: usize, new_size: usize, leaves: &[Hash]) -> Vec<Hash> {
let mut path = Vec::new();
if old_size == new_size {
return path;
}
consistency_subproof(old_size, 0, new_size, leaves, &mut path, true);
path
}
fn consistency_subproof(
old_size: usize,
start: usize,
end: usize,
leaves: &[Hash],
path: &mut Vec<Hash>,
start_from_old_root: bool,
) {
let n = end - start;
if old_size == n {
if !start_from_old_root {
path.push(merkle_tree_hash(&leaves[start..end]));
}
return;
}
if n <= 1 {
return;
}
let k = largest_power_of_two_less_than(n);
if old_size <= k {
consistency_subproof(
old_size,
start,
start + k,
leaves,
path,
start_from_old_root,
);
path.push(merkle_tree_hash(&leaves[start + k..end]));
} else {
consistency_subproof(old_size - k, start + k, end, leaves, path, false);
path.push(merkle_tree_hash(&leaves[start..start + k]));
}
}
#[derive(Debug, Clone, Copy)]
enum Side {
Right,
Left,
}
fn inclusion_decomposition(mut leaf_index: usize, mut tree_size: usize) -> Vec<Side> {
let mut levels = Vec::new();
while tree_size > 1 {
let k = largest_power_of_two_less_than(tree_size);
if leaf_index < k {
levels.push(Side::Right);
tree_size = k;
} else {
levels.push(Side::Left);
leaf_index -= k;
tree_size -= k;
}
}
levels.reverse();
levels
}
pub fn verify_inclusion_proof(proof: &InclusionProof, leaf_hash_hex: &str, root: &str) -> bool {
let Ok(lh) = Hash::from_hex(leaf_hash_hex) else {
return false;
};
let path: Result<Vec<Hash>, _> = proof.proof_path.iter().map(|h| Hash::from_hex(h)).collect();
let Ok(path) = path else {
return false;
};
let tree_size = proof.tree_size as usize;
let leaf_index = proof.leaf_index as usize;
if tree_size == 0 || leaf_index >= tree_size {
return false;
}
if tree_size == 1 {
return path.is_empty() && lh.to_hex() == root;
}
let levels = inclusion_decomposition(leaf_index, tree_size);
if levels.len() != path.len() {
return false;
}
let mut hash = lh;
for (side, sibling) in levels.iter().zip(path.iter()) {
match side {
Side::Right => hash = node_hash(&hash, sibling),
Side::Left => hash = node_hash(sibling, &hash),
}
}
hash.to_hex() == root
}
fn consistency_decomposition(old_size: usize, new_size: usize) -> (Vec<(Side, bool)>, bool) {
let mut levels = Vec::new();
let path_includes_seed = consistency_decomposition_inner(old_size, new_size, true, &mut levels);
(levels, path_includes_seed)
}
fn consistency_decomposition_inner(
old_size: usize,
n: usize,
start_from_old_root: bool,
levels: &mut Vec<(Side, bool)>,
) -> bool {
if old_size == n {
return !start_from_old_root;
}
if n <= 1 {
return false;
}
let k = largest_power_of_two_less_than(n);
if old_size <= k {
let path_includes_seed =
consistency_decomposition_inner(old_size, k, start_from_old_root, levels);
levels.push((Side::Right, false));
path_includes_seed
} else {
let path_includes_seed =
consistency_decomposition_inner(old_size - k, n - k, false, levels);
levels.push((Side::Left, true));
path_includes_seed
}
}
pub fn verify_consistency_proof_full(
proof: &ConsistencyProof,
old_root: &str,
new_root: &str,
) -> bool {
let path: Result<Vec<Hash>, _> = proof.proof_path.iter().map(|h| Hash::from_hex(h)).collect();
let Ok(path) = path else {
return false;
};
let Ok(old_root_hash) = Hash::from_hex(old_root) else {
return false;
};
let Ok(new_root_hash) = Hash::from_hex(new_root) else {
return false;
};
let old_size = proof.old_size as usize;
let new_size = proof.new_size as usize;
if old_size == 0 || old_size > new_size {
return false;
}
if old_size == new_size {
return path.is_empty() && old_root_hash == new_root_hash;
}
let (levels, path_includes_seed) = consistency_decomposition(old_size, new_size);
let (seed, proof_hashes) = if path_includes_seed {
if path.is_empty() {
return false;
}
(path[0], &path[1..])
} else {
(old_root_hash, path.as_slice())
};
if levels.len() != proof_hashes.len() {
return false;
}
let mut old_hash = seed;
let mut new_hash = seed;
for ((side, affects_old), sibling) in levels.iter().zip(proof_hashes.iter()) {
match side {
Side::Right => {
new_hash = node_hash(&new_hash, sibling);
if *affects_old {
old_hash = node_hash(&old_hash, sibling);
}
}
Side::Left => {
new_hash = node_hash(sibling, &new_hash);
if *affects_old {
old_hash = node_hash(sibling, &old_hash);
}
}
}
}
old_hash == old_root_hash && new_hash == new_root_hash
}
#[cfg(test)]
mod tests {
use super::*;
fn make_leaf_data(name: &str, version: &str) -> LeafData {
LeafData {
package_name: name.to_string(),
version: version.to_string(),
content_hash: format!("sha256:{name}-{version}"),
publisher_key: "ed25519:testkey".to_string(),
timestamp: "2026-01-01T00:00:00Z".to_string(),
}
}
#[test]
fn empty_tree_has_no_root() {
let tree = MerkleTree::new();
assert!(tree.root().is_err());
assert_eq!(tree.tree_size(), 0);
}
#[test]
fn single_leaf_root_equals_leaf_hash() {
let mut tree = MerkleTree::new();
tree.append(b"leaf-0");
assert_eq!(tree.tree_size(), 1);
let expected = leaf_hash(b"leaf-0").to_hex();
assert_eq!(tree.root().unwrap(), expected);
}
#[test]
fn power_of_2_trees() {
for size in [2, 4, 8, 16] {
let mut tree = MerkleTree::new();
for i in 0..size {
tree.append(format!("leaf-{i}").as_bytes());
}
let root1 = tree.root().unwrap();
let root2 = tree.root().unwrap();
assert_eq!(root1, root2, "root should be deterministic for size {size}");
}
}
#[test]
fn non_power_of_2_trees() {
for size in [3, 5, 7, 9, 15, 17] {
let mut tree = MerkleTree::new();
for i in 0..size {
tree.append(format!("leaf-{i}").as_bytes());
}
let root = tree.root().unwrap();
assert!(!root.is_empty(), "root should not be empty for size {size}");
}
}
#[test]
fn inclusion_proof_roundtrip_various_sizes() {
for size in [1, 2, 3, 4, 7, 16, 100] {
let mut tree = MerkleTree::new();
let mut leaves = Vec::new();
for i in 0u64..size {
let data = format!("leaf-{i}");
tree.append(data.as_bytes());
leaves.push(data);
}
let root = tree.root().unwrap();
for idx in 0..size {
let proof = tree.generate_inclusion_proof(idx).unwrap();
let lh = leaf_hash(leaves[idx as usize].as_bytes()).to_hex();
assert!(
verify_inclusion_proof(&proof, &lh, &root),
"inclusion proof failed for size={size}, idx={idx}"
);
}
}
}
#[test]
fn inclusion_proof_rejects_tampered_path() {
let mut tree = MerkleTree::new();
for i in 0..8u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let root = tree.root().unwrap();
let mut proof = tree.generate_inclusion_proof(3).unwrap();
if let Some(h) = proof.proof_path.first_mut() {
*h = "ff".repeat(32);
}
let lh = leaf_hash(b"leaf-3").to_hex();
assert!(!verify_inclusion_proof(&proof, &lh, &root));
}
#[test]
fn inclusion_proof_rejects_wrong_leaf() {
let mut tree = MerkleTree::new();
for i in 0..8u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let root = tree.root().unwrap();
let proof = tree.generate_inclusion_proof(3).unwrap();
let wrong_lh = leaf_hash(b"leaf-5").to_hex();
assert!(!verify_inclusion_proof(&proof, &wrong_lh, &root));
}
#[test]
fn inclusion_proof_out_of_range() {
let mut tree = MerkleTree::new();
tree.append(b"leaf-0");
assert!(tree.generate_inclusion_proof(1).is_err());
assert!(tree.generate_inclusion_proof(100).is_err());
}
#[test]
fn consistency_proof_roundtrip() {
let mut tree = MerkleTree::new();
let mut snapshots = Vec::new();
for i in 0..20u64 {
tree.append(format!("leaf-{i}").as_bytes());
snapshots.push((tree.tree_size(), tree.root().unwrap()));
}
for old_idx in [0, 1, 3, 7, 14, 19] {
let (old_size, ref old_root) = snapshots[old_idx];
let new_root = tree.root().unwrap();
let proof = tree.generate_consistency_proof(old_size).unwrap();
assert!(
verify_consistency_proof_full(&proof, old_root, &new_root),
"consistency failed for old_size={old_size}"
);
}
}
#[test]
fn consistency_proof_adjacent_sizes() {
let max = 32;
let mut tree = MerkleTree::new();
let mut roots = Vec::new();
for i in 0..max {
tree.append(format!("leaf-{i}").as_bytes());
roots.push(tree.root().unwrap());
}
for old_size in 1..=max as u64 {
let old_root = &roots[old_size as usize - 1];
let new_root = &roots[roots.len() - 1];
let proof = tree.generate_consistency_proof(old_size).unwrap();
assert!(
verify_consistency_proof_full(&proof, old_root, new_root),
"consistency failed for old_size={old_size} -> {max}"
);
}
}
#[test]
fn consistency_decomposition_matches_generated_path_shape() {
let mut tree = MerkleTree::new();
for i in 0..64u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
for new_size in 2usize..=64 {
let leaves = &tree.leaf_hashes[..new_size];
for old_size in 1usize..new_size {
let path = consistency_proof_path(old_size, new_size, leaves);
let (levels, path_includes_seed) = consistency_decomposition(old_size, new_size);
let expected_len = levels.len() + usize::from(path_includes_seed);
assert_eq!(
path.len(),
expected_len,
"shape mismatch old_size={old_size} new_size={new_size}"
);
}
}
}
#[test]
fn consistency_proof_rejects_tampered_old_root() {
let mut tree = MerkleTree::new();
for i in 0..8u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let root_at_4 = {
let mut t = MerkleTree::new();
for i in 0..4u64 {
t.append(format!("leaf-{i}").as_bytes());
}
t.root().unwrap()
};
let proof = tree.generate_consistency_proof(4).unwrap();
let new_root = tree.root().unwrap();
assert!(verify_consistency_proof_full(&proof, &root_at_4, &new_root));
let fake_root = "ff".repeat(32);
assert!(!verify_consistency_proof_full(
&proof, &fake_root, &new_root
));
}
#[test]
fn consistency_proof_old_size_zero() {
let mut tree = MerkleTree::new();
tree.append(b"leaf-0");
assert!(tree.generate_consistency_proof(0).is_err());
}
#[test]
fn consistency_proof_old_size_exceeds_tree() {
let mut tree = MerkleTree::new();
tree.append(b"leaf-0");
assert!(tree.generate_consistency_proof(5).is_err());
}
#[test]
fn append_only_root_changes() {
let mut tree = MerkleTree::new();
tree.append(b"leaf-0");
let root1 = tree.root().unwrap();
tree.append(b"leaf-1");
let root2 = tree.root().unwrap();
assert_ne!(root1, root2, "root should change after append");
assert_eq!(tree.root().unwrap(), root2);
}
#[test]
fn domain_separation_leaf_vs_interior() {
let data = b"test-data";
let lh = leaf_hash(data);
let ih = node_hash(&lh, &lh);
assert_ne!(lh, ih, "leaf hash and interior hash must differ");
}
#[test]
fn domain_separation_prefix_byte_differs() {
let data = b"hello";
let lh = leaf_hash(data);
let mut prefixed = vec![0x01u8];
prefixed.extend_from_slice(data);
let manual = hush_core::sha256(&prefixed);
assert_ne!(
lh, manual,
"0x00 prefix and 0x01 prefix must produce different hashes"
);
}
#[test]
fn leaf_data_hash_is_deterministic() {
let data = make_leaf_data("my-pkg", "1.0.0");
let h1 = data.leaf_hash().unwrap();
let h2 = data.leaf_hash().unwrap();
assert_eq!(h1, h2);
}
#[test]
fn leaf_data_different_inputs_different_hashes() {
let d1 = make_leaf_data("pkg-a", "1.0.0");
let d2 = make_leaf_data("pkg-b", "1.0.0");
assert_ne!(d1.leaf_hash().unwrap(), d2.leaf_hash().unwrap());
}
#[test]
fn leaf_data_serialization_roundtrip() {
let data = make_leaf_data("my-pkg", "2.0.0");
let json = serde_json::to_string(&data).unwrap();
let restored: LeafData = serde_json::from_str(&json).unwrap();
assert_eq!(data, restored);
}
#[test]
fn leaf_data_in_tree() {
let mut tree = MerkleTree::new();
let data = make_leaf_data("my-pkg", "1.0.0");
let hash = data.leaf_hash().unwrap();
let idx = tree.append_hash(hash);
assert_eq!(idx, 0);
assert_eq!(tree.tree_size(), 1);
let root = tree.root().unwrap();
let proof = tree.generate_inclusion_proof(0).unwrap();
assert!(verify_inclusion_proof(&proof, &hash.to_hex(), &root));
}
#[test]
fn inclusion_proof_serialization_roundtrip() {
let mut tree = MerkleTree::new();
for i in 0..5u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let proof = tree.generate_inclusion_proof(2).unwrap();
let json = serde_json::to_string(&proof).unwrap();
let restored: InclusionProof = serde_json::from_str(&json).unwrap();
assert_eq!(proof.leaf_index, restored.leaf_index);
assert_eq!(proof.tree_size, restored.tree_size);
assert_eq!(proof.proof_path, restored.proof_path);
let lh = leaf_hash(b"leaf-2").to_hex();
let root = tree.root().unwrap();
assert!(verify_inclusion_proof(&restored, &lh, &root));
}
#[test]
fn consistency_proof_serialization_roundtrip() {
let mut tree = MerkleTree::new();
for i in 0..8u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let proof = tree.generate_consistency_proof(4).unwrap();
let json = serde_json::to_string(&proof).unwrap();
let restored: ConsistencyProof = serde_json::from_str(&json).unwrap();
assert_eq!(proof.old_size, restored.old_size);
assert_eq!(proof.new_size, restored.new_size);
assert_eq!(proof.proof_path, restored.proof_path);
}
#[test]
fn consistency_proof_same_size() {
let mut tree = MerkleTree::new();
for i in 0..4u64 {
tree.append(format!("leaf-{i}").as_bytes());
}
let proof = tree.generate_consistency_proof(4).unwrap();
assert!(proof.proof_path.is_empty());
let root = tree.root().unwrap();
assert!(verify_consistency_proof_full(&proof, &root, &root));
}
#[test]
fn append_returns_sequential_indices() {
let mut tree = MerkleTree::new();
for i in 0..10u64 {
let idx = tree.append(format!("leaf-{i}").as_bytes());
assert_eq!(idx, i);
}
}
#[test]
fn two_leaf_tree_root() {
let mut tree = MerkleTree::new();
tree.append(b"left");
tree.append(b"right");
let expected = node_hash(&leaf_hash(b"left"), &leaf_hash(b"right"));
assert_eq!(tree.root().unwrap(), expected.to_hex());
}
}