use crate::hash::{HASH_LEN, Hash, Hasher, domain_digest, hash};
use crate::object::{ChunkedBlob, Tree, TreeEntry};
const CHUNKED_TYPE_DOMAIN: &[u8] = b"mkit.chunked\x00";
const TREE_TYPE_DOMAIN: &[u8] = b"mkit.tree\x00";
const CBLOB_META_DOMAIN: &[u8] = b"mkit-cblob-meta-v1";
const TREE_ENTRY_DOMAIN: &[u8] = b"mkit-tree-entry-v1";
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum MerkleError {
#[error("merkle position {0} is out of range")]
PositionOutOfRange(u32),
#[error("merkle proof is malformed")]
MalformedProof,
#[error("merkle proof verification failed")]
VerificationFailed,
}
fn u32_of(n: usize) -> u32 {
u32::try_from(n).expect("merkle leaf/index count fits u32 (objects capped at 1M)")
}
fn h2(a: &[u8], b: &[u8]) -> Hash {
let mut h = Hasher::new();
h.update(a).update(b);
h.finalize()
}
fn position_leaf(index: u32, leaf: &Hash) -> Hash {
h2(&index.to_be_bytes(), leaf)
}
fn bmt_root(leaves: &[Hash]) -> Hash {
let leaf_count = u32_of(leaves.len());
let mut level: Vec<Hash> = if leaves.is_empty() {
vec![hash(b"")]
} else {
leaves
.iter()
.enumerate()
.map(|(i, l)| position_leaf(u32_of(i), l))
.collect()
};
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for pair in level.chunks(2) {
let right = if pair.len() == 2 { &pair[1] } else { &pair[0] };
next.push(h2(&pair[0], right));
}
level = next;
}
h2(&leaf_count.to_be_bytes(), &level[0])
}
fn bmt_prove(leaves: &[Hash], mut idx: usize) -> Result<Vec<u8>, MerkleError> {
if idx >= leaves.len() {
return Err(MerkleError::PositionOutOfRange(u32_of(idx)));
}
let leaf_count = u32_of(leaves.len());
let mut level: Vec<Hash> = leaves
.iter()
.enumerate()
.map(|(i, l)| position_leaf(u32_of(i), l))
.collect();
let mut siblings: Vec<Hash> = Vec::new();
while level.len() > 1 {
let sib = if idx.is_multiple_of(2) {
if idx + 1 < level.len() {
level[idx + 1]
} else {
level[idx]
}
} else {
level[idx - 1]
};
siblings.push(sib);
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for pair in level.chunks(2) {
let right = if pair.len() == 2 { &pair[1] } else { &pair[0] };
next.push(h2(&pair[0], right));
}
idx /= 2;
level = next;
}
let mut out = Vec::with_capacity(8 + siblings.len() * HASH_LEN);
out.extend_from_slice(&leaf_count.to_le_bytes());
out.extend_from_slice(&u32_of(siblings.len()).to_le_bytes());
for s in &siblings {
out.extend_from_slice(s);
}
Ok(out)
}
fn bmt_verify(root: &Hash, leaf: &Hash, position: u32, proof: &[u8]) -> Result<(), MerkleError> {
if proof.len() < 8 {
return Err(MerkleError::MalformedProof);
}
let leaf_count = u32::from_le_bytes(proof[0..4].try_into().expect("4 bytes"));
let n = u32::from_le_bytes(proof[4..8].try_into().expect("4 bytes")) as usize;
if proof.len() != 8 + n * HASH_LEN {
return Err(MerkleError::MalformedProof);
}
if position >= leaf_count {
return Err(MerkleError::PositionOutOfRange(position));
}
let mut acc = position_leaf(position, leaf);
let mut idx = position as usize;
for k in 0..n {
let off = 8 + k * HASH_LEN;
let sib: Hash = proof[off..off + HASH_LEN].try_into().expect("32 bytes");
acc = if idx.is_multiple_of(2) {
h2(&acc, &sib)
} else {
h2(&sib, &acc)
};
idx /= 2;
}
let recomputed = h2(&leaf_count.to_be_bytes(), &acc);
if &recomputed == root {
Ok(())
} else {
Err(MerkleError::VerificationFailed)
}
}
fn chunked_meta_leaf(cb: &ChunkedBlob) -> Hash {
let mut body = [0u8; 12];
body[..8].copy_from_slice(&cb.total_size.to_le_bytes());
body[8..].copy_from_slice(&cb.chunk_size.to_le_bytes());
domain_digest(CBLOB_META_DOMAIN, &body)
}
fn tree_entry_leaf(e: &TreeEntry) -> Hash {
let mut body = Vec::with_capacity(4 + e.name.len() + 1 + HASH_LEN);
body.extend_from_slice(&u32_of(e.name.len()).to_le_bytes());
body.extend_from_slice(&e.name);
body.push(e.mode as u8);
body.extend_from_slice(&e.object_hash);
domain_digest(TREE_ENTRY_DOMAIN, &body)
}
fn chunked_leaves(cb: &ChunkedBlob) -> Vec<Hash> {
let mut leaves = Vec::with_capacity(1 + cb.chunks.len());
leaves.push(chunked_meta_leaf(cb));
leaves.extend_from_slice(&cb.chunks);
leaves
}
fn tree_leaves(tree: &Tree) -> Vec<Hash> {
tree.entries.iter().map(tree_entry_leaf).collect()
}
#[must_use]
pub fn chunked_inner_root(cb: &ChunkedBlob) -> Hash {
bmt_root(&chunked_leaves(cb))
}
#[must_use]
pub fn tree_inner_root(tree: &Tree) -> Hash {
bmt_root(&tree_leaves(tree))
}
#[must_use]
pub fn compute_chunked_id(cb: &ChunkedBlob) -> Hash {
domain_digest(CHUNKED_TYPE_DOMAIN, &chunked_inner_root(cb))
}
#[must_use]
pub fn compute_tree_id(tree: &Tree) -> Hash {
domain_digest(TREE_TYPE_DOMAIN, &tree_inner_root(tree))
}
pub const TREE_EMPTY_ID: Hash = [
0x1a, 0xb8, 0xd0, 0x78, 0x8b, 0x29, 0xfe, 0x59, 0x92, 0x01, 0x1e, 0x64, 0xd6, 0xc9, 0x22, 0xec,
0x93, 0xf4, 0x24, 0x8b, 0x37, 0x55, 0xb9, 0x2b, 0x15, 0xb0, 0x7e, 0x66, 0x4c, 0xb1, 0x56, 0x52,
];
#[must_use]
pub fn chunk_position(cb: &ChunkedBlob, chunk_hash: &Hash) -> Option<u32> {
cb.chunks
.iter()
.position(|c| c == chunk_hash)
.map(|i| u32_of(i + 1))
}
#[must_use]
pub fn tree_entry_position(tree: &Tree, name: &[u8]) -> Option<u32> {
tree.entries.iter().position(|e| e.name == name).map(u32_of)
}
pub fn build_chunk_inclusion_proof(
cb: &ChunkedBlob,
position: u32,
) -> Result<Vec<u8>, MerkleError> {
bmt_prove(&chunked_leaves(cb), position as usize)
}
pub fn verify_chunk_inclusion_proof(
inner_root: &Hash,
chunk_hash: &Hash,
position: u32,
proof: &[u8],
) -> Result<(), MerkleError> {
bmt_verify(inner_root, chunk_hash, position, proof)
}
pub fn build_tree_inclusion_proof(tree: &Tree, position: u32) -> Result<Vec<u8>, MerkleError> {
bmt_prove(&tree_leaves(tree), position as usize)
}
pub fn verify_tree_inclusion_proof(
inner_root: &Hash,
entry: &TreeEntry,
position: u32,
proof: &[u8],
) -> Result<(), MerkleError> {
bmt_verify(inner_root, &tree_entry_leaf(entry), position, proof)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::EntryMode;
fn cb(total: u64, chunk_size: u32, chunks: &[u8]) -> ChunkedBlob {
ChunkedBlob {
total_size: total,
chunk_size,
chunks: chunks.iter().map(|b| [*b; 32]).collect(),
}
}
fn entry(name: &[u8], mode: EntryMode, h: u8) -> TreeEntry {
TreeEntry {
name: name.to_vec(),
mode,
object_hash: [h; 32],
}
}
fn tree(entries: Vec<TreeEntry>) -> Tree {
Tree { entries }
}
#[test]
fn id_changes_when_a_leaf_changes() {
let a = cb(100, 0, &[1, 2, 3]);
let b = cb(100, 0, &[1, 2, 4]);
assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
}
#[test]
fn id_changes_when_leaf_count_changes() {
let a = cb(100, 0, &[1, 2, 3]);
let b = cb(100, 0, &[1, 2, 3, 3]); assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
}
#[test]
fn chunked_id_changes_when_metadata_changes() {
let a = cb(100, 0, &[1, 2, 3]);
let b = cb(101, 0, &[1, 2, 3]);
let c = cb(100, 64, &[1, 2, 3]);
assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
assert_ne!(compute_chunked_id(&a), compute_chunked_id(&c));
}
#[test]
fn tree_ordering_matters() {
let a = tree(vec![
entry(b"a", EntryMode::Blob, 1),
entry(b"b", EntryMode::Blob, 2),
]);
let b = tree(vec![
entry(b"b", EntryMode::Blob, 2),
entry(b"a", EntryMode::Blob, 1),
]);
assert_ne!(compute_tree_id(&a), compute_tree_id(&b));
}
#[test]
fn empty_tree_id_matches_constant() {
let got = compute_tree_id(&tree(vec![]));
assert_eq!(got, TREE_EMPTY_ID, "update TREE_EMPTY_ID to {got:02x?}");
}
#[test]
fn type_binding_no_cross_collisions() {
let empty_tree = compute_tree_id(&tree(vec![]));
let empty_cblob = compute_chunked_id(&cb(0, 0, &[]));
assert_ne!(empty_tree, empty_cblob);
let t = tree(vec![entry(b"x", EntryMode::Blob, 9)]);
let c = cb(10, 0, &[9]);
assert_ne!(compute_tree_id(&t), compute_chunked_id(&c));
}
#[test]
fn id_ne_flat_blake3_of_serialized_bytes() {
let c = cb(100, 0, &[1, 2, 3]);
let serialized =
crate::serialize::serialize(&crate::object::Object::ChunkedBlob(c.clone())).unwrap();
assert_ne!(compute_chunked_id(&c), crate::hash::hash(&serialized));
}
#[test]
fn chunk_position_offsets_by_one() {
let c = cb(100, 0, &[7, 8, 9]);
assert_eq!(chunk_position(&c, &[7; 32]), Some(1));
assert_eq!(chunk_position(&c, &[9; 32]), Some(3));
assert_eq!(chunk_position(&c, &[0; 32]), None);
}
#[test]
fn chunk_inclusion_proof_round_trips() {
let c = cb(100, 0, &[10, 20, 30, 40]);
let root = chunked_inner_root(&c);
for (idx, byte) in [(0usize, 10u8), (2, 30), (3, 40)] {
let pos = chunk_position(&c, &[byte; 32]).unwrap();
assert_eq!(pos, u32_of(idx) + 1);
let proof = build_chunk_inclusion_proof(&c, pos).unwrap();
verify_chunk_inclusion_proof(&root, &[byte; 32], pos, &proof).unwrap();
assert!(verify_chunk_inclusion_proof(&root, &[0xFF; 32], pos, &proof).is_err());
}
}
#[test]
fn tree_inclusion_proof_round_trips() {
let t = tree(vec![
entry(b"a", EntryMode::Blob, 1),
entry(b"b", EntryMode::Tree, 2),
entry(b"c", EntryMode::Executable, 3),
]);
let root = tree_inner_root(&t);
let pos = tree_entry_position(&t, b"b").unwrap();
assert_eq!(pos, 1);
let proof = build_tree_inclusion_proof(&t, pos).unwrap();
verify_tree_inclusion_proof(&root, &t.entries[1], pos, &proof).unwrap();
let wrong = entry(b"b", EntryMode::Blob, 2);
assert!(verify_tree_inclusion_proof(&root, &wrong, pos, &proof).is_err());
}
#[test]
fn out_of_range_position_rejected() {
let c = cb(10, 0, &[1]);
assert_eq!(
build_chunk_inclusion_proof(&c, 2),
Err(MerkleError::PositionOutOfRange(2))
);
}
#[test]
fn vendored_root_matches_commonware() {
use commonware_cryptography::blake3::{Blake3, Digest};
use commonware_storage::bmt::Builder;
for n in [1usize, 2, 3, 4, 5, 7, 8, 9, 16, 33] {
let leaves: Vec<Hash> = (0..n)
.map(|i| hash(&[u8::try_from(i % 256).unwrap(); 4]))
.collect();
let mut builder = Builder::<Blake3>::new(n);
for l in &leaves {
builder.add(&Digest(*l));
}
let cw_root = builder.build().root().0;
let ours = bmt_root(&leaves);
assert_eq!(ours, cw_root, "vendored BMT root diverged at n={n}");
}
}
}