use sha2::{Digest, Sha256};
pub type Hash = [u8; 32];
const LEAF_PREFIX: u8 = 0x00;
const NODE_PREFIX: u8 = 0x01;
pub fn leaf_hash(record: &[u8]) -> Hash {
let mut h = Sha256::new();
h.update([LEAF_PREFIX]);
h.update(record);
h.finalize().into()
}
pub fn node_hash(left: &Hash, right: &Hash) -> Hash {
let mut h = Sha256::new();
h.update([NODE_PREFIX]);
h.update(left);
h.update(right);
h.finalize().into()
}
pub fn empty_root() -> Hash {
Sha256::new().finalize().into()
}
fn split(n: usize) -> usize {
debug_assert!(n >= 2);
let mut k = 1usize;
while k << 1 < n {
k <<= 1;
}
k
}
pub fn mth(leaves: &[Hash]) -> Hash {
match leaves.len() {
0 => empty_root(),
1 => leaves[0],
n => {
let k = split(n);
node_hash(&mth(&leaves[..k]), &mth(&leaves[k..]))
}
}
}
pub fn inclusion_proof(leaves: &[Hash], m: usize) -> Vec<Hash> {
let n = leaves.len();
assert!(m < n, "leaf index {m} out of range for tree size {n}");
if n == 1 {
return Vec::new();
}
let k = split(n);
if m < k {
let mut path = inclusion_proof(&leaves[..k], m);
path.push(mth(&leaves[k..]));
path
} else {
let mut path = inclusion_proof(&leaves[k..], m - k);
path.push(mth(&leaves[..k]));
path
}
}
pub fn consistency_proof(leaves: &[Hash], m: usize) -> Vec<Hash> {
let n = leaves.len();
assert!(m <= n, "prior size {m} exceeds tree size {n}");
if m == 0 || m == n {
return Vec::new();
}
subproof(m, leaves, true)
}
fn subproof(m: usize, leaves: &[Hash], b: bool) -> Vec<Hash> {
let n = leaves.len();
if m == n {
return if b { Vec::new() } else { vec![mth(leaves)] };
}
let k = split(n);
if m <= k {
let mut p = subproof(m, &leaves[..k], b);
p.push(mth(&leaves[k..]));
p
} else {
let mut p = subproof(m - k, &leaves[k..], false);
p.push(mth(&leaves[..k]));
p
}
}
pub fn verify_inclusion(
leaf: &Hash,
leaf_index: usize,
tree_size: usize,
proof: &[Hash],
root: &Hash,
) -> bool {
if leaf_index >= tree_size {
return false;
}
let mut f_n = leaf_index;
let mut s_n = tree_size - 1;
let mut r = *leaf;
for p in proof {
if s_n == 0 {
return false; }
if f_n & 1 == 1 || f_n == s_n {
r = node_hash(p, &r);
if f_n & 1 == 0 {
loop {
f_n >>= 1;
s_n >>= 1;
if f_n & 1 == 1 || f_n == 0 {
break;
}
}
}
} else {
r = node_hash(&r, p);
}
f_n >>= 1;
s_n >>= 1;
}
s_n == 0 && r == *root
}
pub fn verify_consistency(
first_size: usize,
second_size: usize,
first_root: &Hash,
second_root: &Hash,
proof: &[Hash],
) -> bool {
if first_size > second_size {
return false;
}
if first_size == second_size {
return proof.is_empty() && first_root == second_root;
}
if first_size == 0 {
return proof.is_empty();
}
let mut path: Vec<Hash> = Vec::with_capacity(proof.len() + 1);
if first_size & (first_size - 1) == 0 {
path.push(*first_root);
}
path.extend_from_slice(proof);
if path.is_empty() {
return false;
}
let mut f_n = first_size - 1;
let mut s_n = second_size - 1;
while f_n & 1 == 1 {
f_n >>= 1;
s_n >>= 1;
}
let mut iter = path.iter();
let seed = *iter.next().unwrap();
let mut f_r = seed;
let mut s_r = seed;
for c in iter {
if s_n == 0 {
return false;
}
if f_n & 1 == 1 || f_n == s_n {
f_r = node_hash(c, &f_r);
s_r = node_hash(c, &s_r);
if f_n & 1 == 0 {
loop {
f_n >>= 1;
s_n >>= 1;
if f_n & 1 == 1 || f_n == 0 {
break;
}
}
}
} else {
s_r = node_hash(&s_r, c);
}
f_n >>= 1;
s_n >>= 1;
}
f_n == 0 && f_r == *first_root && s_r == *second_root
}
#[derive(Debug, Clone, Default)]
pub struct Roots {
peaks: Vec<(u32, Hash)>,
size: u64,
}
impl Roots {
pub fn new() -> Self {
Self::default()
}
pub fn from_leaves(leaves: &[Hash]) -> Self {
let mut r = Self::new();
for leaf in leaves {
r.push(*leaf);
}
r
}
pub fn size(&self) -> u64 {
self.size
}
pub fn push(&mut self, leaf: Hash) {
let mut carry = (0u32, leaf);
while let Some(&(h, _)) = self.peaks.last() {
if h != carry.0 {
break;
}
let (_, left) = self.peaks.pop().unwrap();
carry = (carry.0 + 1, node_hash(&left, &carry.1));
}
self.peaks.push(carry);
self.size += 1;
}
pub fn root(&self) -> Hash {
match self.peaks.split_last() {
None => empty_root(),
Some((&(_, last), rest)) => {
let mut acc = last;
for &(_, h) in rest.iter().rev() {
acc = node_hash(&h, &acc);
}
acc
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaves(n: usize) -> Vec<Hash> {
(0..n)
.map(|i| leaf_hash(format!("rec-{i}").as_bytes()))
.collect()
}
#[test]
fn incremental_root_matches_reference_mth() {
for n in 0..=300usize {
let ls = leaves(n);
let inc = Roots::from_leaves(&ls).root();
assert_eq!(inc, mth(&ls), "root mismatch at n={n}");
}
}
#[test]
fn inclusion_proofs_verify_for_every_leaf() {
for n in 1..=130usize {
let ls = leaves(n);
let root = mth(&ls);
for m in 0..n {
let proof = inclusion_proof(&ls, m);
assert!(
verify_inclusion(&ls[m], m, n, &proof, &root),
"inclusion failed n={n} m={m}"
);
let bad = leaf_hash(b"forged");
assert!(
!verify_inclusion(&bad, m, n, &proof, &root),
"forged leaf accepted n={n} m={m}"
);
}
}
}
#[test]
fn consistency_proofs_verify_for_every_prefix() {
for n in 1..=130usize {
let ls = leaves(n);
let second_root = mth(&ls);
for m in 0..=n {
let first_root = mth(&ls[..m]);
let proof = consistency_proof(&ls, m);
assert!(
verify_consistency(m, n, &first_root, &second_root, &proof),
"consistency failed m={m} n={n}"
);
if m > 0 {
let mut forged = first_root;
forged[0] ^= 0xff;
assert!(
!verify_consistency(m, n, &forged, &second_root, &proof),
"forged first_root accepted m={m} n={n}"
);
}
}
}
}
#[test]
fn tampered_consistency_path_is_rejected() {
let ls = leaves(50);
let second_root = mth(&ls);
let first_root = mth(&ls[..21]);
let mut proof = consistency_proof(&ls, 21);
assert!(verify_consistency(
21,
50,
&first_root,
&second_root,
&proof
));
proof[0][0] ^= 0x01;
assert!(!verify_consistency(
21,
50,
&first_root,
&second_root,
&proof
));
}
}