use sha2::{Digest, Sha256};
use super::error::ScittError;
pub const MAX_HASH_PATH_LEN: usize = 63;
const LEAF_PREFIX: u8 = 0x00;
const NODE_PREFIX: u8 = 0x01;
pub fn compute_leaf_hash(event_bytes: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([LEAF_PREFIX]);
hasher.update(event_bytes);
hasher.finalize().into()
}
pub fn compute_node_hash(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([NODE_PREFIX]);
hasher.update(left);
hasher.update(right);
hasher.finalize().into()
}
pub fn walk_inclusion_path(
event_bytes: &[u8],
leaf_index: u64,
tree_size: u64,
hash_path: &[[u8; 32]],
) -> Result<[u8; 32], ScittError> {
tracing::debug!(
leaf_index,
tree_size,
path_len = hash_path.len(),
"Walking Merkle inclusion path"
);
if tree_size == 0 {
return Err(ScittError::InvalidMerkleProof(
"tree_size must be >= 1".to_string(),
));
}
if leaf_index >= tree_size {
return Err(ScittError::InvalidMerkleProof(format!(
"leaf_index {leaf_index} >= tree_size {tree_size}"
)));
}
if hash_path.len() > MAX_HASH_PATH_LEN {
return Err(ScittError::InvalidMerkleProof(format!(
"hash_path length {} exceeds maximum of {MAX_HASH_PATH_LEN}",
hash_path.len()
)));
}
let mut current = compute_leaf_hash(event_bytes);
let mut index = leaf_index;
let mut remaining = tree_size - 1;
for sibling in hash_path {
if index % 2 == 1 || index == remaining {
current = compute_node_hash(sibling, ¤t);
} else {
current = compute_node_hash(¤t, sibling);
}
index /= 2;
remaining /= 2;
}
while remaining > 0 {
if index == remaining {
index /= 2;
remaining /= 2;
} else {
return Err(ScittError::InvalidMerkleProof(format!(
"incomplete inclusion path: remaining={remaining} after consuming {} hashes",
hash_path.len()
)));
}
}
if index != 0 {
return Err(ScittError::InvalidMerkleProof(format!(
"incomplete inclusion path: index={index} != 0 after walk",
)));
}
Ok(current)
}
#[cfg(test)]
pub(crate) fn verify_merkle_inclusion(
event_bytes: &[u8],
leaf_index: u64,
tree_size: u64,
hash_path: &[[u8; 32]],
expected_root: &[u8; 32],
) -> Result<(), ScittError> {
use subtle::ConstantTimeEq;
let computed = walk_inclusion_path(event_bytes, leaf_index, tree_size, hash_path)?;
if bool::from(computed.ct_eq(expected_root)) {
tracing::debug!(leaf_index, tree_size, "Merkle inclusion proof verified");
Ok(())
} else {
tracing::warn!(
leaf_index,
tree_size,
"Merkle root mismatch — computed root does not match expected"
);
Err(ScittError::MerkleRootMismatch)
}
}
#[cfg(test)]
pub(crate) fn build_tree_and_proof(
leaves: &[&[u8]],
leaf_index: usize,
) -> ([u8; 32], Vec<[u8; 32]>) {
assert!(!leaves.is_empty(), "need at least one leaf");
assert!(leaf_index < leaves.len(), "leaf_index out of bounds");
let mut layer: Vec<[u8; 32]> = leaves.iter().map(|b| compute_leaf_hash(b)).collect();
let mut proof: Vec<[u8; 32]> = Vec::new();
let mut idx = leaf_index;
while layer.len() > 1 {
let mut next_layer: Vec<[u8; 32]> = Vec::new();
let sibling_idx = if idx % 2 == 1 {
idx - 1 } else if idx + 1 < layer.len() {
idx + 1 } else {
usize::MAX };
if sibling_idx != usize::MAX {
proof.push(layer[sibling_idx]);
}
let mut i = 0;
while i < layer.len() {
if i + 1 < layer.len() {
next_layer.push(compute_node_hash(&layer[i], &layer[i + 1]));
} else {
next_layer.push(layer[i]);
}
i += 2;
}
idx /= 2;
layer = next_layer;
}
(layer[0], proof)
}
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
fn root_from_leaves(leaves: &[&[u8]]) -> [u8; 32] {
let mut layer: Vec<[u8; 32]> = leaves.iter().map(|b| compute_leaf_hash(b)).collect();
while layer.len() > 1 {
let mut next = Vec::new();
let mut i = 0;
while i < layer.len() {
if i + 1 < layer.len() {
next.push(compute_node_hash(&layer[i], &layer[i + 1]));
} else {
next.push(layer[i]);
}
i += 2;
}
layer = next;
}
layer[0]
}
#[test]
fn leaf_prefix_0x00_domain_separation() {
let event = b"test event";
let leaf = compute_leaf_hash(event);
let mut hasher = Sha256::new();
hasher.update([0x00u8]);
hasher.update(event);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(leaf, expected);
}
#[test]
fn node_prefix_0x01_domain_separation() {
let left = [0u8; 32];
let right = [1u8; 32];
let node = compute_node_hash(&left, &right);
let mut hasher = Sha256::new();
hasher.update([0x01u8]);
hasher.update(left);
hasher.update(right);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(node, expected);
}
#[test]
fn leaf_hash_differs_from_node_hash_for_same_content() {
let data = [0u8; 32];
let leaf = compute_leaf_hash(&data);
let node = compute_node_hash(&data, &data);
assert_ne!(leaf, node, "leaf and node hashes must be distinct");
}
#[test]
fn single_element_tree_empty_path() {
let event = b"only leaf";
let root = compute_leaf_hash(event);
verify_merkle_inclusion(event, 0, 1, &[], &root).unwrap();
}
#[test]
fn single_element_tree_wrong_root_fails() {
let event = b"only leaf";
let mut root = compute_leaf_hash(event);
root[0] ^= 0xff;
let err = verify_merkle_inclusion(event, 0, 1, &[], &root).unwrap_err();
assert!(matches!(err, ScittError::MerkleRootMismatch));
}
#[test]
fn two_element_tree_index_0() {
let leaves: &[&[u8]] = &[b"leaf0", b"leaf1"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
verify_merkle_inclusion(b"leaf0", 0, 2, &proof, &root).unwrap();
}
#[test]
fn two_element_tree_index_1() {
let leaves: &[&[u8]] = &[b"leaf0", b"leaf1"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 1);
verify_merkle_inclusion(b"leaf1", 1, 2, &proof, &root).unwrap();
}
#[test]
fn four_element_tree_index_0() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
verify_merkle_inclusion(b"a", 0, 4, &proof, &root).unwrap();
}
#[test]
fn four_element_tree_index_1() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 1);
verify_merkle_inclusion(b"b", 1, 4, &proof, &root).unwrap();
}
#[test]
fn four_element_tree_index_2() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 2);
verify_merkle_inclusion(b"c", 2, 4, &proof, &root).unwrap();
}
#[test]
fn four_element_tree_index_3() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 3);
verify_merkle_inclusion(b"d", 3, 4, &proof, &root).unwrap();
}
#[test]
fn three_element_tree_rightmost_leaf() {
let leaves: &[&[u8]] = &[b"x", b"y", b"z"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 2);
verify_merkle_inclusion(b"z", 2, 3, &proof, &root).unwrap();
}
#[test]
fn five_element_tree_all_positions() {
let leaves: &[&[u8]] = &[b"v0", b"v1", b"v2", b"v3", b"v4"];
let root = root_from_leaves(leaves);
for (i, leaf) in leaves.iter().enumerate() {
let (_, proof) = build_tree_and_proof(leaves, i);
verify_merkle_inclusion(leaf, i as u64, 5, &proof, &root).unwrap();
}
}
#[test]
fn seven_element_tree_all_positions() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d", b"e", b"f", b"g"];
let root = root_from_leaves(leaves);
for (i, leaf) in leaves.iter().enumerate() {
let (_, proof) = build_tree_and_proof(leaves, i);
verify_merkle_inclusion(leaf, i as u64, 7, &proof, &root).unwrap();
}
}
#[test]
fn error_tree_size_zero() {
let err = verify_merkle_inclusion(b"event", 0, 0, &[], &[0u8; 32]).unwrap_err();
assert!(matches!(err, ScittError::InvalidMerkleProof(_)));
assert!(err.to_string().contains("tree_size"));
}
#[test]
fn error_leaf_index_equals_tree_size() {
let event = b"event";
let root = compute_leaf_hash(event);
let err = verify_merkle_inclusion(event, 1, 1, &[], &root).unwrap_err();
assert!(matches!(err, ScittError::InvalidMerkleProof(_)));
assert!(err.to_string().contains("leaf_index"));
}
#[test]
fn error_leaf_index_exceeds_tree_size() {
let event = b"event";
let root = compute_leaf_hash(event);
let err = verify_merkle_inclusion(event, 100, 5, &[], &root).unwrap_err();
assert!(matches!(err, ScittError::InvalidMerkleProof(_)));
}
#[test]
fn error_hash_path_too_long() {
let path = vec![[0u8; 32]; MAX_HASH_PATH_LEN + 1];
let err = verify_merkle_inclusion(b"event", 0, u64::MAX, &path, &[0u8; 32]).unwrap_err();
assert!(matches!(err, ScittError::InvalidMerkleProof(_)));
assert!(err.to_string().contains("64"));
}
#[test]
fn error_hash_path_exactly_max_allowed() {
let path = vec![[0u8; 32]; MAX_HASH_PATH_LEN];
let err = verify_merkle_inclusion(b"event", 0, u64::MAX, &path, &[0u8; 32]).unwrap_err();
assert!(
matches!(err, ScittError::InvalidMerkleProof(_)),
"expected InvalidMerkleProof for truncated path, got: {err:?}"
);
}
#[test]
fn error_truncated_path_rejected() {
let event = b"attacker event";
let leaf = compute_leaf_hash(event);
let err = verify_merkle_inclusion(event, 5, 1000, &[], &leaf).unwrap_err();
assert!(
matches!(err, ScittError::InvalidMerkleProof(_)),
"expected InvalidMerkleProof for truncated path, got: {err:?}"
);
assert!(err.to_string().contains("incomplete inclusion path"));
}
#[test]
fn error_partially_truncated_path_rejected() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
assert!(
proof.len() >= 2,
"need at least 2 path elements for this test"
);
let truncated = &proof[..1];
let err = verify_merkle_inclusion(b"a", 0, 4, truncated, &root).unwrap_err();
assert!(
matches!(err, ScittError::InvalidMerkleProof(_)),
"expected InvalidMerkleProof for partial truncation, got: {err:?}"
);
}
#[test]
fn tamper_wrong_root_hash() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let mut root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
root[0] ^= 0x01;
let err = verify_merkle_inclusion(b"a", 0, 4, &proof, &root).unwrap_err();
assert!(matches!(err, ScittError::MerkleRootMismatch));
}
#[test]
fn tamper_path_element() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, mut proof) = build_tree_and_proof(leaves, 0);
proof[0][15] ^= 0xff;
let err = verify_merkle_inclusion(b"a", 0, 4, &proof, &root).unwrap_err();
assert!(matches!(err, ScittError::MerkleRootMismatch));
}
#[test]
fn tamper_event_bytes() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
let err = verify_merkle_inclusion(b"TAMPERED", 0, 4, &proof, &root).unwrap_err();
assert!(matches!(err, ScittError::MerkleRootMismatch));
}
#[test]
fn tamper_wrong_leaf_index() {
let leaves: &[&[u8]] = &[b"a", b"b", b"c", b"d"];
let root = root_from_leaves(leaves);
let (_, proof) = build_tree_and_proof(leaves, 0);
let err = verify_merkle_inclusion(b"a", 1, 4, &proof, &root).unwrap_err();
assert!(matches!(err, ScittError::MerkleRootMismatch));
}
}