use crate::core::Digest;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LeafHash(Digest);
impl LeafHash {
#[must_use]
pub const fn digest(self) -> Digest {
self.0
}
}
const LEAF: u8 = 0x00;
const NODE: u8 = 0x01;
#[must_use]
pub fn leaf_hash(value: &Digest) -> LeafHash {
let mut bytes = Vec::with_capacity(33);
bytes.push(LEAF);
bytes.extend_from_slice(value.as_bytes());
LeafHash(Digest::of(&bytes))
}
fn node_hash(left: &Digest, right: &Digest) -> Digest {
let mut bytes = Vec::with_capacity(65);
bytes.push(NODE);
bytes.extend_from_slice(left.as_bytes());
bytes.extend_from_slice(right.as_bytes());
Digest::of(&bytes)
}
#[must_use]
pub fn root(leaves: &[LeafHash]) -> Digest {
if leaves.is_empty() {
return empty_root();
}
if leaves.len() == 1 {
return leaves[0].0;
}
let k = split_point(leaves.len());
let (l, r) = leaves.split_at(k);
node_hash(&root(l), &root(r))
}
#[must_use]
pub fn empty_root() -> Digest {
Digest::of(b"")
}
fn split_point(n: usize) -> usize {
debug_assert!(n > 1);
let mut k: usize = 1;
while k.checked_mul(2).is_some_and(|next| next < n) {
k *= 2;
}
k
}
#[must_use]
pub fn inclusion_proof(leaves: &[LeafHash], index: usize) -> Vec<Digest> {
if index >= leaves.len() {
return Vec::new();
}
let mut proof = Vec::new();
build_proof(leaves, index, &mut proof);
proof
}
fn build_proof(leaves: &[LeafHash], index: usize, out: &mut Vec<Digest>) {
if leaves.len() <= 1 {
return;
}
let k = split_point(leaves.len());
let (l, r) = leaves.split_at(k);
if index < k {
build_proof(l, index, out);
out.push(root(r));
} else {
build_proof(r, index - k, out);
out.push(root(l));
}
}
#[must_use]
pub fn verify_inclusion(
leaf: LeafHash,
index: usize,
size: usize,
proof: &[Digest],
expected: &Digest,
) -> bool {
if index >= size {
return false;
}
let mut went_left = Vec::new();
let mut idx = index;
let mut len = size;
while len > 1 {
let k = split_point(len);
if idx < k {
went_left.push(true);
len = k;
} else {
went_left.push(false);
idx -= k;
len -= k;
}
}
if went_left.len() != proof.len() {
return false;
}
let mut hash = leaf.digest();
for (sibling, left) in proof.iter().zip(went_left.iter().rev()) {
hash = if *left {
node_hash(&hash, sibling)
} else {
node_hash(sibling, &hash)
};
}
hash == *expected
}
#[must_use]
pub fn consistency_proof(leaves: &[LeafHash], old_size: usize) -> Vec<Digest> {
if old_size == 0 || old_size > leaves.len() {
return Vec::new();
}
let mut out = Vec::new();
subproof(old_size, leaves, true, &mut out);
out
}
fn subproof(m: usize, leaves: &[LeafHash], complete: bool, out: &mut Vec<Digest>) {
if m == leaves.len() {
if !complete {
out.push(root(leaves));
}
return;
}
let k = split_point(leaves.len());
let (l, r) = leaves.split_at(k);
if m <= k {
subproof(m, l, complete, out);
out.push(root(r));
} else {
subproof(m - k, r, false, out);
out.push(root(l));
}
}
#[must_use]
pub fn verify_consistency(
old_size: usize,
old_root: &Digest,
new_size: usize,
new_root: &Digest,
proof: &[Digest],
) -> bool {
if old_size > new_size {
return false;
}
if old_size == 0 {
return proof.is_empty() && *old_root == empty_root();
}
if old_size == new_size {
return proof.is_empty() && old_root == new_root;
}
let mut fed = 0;
let Some((old, new)) = rebuild(old_size, new_size, proof, &mut fed, true, old_root) else {
return false;
};
old == *old_root && new == *new_root && fed == proof.len()
}
fn rebuild(
m: usize,
n: usize,
proof: &[Digest],
fed: &mut usize,
complete: bool,
old_root: &Digest,
) -> Option<(Digest, Digest)> {
if m == n {
if complete {
return Some((*old_root, *old_root));
}
let h = *proof.get(*fed)?;
*fed += 1;
return Some((h, h));
}
let k = split_point(n);
if m <= k {
let (old, new_left) = rebuild(m, k, proof, fed, complete, old_root)?;
let right = *proof.get(*fed)?;
*fed += 1;
Some((old, node_hash(&new_left, &right)))
} else {
let (old_right, new_right) = rebuild(m - k, n - k, proof, fed, false, old_root)?;
let left = *proof.get(*fed)?;
*fed += 1;
Some((node_hash(&left, &old_right), node_hash(&left, &new_right)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn leaves(n: usize) -> Vec<LeafHash> {
(0..n)
.map(|i| leaf_hash(&Digest::of(&[u8::try_from(i).unwrap()])))
.collect()
}
#[test]
fn the_tree_matches_rfc_6962_computed_elsewhere() {
let a = Digest::of(b"a");
let b = Digest::of(b"b");
assert_eq!(
leaf_hash(&a).digest().to_hex(),
"a23bd5b06da9048238a65b3f1d9d0b9e15fae3dde262688e6489aa4c763d1820",
"the leaf hash left RFC 6962, and every published checkpoint with it"
);
assert_eq!(
root(&[leaf_hash(&a), leaf_hash(&b)]).to_hex(),
"ad5ca6cddc0b27c6a83e332bf28011769236e6c6a1f786ebf7b5267b37a5bd22",
"the interior hash left RFC 6962"
);
}
#[test]
fn a_raw_digest_is_not_a_leaf() {
let a = Digest::of(b"a");
assert_ne!(
leaf_hash(&a).digest(),
a,
"leaf_hash returned its input, so the prefix is not being applied"
);
}
#[test]
fn growth_from_the_empty_log_still_names_the_empty_root() {
let after = root(&leaves(3));
assert!(
verify_consistency(0, &empty_root(), 3, &after, &[]),
"the honest empty checkpoint must verify, or nothing can ever grow"
);
assert!(
!verify_consistency(0, &Digest::of(b"a root no log ever had"), 3, &after, &[]),
"size 0 was accepted beside a root that is not the empty log's — a \
checkpoint that never existed verified as the ancestor of one that does"
);
assert!(
!verify_consistency(0, &empty_root(), 3, &after, &[after]),
"a proof was accepted where there is nothing to prove"
);
}
#[test]
fn an_empty_log_hashes_the_way_rfc_6962_says() {
assert_eq!(
root(&[]).to_hex(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"the empty tree's root is SHA-256 of the empty string; it was thirty-two \
zero bytes, which no conforming verifier computes and which an \
uninitialised buffer produces by accident"
);
assert_ne!(
root(&[]),
Digest::ZERO,
"and it must not be the value a default-constructed struct carries"
);
}
#[test]
fn every_leaf_proves_its_own_inclusion() {
for n in 1..=17 {
let l = leaves(n);
let r = root(&l);
for i in 0..n {
let proof = inclusion_proof(&l, i);
assert!(
verify_inclusion(l[i], i, n, &proof, &r),
"leaf {i} of {n} failed to prove"
);
}
}
}
#[test]
fn removing_a_leaf_changes_the_root() {
let l = leaves(8);
let before = root(&l);
let mut without = l;
without.remove(3);
assert_ne!(
before,
root(&without),
"a run was deleted and the root did not move — which is the entire \
thing this is for"
);
}
#[test]
fn a_proof_does_not_transfer() {
let l = leaves(8);
let r = root(&l);
let proof = inclusion_proof(&l, 2);
assert!(!verify_inclusion(l[5], 5, 8, &proof, &r));
assert!(!verify_inclusion(l[2], 3, 8, &proof, &r));
}
#[test]
fn a_padded_proof_is_rejected() {
let l = leaves(8);
let r = root(&l);
let mut proof = inclusion_proof(&l, 2);
proof.push(Digest::ZERO);
assert!(
!verify_inclusion(l[2], 2, 8, &proof, &r),
"a proof with trailing junk verified, so any valid proof can be \
padded into a different-looking one"
);
}
#[test]
fn leaves_and_nodes_are_domain_separated() {
let a = Digest::of(b"a");
let b = Digest::of(b"b");
assert_ne!(
leaf_hash(&a).digest(),
Digest::of(a.as_bytes()),
"a leaf hash is a plain hash of its value, so the prefix is missing"
);
assert_ne!(node_hash(&a, &b), leaf_hash(&a).digest());
}
#[test]
fn growth_is_provably_append_only() {
for n in 1..=17usize {
let new = leaves(n);
let new_root = root(&new);
for m in 1..=n {
let old = &new[..m];
let old_root = root(old);
let proof = consistency_proof(&new, m);
assert!(
verify_consistency(m, &old_root, n, &new_root, &proof),
"a log of {m} growing to {n} could not prove it only appended"
);
}
}
}
#[test]
fn a_deletion_cannot_be_passed_off_as_growth() {
let original = leaves(8);
let old_root = root(&original);
let mut forked = original.clone();
forked.remove(3);
forked.push(leaf_hash(&Digest::of(b"new-a")));
forked.push(leaf_hash(&Digest::of(b"new-b")));
let forked_root = root(&forked);
assert!(forked.len() > original.len(), "the log did grow");
let attempted = consistency_proof(&forked, original.len());
assert!(
!verify_consistency(
original.len(),
&old_root,
forked.len(),
&forked_root,
&attempted
),
"a log with a run deleted from the middle passed as an append-only \
extension — which is the only thing a published checkpoint is for"
);
}
#[test]
fn a_forged_old_root_is_rejected() {
let l = leaves(9);
let new_root = root(&l);
let proof = consistency_proof(&l, 5);
let real = root(&l[..5]);
assert!(verify_consistency(5, &real, 9, &new_root, &proof));
let forged = Digest::of(b"not the old root");
assert!(
!verify_consistency(5, &forged, 9, &new_root, &proof),
"a consistency proof verified against an old root the log never had, \
which is how a fork is presented as an extension"
);
}
#[test]
fn a_consistency_proof_does_not_transfer() {
let l = leaves(9);
let r = root(&l);
let proof = consistency_proof(&l, 4);
assert!(!verify_consistency(5, &root(&l[..5]), 9, &r, &proof));
assert!(!verify_consistency(
4,
&root(&l[..4]),
8,
&root(&l[..8]),
&proof
));
}
#[test]
fn a_log_cannot_shrink() {
let l = leaves(8);
assert!(!verify_consistency(8, &root(&l), 4, &root(&l[..4]), &[]));
}
#[test]
fn a_padded_consistency_proof_is_rejected() {
let l = leaves(8);
let mut proof = consistency_proof(&l, 3);
proof.push(Digest::ZERO);
assert!(!verify_consistency(3, &root(&l[..3]), 8, &root(&l), &proof));
}
#[test]
fn the_empty_log_is_a_prefix_of_everything() {
let l = leaves(5);
assert!(verify_consistency(0, &empty_root(), 5, &root(&l), &[]));
assert!(!verify_consistency(
0,
&empty_root(),
5,
&root(&l),
&[Digest::ZERO]
));
}
#[test]
fn a_size_that_changes_the_shape_is_rejected() {
let l = leaves(8);
let r = root(&l);
let proof = inclusion_proof(&l, 2);
assert!(!verify_inclusion(l[2], 2, 9, &proof, &r));
assert!(!verify_inclusion(l[2], 2, 4, &proof, &r));
}
}