use spine::{Hasher, LeafProof};
use crate::Sealed;
use crate::binding_proof::TrustedBindingRoot;
use crate::root::{combined_root, committed_active_algs, validate_committed_epochs};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClaimedLeaf {
pub alg_id: u64,
pub leaf_proof: LeafProof,
}
impl ClaimedLeaf {
#[must_use]
pub fn new(alg_id: u64, leaf_proof: LeafProof) -> Self {
Self { alg_id, leaf_proof }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotProof {
member_roots: Vec<(u64, Vec<u8>)>,
alg_epochs: Vec<(u64, Vec<(u64, u64)>)>,
tree_size: u64,
arity: u64,
claims: Vec<ClaimedLeaf>,
}
impl SnapshotProof {
#[must_use]
pub fn produce(
sealed: &Sealed,
hashers: &[(u64, &dyn Hasher)],
bag: spine::BagFn,
claims: Vec<ClaimedLeaf>,
) -> Self {
Self {
member_roots: sealed.member_roots(hashers, bag),
alg_epochs: sealed.alg_epochs().to_vec(),
tree_size: sealed.tree_size(),
arity: sealed.arity(),
claims,
}
}
fn member_root(&self, alg_id: u64) -> Option<&[u8]> {
self.member_roots
.iter()
.find(|(id, _)| *id == alg_id)
.map(|(_, r)| r.as_slice())
}
fn recompute_head(&self, hasher: &dyn Hasher) -> Vec<u8> {
combined_root(
hasher,
&self.member_roots,
&self.alg_epochs,
self.tree_size,
self.arity,
)
}
#[must_use]
pub fn verify(
&self,
trusted: &[TrustedBindingRoot<'_>],
hashers: &[(u64, &dyn Hasher)],
skeleton: spine::SkeletonFn,
) -> bool {
if trusted.is_empty() {
return false;
}
if !validate_committed_epochs(&self.alg_epochs, self.tree_size) {
return false;
}
let derived = committed_active_algs(&self.alg_epochs, self.tree_size);
if derived.len() != self.member_roots.len()
|| derived
.iter()
.zip(self.member_roots.iter())
.any(|(&d, &(id, _))| d != id)
{
return false;
}
if self.member_roots.windows(2).any(|w| w[0].0 >= w[1].0) {
return false;
}
for t in trusted {
if self.member_root(t.alg_id).is_none() {
return false;
}
}
for t in trusted {
let head = self.recompute_head(t.hasher);
if !spine::constant_time_eq(&head, t.root) {
return false;
}
}
for claim in &self.claims {
let Some(member_root) = self.member_root(claim.alg_id) else {
return false;
};
let Some(&(_, hasher)) = hashers.iter().find(|(id, _)| *id == claim.alg_id) else {
return false;
};
let lp = &claim.leaf_proof;
let Some(sk) = skeleton(lp.arity, lp.tree_size, lp.index) else {
return false;
};
if !lp.verify(hasher, &sk, member_root) {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use sha2::{Digest, Sha256};
use spine::Hasher;
use super::*;
use crate::storage::MemoryStorage;
use crate::tree::{NaryMerkleLog, TreeConfig};
#[derive(Debug, Clone)]
struct Sha256Hasher;
impl 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 Hasher> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
struct PrefixedSha256Hasher;
impl PrefixedSha256Hasher {
const PREFIX: &'static [u8] = b"ALG1:";
}
impl Hasher for PrefixedSha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(Self::PREFIX);
h.update(data);
h.finalize().to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(Self::PREFIX);
for child in children {
h.update(child);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
let mut h = Sha256::new();
h.update(Self::PREFIX);
h.update(b"");
h.finalize().to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(Self::PREFIX);
h.update(data);
h.finalize().to_vec()
}
fn clone_box(&self) -> Box<dyn Hasher> {
Box::new(self.clone())
}
}
async fn log_with(n: u64, k: usize) -> NaryMerkleLog<MemoryStorage> {
let config = TreeConfig { arity: k as u64 };
let mut log = NaryMerkleLog::new(MemoryStorage::new(), Box::new(Sha256Hasher), config)
.await
.unwrap();
for i in 0..n {
log.append_leaf(format!("leaf-{i}").as_bytes())
.await
.unwrap();
}
log
}
async fn claims_for_all(log: &NaryMerkleLog<MemoryStorage>, n: u64) -> Vec<ClaimedLeaf> {
let mut claims = Vec::new();
for i in 0..n {
let lp = log.leaf_proof(i, n).await.unwrap().expect("leaf proof");
claims.push(ClaimedLeaf::new(0, lp));
}
claims
}
#[test]
fn valid_snapshot_verifies_across_sizes_and_arities() {
smol::block_on(async {
let h = Sha256Hasher;
for k in [2usize, 3, 4] {
for n in 1u64..18 {
let log = log_with(n, k).await;
let claims = claims_for_all(&log, n).await;
let br = log.combined_root_at(0, n).await.unwrap();
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof =
SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &br,
}];
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
assert!(
proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton),
"valid snapshot must verify (n={n}, k={k})"
);
}
}
});
}
#[test]
fn trust_rides_opaque_metadata_untouched() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(6, 2).await;
let claims = claims_for_all(&log, 6).await;
let br = log.combined_root_at(0, 6).await.unwrap();
let sealed = log
.seal_with_meta(spine::Meta::new(vec![0xAB; 64]))
.await
.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &br,
}];
assert!(proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton));
assert_eq!(
sealed.meta().map(spine::Meta::as_bytes),
Some([0xAB; 64].as_slice())
);
});
}
#[test]
fn forged_binding_root_is_rejected() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(7, 2).await;
let claims = claims_for_all(&log, 7).await;
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let forged = vec![0x00; 32];
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &forged,
}];
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
assert!(
!proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton),
"a head the member roots do not reconstruct must be rejected"
);
});
}
#[test]
fn forged_leaf_breaks_the_proof() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(8, 2).await;
let mut claims = claims_for_all(&log, 8).await;
let br = log.combined_root_at(0, 8).await.unwrap();
let sealed = log.seal().await.unwrap();
claims[3].leaf_proof.leaf_hash = h.leaf(b"forged-payload");
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &br,
}];
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
assert!(
!proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton),
"a forged leaf must break the aggregate proof"
);
});
}
#[test]
fn multi_algorithm_snapshot_verifies() {
smol::block_on(async {
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(MemoryStorage::new(), Box::new(Sha256Hasher), config)
.await
.unwrap();
log.add_algorithm(1, Box::new(PrefixedSha256Hasher))
.await
.unwrap();
for i in 0..4u64 {
log.append_leaf(&i.to_be_bytes()).await.unwrap();
}
let n = log.count();
let mut claims = Vec::new();
for &(alg, idx) in &[(0u64, 0u64), (0, 3), (1, 1), (1, 2)] {
let lp = log
.leaf_proof_for(alg, idx, n)
.await
.unwrap()
.expect("leaf proof");
claims.push(ClaimedLeaf::new(alg, lp));
}
let br0 = log.combined_root_at(0, n).await.unwrap();
let br1 = log.combined_root_at(1, n).await.unwrap();
let sealed = log.seal().await.unwrap();
let h0 = Sha256Hasher;
let h1 = PrefixedSha256Hasher;
let hashers: [(u64, &dyn Hasher); 2] = [(0, &h0), (1, &h1)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [
TrustedBindingRoot {
alg_id: 0,
hasher: &h0,
root: &br0,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h1,
root: &br1,
},
];
assert!(proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton));
let bad = [
TrustedBindingRoot {
alg_id: 0,
hasher: &h0,
root: &br0,
},
TrustedBindingRoot {
alg_id: 1,
hasher: &h0,
root: &br1,
},
];
assert!(!proof.verify(&bad, &hashers, cml::mountain::mountain_skeleton));
});
}
#[test]
fn empty_trusted_set_is_rejected() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(4, 2).await;
let claims = claims_for_all(&log, 4).await;
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
assert!(!proof.verify(&[], &hashers, cml::mountain::mountain_skeleton));
});
}
#[test]
fn trusted_head_for_unknown_algorithm_is_rejected() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(4, 2).await;
let claims = claims_for_all(&log, 4).await;
let br = log.combined_root_at(0, 4).await.unwrap();
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [TrustedBindingRoot {
alg_id: 9,
hasher: &h,
root: &br,
}];
let hashers: [(u64, &dyn Hasher); 1] = [(9, &h)];
assert!(!proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton));
});
}
#[test]
fn claim_with_missing_hasher_is_rejected() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(5, 2).await;
let claims = claims_for_all(&log, 5).await;
let br = log.combined_root_at(0, 5).await.unwrap();
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, claims);
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &br,
}];
let hashers: [(u64, &dyn Hasher); 0] = [];
assert!(!proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton));
});
}
#[test]
fn no_claims_still_binds_the_head() {
smol::block_on(async {
let h = Sha256Hasher;
let log = log_with(6, 2).await;
let br = log.combined_root_at(0, 6).await.unwrap();
let sealed = log.seal().await.unwrap();
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
let proof = SnapshotProof::produce(&sealed, &hashers, cml::mountain::bag_peaks, vec![]);
let trusted = [TrustedBindingRoot {
alg_id: 0,
hasher: &h,
root: &br,
}];
let hashers: [(u64, &dyn Hasher); 1] = [(0, &h)];
assert!(proof.verify(&trusted, &hashers, cml::mountain::mountain_skeleton));
});
}
}