mod error;
mod proof;
pub mod shape;
mod tree;
pub use error::{Error, Result};
pub use shape::{rebalanced_bag, rebalanced_skeleton};
pub use spine::{Hasher, LeafProof, ProofStep, Seal, verify_inclusion};
pub use tree::{Cmt, Config};
#[cfg(test)]
mod tests {
use sha2::{Digest, Sha256};
use super::*;
#[derive(Debug)]
struct Sha256Hasher;
impl spine::Hasher for Sha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for child in children {
h.update(child);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(b"").to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn clone_box(&self) -> Box<dyn spine::Hasher> {
Box::new(Sha256Hasher)
}
}
const ALG: u64 = 0;
const K: u64 = 2;
fn tree_with(payloads: &[&[u8]]) -> Cmt {
let mut t = Cmt::new(Config { arity: K }).expect("valid arity");
t.register_algorithm(ALG, Box::new(Sha256Hasher))
.expect("fresh alg");
for (i, p) in payloads.iter().enumerate() {
t.set(i as u64, p.to_vec(), Vec::new()).expect("dense set");
}
t
}
#[test]
fn empty_tree_has_no_root() {
let t = Cmt::new(Config::default()).unwrap();
assert!(t.is_empty());
assert_eq!(t.root(ALG), None);
}
#[test]
fn rejects_out_of_range_arity() {
assert_eq!(
Cmt::new(Config { arity: 1 }).unwrap_err(),
Error::InvalidArity(1)
);
assert_eq!(
Cmt::new(Config { arity: 257 }).unwrap_err(),
Error::InvalidArity(257)
);
}
#[test]
fn set_get_round_trips_and_overwrites() {
let mut t = tree_with(&[b"a", b"b", b"c"]);
assert_eq!(t.get(1), Some(b"b".as_slice()));
t.set(1, b"B".to_vec(), Vec::new()).unwrap();
assert_eq!(t.get(1), Some(b"B".as_slice()));
assert_eq!(t.len(), 3);
}
#[test]
fn set_rejects_a_gap() {
let mut t = tree_with(&[b"a"]);
assert_eq!(
t.set(5, b"x".to_vec(), Vec::new()).unwrap_err(),
Error::IndexGap { index: 5, len: 1 }
);
}
#[test]
fn metadata_is_carried_verbatim_and_uninterpreted() {
let mut t = tree_with(&[b"a"]);
let meta = vec![0xDE, 0xAD, 0xBE, 0xEF];
t.set(0, b"a".to_vec(), meta.clone()).unwrap();
assert_eq!(t.metadata(0), Some(meta.as_slice()));
let r_with_meta = t.root(ALG).unwrap();
let mut t2 = tree_with(&[b"a"]);
t2.set(0, b"a".to_vec(), vec![0x00]).unwrap();
assert_eq!(t2.root(ALG).unwrap(), r_with_meta);
}
#[test]
fn root_matches_spine_evaluate() {
for size in 1u64..=20 {
let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("p{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
let t = tree_with(&refs);
let expected = spine_root(&payloads);
assert_eq!(t.root(ALG).unwrap(), expected, "size={size}");
}
}
fn spine_root(payloads: &[Vec<u8>]) -> Vec<u8> {
use spine::{Subtree, frontier_for_size};
let h = Sha256Hasher;
let leaves: Vec<Subtree> = payloads.iter().map(|p| Subtree::Leaf(p.clone())).collect();
let coords = frontier_for_size(payloads.len() as u64, K);
let mut frontier: Vec<Subtree> = coords
.iter()
.map(|&(left, height)| perfect(&leaves, left, height))
.collect();
let k = K as usize;
while frontier.len() > k {
let split = frontier.len() - k;
let group = frontier.split_off(split);
frontier.push(Subtree::Node(group));
}
let shape = if frontier.len() == 1 {
frontier.pop().unwrap()
} else {
Subtree::Node(frontier)
};
spine::evaluate(&h, &shape)
}
fn perfect(leaves: &[spine::Subtree], left: u64, height: u32) -> spine::Subtree {
use spine::Subtree;
if height == 0 {
return leaves[left as usize].clone();
}
let span = K.pow(height - 1);
let children = (0..K)
.map(|c| perfect(leaves, left + c * span, height - 1))
.collect();
Subtree::Node(children)
}
#[test]
fn inclusion_proof_verifies_against_spine() {
for size in 1u64..=20 {
let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("v{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
let t = tree_with(&refs);
let root = t.root(ALG).unwrap();
let h = Sha256Hasher;
for index in 0..size {
let (leaf, path) = t.inclusion_proof(ALG, index).unwrap();
let skeleton = rebalanced_skeleton(size, K, index).expect("valid position");
assert!(
spine::verify_inclusion(&h, &leaf, &skeleton, &path, &root),
"size={size} index={index}"
);
}
}
}
#[test]
fn inclusion_proof_uses_cache_zero_misses() {
let size = 32u64;
let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("p{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
let t = tree_with(&refs);
for index in 0..size {
let (_, _, misses) = t
.inclusion_proof_miss_count(ALG, index)
.expect("in-range index");
assert_eq!(
misses, 0,
"inclusion_proof for index={index} size={size} had {misses} cache miss(es): \
off-path siblings must be served from the materialized cache (F4)"
);
}
}
#[test]
fn inclusion_proof_rejects_wrong_leaf() {
let t = tree_with(&[b"a", b"b", b"c", b"d"]);
let root = t.root(ALG).unwrap();
let h = Sha256Hasher;
let (_, path) = t.inclusion_proof(ALG, 2).unwrap();
let forged = h.leaf(b"not-c");
let skeleton = rebalanced_skeleton(4, K, 2).expect("valid position");
assert!(!spine::verify_inclusion(
&h, &forged, &skeleton, &path, &root
));
}
#[test]
fn non_membership_of_a_null_cell_verifies() {
let h = Sha256Hasher;
let mut t = tree_with(&[b"real", b"x", b"y"]);
t.set(1, b"null".to_vec(), Vec::new()).unwrap();
let root = t.root(ALG).unwrap();
let (leaf, path) = t.non_membership_proof(ALG, 1).expect("cell hashes to null");
assert_eq!(leaf, h.null());
let skeleton = rebalanced_skeleton(t.len(), K, 1).expect("valid position");
assert!(spine::verify_inclusion(&h, &leaf, &skeleton, &path, &root));
assert_eq!(t.non_membership_proof(ALG, 0), None);
}
#[test]
fn leaf_proof_accepts_legit_and_rejects_forged() {
let h = Sha256Hasher;
for size in 1u64..=20 {
let payloads: Vec<Vec<u8>> = (0..size).map(|i| format!("v{i}").into_bytes()).collect();
let refs: Vec<&[u8]> = payloads.iter().map(Vec::as_slice).collect();
let t = tree_with(&refs);
let root = t.root(ALG).unwrap();
for index in 0..size {
let proof = t.leaf_proof(ALG, index).expect("in range");
let skeleton = rebalanced_skeleton(size, K, index).expect("valid position");
assert!(
proof.verify(&h, &skeleton, &root),
"size={size} index={index}"
);
let mut forged = proof.clone();
forged.leaf_hash = h.leaf(b"forged");
assert!(
!forged.verify(&h, &skeleton, &root),
"size={size} index={index}"
);
}
}
let t = tree_with(&[b"a", b"b"]);
assert!(t.leaf_proof(ALG, 2).is_none());
assert!(t.leaf_proof(99, 0).is_none());
}
#[test]
fn seal_is_one_way_into_structural_seal() {
let t = tree_with(&[b"a", b"b", b"c"]);
let root = t.root(ALG).unwrap();
let sealed = t.seal().expect("non-empty seal");
assert_eq!(sealed.tree_size(), 3);
assert_eq!(sealed.arity(), 2);
assert!(sealed.peaks(ALG).is_some());
assert_eq!(
sealed.member_root(ALG, &Sha256Hasher, rebalanced_bag),
Some(root)
);
}
#[test]
fn empty_tree_cannot_be_sealed() {
let t = Cmt::new(Config::default()).unwrap();
assert_eq!(t.seal().unwrap_err(), Error::EmptySeal);
}
}