1use std::collections::BTreeSet;
96
97use bytes::{Buf, BufMut};
98use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt, ReadRangeExt, Write};
99
100use crate::hash::{HASH_LEN, Hash, Hasher, domain_digest, hash};
101use crate::object::{ChunkedBlob, Tree, TreeEntry};
102
103const CHUNKED_TYPE_DOMAIN: &[u8] = b"mkit.chunked\x00";
105const TREE_TYPE_DOMAIN: &[u8] = b"mkit.tree\x00";
107const CBLOB_META_DOMAIN: &[u8] = b"mkit-cblob-meta-v1";
109const TREE_ENTRY_DOMAIN: &[u8] = b"mkit-tree-entry-v1";
111
112pub const MAX_LEVELS: usize = u32::BITS as usize;
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum ObjectKind {
124 Tree,
126 ChunkedBlob,
128}
129
130impl ObjectKind {
131 fn type_domain(self) -> &'static [u8] {
132 match self {
133 Self::Tree => TREE_TYPE_DOMAIN,
134 Self::ChunkedBlob => CHUNKED_TYPE_DOMAIN,
135 }
136 }
137}
138
139#[must_use]
145pub fn wrap_id(kind: ObjectKind, inner_root: &Hash) -> Hash {
146 domain_digest(kind.type_domain(), inner_root)
147}
148
149#[derive(Debug, thiserror::Error, PartialEq, Eq)]
151pub enum MerkleError {
152 #[error("merkle position {0} is out of range")]
154 PositionOutOfRange(u32),
155 #[error("merkle position {0} is duplicated")]
157 DuplicatePosition(u32),
158 #[error("no merkle positions given")]
160 NoPositions,
161 #[error("merkle range start {start} is greater than end {end}")]
163 InvalidRange {
164 start: u32,
166 end: u32,
168 },
169 #[error("merkle proof is malformed")]
171 MalformedProof,
172 #[error("merkle proof is unaligned with the requested position(s)")]
175 UnalignedProof,
176 #[error("merkle proof verification failed")]
178 VerificationFailed,
179}
180
181impl From<CodecError> for MerkleError {
182 fn from(_: CodecError) -> Self {
183 Self::MalformedProof
184 }
185}
186
187fn u32_of(n: usize) -> u32 {
195 u32::try_from(n).expect("merkle leaf/index count fits u32 (objects capped at 1M)")
196}
197
198fn h2(a: &[u8], b: &[u8]) -> Hash {
200 let mut h = Hasher::new();
201 h.update(a).update(b);
202 h.finalize()
203}
204
205fn position_leaf(index: u32, leaf: &Hash) -> Hash {
208 h2(&index.to_be_bytes(), leaf)
209}
210
211fn levels_in_tree(leaf_count: u32) -> usize {
216 (u32::BITS - leaf_count.saturating_sub(1).leading_zeros() + 1) as usize
217}
218
219struct BmtTree {
224 leaf_count: u32,
227 empty: bool,
229 levels: Vec<Vec<Hash>>,
233 root: Hash,
235}
236
237fn build_bmt(leaves: &[Hash]) -> BmtTree {
238 let leaf_count = u32_of(leaves.len());
239 let empty = leaves.is_empty();
240 let position_hashed: Vec<Hash> = if empty {
241 vec![hash(b"")]
242 } else {
243 leaves
244 .iter()
245 .enumerate()
246 .map(|(i, l)| position_leaf(u32_of(i), l))
247 .collect()
248 };
249 let mut levels = vec![position_hashed];
250 while levels.last().expect("levels is never empty").len() > 1 {
251 let cur = levels.last().expect("levels is never empty");
252 let mut next = Vec::with_capacity(cur.len().div_ceil(2));
253 for pair in cur.chunks(2) {
254 let right = if pair.len() == 2 { &pair[1] } else { &pair[0] };
255 next.push(h2(&pair[0], right));
256 }
257 levels.push(next);
258 }
259 let tree_root = levels.last().expect("levels is never empty")[0];
260 let root = h2(&leaf_count.to_be_bytes(), &tree_root);
261 BmtTree {
262 leaf_count,
263 empty,
264 levels,
265 root,
266 }
267}
268
269fn siblings_required_for_multi_proof(
277 leaf_count: u32,
278 positions: impl IntoIterator<Item = u32>,
279) -> Result<BTreeSet<(usize, usize)>, MerkleError> {
280 let mut current = BTreeSet::new();
281 for pos in positions {
282 if pos >= leaf_count {
283 return Err(MerkleError::PositionOutOfRange(pos));
284 }
285 if !current.insert(pos as usize) {
286 return Err(MerkleError::DuplicatePosition(pos));
287 }
288 }
289 if current.is_empty() {
290 return Err(MerkleError::NoPositions);
291 }
292
293 let mut sibling_positions = BTreeSet::new();
294 let levels_count = levels_in_tree(leaf_count);
295 let mut level_size = leaf_count as usize;
296 for level in 0..levels_count.saturating_sub(1) {
297 for &index in ¤t {
298 let sibling_index = if index.is_multiple_of(2) {
299 if index + 1 < level_size {
300 index + 1
301 } else {
302 index
303 }
304 } else {
305 index - 1
306 };
307 if sibling_index != index && !current.contains(&sibling_index) {
308 sibling_positions.insert((level, sibling_index));
309 }
310 }
311 current = current.iter().map(|idx| idx / 2).collect();
312 level_size = level_size.div_ceil(2);
313 }
314 Ok(sibling_positions)
315}
316
317fn siblings_required_for_range_proof(
322 leaf_count: u32,
323 start: u32,
324 end: u32,
325) -> Result<BTreeSet<(usize, usize)>, MerkleError> {
326 if leaf_count == 0 {
327 return Err(MerkleError::NoPositions);
328 }
329 if start > end {
330 return Err(MerkleError::InvalidRange { start, end });
331 }
332 if start >= leaf_count {
333 return Err(MerkleError::PositionOutOfRange(start));
334 }
335 if end >= leaf_count {
336 return Err(MerkleError::PositionOutOfRange(end));
337 }
338
339 let mut sibling_positions = BTreeSet::new();
340 let levels_count = levels_in_tree(leaf_count);
341 let mut level_start = start as usize;
342 let mut level_end = end as usize;
343 let mut level_size = leaf_count as usize;
344 for level in 0..levels_count.saturating_sub(1) {
345 if !level_start.is_multiple_of(2) {
346 sibling_positions.insert((level, level_start - 1));
347 }
348 if level_end.is_multiple_of(2) {
349 let right = level_end + 1;
350 if right < level_size {
351 sibling_positions.insert((level, right));
352 }
353 }
354 level_start /= 2;
355 level_end /= 2;
356 level_size = level_size.div_ceil(2);
357 }
358 Ok(sibling_positions)
359}
360
361impl BmtTree {
362 fn proof(&self, position: u32) -> Result<Proof, MerkleError> {
365 self.multi_proof(core::iter::once(position))
366 }
367
368 fn range_proof(&self, start: u32, end: u32) -> Result<Proof, MerkleError> {
370 if self.empty {
377 return Err(MerkleError::PositionOutOfRange(start));
378 }
379 if start > end {
380 return Err(MerkleError::InvalidRange { start, end });
381 }
382 let leaf_count = self.leaf_count;
383 if start >= leaf_count {
384 return Err(MerkleError::PositionOutOfRange(start));
385 }
386 if end >= leaf_count {
387 return Err(MerkleError::PositionOutOfRange(end));
388 }
389 let sibling_positions = siblings_required_for_range_proof(leaf_count, start, end)?;
390 let siblings = sibling_positions
391 .iter()
392 .map(|&(level, index)| self.levels[level][index])
393 .collect();
394 Ok(Proof {
395 leaf_count,
396 siblings,
397 })
398 }
399
400 fn multi_proof(&self, positions: impl IntoIterator<Item = u32>) -> Result<Proof, MerkleError> {
403 let mut positions = positions.into_iter().peekable();
404 let first = *positions.peek().ok_or(MerkleError::NoPositions)?;
405 if self.empty {
406 return Err(MerkleError::PositionOutOfRange(first));
407 }
408 let leaf_count = self.leaf_count;
409 let sibling_positions = siblings_required_for_multi_proof(leaf_count, positions)?;
410 let siblings = sibling_positions
411 .iter()
412 .map(|&(level, index)| self.levels[level][index])
413 .collect();
414 Ok(Proof {
415 leaf_count,
416 siblings,
417 })
418 }
419}
420
421#[derive(Debug, Clone, Default, PartialEq, Eq)]
435pub struct Proof {
436 pub leaf_count: u32,
440 pub siblings: Vec<Hash>,
444}
445
446impl Write for Proof {
447 fn write(&self, writer: &mut impl BufMut) {
448 self.leaf_count.write(writer);
449 self.siblings.write(writer);
450 }
451}
452
453impl EncodeSize for Proof {
454 fn encode_size(&self) -> usize {
455 self.leaf_count.encode_size() + self.siblings.encode_size()
456 }
457}
458
459impl Read for Proof {
460 type Cfg = usize;
464
465 fn read_cfg(reader: &mut impl Buf, max_items: &Self::Cfg) -> Result<Self, CodecError> {
466 let leaf_count = u32::read(reader)?;
467 let max_siblings = max_items.saturating_mul(MAX_LEVELS);
468 let siblings = Vec::<Hash>::read_range(reader, ..=max_siblings)?;
469 Ok(Self {
470 leaf_count,
471 siblings,
472 })
473 }
474}
475
476impl Proof {
477 #[must_use]
479 pub fn encode(&self) -> Vec<u8> {
480 let mut out = Vec::with_capacity(self.encode_size());
481 self.write(&mut out);
482 out
483 }
484
485 pub fn decode(bytes: &[u8], max_items: usize) -> Result<Self, MerkleError> {
496 let mut buf = bytes;
497 let proof = Self::read_cfg(&mut buf, &max_items)?;
498 if buf.has_remaining() {
499 return Err(MerkleError::MalformedProof);
500 }
501 Ok(proof)
502 }
503
504 pub(crate) fn reconstruct_element_root(
511 &self,
512 leaf: &Hash,
513 mut position: u32,
514 ) -> Result<Hash, MerkleError> {
515 if position >= self.leaf_count {
516 return Err(MerkleError::PositionOutOfRange(position));
517 }
518 let mut computed = position_leaf(position, leaf);
519 let mut level_size = self.leaf_count as usize;
520 let mut sibling_iter = self.siblings.iter();
521
522 while level_size > 1 {
523 let is_last_odd = position.is_multiple_of(2) && position as usize + 1 >= level_size;
524 let (left, right) = if is_last_odd {
525 (computed, computed)
526 } else if position.is_multiple_of(2) {
527 let sib = *sibling_iter.next().ok_or(MerkleError::UnalignedProof)?;
528 (computed, sib)
529 } else {
530 let sib = *sibling_iter.next().ok_or(MerkleError::UnalignedProof)?;
531 (sib, computed)
532 };
533 computed = h2(&left, &right);
534 position /= 2;
535 level_size = level_size.div_ceil(2);
536 }
537
538 if sibling_iter.next().is_some() {
539 return Err(MerkleError::UnalignedProof);
540 }
541 Ok(h2(&self.leaf_count.to_be_bytes(), &computed))
542 }
543
544 pub(crate) fn reconstruct_multi_root(
548 &self,
549 elements: &[(Hash, u32)],
550 ) -> Result<Hash, MerkleError> {
551 if elements.is_empty() {
564 return Err(MerkleError::NoPositions);
565 }
566 for (_, position) in elements {
567 if *position >= self.leaf_count {
568 return Err(MerkleError::PositionOutOfRange(*position));
569 }
570 }
571
572 let mut sorted: Vec<(u32, Hash)> = elements
573 .iter()
574 .map(|(leaf, pos)| (*pos, position_leaf(*pos, leaf)))
575 .collect();
576 sorted.sort_unstable_by_key(|(pos, _)| *pos);
577 for i in 1..sorted.len() {
578 if sorted[i - 1].0 == sorted[i].0 {
579 return Err(MerkleError::DuplicatePosition(sorted[i].0));
580 }
581 }
582
583 let levels = levels_in_tree(self.leaf_count);
584 let mut level_size = self.leaf_count;
585 let mut sibling_iter = self.siblings.iter();
586 let mut current = sorted;
587
588 for _ in 0..levels.saturating_sub(1) {
589 let mut next_level: Vec<(u32, Hash)> = Vec::with_capacity(current.len().div_ceil(2));
590 let mut idx = 0;
591 while idx < current.len() {
592 let (pos, digest) = current[idx];
593 let parent_pos = pos / 2;
594 let (left, right) = if pos.is_multiple_of(2) {
595 let left = digest;
596 let right = if idx + 1 < current.len() && current[idx + 1].0 == pos + 1 {
597 idx += 1;
598 current[idx].1
599 } else if pos + 1 >= level_size {
600 left
601 } else {
602 *sibling_iter.next().ok_or(MerkleError::UnalignedProof)?
603 };
604 (left, right)
605 } else {
606 let right = digest;
609 let left = *sibling_iter.next().ok_or(MerkleError::UnalignedProof)?;
610 (left, right)
611 };
612 next_level.push((parent_pos, h2(&left, &right)));
613 idx += 1;
614 }
615 current = next_level;
616 level_size = level_size.div_ceil(2);
617 }
618
619 if sibling_iter.next().is_some() {
620 return Err(MerkleError::UnalignedProof);
621 }
622 if current.len() != 1 {
623 return Err(MerkleError::UnalignedProof);
624 }
625 Ok(h2(&self.leaf_count.to_be_bytes(), ¤t[0].1))
626 }
627
628 fn reconstruct_range_root(&self, position: u32, leaves: &[Hash]) -> Result<Hash, MerkleError> {
632 if leaves.is_empty() && position != 0 {
633 return Err(MerkleError::PositionOutOfRange(position));
634 }
635 if !leaves.is_empty() {
636 let leaves_len = u32_of(leaves.len());
637 let end = position
638 .checked_add(leaves_len - 1)
639 .ok_or(MerkleError::PositionOutOfRange(position))?;
640 if end >= self.leaf_count {
641 return Err(MerkleError::PositionOutOfRange(end));
642 }
643 }
644 let elements: Vec<(Hash, u32)> = leaves
645 .iter()
646 .enumerate()
647 .map(|(i, l)| (*l, position + u32_of(i)))
648 .collect();
649 self.reconstruct_multi_root(&elements)
650 }
651
652 #[cfg(test)]
662 pub(crate) fn verify_element_inclusion(
663 &self,
664 leaf: &Hash,
665 position: u32,
666 inner_root: &Hash,
667 ) -> Result<(), MerkleError> {
668 let got = self.reconstruct_element_root(leaf, position)?;
669 if &got == inner_root {
670 Ok(())
671 } else {
672 Err(MerkleError::VerificationFailed)
673 }
674 }
675
676 #[cfg(test)]
679 pub(crate) fn verify_multi_inclusion(
680 &self,
681 elements: &[(Hash, u32)],
682 inner_root: &Hash,
683 ) -> Result<(), MerkleError> {
684 let got = self.reconstruct_multi_root(elements)?;
685 if &got == inner_root {
686 Ok(())
687 } else {
688 Err(MerkleError::VerificationFailed)
689 }
690 }
691
692 #[cfg(test)]
695 pub(crate) fn verify_range_inclusion(
696 &self,
697 position: u32,
698 leaves: &[Hash],
699 inner_root: &Hash,
700 ) -> Result<(), MerkleError> {
701 let got = self.reconstruct_range_root(position, leaves)?;
702 if &got == inner_root {
703 Ok(())
704 } else {
705 Err(MerkleError::VerificationFailed)
706 }
707 }
708}
709
710pub(crate) fn chunked_meta_leaf_raw(total_size: u64, chunk_size: u32) -> Hash {
720 let mut body = [0u8; 12];
721 body[..8].copy_from_slice(&total_size.to_le_bytes());
722 body[8..].copy_from_slice(&chunk_size.to_le_bytes());
723 domain_digest(CBLOB_META_DOMAIN, &body)
724}
725
726fn chunked_meta_leaf(cb: &ChunkedBlob) -> Hash {
730 chunked_meta_leaf_raw(cb.total_size, cb.chunk_size)
731}
732
733pub(crate) fn tree_entry_leaf(e: &TreeEntry) -> Hash {
738 let mut body = Vec::with_capacity(4 + e.name.len() + 1 + HASH_LEN);
739 body.extend_from_slice(&u32_of(e.name.len()).to_le_bytes());
740 body.extend_from_slice(&e.name);
741 body.push(e.mode as u8);
742 body.extend_from_slice(&e.object_hash);
743 domain_digest(TREE_ENTRY_DOMAIN, &body)
744}
745
746fn chunked_leaves(cb: &ChunkedBlob) -> Vec<Hash> {
747 let mut leaves = Vec::with_capacity(1 + cb.chunks.len());
748 leaves.push(chunked_meta_leaf(cb));
749 leaves.extend_from_slice(&cb.chunks);
750 leaves
751}
752
753fn tree_leaves(tree: &Tree) -> Vec<Hash> {
754 tree.entries.iter().map(tree_entry_leaf).collect()
755}
756
757#[must_use]
765pub fn chunked_inner_root(cb: &ChunkedBlob) -> Hash {
766 build_bmt(&chunked_leaves(cb)).root
767}
768
769#[must_use]
773pub fn tree_inner_root(tree: &Tree) -> Hash {
774 build_bmt(&tree_leaves(tree)).root
775}
776
777#[must_use]
779pub fn compute_chunked_id(cb: &ChunkedBlob) -> Hash {
780 wrap_id(ObjectKind::ChunkedBlob, &chunked_inner_root(cb))
781}
782
783#[must_use]
785pub fn compute_tree_id(tree: &Tree) -> Hash {
786 wrap_id(ObjectKind::Tree, &tree_inner_root(tree))
787}
788
789pub const TREE_EMPTY_ID: Hash = [
793 0x1a, 0xb8, 0xd0, 0x78, 0x8b, 0x29, 0xfe, 0x59, 0x92, 0x01, 0x1e, 0x64, 0xd6, 0xc9, 0x22, 0xec,
794 0x93, 0xf4, 0x24, 0x8b, 0x37, 0x55, 0xb9, 0x2b, 0x15, 0xb0, 0x7e, 0x66, 0x4c, 0xb1, 0x56, 0x52,
795];
796
797#[must_use]
805pub fn chunk_position(cb: &ChunkedBlob, chunk_hash: &Hash) -> Option<u32> {
806 cb.chunks
807 .iter()
808 .position(|c| c == chunk_hash)
809 .map(|i| u32_of(i + 1))
810}
811
812#[must_use]
815pub fn tree_entry_position(tree: &Tree, name: &[u8]) -> Option<u32> {
816 tree.entries.iter().position(|e| e.name == name).map(u32_of)
817}
818
819pub(crate) fn proof_encoded_size(
821 leaf_count: u32,
822 positions: impl IntoIterator<Item = u32>,
823) -> Result<usize, MerkleError> {
824 let siblings = siblings_required_for_multi_proof(leaf_count, positions)?.len();
825 let mut count = siblings;
826 let mut prefix = 1;
827 while count >= 128 {
828 prefix += 1;
829 count >>= 7;
830 }
831 Ok(4 + prefix + siblings * 32)
832}
833
834pub fn build_chunk_proof(cb: &ChunkedBlob, position: u32) -> Result<Proof, MerkleError> {
841 build_bmt(&chunked_leaves(cb)).proof(position)
842}
843
844pub fn build_chunks_range_proof(
846 cb: &ChunkedBlob,
847 start: u32,
848 end: u32,
849) -> Result<Proof, MerkleError> {
850 build_bmt(&chunked_leaves(cb)).range_proof(start, end)
851}
852
853pub fn build_chunks_multi_proof(
855 cb: &ChunkedBlob,
856 positions: impl IntoIterator<Item = u32>,
857) -> Result<Proof, MerkleError> {
858 build_bmt(&chunked_leaves(cb)).multi_proof(positions)
859}
860
861pub fn build_tree_entry_proof(tree: &Tree, position: u32) -> Result<Proof, MerkleError> {
864 build_bmt(&tree_leaves(tree)).proof(position)
865}
866
867pub fn build_tree_entries_range_proof(
869 tree: &Tree,
870 start: u32,
871 end: u32,
872) -> Result<Proof, MerkleError> {
873 build_bmt(&tree_leaves(tree)).range_proof(start, end)
874}
875
876pub fn build_tree_entries_multi_proof(
879 tree: &Tree,
880 positions: impl IntoIterator<Item = u32>,
881) -> Result<Proof, MerkleError> {
882 build_bmt(&tree_leaves(tree)).multi_proof(positions)
883}
884
885pub fn verify_tree_entry(
892 tree_id: &Hash,
893 entry: &TreeEntry,
894 position: u32,
895 proof: &Proof,
896) -> Result<(), MerkleError> {
897 let root = proof.reconstruct_element_root(&tree_entry_leaf(entry), position)?;
898 check_wrapped(ObjectKind::Tree, &root, tree_id)
899}
900
901pub fn verify_tree_entries_range(
904 tree_id: &Hash,
905 start: u32,
906 entries: &[TreeEntry],
907 proof: &Proof,
908) -> Result<(), MerkleError> {
909 let leaves: Vec<Hash> = entries.iter().map(tree_entry_leaf).collect();
910 let root = proof.reconstruct_range_root(start, &leaves)?;
911 check_wrapped(ObjectKind::Tree, &root, tree_id)
912}
913
914pub fn verify_tree_entries_multi(
917 tree_id: &Hash,
918 entries: &[(TreeEntry, u32)],
919 proof: &Proof,
920) -> Result<(), MerkleError> {
921 let elements: Vec<(Hash, u32)> = entries
922 .iter()
923 .map(|(e, pos)| (tree_entry_leaf(e), *pos))
924 .collect();
925 let root = proof.reconstruct_multi_root(&elements)?;
926 check_wrapped(ObjectKind::Tree, &root, tree_id)
927}
928
929pub fn verify_chunk(
934 chunked_id: &Hash,
935 chunk_hash: &Hash,
936 position: u32,
937 proof: &Proof,
938) -> Result<(), MerkleError> {
939 if position == 0 {
940 return Err(MerkleError::PositionOutOfRange(0));
941 }
942 let root = proof.reconstruct_element_root(chunk_hash, position)?;
943 check_wrapped(ObjectKind::ChunkedBlob, &root, chunked_id)
944}
945
946pub fn verify_chunks_range(
950 chunked_id: &Hash,
951 start: u32,
952 chunk_hashes: &[Hash],
953 proof: &Proof,
954) -> Result<(), MerkleError> {
955 if start == 0 {
956 return Err(MerkleError::PositionOutOfRange(0));
957 }
958 let root = proof.reconstruct_range_root(start, chunk_hashes)?;
959 check_wrapped(ObjectKind::ChunkedBlob, &root, chunked_id)
960}
961
962pub fn verify_chunks_multi(
966 chunked_id: &Hash,
967 chunks: &[(Hash, u32)],
968 proof: &Proof,
969) -> Result<(), MerkleError> {
970 if chunks.iter().any(|(_, pos)| *pos == 0) {
971 return Err(MerkleError::PositionOutOfRange(0));
972 }
973 let elements: Vec<(Hash, u32)> = chunks.to_vec();
974 let root = proof.reconstruct_multi_root(&elements)?;
975 check_wrapped(ObjectKind::ChunkedBlob, &root, chunked_id)
976}
977
978pub(crate) fn verify_chunk_with_meta_leaf(
995 chunked_id: &Hash,
996 total_size: u64,
997 chunk_size: u32,
998 chunk_hash: &Hash,
999 chunk_position: u32,
1000 proof: &Proof,
1001) -> Result<(), MerkleError> {
1002 if chunk_position == 0 {
1003 return Err(MerkleError::PositionOutOfRange(0));
1004 }
1005 let meta_leaf = chunked_meta_leaf_raw(total_size, chunk_size);
1006 let elements = [(meta_leaf, 0u32), (*chunk_hash, chunk_position)];
1007 let root = proof.reconstruct_multi_root(&elements)?;
1008 check_wrapped(ObjectKind::ChunkedBlob, &root, chunked_id)
1009}
1010
1011fn check_wrapped(kind: ObjectKind, root: &Hash, expected_id: &Hash) -> Result<(), MerkleError> {
1013 if &wrap_id(kind, root) == expected_id {
1014 Ok(())
1015 } else {
1016 Err(MerkleError::VerificationFailed)
1017 }
1018}
1019
1020#[cfg(test)]
1025mod tests {
1026 use super::*;
1027 use crate::object::EntryMode;
1028
1029 fn cb(total: u64, chunk_size: u32, chunks: &[u8]) -> ChunkedBlob {
1030 ChunkedBlob {
1031 total_size: total,
1032 chunk_size,
1033 chunks: chunks.iter().map(|b| [*b; 32]).collect(),
1034 }
1035 }
1036
1037 fn entry(name: &[u8], mode: EntryMode, h: u8) -> TreeEntry {
1038 TreeEntry {
1039 name: name.to_vec(),
1040 mode,
1041 object_hash: [h; 32],
1042 }
1043 }
1044
1045 fn tree(entries: Vec<TreeEntry>) -> Tree {
1046 Tree { entries }
1047 }
1048
1049 #[test]
1050 fn id_changes_when_a_leaf_changes() {
1051 let a = cb(100, 0, &[1, 2, 3]);
1052 let b = cb(100, 0, &[1, 2, 4]);
1053 assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
1054 }
1055
1056 #[test]
1057 fn id_changes_when_leaf_count_changes() {
1058 let a = cb(100, 0, &[1, 2, 3]);
1059 let b = cb(100, 0, &[1, 2, 3, 3]); assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
1061 }
1062
1063 #[test]
1064 fn chunked_id_changes_when_metadata_changes() {
1065 let a = cb(100, 0, &[1, 2, 3]);
1066 let b = cb(101, 0, &[1, 2, 3]);
1067 let c = cb(100, 64, &[1, 2, 3]);
1068 assert_ne!(compute_chunked_id(&a), compute_chunked_id(&b));
1069 assert_ne!(compute_chunked_id(&a), compute_chunked_id(&c));
1070 }
1071
1072 #[test]
1073 fn tree_ordering_matters() {
1074 let a = tree(vec![
1075 entry(b"a", EntryMode::Blob, 1),
1076 entry(b"b", EntryMode::Blob, 2),
1077 ]);
1078 let b = tree(vec![
1079 entry(b"b", EntryMode::Blob, 2),
1080 entry(b"a", EntryMode::Blob, 1),
1081 ]);
1082 assert_ne!(compute_tree_id(&a), compute_tree_id(&b));
1083 }
1084
1085 #[test]
1086 fn empty_tree_id_matches_constant() {
1087 let got = compute_tree_id(&tree(vec![]));
1088 assert_eq!(got, TREE_EMPTY_ID, "update TREE_EMPTY_ID to {got:02x?}");
1089 }
1090
1091 #[test]
1092 fn type_binding_no_cross_collisions() {
1093 let empty_tree = compute_tree_id(&tree(vec![]));
1094 let empty_cblob = compute_chunked_id(&cb(0, 0, &[]));
1095 assert_ne!(empty_tree, empty_cblob);
1096
1097 let t = tree(vec![entry(b"x", EntryMode::Blob, 9)]);
1098 let c = cb(10, 0, &[9]);
1099 assert_ne!(compute_tree_id(&t), compute_chunked_id(&c));
1100 }
1101
1102 #[test]
1103 fn id_ne_flat_blake3_of_serialized_bytes() {
1104 let c = cb(100, 0, &[1, 2, 3]);
1105 let serialized =
1106 crate::serialize::serialize(&crate::object::Object::ChunkedBlob(c.clone())).unwrap();
1107 assert_ne!(compute_chunked_id(&c), crate::hash::hash(&serialized));
1108 }
1109
1110 #[test]
1111 fn chunk_position_offsets_by_one() {
1112 let c = cb(100, 0, &[7, 8, 9]);
1113 assert_eq!(chunk_position(&c, &[7; 32]), Some(1));
1114 assert_eq!(chunk_position(&c, &[9; 32]), Some(3));
1115 assert_eq!(chunk_position(&c, &[0; 32]), None);
1116 }
1117
1118 #[test]
1119 fn chunk_inclusion_proof_round_trips() {
1120 let c = cb(100, 0, &[10, 20, 30, 40]);
1121 let id = compute_chunked_id(&c);
1122 for (idx, byte) in [(0usize, 10u8), (2, 30), (3, 40)] {
1123 let pos = chunk_position(&c, &[byte; 32]).unwrap();
1124 assert_eq!(pos, u32_of(idx) + 1);
1125 let proof = build_chunk_proof(&c, pos).unwrap();
1126 verify_chunk(&id, &[byte; 32], pos, &proof).unwrap();
1127 assert!(verify_chunk(&id, &[0xFF; 32], pos, &proof).is_err());
1128 }
1129 }
1130
1131 #[test]
1132 fn verify_chunk_rejects_meta_leaf() {
1133 let c = cb(100, 0, &[10, 20, 30]);
1134 let id = compute_chunked_id(&c);
1135 let meta_leaf = chunked_meta_leaf(&c);
1136 let proof = build_chunk_proof(&c, 0).unwrap();
1138 proof
1139 .verify_element_inclusion(&meta_leaf, 0, &chunked_inner_root(&c))
1140 .expect("meta leaf is a valid BMT element at position 0");
1141 assert_eq!(
1143 verify_chunk(&id, &meta_leaf, 0, &proof),
1144 Err(MerkleError::PositionOutOfRange(0))
1145 );
1146 }
1147
1148 #[test]
1149 fn verify_chunk_with_meta_leaf_round_trips_and_rejects_forgery() {
1150 let c = cb(100, 0, &[10, 20, 30]);
1151 let id = compute_chunked_id(&c);
1152 let pos = chunk_position(&c, &[20; 32]).unwrap(); let proof = build_chunks_multi_proof(&c, [0, pos]).unwrap();
1154
1155 verify_chunk_with_meta_leaf(&id, 100, 0, &[20; 32], pos, &proof).unwrap();
1156
1157 assert!(verify_chunk_with_meta_leaf(&id, 101, 0, &[20; 32], pos, &proof).is_err());
1160 assert!(verify_chunk_with_meta_leaf(&id, 100, 64, &[20; 32], pos, &proof).is_err());
1162 assert!(verify_chunk_with_meta_leaf(&id, 100, 0, &[0xFF; 32], pos, &proof).is_err());
1164 assert_eq!(
1167 verify_chunk_with_meta_leaf(&id, 100, 0, &[10; 32], 0, &proof),
1168 Err(MerkleError::PositionOutOfRange(0))
1169 );
1170 }
1171
1172 #[test]
1173 fn tree_inclusion_proof_round_trips() {
1174 let t = tree(vec![
1175 entry(b"a", EntryMode::Blob, 1),
1176 entry(b"b", EntryMode::Tree, 2),
1177 entry(b"c", EntryMode::Executable, 3),
1178 ]);
1179 let id = compute_tree_id(&t);
1180 let pos = tree_entry_position(&t, b"b").unwrap();
1181 assert_eq!(pos, 1);
1182 let proof = build_tree_entry_proof(&t, pos).unwrap();
1183 verify_tree_entry(&id, &t.entries[1], pos, &proof).unwrap();
1184 let wrong = entry(b"b", EntryMode::Blob, 2);
1185 assert!(verify_tree_entry(&id, &wrong, pos, &proof).is_err());
1186 }
1187
1188 #[test]
1189 fn range_and_multi_proofs_round_trip() {
1190 let t = tree(
1191 (0..9)
1192 .map(|i| entry(&[b'a' + i], EntryMode::Blob, i))
1193 .collect(),
1194 );
1195 let id = compute_tree_id(&t);
1196
1197 let range_proof = build_tree_entries_range_proof(&t, 2, 4).unwrap();
1198 verify_tree_entries_range(&id, 2, &t.entries[2..=4], &range_proof).unwrap();
1199 assert!(verify_tree_entries_range(&id, 2, &t.entries[2..4], &range_proof).is_err());
1200
1201 let multi_proof = build_tree_entries_multi_proof(&t, [0, 4, 8]).unwrap();
1202 let elements = [
1203 (t.entries[0].clone(), 0),
1204 (t.entries[4].clone(), 4),
1205 (t.entries[8].clone(), 8),
1206 ];
1207 verify_tree_entries_multi(&id, &elements, &multi_proof).unwrap();
1208 let wrong_elements = [
1209 (t.entries[0].clone(), 0),
1210 (t.entries[4].clone(), 5), (t.entries[8].clone(), 8),
1212 ];
1213 assert!(verify_tree_entries_multi(&id, &wrong_elements, &multi_proof).is_err());
1214 }
1215
1216 #[test]
1217 fn chunk_range_and_multi_proofs_reject_position_zero() {
1218 let c = cb(100, 0, &[1, 2, 3]);
1219 let id = compute_chunked_id(&c);
1220 let proof = build_chunks_range_proof(&c, 0, 1).unwrap();
1221 assert_eq!(
1222 verify_chunks_range(&id, 0, &c.chunks[..2], &proof),
1223 Err(MerkleError::PositionOutOfRange(0))
1224 );
1225 let multi = build_chunks_multi_proof(&c, [0, 2]).unwrap();
1226 assert_eq!(
1227 verify_chunks_multi(&id, &[(c.chunks[0], 0), (c.chunks[1], 2)], &multi),
1228 Err(MerkleError::PositionOutOfRange(0))
1229 );
1230 }
1231
1232 #[test]
1233 fn single_leaf_tree_proof_has_no_siblings() {
1234 let t = tree(vec![entry(b"only", EntryMode::Blob, 1)]);
1238 let id = compute_tree_id(&t);
1239 let proof = build_tree_entry_proof(&t, 0).unwrap();
1240 assert!(
1241 proof.siblings.is_empty(),
1242 "single-leaf tree proof must have zero siblings"
1243 );
1244 verify_tree_entry(&id, &t.entries[0], 0, &proof).unwrap();
1245 }
1246
1247 #[test]
1248 fn verify_rejects_empty_element_set() {
1249 assert_eq!(
1259 verify_tree_entries_range(&TREE_EMPTY_ID, 0, &[], &Proof::default()),
1260 Err(MerkleError::NoPositions)
1261 );
1262 assert_eq!(
1263 verify_tree_entries_multi(&TREE_EMPTY_ID, &[], &Proof::default()),
1264 Err(MerkleError::NoPositions)
1265 );
1266
1267 let t = tree(vec![entry(b"a", EntryMode::Blob, 1)]);
1271 let id = compute_tree_id(&t);
1272 let real_proof = build_tree_entry_proof(&t, 0).unwrap();
1273 assert_eq!(
1274 verify_tree_entries_range(&id, 0, &[], &real_proof),
1275 Err(MerkleError::NoPositions)
1276 );
1277 assert_eq!(
1278 verify_tree_entries_multi(&id, &[], &real_proof),
1279 Err(MerkleError::NoPositions)
1280 );
1281 }
1282
1283 #[test]
1284 fn empty_tree_builders_refuse_position_zero() {
1285 let empty = tree(vec![]);
1289 assert_eq!(
1290 build_tree_entries_range_proof(&empty, 0, 0),
1291 Err(MerkleError::PositionOutOfRange(0))
1292 );
1293 assert_eq!(
1294 build_tree_entry_proof(&empty, 0),
1295 Err(MerkleError::PositionOutOfRange(0))
1296 );
1297 assert_eq!(
1298 build_tree_entries_multi_proof(&empty, [0]),
1299 Err(MerkleError::PositionOutOfRange(0))
1300 );
1301 }
1302
1303 #[test]
1304 fn out_of_range_position_rejected() {
1305 let c = cb(10, 0, &[1]);
1306 assert_eq!(
1307 build_chunk_proof(&c, 2),
1308 Err(MerkleError::PositionOutOfRange(2))
1309 );
1310 }
1311
1312 #[test]
1313 fn proof_round_trips_through_encode_decode() {
1314 let t = tree(
1315 (0..7)
1316 .map(|i| entry(&[b'a' + i], EntryMode::Blob, i))
1317 .collect(),
1318 );
1319 let proof = build_tree_entry_proof(&t, 6).unwrap();
1320 let bytes = proof.encode();
1321 let decoded = Proof::decode(&bytes, 1).unwrap();
1322 assert_eq!(proof, decoded);
1323
1324 let mut truncated_extra = bytes.clone();
1326 truncated_extra.push(0);
1327 assert_eq!(
1328 Proof::decode(&truncated_extra, 1),
1329 Err(MerkleError::MalformedProof)
1330 );
1331
1332 assert_eq!(
1334 Proof::decode(&bytes[..bytes.len() - 1], 1),
1335 Err(MerkleError::MalformedProof)
1336 );
1337
1338 assert_eq!(Proof::decode(&bytes, 0), Err(MerkleError::MalformedProof));
1340 }
1341
1342 #[test]
1347 fn vendored_root_matches_commonware() {
1348 use commonware_cryptography::blake3::{Blake3, Digest};
1349 use commonware_storage::bmt::Builder;
1350
1351 for n in [1usize, 2, 3, 4, 5, 7, 8, 9, 16, 33] {
1352 let leaves: Vec<Hash> = (0..n)
1353 .map(|i| hash(&[u8::try_from(i % 256).unwrap(); 4]))
1354 .collect();
1355
1356 let mut builder = Builder::<Blake3>::new(n);
1357 for l in &leaves {
1358 builder.add(&Digest(*l));
1359 }
1360 let cw_root = builder.build().root().0;
1361 let ours = build_bmt(&leaves).root;
1362 assert_eq!(ours, cw_root, "vendored BMT root diverged at n={n}");
1363 }
1364 }
1365
1366 fn tree_and_positions()
1371 -> impl proptest::strategy::Strategy<Value = (usize, u32, (u32, u32), Vec<u32>)> {
1372 use proptest::prelude::*;
1373
1374 let boundary_counts: Vec<usize> = [1usize, 2, 4, 8, 16, 32, 64, 128, 256]
1375 .into_iter()
1376 .flat_map(|p| [p.saturating_sub(1).max(1), p, p + 1])
1377 .chain([
1378 3usize, 5, 7, 9, 15, 17, 31, 33, 63, 65, 127, 129, 255, 257, 300,
1379 ])
1380 .collect();
1381
1382 prop_oneof![
1383 3 => 1usize..=300,
1384 2 => proptest::sample::select(boundary_counts),
1385 ]
1386 .prop_flat_map(|n| {
1387 let n_u32 =
1388 u32::try_from(n).expect("n is bounded well under u32::MAX by the strategy above");
1389 (
1390 Just(n),
1391 0..n_u32,
1392 (0..n_u32).prop_flat_map(move |s| (Just(s), s..n_u32)),
1393 proptest::collection::vec(0..n_u32, 1..=n.min(6)).prop_map(|mut v| {
1394 v.sort_unstable();
1395 v.dedup();
1396 v
1397 }),
1398 )
1399 })
1400 }
1401
1402 proptest::proptest! {
1403 #![proptest_config(proptest::prelude::ProptestConfig::with_cases(400))]
1404
1405 #[test]
1414 fn proofs_match_commonware((n, pos, (start, end), positions) in tree_and_positions()) {
1415 use commonware_cryptography::blake3::{Blake3, Digest};
1416 use commonware_storage::bmt::Builder;
1417
1418 let leaves: Vec<Hash> = (0..n).map(|i| hash(&(i as u64).to_le_bytes())).collect();
1419
1420 let ours = build_bmt(&leaves);
1421 let mut cw_builder = Builder::<Blake3>::new(n);
1422 for l in &leaves {
1423 cw_builder.add(&Digest(*l));
1424 }
1425 let cw_tree = cw_builder.build();
1426 let cw_root = cw_tree.root();
1427 assert_eq!(ours.root, cw_root.0, "root mismatch at n={n}");
1428
1429 let our_proof = ours.proof(pos).unwrap();
1431 let cw_proof = cw_tree.proof(pos).unwrap();
1432 assert_eq!(
1433 our_proof.encode(),
1434 commonware_codec::Encode::encode(&cw_proof).to_vec(),
1435 "single-proof bytes diverged at n={n} pos={pos}"
1436 );
1437 cw_proof
1438 .verify_element_inclusion::<Blake3>(&Digest(leaves[pos as usize]), pos, &cw_root)
1439 .expect("upstream must accept its own proof");
1440 our_proof
1441 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1442 .expect("ours must accept its own proof");
1443 let cw_bytes = commonware_codec::Encode::encode(&cw_proof).to_vec();
1445 let our_decoded = Proof::decode(&cw_bytes, 1).unwrap();
1446 our_decoded
1447 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1448 .expect("ours must accept upstream's proof bytes");
1449 let our_bytes = our_proof.encode();
1451 let mut our_bytes_buf: &[u8] = &our_bytes;
1452 let cw_decoded =
1453 <commonware_storage::bmt::Proof<Digest> as commonware_codec::Read>::read_cfg(
1454 &mut our_bytes_buf,
1455 &1usize,
1456 )
1457 .unwrap();
1458 cw_decoded
1459 .verify_element_inclusion::<Blake3>(&Digest(leaves[pos as usize]), pos, &cw_root)
1460 .expect("upstream must accept our proof bytes");
1461
1462 if !our_proof.siblings.is_empty() {
1464 let mut mutated = our_proof.clone();
1465 mutated.siblings[0][0] ^= 0x01;
1466 assert!(
1467 mutated
1468 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1469 .is_err()
1470 );
1471
1472 let mut dropped = our_proof.clone();
1473 dropped.siblings.pop();
1474 assert!(
1475 dropped
1476 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1477 .is_err()
1478 );
1479
1480 let mut extra = our_proof.clone();
1481 extra.siblings.push(hash(b"extra"));
1482 assert!(
1483 extra
1484 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1485 .is_err()
1486 );
1487 }
1488 let mut wrong_count = our_proof.clone();
1489 wrong_count.leaf_count = wrong_count.leaf_count.wrapping_add(1);
1490 assert!(
1491 wrong_count
1492 .verify_element_inclusion(&leaves[pos as usize], pos, &ours.root)
1493 .is_err()
1494 );
1495
1496 if n >= 2 {
1497 let our_range = ours.range_proof(start, end).unwrap();
1499 let cw_range = cw_tree.range_proof(start, end).unwrap();
1500 assert_eq!(
1501 our_range.encode(),
1502 commonware_codec::Encode::encode(&cw_range).to_vec(),
1503 "range-proof bytes diverged at n={n} start={start} end={end}"
1504 );
1505 let range_leaves: Vec<Hash> = leaves[start as usize..=end as usize].to_vec();
1506 let cw_range_leaves: Vec<Digest> =
1507 range_leaves.iter().map(|h| Digest(*h)).collect();
1508 cw_range
1509 .verify_range_inclusion::<Blake3>(start, &cw_range_leaves, &cw_root)
1510 .expect("upstream must accept its own range proof");
1511 our_range
1512 .verify_range_inclusion(start, &range_leaves, &ours.root)
1513 .expect("ours must accept its own range proof");
1514
1515 if !positions.is_empty() {
1517 let our_multi = ours.multi_proof(positions.iter().copied()).unwrap();
1518 let cw_multi = cw_tree.multi_proof(positions.iter().copied()).unwrap();
1519 assert_eq!(
1520 our_multi.encode(),
1521 commonware_codec::Encode::encode(&cw_multi).to_vec(),
1522 "multi-proof bytes diverged at n={n} positions={positions:?}"
1523 );
1524 let elements: Vec<(Hash, u32)> =
1525 positions.iter().map(|&p| (leaves[p as usize], p)).collect();
1526 let cw_elements: Vec<(Digest, u32)> = positions
1527 .iter()
1528 .map(|&p| (Digest(leaves[p as usize]), p))
1529 .collect();
1530 cw_multi
1531 .verify_multi_inclusion::<Blake3>(&cw_elements, &cw_root)
1532 .expect("upstream must accept its own multi proof");
1533 our_multi
1534 .verify_multi_inclusion(&elements, &ours.root)
1535 .expect("ours must accept its own multi proof");
1536 }
1537 }
1538 }
1539 }
1540}
1541
1542#[cfg(kani)]
1551mod kani_proofs {
1552 use super::*;
1553
1554 fn toy(a: &[u8], b: &[u8]) -> Hash {
1562 fn first16(x: &[u8]) -> u128 {
1563 let mut w = [0u8; 16];
1564 let n = x.len().min(16);
1565 w[..n].copy_from_slice(&x[..n]);
1566 u128::from_le_bytes(w)
1567 }
1568 let (ha, hb) = (first16(a), first16(b));
1569 let lo = ha ^ hb.rotate_left(8) ^ a.len() as u128;
1570 let hi = hb ^ ha.rotate_left(16) ^ ((b.len() as u128) << 64);
1571 let mut out = [0u8; HASH_LEN];
1572 out[..16].copy_from_slice(&lo.to_le_bytes());
1573 out[16..].copy_from_slice(&hi.to_le_bytes());
1574 out
1575 }
1576 fn toy_h2(a: &[u8], b: &[u8]) -> Hash {
1577 toy(a, b)
1578 }
1579 fn toy_domain_digest(domain: &[u8], body: &[u8]) -> Hash {
1580 toy(body, domain)
1581 }
1582 fn toy_hash(data: &[u8]) -> Hash {
1583 toy(data, &[])
1584 }
1585
1586 fn spec_sibling_count(leaf_count: u32, mut position: u32) -> usize {
1590 let mut level_size = u64::from(leaf_count);
1591 let mut need = 0;
1592 while level_size > 1 {
1593 if !(position % 2 == 0 && u64::from(position) + 1 >= level_size) {
1594 need += 1;
1595 }
1596 position /= 2;
1597 level_size = level_size.div_ceil(2);
1598 }
1599 need
1600 }
1601
1602 fn short_at<const N: usize>() {
1603 let buf: [u8; N] = kani::any();
1604 let b: &[u8] = &buf;
1605 if let Ok(p) = Proof::decode(b, 1) {
1606 assert!(N == 5 && b[4] == 0 && p.siblings.is_empty());
1607 let lc: [u8; 4] = b[..4].try_into().expect("4 bytes");
1608 assert_eq!(p.leaf_count, u32::from_be_bytes(lc));
1609 kani::cover!(true, "ok_no_siblings");
1610 }
1611 }
1612
1613 #[kani::proof]
1618 #[kani::unwind(4)]
1619 fn merkle_proof_decode_no_panic() {
1620 short_at::<0>();
1621 short_at::<1>();
1622 short_at::<2>();
1623 short_at::<3>();
1624 short_at::<4>();
1625 }
1626
1627 #[kani::proof]
1630 #[kani::unwind(4)]
1632 fn merkle_proof_decode_empty_proof() {
1633 short_at::<5>();
1634 }
1635
1636 #[kani::proof]
1642 #[kani::unwind(6)]
1644 fn merkle_proof_decode_one_sibling() {
1645 let mut buf: [u8; 37] = kani::any();
1646 buf[4] = 1;
1647 let p = Proof::decode(&buf, 1).expect("one-sibling proof decodes");
1648 assert_eq!(
1649 p.leaf_count,
1650 u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])
1651 );
1652 assert_eq!(p.siblings.len(), 1);
1653 let d = &p.siblings[0];
1654 let w = |x: &[u8], i: usize| u64::from_le_bytes(x[i..i + 8].try_into().expect("8"));
1655 assert!((0..4).all(|k| w(d, 8 * k) == w(&buf[5..], 8 * k)));
1656 }
1657
1658 fn verify_with<const S: usize>(max_leaves: u32) -> bool {
1661 let proof = Proof {
1662 leaf_count: kani::any_where(|&n| n <= max_leaves),
1663 siblings: kani::any::<[Hash; S]>().to_vec(),
1664 };
1665 let position: u32 = kani::any();
1666 let leaf: Hash = kani::any();
1667 let id: Hash = kani::any();
1668
1669 let folded = proof.reconstruct_element_root(&leaf, position);
1670 let shape_ok = position < proof.leaf_count
1671 && proof.siblings.len() == spec_sibling_count(proof.leaf_count, position);
1672 assert_eq!(folded.is_ok(), shape_ok);
1673
1674 let chunk = verify_chunk(&id, &leaf, position, &proof);
1675 if position == 0 {
1676 assert_eq!(chunk, Err(MerkleError::PositionOutOfRange(0)));
1677 }
1678 if chunk.is_ok() {
1679 assert!(shape_ok);
1680 }
1681 let entry = TreeEntry {
1682 name: vec![b'a'],
1683 mode: crate::object::EntryMode::Blob,
1684 object_hash: leaf,
1685 };
1686 let _ = verify_tree_entry(&id, &entry, position, &proof);
1687 chunk.is_ok()
1688 }
1689
1690 #[kani::proof]
1698 #[kani::stub(h2, toy_h2)]
1699 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1700 #[kani::stub(crate::hash::hash, toy_hash)]
1701 #[kani::unwind(34)]
1703 fn merkle_verify_s0() {
1704 verify_with::<0>(u32::MAX);
1705 }
1706
1707 #[kani::proof]
1711 #[kani::stub(h2, toy_h2)]
1712 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1713 #[kani::stub(crate::hash::hash, toy_hash)]
1714 #[kani::unwind(5)]
1715 fn merkle_verify_s1() {
1716 kani::cover!(verify_with::<1>(8), "accepts_one_sibling_proof");
1717 }
1718
1719 #[kani::proof]
1721 #[kani::stub(h2, toy_h2)]
1722 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1723 #[kani::stub(crate::hash::hash, toy_hash)]
1724 #[kani::unwind(5)]
1725 fn merkle_verify_s2() {
1726 kani::cover!(verify_with::<2>(8), "accepts_two_sibling_proof");
1727 }
1728
1729 fn spec_proof(leaves: &[Hash], pos: usize) -> (Hash, Vec<Hash>) {
1735 #[allow(clippy::cast_possible_truncation)]
1736 let mut level: Vec<Hash> = leaves
1737 .iter()
1738 .enumerate()
1739 .map(|(i, l)| toy_h2(&(i as u32).to_be_bytes(), l))
1740 .collect();
1741 let mut p = pos;
1742 let mut siblings = Vec::new();
1743 while level.len() > 1 {
1744 if p % 2 == 1 {
1745 siblings.push(level[p - 1]);
1746 } else if p + 1 < level.len() {
1747 siblings.push(level[p + 1]);
1748 }
1749 let mut next = Vec::new();
1750 let mut i = 0;
1751 while i < level.len() {
1752 let right = if i + 1 < level.len() {
1753 level[i + 1]
1754 } else {
1755 level[i]
1756 };
1757 next.push(toy_h2(&level[i], &right));
1758 i += 2;
1759 }
1760 level = next;
1761 p /= 2;
1762 }
1763 #[allow(clippy::cast_possible_truncation)]
1764 let root = toy_h2(&(leaves.len() as u32).to_be_bytes(), &level[0]);
1765 (root, siblings)
1766 }
1767
1768 fn chunked_fixture<const N: usize>() -> (ChunkedBlob, Vec<Hash>) {
1772 let cb = ChunkedBlob {
1773 total_size: kani::any(),
1774 chunk_size: kani::any(),
1775 chunks: kani::any::<[Hash; N]>().to_vec(),
1776 };
1777 let mut leaves = vec![chunked_meta_leaf_raw(cb.total_size, cb.chunk_size)];
1778 leaves.extend_from_slice(&cb.chunks);
1779 (cb, leaves)
1780 }
1781
1782 fn chunk_rt<const N: usize>(pos: u32) {
1785 let (cb, leaves) = chunked_fixture::<N>();
1786 let (root, siblings) = spec_proof(&leaves, pos as usize);
1787 let id = wrap_id(ObjectKind::ChunkedBlob, &root);
1788 #[allow(clippy::cast_possible_truncation)]
1789 let proof = Proof {
1790 leaf_count: leaves.len() as u32,
1791 siblings,
1792 };
1793 assert_eq!(
1794 proof.siblings.len(),
1795 spec_sibling_count(proof.leaf_count, pos)
1796 );
1797 assert_eq!(
1798 verify_chunk(&id, &cb.chunks[(pos - 1) as usize], pos, &proof),
1799 Ok(())
1800 );
1801 }
1802
1803 #[kani::proof]
1812 #[kani::stub(h2, toy_h2)]
1813 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1814 #[kani::stub(crate::hash::hash, toy_hash)]
1815 #[kani::unwind(5)]
1819 fn merkle_roundtrip_one_chunk() {
1820 chunk_rt::<1>(1);
1821 }
1822
1823 #[kani::proof]
1827 #[kani::stub(h2, toy_h2)]
1828 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1829 #[kani::stub(crate::hash::hash, toy_hash)]
1830 #[kani::unwind(5)]
1832 fn merkle_roundtrip_two_chunks() {
1833 chunk_rt::<2>(1);
1834 chunk_rt::<2>(2);
1835 }
1836
1837 #[kani::proof]
1845 #[kani::stub(h2, toy_h2)]
1846 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1847 #[kani::stub(crate::hash::hash, toy_hash)]
1848 #[kani::unwind(3)]
1849 fn merkle_builder_empty_tree_refuses() {
1850 let empty = Tree {
1851 entries: Vec::new(),
1852 };
1853 let (p, start, end): (u32, u32, u32) = (kani::any(), kani::any(), kani::any());
1854 assert!(build_tree_entry_proof(&empty, p).is_err());
1855 assert!(build_tree_entries_multi_proof(&empty, [p]).is_err());
1856 let range = build_tree_entries_range_proof(&empty, start, end);
1857 kani::cover!(range.is_err(), "range_refused");
1858 assert!(
1859 range.is_err(),
1860 "empty-Tree range proof must be refused (SPEC-MERKLE-OBJECTS §5.4)"
1861 );
1862 }
1863
1864 #[kani::proof]
1868 #[kani::stub(h2, toy_h2)]
1869 #[kani::stub(crate::hash::domain_digest, toy_domain_digest)]
1870 #[kani::stub(crate::hash::hash, toy_hash)]
1871 #[kani::unwind(6)]
1873 #[kani::should_panic]
1874 fn merkle_canary_tampered_leaf_verifies() {
1875 let (_cb, leaves) = chunked_fixture::<2>();
1876 let (root, siblings) = spec_proof(&leaves, 1);
1877 let id = wrap_id(ObjectKind::ChunkedBlob, &root);
1878 let proof = Proof {
1879 leaf_count: 3,
1880 siblings,
1881 };
1882 let forged: Hash = kani::any();
1883 assert!(verify_chunk(&id, &forged, 1, &proof).is_ok());
1884 }
1885}