use sha2::{Digest, Sha256};
pub fn hash_leaf(data: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update([0x00]);
h.update(data);
h.finalize().into()
}
pub fn hash_children(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut h = Sha256::new();
h.update([0x01]);
h.update(left);
h.update(right);
h.finalize().into()
}
fn split(n: usize) -> usize {
debug_assert!(n >= 2);
let mut k = 1;
while k * 2 < n {
k *= 2;
}
k
}
pub fn tree_hash(d: &[Vec<u8>]) -> [u8; 32] {
match d.len() {
0 => Sha256::new().finalize().into(),
1 => hash_leaf(&d[0]),
n => {
let k = split(n);
hash_children(&tree_hash(&d[..k]), &tree_hash(&d[k..]))
}
}
}
pub fn inclusion_proof(d: &[Vec<u8>], m: usize) -> Option<Vec<[u8; 32]>> {
if m >= d.len() {
return None;
}
fn path(d: &[Vec<u8>], m: usize) -> Vec<[u8; 32]> {
let n = d.len();
if n == 1 {
return Vec::new();
}
let k = split(n);
if m < k {
let mut p = path(&d[..k], m);
p.push(tree_hash(&d[k..]));
p
} else {
let mut p = path(&d[k..], m - k);
p.push(tree_hash(&d[..k]));
p
}
}
Some(path(d, m))
}
pub fn consistency_proof(d: &[Vec<u8>], m: usize) -> Option<Vec<[u8; 32]>> {
if m == 0 || m > d.len() {
return None;
}
fn subproof(d: &[Vec<u8>], m: usize, b: bool) -> Vec<[u8; 32]> {
let n = d.len();
if m == n {
return if b { Vec::new() } else { vec![tree_hash(d)] };
}
let k = split(n);
if m <= k {
let mut p = subproof(&d[..k], m, b);
p.push(tree_hash(&d[k..]));
p
} else {
let mut p = subproof(&d[k..], m - k, false);
p.push(tree_hash(&d[..k]));
p
}
}
Some(subproof(d, m, true))
}
pub fn verify_inclusion(
leaf_index: u64,
tree_size: u64,
leaf_hash: &[u8; 32],
root: &[u8; 32],
path: &[[u8; 32]],
) -> bool {
if leaf_index >= tree_size {
return false;
}
let mut fn_ = leaf_index;
let mut sn = tree_size - 1;
let mut r = *leaf_hash;
for p in path {
if sn == 0 {
return false;
}
if (fn_ & 1) == 1 || fn_ == sn {
r = hash_children(p, &r);
if (fn_ & 1) == 0 {
while (fn_ & 1) == 0 && fn_ != 0 {
fn_ >>= 1;
sn >>= 1;
}
}
} else {
r = hash_children(&r, p);
}
fn_ >>= 1;
sn >>= 1;
}
sn == 0 && r == *root
}
pub fn verify_consistency(
first: u64,
second: u64,
first_root: &[u8; 32],
second_root: &[u8; 32],
path: &[[u8; 32]],
) -> bool {
if first == 0 || first > second {
return false;
}
if first == second {
return path.is_empty() && first_root == second_root;
}
let mut work: Vec<[u8; 32]> = Vec::with_capacity(path.len() + 1);
if first.is_power_of_two() {
work.push(*first_root);
}
work.extend_from_slice(path);
if work.is_empty() {
return false;
}
let mut fn_ = first - 1;
let mut sn = second - 1;
while (fn_ & 1) == 1 {
fn_ >>= 1;
sn >>= 1;
}
let mut fr = work[0];
let mut sr = work[0];
for c in &work[1..] {
if sn == 0 {
return false;
}
if (fn_ & 1) == 1 || fn_ == sn {
fr = hash_children(c, &fr);
sr = hash_children(c, &sr);
if (fn_ & 1) == 0 {
while (fn_ & 1) == 0 && fn_ != 0 {
fn_ >>= 1;
sn >>= 1;
}
}
} else {
sr = hash_children(&sr, c);
}
fn_ >>= 1;
sn >>= 1;
}
fr == *first_root && sr == *second_root && sn == 0
}
#[cfg(test)]
mod tests {
use super::*;
fn leaves(n: usize) -> Vec<Vec<u8>> {
(0..n).map(|i| vec![i as u8; 4]).collect()
}
#[test]
fn mth_structural_base_cases() {
assert_eq!(tree_hash(&[]), <[u8; 32]>::from(Sha256::new().finalize()));
let d = leaves(1);
assert_eq!(tree_hash(&d), hash_leaf(&d[0]));
let d = leaves(2);
assert_eq!(
tree_hash(&d),
hash_children(&hash_leaf(&d[0]), &hash_leaf(&d[1]))
);
let d = leaves(3);
let left = hash_children(&hash_leaf(&d[0]), &hash_leaf(&d[1]));
assert_eq!(tree_hash(&d), hash_children(&left, &hash_leaf(&d[2])));
}
#[test]
fn split_point_is_largest_power_of_two_below_n() {
assert_eq!(split(2), 1);
assert_eq!(split(3), 2);
assert_eq!(split(4), 2);
assert_eq!(split(5), 4);
assert_eq!(split(7), 4);
assert_eq!(split(8), 4);
assert_eq!(split(9), 8);
}
#[test]
fn inclusion_proof_round_trips_for_every_leaf_and_size() {
for n in 1..=33usize {
let d = leaves(n);
let root = tree_hash(&d);
for m in 0..n {
let path = inclusion_proof(&d, m).expect("valid index");
let lh = hash_leaf(&d[m]);
assert!(
verify_inclusion(m as u64, n as u64, &lh, &root, &path),
"inclusion must verify for n={n} m={m}"
);
}
assert!(inclusion_proof(&d, n).is_none(), "OOB index rejected");
}
}
#[test]
fn inclusion_proof_rejects_tampering() {
let d = leaves(11);
let root = tree_hash(&d);
let path = inclusion_proof(&d, 6).unwrap();
let lh = hash_leaf(&d[6]);
assert!(verify_inclusion(6, 11, &lh, &root, &path));
let wrong_leaf = hash_leaf(&d[5]);
assert!(!verify_inclusion(6, 11, &wrong_leaf, &root, &path));
assert!(!verify_inclusion(7, 11, &lh, &root, &path));
let mut bad_root = root;
bad_root[0] ^= 0xFF;
assert!(!verify_inclusion(6, 11, &lh, &bad_root, &path));
let mut bad_path = path.clone();
bad_path[0][0] ^= 0xFF;
assert!(!verify_inclusion(6, 11, &lh, &root, &bad_path));
}
#[test]
fn consistency_proof_round_trips_for_every_prefix_and_size() {
for n in 1..=33usize {
let d = leaves(n);
let second_root = tree_hash(&d);
for first in 1..=n {
let first_root = tree_hash(&d[..first]);
let path = consistency_proof(&d, first).expect("valid first");
assert!(
verify_consistency(first as u64, n as u64, &first_root, &second_root, &path),
"consistency must verify for first={first} second={n}"
);
}
}
}
#[test]
fn consistency_proof_rejects_a_divergent_or_truncated_log() {
let d = leaves(9);
let second_root = tree_hash(&d);
let first = 5usize;
let first_root = tree_hash(&d[..first]);
let path = consistency_proof(&d, first).unwrap();
assert!(verify_consistency(5, 9, &first_root, &second_root, &path));
let mut forged_first = first_root;
forged_first[0] ^= 0xFF;
assert!(!verify_consistency(
5,
9,
&forged_first,
&second_root,
&path
));
let mut forged_second = second_root;
forged_second[0] ^= 0xFF;
assert!(!verify_consistency(
5,
9,
&first_root,
&forged_second,
&path
));
let mut bad = path.clone();
if let Some(x) = bad.first_mut() {
x[0] ^= 0xFF;
}
assert!(!verify_consistency(5, 9, &first_root, &second_root, &bad));
}
#[test]
fn consistency_equal_size_needs_no_path_but_equal_roots() {
let d = leaves(6);
let root = tree_hash(&d);
assert!(verify_consistency(6, 6, &root, &root, &[]));
let mut other = root;
other[0] ^= 0xFF;
assert!(!verify_consistency(6, 6, &root, &other, &[]));
}
}