1use crate::{
36 journal::contiguous::Contiguous,
37 merkle::{
38 self, Family, Graftable, Location, PendingChunk, Position, Proof, hasher::Hasher as _,
39 storage::Storage,
40 },
41 qmdb::{
42 self, Error,
43 current::{
44 db::{combine_roots, graftable_chunk_window, partial_chunk, pending_chunk},
45 grafting,
46 },
47 },
48};
49use ahash::AHashMap;
50use bytes::BufMut;
51use commonware_codec::{Buf, Encode, EncodeSize, Read, ReadExt as _, Write, varint::UInt};
52use commonware_cryptography::{Digest, Hasher};
53use commonware_utils::{
54 Widen,
55 bitmap::{Prunable as BitMap, Readable as BitmapReadable},
56};
57use core::{num::NonZeroU64, ops::Range};
58use tracing::debug;
59
60pub mod operation;
61
62pub(super) fn chunk_bits(chunk_size: usize) -> Result<u64, commonware_codec::Error> {
64 chunk_size
65 .checked_mul(8)
66 .map(|bits| -> u64 { Widen::widen(bits) })
67 .filter(|bits| bits.is_power_of_two() && bits.trailing_zeros() < 63)
68 .ok_or(commonware_codec::Error::Invalid(
69 "current proof",
70 "invalid bitmap chunk size",
71 ))
72}
73
74pub fn required_chunks<F: Graftable, const N: usize>(
92 ops_leaves: Location<F>,
93 bitmap_len: u64,
94 pruned_chunks: u64,
95 location: Option<Location<F>>,
96) -> Result<impl Iterator<Item = u64>, Error<F>> {
97 const { assert!(N.is_power_of_two() && N <= usize::MAX / 8) };
98 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
99 let complete = bitmap_len / chunk_bits;
100 let graftable =
101 graftable_chunk_window(ops_leaves, complete, pruned_chunks, grafting::height::<N>())?;
102 let queried = if let Some(loc) = location {
103 if loc >= ops_leaves || *loc >= bitmap_len {
104 return Err(merkle::Error::RangeOutOfBounds(loc).into());
105 }
106 let chunk = *loc / chunk_bits;
107 if chunk < pruned_chunks {
108 return Err(Error::OperationPruned(loc));
109 }
110 Some(chunk)
111 } else {
112 None
113 };
114 let mut chunks = [
115 (!bitmap_len.is_multiple_of(chunk_bits)).then_some(complete),
116 (complete > graftable).then_some(graftable),
117 queried,
118 ];
119 chunks.sort_unstable();
120 let mut previous = None;
121 Ok(chunks.into_iter().flatten().filter(move |&chunk| {
122 let distinct = previous != Some(chunk);
123 previous = Some(chunk);
124 distinct
125 }))
126}
127
128#[derive(Clone, Eq, PartialEq, Debug)]
133pub struct OpsRootWitness<F: Graftable, D: Digest> {
134 pub grafted_root: D,
136
137 pub pending_chunk_digest: F::PendingChunk<D>,
139
140 pub partial_chunk: Option<(u64, D)>,
143}
144
145impl<F: Graftable, D: Digest> OpsRootWitness<F, D> {
146 pub fn root<H: Hasher<Digest = D>>(&self, ops_root: &D) -> D {
151 let partial = self.partial_chunk.as_ref().map(|(nb, d)| (*nb, d));
152 combine_roots::<H>(
153 ops_root,
154 &self.grafted_root,
155 self.pending_chunk_digest.as_ref(),
156 partial,
157 )
158 }
159
160 pub fn verify<H: Hasher<Digest = D>>(&self, ops_root: &D, root: &D) -> bool {
162 self.root::<H>(ops_root) == *root
163 }
164}
165
166impl<F: Graftable, D: Digest> Write for OpsRootWitness<F, D> {
167 fn write(&self, buf: &mut impl BufMut) {
168 self.grafted_root.write(buf);
169 self.pending_chunk_digest.write(buf);
170 self.partial_chunk.is_some().write(buf);
171 if let Some((next_bit, digest)) = &self.partial_chunk {
172 UInt(*next_bit).write(buf);
173 digest.write(buf);
174 }
175 }
176}
177
178impl<F: Graftable, D: Digest> EncodeSize for OpsRootWitness<F, D> {
179 fn encode_size(&self) -> usize {
180 self.grafted_root.encode_size()
181 + self.pending_chunk_digest.encode_size()
182 + self
183 .partial_chunk
184 .as_ref()
185 .map_or(1, |(nb, d)| 1 + UInt(*nb).encode_size() + d.encode_size())
186 }
187}
188
189impl<F: Graftable, D: Digest> Read for OpsRootWitness<F, D> {
190 type Cfg = ();
191
192 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
193 let grafted_root = D::read(buf)?;
194 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
195 let partial_chunk = if bool::read(buf)? {
196 let next_bit = UInt::<u64>::read(buf)?.into();
197 let digest = D::read(buf)?;
198 Some((next_bit, digest))
199 } else {
200 None
201 };
202 Ok(Self {
203 grafted_root,
204 pending_chunk_digest,
205 partial_chunk,
206 })
207 }
208}
209
210#[cfg(feature = "arbitrary")]
211impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for OpsRootWitness<F, D>
212where
213 D: for<'a> arbitrary::Arbitrary<'a>,
214 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
215{
216 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
217 Ok(Self {
218 grafted_root: u.arbitrary()?,
219 pending_chunk_digest: u.arbitrary()?,
220 partial_chunk: u.arbitrary()?,
221 })
222 }
223}
224
225#[derive(Clone, Eq, PartialEq, Debug)]
227pub struct RangeProof<F: Graftable, D: Digest> {
228 pub proof: Proof<F, D>,
230
231 pub pending_chunk_digest: F::PendingChunk<D>,
233
234 pub partial_chunk_digest: Option<D>,
236
237 pub ops_root: D,
239}
240
241#[derive(Clone, Copy, Eq, PartialEq, Debug)]
243pub struct RangeProofSpec<F: Family, D: Digest> {
244 pub start_loc: Location<F>,
246
247 pub max_ops: NonZeroU64,
249
250 pub inactivity_floor: Location<F>,
252
253 pub ops_root: D,
255}
256
257impl<F: Graftable, D: Digest> RangeProof<F, D> {
258 pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>, const N: usize>(
261 status: &impl BitmapReadable<N>,
262 storage: &S,
263 inactivity_floor: Location<F>,
264 range: Range<Location<F>>,
265 ops_root: D,
266 ) -> Result<Self, Error<F>> {
267 let ops_leaves = Location::try_from(storage.size())?;
268 let plan = merkle::RangePlan::new(ops_leaves, range)?;
269 let positions = plan.positions();
270 let digests = storage.get_nodes(&positions).await?;
271 let fetched: AHashMap<_, _> = positions.into_iter().zip(digests).collect();
272 Self::build::<H, N>(
273 status,
274 plan,
275 |pos| fetched.get(&pos).copied(),
276 inactivity_floor,
277 ops_root,
278 )
279 }
280
281 pub fn build<H: Hasher<Digest = D>, const N: usize>(
295 status: &impl BitmapReadable<N>,
296 plan: merkle::RangePlan<F>,
297 get_node: impl Fn(Position<F>) -> Option<D>,
298 inactivity_floor: Location<F>,
299 ops_root: D,
300 ) -> Result<Self, Error<F>> {
301 let ops_leaves = plan.leaves();
302 let grafting_height = grafting::height::<N>();
303 let inactive_peaks = grafting::chunk_aligned_inactive_peaks::<F>(
304 ops_leaves,
305 inactivity_floor,
306 grafting_height,
307 )?;
308
309 let hasher = qmdb::hasher::<H>();
310 let proof = plan.build(&hasher, inactive_peaks, get_node)?;
311
312 let partial_chunk_digest =
313 partial_chunk::<_, N>(status).map(|(chunk, _)| hasher.digest(chunk.as_slice()));
314
315 let pending_chunk_digest: F::PendingChunk<D> =
316 pending_chunk::<_, _, N>(status, ops_leaves, grafting_height)?
317 .map(|chunk| hasher.digest(chunk.as_slice()))
318 .try_into()
319 .expect("pending_chunk must be consistent with family");
320
321 Ok(Self {
322 proof,
323 pending_chunk_digest,
324 partial_chunk_digest,
325 ops_root,
326 })
327 }
328
329 pub async fn new_with_ops<
339 H: Hasher<Digest = D>,
340 C: Contiguous,
341 S: Storage<F, Digest = D>,
342 const N: usize,
343 >(
344 status: &impl BitmapReadable<N>,
345 storage: &S,
346 log: &C,
347 request: RangeProofSpec<F, D>,
348 ) -> Result<(Self, Vec<C::Item>, Vec<[u8; N]>), Error<F>> {
349 let leaves = Location::new(status.len());
351 if request.start_loc >= leaves {
352 return Err(merkle::Error::RangeOutOfBounds(request.start_loc).into());
353 }
354
355 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
357 let start = *request.start_loc / chunk_bits;
358 if (start as usize) < status.pruned_chunks() {
359 return Err(Error::OperationPruned(request.start_loc));
360 }
361
362 let max_loc = request.start_loc.saturating_add(request.max_ops.get());
363 let end_loc = core::cmp::min(max_loc, leaves);
364
365 let proof = Self::new::<H, S, N>(
367 status,
368 storage,
369 request.inactivity_floor,
370 request.start_loc..end_loc,
371 request.ops_root,
372 )
373 .await?;
374
375 let positions: Vec<u64> = (*request.start_loc..*end_loc).collect();
377 let ops = log.read_many(&positions).await?;
378
379 let end = (*end_loc - 1) / chunk_bits; let chunks = (start..=end)
382 .map(|i| status.get_chunk(i as usize))
383 .collect::<Vec<_>>();
384
385 Ok((proof, ops, chunks))
386 }
387
388 fn reconstruct_root<H, O>(
391 &self,
392 start_loc: Location<F>,
393 ops: &[O],
394 chunks: &[impl AsRef<[u8]>],
395 chunk_size: usize,
396 collected: Option<&mut Vec<(Position<F>, D)>>,
397 ) -> Result<D, merkle::Error<F>>
398 where
399 H: Hasher<Digest = D>,
400 O: Encode,
401 {
402 let chunk_bits = chunk_bits(chunk_size).map_err(|_| merkle::Error::InvalidProof)?;
403 if ops.is_empty() || chunks.is_empty() {
404 debug!("verification failed, empty input");
405 return Err(merkle::Error::InvalidProof);
406 }
407 let Some(end_loc) = start_loc.checked_add(ops.len() as u64) else {
409 debug!("verification failed, end_loc overflow");
410 return Err(merkle::Error::InvalidProof);
411 };
412
413 let leaves = self.proof.leaves;
414 if end_loc > leaves {
415 debug!(
416 loc = ?end_loc,
417 ?leaves, "verification failed, invalid range"
418 );
419 return Err(merkle::Error::InvalidProof);
420 }
421
422 let start_chunk = *start_loc / chunk_bits;
424 let end_chunk = (*end_loc - 1) / chunk_bits;
425 let complete_chunks = *leaves / chunk_bits;
426
427 if (end_chunk - start_chunk + 1) != chunks.len() as u64 {
428 debug!("verification failed, chunk metadata length mismatch");
429 return Err(merkle::Error::InvalidProof);
430 }
431 if chunks
432 .iter()
433 .any(|chunk| chunk.as_ref().len() != chunk_size)
434 {
435 debug!("verification failed, chunk size mismatch");
436 return Err(merkle::Error::InvalidProof);
437 }
438
439 let next_bit = *leaves % chunk_bits;
440 let has_partial_chunk = next_bit != 0;
441
442 let elements = ops.iter().map(|op| op.encode()).collect::<Vec<_>>();
443 let chunk_vec = chunks.iter().map(|c| c.as_ref()).collect::<Vec<_>>();
444 let grafting_height = chunk_bits.trailing_zeros();
445
446 let graftable_chunks =
447 grafting::graftable_chunks::<F>(*leaves, grafting_height).min(complete_chunks);
448 let pending_chunks = complete_chunks - graftable_chunks;
449 if pending_chunks > 1 {
450 debug!(
451 ?complete_chunks,
452 ?graftable_chunks,
453 "verification failed, multiple pending chunks"
454 );
455 return Err(merkle::Error::InvalidProof);
456 }
457 let has_pending_chunk = pending_chunks == 1;
458
459 let grafting_verifier = grafting::Verifier::<F, H>::new(
460 grafting_height,
461 start_chunk,
462 chunk_vec,
463 graftable_chunks,
464 );
465
466 if self.pending_chunk_digest.as_ref().is_some() != has_pending_chunk {
467 debug!(
468 pending_in_proof = self.pending_chunk_digest.as_ref().is_some(),
469 expected = has_pending_chunk,
470 "pending_chunk_digest presence does not match bitmap state"
471 );
472 return Err(merkle::Error::InvalidProof);
473 }
474
475 if has_partial_chunk {
477 let Some(last_chunk_digest) = self.partial_chunk_digest else {
478 debug!("proof has no partial chunk digest");
479 return Err(merkle::Error::InvalidProof);
480 };
481
482 if end_chunk == complete_chunks {
485 let last_chunk = chunks.last().expect("chunks non-empty");
486 if last_chunk_digest != grafting_verifier.digest(last_chunk.as_ref()) {
487 debug!("last chunk digest does not match expected value");
488 return Err(merkle::Error::InvalidProof);
489 }
490 }
491 } else if self.partial_chunk_digest.is_some() {
492 debug!("proof has unexpected partial chunk digest");
493 return Err(merkle::Error::InvalidProof);
494 }
495
496 if let Some(pending_digest) = self.pending_chunk_digest.as_ref() {
500 let pending_idx = graftable_chunks;
501 if pending_idx >= start_chunk && pending_idx <= end_chunk {
502 let local = (pending_idx - start_chunk) as usize;
503 let Some(pending_chunk_bytes) = chunks.get(local) else {
507 debug!(
508 ?pending_idx,
509 chunks_len = chunks.len(),
510 "pending chunk index out of range in supplied chunks"
511 );
512 return Err(merkle::Error::InvalidProof);
513 };
514 if *pending_digest != grafting_verifier.digest(pending_chunk_bytes.as_ref()) {
515 debug!("pending chunk digest does not match expected value");
516 return Err(merkle::Error::InvalidProof);
517 }
518 }
519 }
520
521 let merkle_root = match self.proof.reconstruct_root_inner(
522 &grafting_verifier,
523 &elements,
524 start_loc,
525 collected,
526 ) {
527 Ok(root) => root,
528 Err(error) => {
529 debug!(?error, "invalid proof input");
530 return Err(merkle::Error::InvalidProof);
531 }
532 };
533
534 let partial =
535 has_partial_chunk.then(|| (next_bit, self.partial_chunk_digest.as_ref().unwrap()));
536 Ok(combine_roots::<H>(
537 &self.ops_root,
538 &merkle_root,
539 self.pending_chunk_digest.as_ref(),
540 partial,
541 ))
542 }
543
544 pub fn verify<H: Hasher<Digest = D>, O: Encode, const N: usize>(
547 &self,
548 start_loc: Location<F>,
549 ops: &[O],
550 chunks: &[[u8; N]],
551 root: &H::Digest,
552 ) -> bool {
553 self.verify_with_chunk_size::<H, O>(start_loc, ops, chunks, N, root)
554 }
555
556 pub fn verify_with_chunk_size<H: Hasher<Digest = D>, O: Encode>(
566 &self,
567 start_loc: Location<F>,
568 ops: &[O],
569 chunks: &[impl AsRef<[u8]>],
570 chunk_size: usize,
571 root: &D,
572 ) -> bool {
573 matches!(
574 self.reconstruct_root::<H, O>(start_loc, ops, chunks, chunk_size, None),
575 Ok(reconstructed_root) if reconstructed_root == *root
576 )
577 }
578}
579
580pub fn verify_proof_and_extract_digests<F, Op, H, const N: usize>(
583 proof: &RangeProof<F, H::Digest>,
584 start_loc: Location<F>,
585 operations: &[Op],
586 chunks: &[[u8; N]],
587 target_root: &H::Digest,
588) -> Result<qmdb::verify::ExtractedDigests<F, H>, merkle::Error<F>>
589where
590 F: Graftable,
591 Op: Encode,
592 H: Hasher,
593{
594 let mut collected = Vec::new();
595 let reconstructed_root =
596 proof.reconstruct_root::<H, Op>(start_loc, operations, chunks, N, Some(&mut collected))?;
597 if reconstructed_root != *target_root {
598 debug!("verification failed, root mismatch");
599 return Err(merkle::Error::RootMismatch);
600 }
601
602 Ok(collected)
603}
604
605impl<F: Graftable, D: Digest> Write for RangeProof<F, D> {
606 fn write(&self, buf: &mut impl BufMut) {
607 self.proof.write(buf);
608 self.pending_chunk_digest.write(buf);
609 self.partial_chunk_digest.write(buf);
610 self.ops_root.write(buf);
611 }
612}
613
614impl<F: Graftable, D: Digest> EncodeSize for RangeProof<F, D> {
615 fn encode_size(&self) -> usize {
616 self.proof.encode_size()
617 + self.pending_chunk_digest.encode_size()
618 + self.partial_chunk_digest.encode_size()
619 + self.ops_root.encode_size()
620 }
621}
622
623impl<F: Graftable, D: Digest> Read for RangeProof<F, D> {
624 type Cfg = usize;
626
627 fn read_cfg(
628 buf: &mut impl Buf,
629 max_digests: &Self::Cfg,
630 ) -> Result<Self, commonware_codec::Error> {
631 let proof = Proof::<F, D>::read_cfg(buf, max_digests)?;
632 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
633 let partial_chunk_digest = Option::<D>::read(buf)?;
634 let ops_root = D::read(buf)?;
635 Ok(Self {
636 proof,
637 pending_chunk_digest,
638 partial_chunk_digest,
639 ops_root,
640 })
641 }
642}
643
644#[cfg(feature = "arbitrary")]
645impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for RangeProof<F, D>
646where
647 D: for<'a> arbitrary::Arbitrary<'a>,
648 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
649{
650 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
651 Ok(Self {
652 proof: u.arbitrary()?,
653 pending_chunk_digest: u.arbitrary()?,
654 partial_chunk_digest: u.arbitrary()?,
655 ops_root: u.arbitrary()?,
656 })
657 }
658}
659
660pub mod constant {
662 pub type OperationProof<F, D, const N: usize> = super::operation::Proof<F, D, [u8; N]>;
664}
665
666pub mod dynamic {
668 pub type OperationProof<F, D> = super::operation::Proof<F, D, bytes::Bytes>;
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705 use crate::{
706 merkle::{conformance::build_test_mem, hasher::Standard as StandardHasher, mem::Mem},
707 mmb, mmr,
708 qmdb::current::{db, grafting},
709 };
710 use bytes::Bytes;
711 use commonware_codec::{Decode as _, DecodeExt as _};
712 use commonware_cryptography::{Sha256, sha256};
713 use commonware_macros::test_async;
714 use commonware_parallel::{Sequential, Strategy};
715 use commonware_utils::{
716 Widen,
717 bitmap::{Prunable as BitMap, Readable as BitmapReadable},
718 sync::Mutex,
719 };
720 use core::ops::Range;
721 use std::collections::{BTreeMap, BTreeSet};
722
723 async fn compute_grafted_leaves<
729 F: merkle::Graftable,
730 H: Hasher,
731 S: Strategy,
732 const N: usize,
733 >(
734 ops_tree: &impl Storage<F, Digest = H::Digest>,
735 chunks: impl IntoIterator<Item = (usize, [u8; N])>,
736 strategy: &S,
737 ) -> Result<Vec<(usize, H::Digest)>, Error<F>> {
738 let inputs = db::read_graft_inputs::<F, _, N>(ops_tree, chunks).await?;
739 Ok(grafting::graft_chunk_digests::<H, _, N>(strategy, inputs))
740 }
741
742 #[test]
743 fn test_ops_root_witness_codec_roundtrip() {
744 type F = mmb::Family;
745 for partial_chunk in [
746 None,
747 Some((0u64, Sha256::hash(&[b"partial-zero"]))),
748 Some((123u64, Sha256::hash(&[b"partial-nonzero"]))),
749 ] {
750 let witness: OpsRootWitness<F, _> = OpsRootWitness {
751 grafted_root: Sha256::hash(&[b"grafted"]),
752 pending_chunk_digest: None,
753 partial_chunk,
754 };
755 let encoded = witness.encode();
756 assert_eq!(encoded.len(), witness.encode_size());
757 let decoded = OpsRootWitness::<F, sha256::Digest>::decode(encoded).unwrap();
758 assert_eq!(decoded, witness);
759 }
760 }
761
762 #[test]
763 fn test_ops_root_witness_root_matches_verify() {
764 type F = mmb::Family;
765
766 let ops_root = Sha256::hash(&[b"ops root"]);
767 let witness: OpsRootWitness<F, _> = OpsRootWitness {
768 grafted_root: Sha256::hash(&[b"grafted root"]),
769 pending_chunk_digest: Some(Sha256::hash(&[b"pending chunk"])),
770 partial_chunk: Some((13, Sha256::hash(&[b"partial chunk"]))),
771 };
772
773 let root = witness.root::<Sha256>(&ops_root);
774
775 assert!(witness.verify::<Sha256>(&ops_root, &root));
776 assert_ne!(root, ops_root);
777
778 let wrong_ops_root = Sha256::hash(&[b"wrong ops root"]);
779 assert!(!witness.verify::<Sha256>(&wrong_ops_root, &root));
780 }
781
782 fn range_proof_digest_count<F: Graftable, D: Digest>(proof: &RangeProof<F, D>) -> usize {
783 proof.proof.digests.len()
784 }
785
786 #[test]
787 fn test_range_proof_codec_roundtrip() {
788 type F = mmb::Family;
789 const MAX_DIGESTS: usize = 64;
790
791 let proof = Proof::<F, sha256::Digest> {
792 leaves: mmb::Location::new(42),
793 inactive_peaks: 0,
794 digests: vec![
795 Sha256::hash(&[b"d0"]),
796 Sha256::hash(&[b"d1"]),
797 Sha256::hash(&[b"d2"]),
798 ],
799 };
800 let ops_root = Sha256::hash(&[b"ops-root"]);
801
802 let cases = [
803 RangeProof {
805 proof: proof.clone(),
806 pending_chunk_digest: None,
807 partial_chunk_digest: None,
808 ops_root,
809 },
810 RangeProof {
812 proof,
813 pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
814 partial_chunk_digest: Some(Sha256::hash(&[b"partial"])),
815 ops_root,
816 },
817 RangeProof {
819 proof: Proof::<F, sha256::Digest>::default(),
820 pending_chunk_digest: None,
821 partial_chunk_digest: Some(Sha256::hash(&[b"only-partial"])),
822 ops_root,
823 },
824 ];
825
826 for proof in cases {
827 let encoded = proof.encode();
828 assert_eq!(encoded.len(), proof.encode_size());
829 let decoded =
830 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
831 assert_eq!(decoded, proof);
832 }
833 }
834
835 #[test]
836 fn test_range_proof_codec_enforces_merkle_digest_budget() {
837 type F = mmb::Family;
838
839 let proof = RangeProof {
840 proof: Proof::<F, sha256::Digest> {
841 leaves: mmb::Location::new(42),
842 inactive_peaks: 0,
843 digests: vec![Sha256::hash(&[b"d0"])],
844 },
845 pending_chunk_digest: None,
846 partial_chunk_digest: None,
847 ops_root: Sha256::hash(&[b"ops-root"]),
848 };
849
850 let encoded = proof.encode();
851 let total_digests = range_proof_digest_count(&proof);
852
853 let decoded =
854 RangeProof::<F, sha256::Digest>::decode_cfg(encoded.clone(), &total_digests).unwrap();
855 assert_eq!(decoded, proof);
856 assert!(
857 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &(total_digests - 1)).is_err()
858 );
859 }
860
861 #[test]
862 fn test_range_proof_decode_rejects_pending_for_mmr() {
863 const MAX_DIGESTS: usize = 64;
864
865 let proof = RangeProof {
866 proof: Proof::<mmb::Family, sha256::Digest> {
867 leaves: mmb::Location::new(42),
868 inactive_peaks: 0,
869 digests: vec![Sha256::hash(&[b"d0"])],
870 },
871 pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
872 partial_chunk_digest: None,
873 ops_root: Sha256::hash(&[b"ops-root"]),
874 };
875 let encoded = proof.encode();
876
877 assert!(
879 RangeProof::<mmb::Family, sha256::Digest>::decode_cfg(encoded.clone(), &MAX_DIGESTS)
880 .is_ok()
881 );
882
883 assert!(
885 RangeProof::<crate::merkle::mmr::Family, sha256::Digest>::decode_cfg(
886 encoded,
887 &MAX_DIGESTS
888 )
889 .is_err()
890 );
891 }
892
893 #[test]
894 fn test_operation_proof_codec_roundtrip() {
895 type F = mmb::Family;
896 const N: usize = 32;
897 const MAX_DIGESTS: usize = 64;
898
899 let range_proof = RangeProof {
900 proof: Proof::<F, sha256::Digest> {
901 leaves: mmb::Location::new(7),
902 inactive_peaks: 0,
903 digests: vec![Sha256::hash(&[b"sib"])],
904 },
905 pending_chunk_digest: None,
906 partial_chunk_digest: None,
907 ops_root: Sha256::hash(&[b"ops"]),
908 };
909
910 let chunk: [u8; N] = core::array::from_fn(|i| i as u8);
911
912 let proof = constant::OperationProof::<F, sha256::Digest, N> {
913 loc: mmb::Location::new(5),
914 chunk,
915 range_proof,
916 };
917
918 let encoded = proof.encode();
919 assert_eq!(encoded.len(), proof.encode_size());
920 let decoded =
921 constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &MAX_DIGESTS)
922 .unwrap();
923 assert_eq!(decoded, proof);
924 }
925
926 #[test]
927 fn test_operation_proof_codec_enforces_merkle_digest_budget() {
928 type F = mmb::Family;
929 const N: usize = 32;
930
931 let range_proof = RangeProof {
932 proof: Proof::<F, sha256::Digest> {
933 leaves: mmb::Location::new(7),
934 inactive_peaks: 0,
935 digests: vec![Sha256::hash(&[b"sib"])],
936 },
937 pending_chunk_digest: None,
938 partial_chunk_digest: None,
939 ops_root: Sha256::hash(&[b"ops"]),
940 };
941 let total_digests = range_proof_digest_count(&range_proof);
942 let proof = constant::OperationProof::<F, sha256::Digest, N> {
943 loc: mmb::Location::new(5),
944 chunk: core::array::from_fn(|i| i as u8),
945 range_proof,
946 };
947
948 let encoded = proof.encode();
949 let decoded = constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(
950 encoded.clone(),
951 &total_digests,
952 )
953 .unwrap();
954 assert_eq!(decoded, proof);
955 assert!(
956 constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(
957 encoded,
958 &(total_digests - 1)
959 )
960 .is_err()
961 );
962 }
963
964 #[test_async]
965 async fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
966 type F = mmb::Family;
967 const N: usize = 1;
968
969 let hasher = qmdb::hasher::<Sha256>();
970 let grafting_height = grafting::height::<N>();
971
972 let leaf_count = (16..=64u64)
973 .find(|&leaves| {
974 let size = F::location_to_position(mmb::Location::new(leaves));
975 F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
976 })
977 .expect("expected an MMB size whose second chunk spans multiple peaks");
978
979 let mut status = BitMap::<N>::new();
980 for _ in 0..leaf_count {
981 status.push(true);
982 }
983 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
984 let ops_root = ops.root(&hasher, 0).unwrap();
985
986 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
987 *Location::<F>::try_from(ops.size()).unwrap(),
988 grafting_height,
989 )
990 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
991 as usize;
992 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
993 .map(|chunk_idx| {
994 (
995 chunk_idx,
996 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
997 )
998 })
999 .collect();
1000 let mut leaf_digests =
1001 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1002 .await
1003 .unwrap();
1004 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1005
1006 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1007 let mut grafted = Mem::<F, sha256::Digest>::new();
1008 let merkleized = {
1009 let mut batch = grafted.new_batch();
1010 for (_, digest) in leaf_digests {
1011 batch = batch.add_leaf_digest(digest);
1012 }
1013 batch.merkleize(&grafted, &grafted_hasher)
1014 };
1015 grafted.apply_batch(&merkleized).unwrap();
1016
1017 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1018 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1019 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1020 &status,
1021 &storage,
1022 ops_leaves_for_root,
1023 Location::new(0),
1024 )
1025 .await
1026 .unwrap()
1027 .root::<Sha256>(&ops_root);
1028
1029 let loc = mmb::Location::new(BitMap::<N>::CHUNK_SIZE_BITS + 4);
1030 let proof = RangeProof::new::<Sha256, _, N>(
1031 &status,
1032 &storage,
1033 Location::new(0),
1034 loc..loc + 1,
1035 ops_root,
1036 )
1037 .await
1038 .unwrap();
1039
1040 let element = hasher.digest(&(*loc).to_be_bytes());
1041 assert!(proof.verify::<Sha256, _, N>(
1042 loc,
1043 &[element],
1044 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 1)],
1045 &root,
1046 ));
1047 }
1048
1049 #[test_async]
1050 async fn test_range_proof_verifies_with_partial_suffix_mmb() {
1051 type F = mmb::Family;
1052 const N: usize = 1;
1053
1054 let hasher = qmdb::hasher::<Sha256>();
1055 let grafting_height = grafting::height::<N>();
1056 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1057
1058 let (leaf_count, loc) = (chunk_bits * 2 + 1..=64u64)
1059 .find_map(|leaves| {
1060 let complete_chunks = leaves / chunk_bits;
1061 if complete_chunks < 2 || leaves % chunk_bits == 0 {
1062 return None;
1063 }
1064
1065 let size = F::location_to_position(mmb::Location::new(leaves));
1066 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1067 Some((leaves, mmb::Location::new(chunk_bits + 1)))
1068 })
1069 .expect("expected an MMB proof with a partial trailing suffix chunk");
1070
1071 let mut status = BitMap::<N>::new();
1072 for _ in 0..leaf_count {
1073 status.push(true);
1074 }
1075 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1076 let ops_root = ops.root(&hasher, 0).unwrap();
1077
1078 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1079 *Location::<F>::try_from(ops.size()).unwrap(),
1080 grafting_height,
1081 )
1082 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1083 as usize;
1084 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1085 .map(|chunk_idx| {
1086 (
1087 chunk_idx,
1088 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1089 )
1090 })
1091 .collect();
1092 let mut leaf_digests =
1093 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1094 .await
1095 .unwrap();
1096 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1097
1098 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1099 let mut grafted = Mem::<F, sha256::Digest>::new();
1100 let merkleized = {
1101 let mut batch = grafted.new_batch();
1102 for (_, digest) in leaf_digests {
1103 batch = batch.add_leaf_digest(digest);
1104 }
1105 batch.merkleize(&grafted, &grafted_hasher)
1106 };
1107 grafted.apply_batch(&merkleized).unwrap();
1108
1109 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1110 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1111 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1112 &status,
1113 &storage,
1114 ops_leaves_for_root,
1115 Location::new(0),
1116 )
1117 .await
1118 .unwrap()
1119 .root::<Sha256>(&ops_root);
1120 let proof = RangeProof::new::<Sha256, _, N>(
1121 &status,
1122 &storage,
1123 Location::new(0),
1124 loc..loc + 1,
1125 ops_root,
1126 )
1127 .await
1128 .unwrap();
1129
1130 let element = hasher.digest(&(*loc).to_be_bytes());
1131 let chunk_idx = (*loc / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
1132 assert!(proof.verify::<Sha256, _, N>(
1133 loc,
1134 &[element],
1135 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1136 &status, chunk_idx
1137 )],
1138 &root,
1139 ));
1140 }
1141
1142 #[test_async]
1143 async fn test_range_proof_verifies_when_range_reaches_partial_chunk_mmb() {
1144 type F = mmb::Family;
1145 const N: usize = 1;
1146
1147 let hasher = qmdb::hasher::<Sha256>();
1148 let grafting_height = grafting::height::<N>();
1149 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1150
1151 let (leaf_count, start_loc, complete_chunks) = (17..=128u64)
1155 .find_map(|leaves| {
1156 let complete_chunks = leaves / chunk_bits;
1157 if complete_chunks < 2 || leaves % chunk_bits == 0 {
1158 return None;
1159 }
1160 let leaves_loc = mmb::Location::new(leaves);
1161 let size = F::location_to_position(leaves_loc);
1162 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1163 let start_loc = mmb::Location::new(chunk_bits + 1);
1164 Some((leaves, start_loc, complete_chunks))
1165 })
1166 .expect("expected an MMB size with chunk 1 multi-peak and a partial trailing chunk");
1167
1168 let mut status = BitMap::<N>::new();
1169 for _ in 0..leaf_count {
1170 status.push(true);
1171 }
1172 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1173 let ops_root = ops.root(&hasher, 0).unwrap();
1174
1175 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1176 *Location::<F>::try_from(ops.size()).unwrap(),
1177 grafting_height,
1178 )
1179 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1180 as usize;
1181 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1182 .map(|chunk_idx| {
1183 (
1184 chunk_idx,
1185 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1186 )
1187 })
1188 .collect();
1189 let mut leaf_digests =
1190 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1191 .await
1192 .unwrap();
1193 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1194
1195 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1196 let mut grafted = Mem::<F, sha256::Digest>::new();
1197 let merkleized = {
1198 let mut batch = grafted.new_batch();
1199 for (_, digest) in leaf_digests {
1200 batch = batch.add_leaf_digest(digest);
1201 }
1202 batch.merkleize(&grafted, &grafted_hasher)
1203 };
1204 grafted.apply_batch(&merkleized).unwrap();
1205
1206 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1207 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1208 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1209 &status,
1210 &storage,
1211 ops_leaves_for_root,
1212 Location::new(0),
1213 )
1214 .await
1215 .unwrap()
1216 .root::<Sha256>(&ops_root);
1217
1218 let leaves_loc = mmb::Location::new(leaf_count);
1219 let proof = RangeProof::new::<Sha256, _, N>(
1220 &status,
1221 &storage,
1222 Location::new(0),
1223 start_loc..leaves_loc,
1224 ops_root,
1225 )
1226 .await
1227 .unwrap();
1228
1229 let elements = (*start_loc..leaf_count)
1230 .map(|idx| hasher.digest(&idx.to_be_bytes()))
1231 .collect::<Vec<_>>();
1232 let start_chunk_idx = (*start_loc / chunk_bits) as usize;
1233 let end_chunk_idx = complete_chunks as usize;
1234 let chunks = (start_chunk_idx..=end_chunk_idx)
1235 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1236 .collect::<Vec<_>>();
1237 assert!(proof.verify::<Sha256, _, N>(start_loc, &elements, &chunks, &root,));
1238
1239 let mut bad_chunks = chunks;
1241 let last = bad_chunks.last_mut().unwrap();
1242 last[0] ^= 1;
1243 assert!(
1244 !proof.verify::<Sha256, _, N>(start_loc, &elements, &bad_chunks, &root),
1245 "tampered partial chunk bytes should not verify"
1246 );
1247 }
1248
1249 #[test_async]
1250 async fn test_range_proof_rejects_unexpected_partial_chunk_digest() {
1251 type F = mmb::Family;
1252 const N: usize = 1;
1253
1254 let hasher = qmdb::hasher::<Sha256>();
1255 let grafting_height = grafting::height::<N>();
1256 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1257
1258 let leaf_count = chunk_bits * 2; let mut status = BitMap::<N>::new();
1260 for _ in 0..leaf_count {
1261 status.push(true);
1262 }
1263 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1264 let ops_root = ops.root(&hasher, 0).unwrap();
1265
1266 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1267 *Location::<F>::try_from(ops.size()).unwrap(),
1268 grafting_height,
1269 )
1270 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1271 as usize;
1272 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1273 .map(|chunk_idx| {
1274 (
1275 chunk_idx,
1276 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1277 )
1278 })
1279 .collect();
1280 let mut leaf_digests =
1281 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1282 .await
1283 .unwrap();
1284 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1285
1286 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1287 let mut grafted = Mem::<F, sha256::Digest>::new();
1288 let merkleized = {
1289 let mut batch = grafted.new_batch();
1290 for (_, digest) in leaf_digests {
1291 batch = batch.add_leaf_digest(digest);
1292 }
1293 batch.merkleize(&grafted, &grafted_hasher)
1294 };
1295 grafted.apply_batch(&merkleized).unwrap();
1296
1297 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1298 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1299 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1300 &status,
1301 &storage,
1302 ops_leaves_for_root,
1303 Location::new(0),
1304 )
1305 .await
1306 .unwrap()
1307 .root::<Sha256>(&ops_root);
1308
1309 let loc = mmb::Location::new(0);
1310 let mut proof = RangeProof::new::<Sha256, _, N>(
1311 &status,
1312 &storage,
1313 Location::new(0),
1314 loc..loc + 1,
1315 ops_root,
1316 )
1317 .await
1318 .unwrap();
1319
1320 let element = hasher.digest(&(*loc).to_be_bytes());
1321 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
1322
1323 let mut tampered = proof.clone();
1325 tampered.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1326 assert!(!tampered.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1327
1328 proof.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1329 assert!(!proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1330 }
1331
1332 async fn current_range_proof_fixture<F: Graftable, const N: usize>(
1333 leaf_count: u64,
1334 range: Range<Location<F>>,
1335 ) -> (
1336 StandardHasher<Sha256>,
1337 RangeProof<F, sha256::Digest>,
1338 Vec<sha256::Digest>,
1339 Vec<[u8; N]>,
1340 sha256::Digest,
1341 Mem<F, sha256::Digest>,
1342 ) {
1343 let hasher = qmdb::hasher::<Sha256>();
1344 let grafting_height = grafting::height::<N>();
1345 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1346
1347 let mut status = BitMap::<N>::new();
1348 for _ in 0..leaf_count {
1349 status.push(true);
1350 }
1351
1352 let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaf_count);
1353 let ops_root = ops.root(&hasher, 0).unwrap();
1354 let ops_leaves = Location::<F>::try_from(ops.size()).unwrap();
1355
1356 let graftable_chunks_for_test =
1357 grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1358 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1359 as usize;
1360 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1361 .map(|chunk_idx| {
1362 (
1363 chunk_idx,
1364 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1365 )
1366 })
1367 .collect();
1368 let mut leaf_digests =
1369 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1370 .await
1371 .unwrap();
1372 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1373
1374 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1375 let mut grafted = Mem::<F, sha256::Digest>::new();
1376 if !leaf_digests.is_empty() {
1377 let merkleized = {
1378 let mut batch = grafted.new_batch();
1379 for (_, digest) in leaf_digests {
1380 batch = batch.add_leaf_digest(digest);
1381 }
1382 batch.merkleize(&grafted, &grafted_hasher)
1383 };
1384 grafted.apply_batch(&merkleized).unwrap();
1385 }
1386
1387 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1388 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1389 &status,
1390 &storage,
1391 ops_leaves,
1392 Location::new(0),
1393 )
1394 .await
1395 .unwrap()
1396 .root::<Sha256>(&ops_root);
1397
1398 let proof = RangeProof::new::<Sha256, _, N>(
1399 &status,
1400 &storage,
1401 Location::new(0),
1402 range.clone(),
1403 ops_root,
1404 )
1405 .await
1406 .unwrap();
1407 let operations = (*range.start..*range.end)
1408 .map(|i| hasher.digest(&i.to_be_bytes()))
1409 .collect::<Vec<_>>();
1410
1411 let start_chunk = (*range.start / chunk_bits) as usize;
1413 let end_chunk = ((*range.end - 1) / chunk_bits) as usize;
1414 let chunks = (start_chunk..=end_chunk)
1415 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1416 .collect::<Vec<_>>();
1417
1418 assert!(proof.verify::<Sha256, _, N>(range.start, &operations, &chunks, &root));
1419
1420 (hasher, proof, operations, chunks, root, ops)
1421 }
1422
1423 async fn verify_proof_and_extract_digests_inner<F: Graftable>() {
1424 const N: usize = 1;
1425 let start = Location::<F>::new(14);
1426 let end = Location::<F>::new(18);
1427 let (hasher, proof, operations, chunks, root, ops) =
1428 current_range_proof_fixture::<F, N>(18, start..end).await;
1429
1430 let extracted = verify_proof_and_extract_digests::<F, _, Sha256, N>(
1431 &proof,
1432 start,
1433 &operations,
1434 &chunks,
1435 &root,
1436 )
1437 .unwrap();
1438 assert!(!extracted.is_empty());
1439
1440 for loc in *start..*end {
1442 let pos = F::location_to_position(Location::<F>::new(loc));
1443 let expected = ops.get_node(pos).unwrap();
1444 assert!(
1445 extracted
1446 .iter()
1447 .any(|(actual_pos, actual)| *actual_pos == pos && *actual == expected),
1448 "missing extracted leaf digest at {pos:?}",
1449 );
1450 }
1451
1452 let wrong_root = hasher.digest(b"wrong current root");
1454 assert!(matches!(
1455 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1456 &proof,
1457 start,
1458 &operations,
1459 &chunks,
1460 &wrong_root,
1461 ),
1462 Err(merkle::Error::RootMismatch)
1463 ));
1464
1465 let mut wrong_operations = operations.clone();
1467 wrong_operations[0] = hasher.digest(b"wrong operation");
1468 assert!(
1469 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1470 &proof,
1471 start,
1472 &wrong_operations,
1473 &chunks,
1474 &root,
1475 )
1476 .is_err()
1477 );
1478
1479 let mut bad_chunks = chunks;
1480 bad_chunks.last_mut().unwrap()[0] ^= 1;
1481 assert!(
1482 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1483 &proof,
1484 start,
1485 &operations,
1486 &bad_chunks,
1487 &root,
1488 )
1489 .is_err()
1490 );
1491 }
1492
1493 #[test_async]
1494 async fn test_verify_proof_and_extract_digests_handles_no_grafted_chunks_mmb() {
1495 type F = mmb::Family;
1496 const N: usize = 1;
1497 let start = Location::<F>::new(2);
1498 let end = Location::<F>::new(4);
1499 let (_, proof, operations, chunks, root, _ops) =
1500 current_range_proof_fixture::<F, N>(6, start..end).await;
1501
1502 let extracted = verify_proof_and_extract_digests::<F, _, Sha256, N>(
1503 &proof,
1504 start,
1505 &operations,
1506 &chunks,
1507 &root,
1508 )
1509 .unwrap();
1510
1511 assert!(!extracted.is_empty());
1512 }
1513
1514 #[test_async]
1515 async fn test_verify_proof_and_extract_digests_rejects_malformed_inputs_mmb() {
1516 type F = mmb::Family;
1517 const N: usize = 1;
1518 let start = Location::<F>::new(14);
1519 let end = Location::<F>::new(18);
1520 let (_, proof, operations, chunks, root, _ops) =
1521 current_range_proof_fixture::<F, N>(18, start..end).await;
1522
1523 let no_operations = Vec::<sha256::Digest>::new();
1524 assert!(matches!(
1525 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1526 &proof,
1527 start,
1528 &no_operations,
1529 &chunks,
1530 &root,
1531 ),
1532 Err(merkle::Error::InvalidProof)
1533 ));
1534
1535 let no_chunks = Vec::<[u8; N]>::new();
1536 assert!(matches!(
1537 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1538 &proof,
1539 start,
1540 &operations[..1],
1541 &no_chunks,
1542 &root,
1543 ),
1544 Err(merkle::Error::InvalidProof)
1545 ));
1546
1547 assert!(matches!(
1548 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1549 &proof,
1550 F::MAX_LEAVES,
1551 &operations[..1],
1552 &chunks,
1553 &root,
1554 ),
1555 Err(merkle::Error::InvalidProof)
1556 ));
1557
1558 assert!(matches!(
1559 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1560 &proof,
1561 proof.proof.leaves,
1562 &operations[..1],
1563 &chunks,
1564 &root,
1565 ),
1566 Err(merkle::Error::InvalidProof)
1567 ));
1568
1569 assert!(matches!(
1570 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1571 &proof,
1572 start,
1573 &operations,
1574 &chunks[..chunks.len() - 1],
1575 &root,
1576 ),
1577 Err(merkle::Error::InvalidProof)
1578 ));
1579
1580 let mut missing_partial = proof.clone();
1581 missing_partial.partial_chunk_digest = None;
1582 assert!(matches!(
1583 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1584 &missing_partial,
1585 start,
1586 &operations,
1587 &chunks,
1588 &root,
1589 ),
1590 Err(merkle::Error::InvalidProof)
1591 ));
1592
1593 let mut broken_merkle = proof;
1594 assert!(!broken_merkle.proof.digests.is_empty());
1595 broken_merkle.proof.digests.clear();
1596 assert!(matches!(
1597 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1598 &broken_merkle,
1599 start,
1600 &operations,
1601 &chunks,
1602 &root,
1603 ),
1604 Err(merkle::Error::InvalidProof)
1605 ));
1606 }
1607
1608 #[test_async]
1609 async fn test_verify_proof_and_extract_digests_rejects_metadata_mismatches_mmb() {
1610 type F = mmb::Family;
1611 const N: usize = 1;
1612 let start = Location::<F>::new(14);
1613 let end = Location::<F>::new(18);
1614 let (hasher, proof, operations, chunks, root, _ops) =
1615 current_range_proof_fixture::<F, N>(18, start..end).await;
1616
1617 assert!(proof.pending_chunk_digest.is_some());
1618
1619 let mut missing_pending = proof.clone();
1620 missing_pending.pending_chunk_digest = None;
1621 assert!(matches!(
1622 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1623 &missing_pending,
1624 start,
1625 &operations,
1626 &chunks,
1627 &root,
1628 ),
1629 Err(merkle::Error::InvalidProof)
1630 ));
1631
1632 let mut wrong_pending = proof.clone();
1633 wrong_pending.pending_chunk_digest = Some(hasher.digest(b"wrong pending"));
1634 assert!(matches!(
1635 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1636 &wrong_pending,
1637 start,
1638 &operations,
1639 &chunks,
1640 &root,
1641 ),
1642 Err(merkle::Error::InvalidProof)
1643 ));
1644
1645 let aligned_start = Location::<F>::new(8);
1646 let aligned_end = Location::<F>::new(12);
1647 let (hasher, proof, operations, chunks, root, _ops) =
1648 current_range_proof_fixture::<F, N>(16, aligned_start..aligned_end).await;
1649 assert!(proof.partial_chunk_digest.is_none());
1650
1651 let mut unexpected_partial = proof;
1653 unexpected_partial.partial_chunk_digest = Some(hasher.digest(b"unexpected partial"));
1654 assert!(matches!(
1655 verify_proof_and_extract_digests::<F, _, Sha256, N>(
1656 &unexpected_partial,
1657 aligned_start,
1658 &operations,
1659 &chunks,
1660 &root,
1661 ),
1662 Err(merkle::Error::InvalidProof)
1663 ));
1664 }
1665
1666 #[test_async]
1667 async fn test_verify_proof_and_extract_digests_mmr() {
1668 verify_proof_and_extract_digests_inner::<mmr::Family>().await;
1669 }
1670
1671 #[test_async]
1672 async fn test_verify_proof_and_extract_digests_mmb() {
1673 verify_proof_and_extract_digests_inner::<mmb::Family>().await;
1674 }
1675
1676 #[test_async]
1681 async fn test_graftable_chunks_always_single_peak_at_pending_sizes() {
1682 type F = mmb::Family;
1683 const N: usize = 1;
1684
1685 let hasher = qmdb::hasher::<Sha256>();
1686 let grafting_height = grafting::height::<N>();
1687 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1688
1689 let mut found_any_pending = false;
1690 for leaves in chunk_bits * 3..=128u64 {
1691 let leaves_loc = mmb::Location::new(leaves);
1692 let leaves_count = *leaves_loc;
1693 let complete = leaves_count / chunk_bits;
1694 let graftable =
1695 grafting::graftable_chunks::<F>(leaves_count, grafting_height).min(complete);
1696 if graftable == complete {
1697 continue; }
1699 found_any_pending = true;
1700
1701 let size = F::location_to_position(leaves_loc);
1704 for chunk_idx in 0..graftable {
1705 let count = F::chunk_peaks(size, chunk_idx, grafting_height).count();
1706 assert_eq!(
1707 count, 1,
1708 "graftable chunk {chunk_idx} has {count} peaks (leaves={leaves_count}, graftable={graftable}, complete={complete})"
1709 );
1710 }
1711 }
1712 assert!(
1713 found_any_pending,
1714 "expected at least one MMB size in [{}, 128] with a pending chunk",
1715 chunk_bits * 3
1716 );
1717
1718 let leaf_count = (chunk_bits * 2..=256u64)
1721 .filter(|leaves| leaves % chunk_bits == 0)
1722 .find(|&leaves| {
1723 let size = F::location_to_position(mmb::Location::new(leaves));
1724 F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
1725 })
1726 .expect("expected a chunk-aligned MMB size whose chunk 1 is multi-peak");
1727 let loc = mmb::Location::new(chunk_bits + 1);
1728
1729 let mut status = BitMap::<N>::new();
1730 for _ in 0..leaf_count {
1731 status.push(true);
1732 }
1733 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1734 let ops_root = ops.root(&hasher, 0).unwrap();
1735
1736 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1737 *Location::<F>::try_from(ops.size()).unwrap(),
1738 grafting_height,
1739 )
1740 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1741 as usize;
1742 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1743 .map(|chunk_idx| {
1744 (
1745 chunk_idx,
1746 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1747 )
1748 })
1749 .collect();
1750 let mut leaf_digests =
1751 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1752 .await
1753 .unwrap();
1754 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1755
1756 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1757 let mut grafted = Mem::<F, sha256::Digest>::new();
1758 let merkleized = {
1759 let mut batch = grafted.new_batch();
1760 for (_, digest) in leaf_digests {
1761 batch = batch.add_leaf_digest(digest);
1762 }
1763 batch.merkleize(&grafted, &grafted_hasher)
1764 };
1765 grafted.apply_batch(&merkleized).unwrap();
1766
1767 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1768 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1769 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1770 &status,
1771 &storage,
1772 ops_leaves_for_root,
1773 Location::new(0),
1774 )
1775 .await
1776 .unwrap()
1777 .root::<Sha256>(&ops_root);
1778 let proof = RangeProof::new::<Sha256, _, N>(
1779 &status,
1780 &storage,
1781 Location::new(0),
1782 loc..loc + 1,
1783 ops_root,
1784 )
1785 .await
1786 .unwrap();
1787
1788 let element = hasher.digest(&(*loc).to_be_bytes());
1789 let chunk_idx = (*loc / chunk_bits) as usize;
1790 assert!(proof.verify::<Sha256, _, N>(
1791 loc,
1792 &[element],
1793 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1794 &status, chunk_idx
1795 )],
1796 &root,
1797 ));
1798
1799 let mut tampered = proof.clone();
1800 tampered.proof.inactive_peaks = 1;
1801 assert!(!tampered.verify::<Sha256, _, N>(
1802 loc,
1803 &[element],
1804 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1805 &status, chunk_idx
1806 )],
1807 &root,
1808 ));
1809
1810 let mut tampered = proof.clone();
1811 tampered.proof.inactive_peaks = usize::MAX;
1812 assert!(!tampered.verify::<Sha256, _, N>(
1813 loc,
1814 &[element],
1815 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1816 &status, chunk_idx
1817 )],
1818 &root,
1819 ));
1820
1821 let mut tampered = proof;
1822 assert!(!tampered.proof.digests.is_empty());
1823 tampered.proof.digests[0] = hasher.digest(b"fake generic sibling");
1824 assert!(!tampered.verify::<Sha256, _, N>(
1825 loc,
1826 &[element],
1827 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1828 &status, chunk_idx
1829 )],
1830 &root,
1831 ));
1832 }
1833
1834 #[test_async]
1840 async fn test_pending_and_partial_coexist_at_g_3() {
1841 type F = mmb::Family;
1842 const N: usize = 1; let hasher = qmdb::hasher::<Sha256>();
1845 let grafting_height = grafting::height::<N>();
1846 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1847 assert_eq!(grafting_height, 3);
1848 assert_eq!(chunk_bits, 8);
1849
1850 for k in 1u64..=2 {
1853 let leaf_count = chunk_bits + k;
1854 let mut status = BitMap::<N>::new();
1855 for _ in 0..leaf_count {
1856 status.push(true);
1857 }
1858 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1859 let ops_root = ops.root(&hasher, 0).unwrap();
1860
1861 let complete = <BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64;
1862 let graftable =
1863 grafting::graftable_chunks::<F>(leaf_count, grafting_height).min(complete);
1864 let next_bit = leaf_count % chunk_bits;
1865 assert_eq!(complete, 1);
1866 assert_eq!(graftable, 0);
1867 assert!(next_bit > 0, "expected partial chunk for k={k}");
1868
1869 let chunk_inputs: Vec<_> = (0..graftable as usize)
1872 .map(|chunk_idx| {
1873 (
1874 chunk_idx,
1875 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1876 )
1877 })
1878 .collect();
1879 let leaf_digests =
1880 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1881 .await
1882 .unwrap();
1883 let grafted_hasher =
1884 grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1885 let mut grafted = Mem::<F, sha256::Digest>::new();
1886 if !leaf_digests.is_empty() {
1887 let merkleized = {
1888 let mut batch = grafted.new_batch();
1889 for (_, digest) in leaf_digests {
1890 batch = batch.add_leaf_digest(digest);
1891 }
1892 batch.merkleize(&grafted, &grafted_hasher)
1893 };
1894 grafted.apply_batch(&merkleized).unwrap();
1895 }
1896 let storage =
1897 grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1898
1899 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1900 let witness = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1901 &status,
1902 &storage,
1903 ops_leaves_for_root,
1904 Location::new(0),
1905 )
1906 .await
1907 .unwrap();
1908 let canonical_root = witness.root::<Sha256>(&ops_root);
1909 assert!(
1910 witness.pending_chunk_digest.is_some(),
1911 "expected pending chunk at k={k}"
1912 );
1913 assert!(
1914 witness.partial_chunk.is_some(),
1915 "expected partial chunk at k={k}"
1916 );
1917
1918 let start = mmb::Location::new(0);
1920 let end = mmb::Location::new(leaf_count);
1921 let proof = RangeProof::new::<Sha256, _, N>(
1922 &status,
1923 &storage,
1924 Location::new(0),
1925 start..end,
1926 ops_root,
1927 )
1928 .await
1929 .unwrap();
1930 assert!(
1931 proof.pending_chunk_digest.is_some(),
1932 "expected RangeProof pending_chunk_digest at k={k}"
1933 );
1934 assert!(
1935 proof.partial_chunk_digest.is_some(),
1936 "expected RangeProof partial_chunk_digest at k={k}"
1937 );
1938
1939 let elements: Vec<sha256::Digest> = (0..leaf_count)
1940 .map(|i| hasher.digest(&i.to_be_bytes()))
1941 .collect();
1942 let chunks: Vec<[u8; N]> = (0..=1)
1944 .map(|i| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, i))
1945 .collect();
1946 assert!(
1947 proof.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1948 "RangeProof verify failed at k={k}"
1949 );
1950
1951 let pending_loc = mmb::Location::new(3);
1952 let pending_proof = RangeProof::new::<Sha256, _, N>(
1953 &status,
1954 &storage,
1955 Location::new(0),
1956 pending_loc..pending_loc + 1,
1957 ops_root,
1958 )
1959 .await
1960 .unwrap();
1961 assert!(
1962 pending_proof.pending_chunk_digest.is_some(),
1963 "expected single-element proof to carry pending chunk digest at k={k}"
1964 );
1965 let pending_element = hasher.digest(&(*pending_loc).to_be_bytes());
1966 assert!(
1967 pending_proof.verify::<Sha256, _, N>(
1968 pending_loc,
1969 &[pending_element],
1970 &[chunks[0]],
1971 &canonical_root,
1972 ),
1973 "single-element proof inside pending chunk failed at k={k}"
1974 );
1975
1976 let mut tampered = proof.clone();
1978 tampered.pending_chunk_digest = Some(hasher.digest(b"fake pending"));
1979 assert!(
1980 !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1981 "tampered pending digest accepted at k={k}"
1982 );
1983
1984 let mut tampered = proof.clone();
1985 tampered.pending_chunk_digest = None;
1986 assert!(
1987 !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1988 "missing pending digest accepted at k={k}"
1989 );
1990
1991 let mut bad_chunks = chunks.clone();
1992 bad_chunks[0][0] ^= 1;
1993 assert!(
1994 !proof.verify::<Sha256, _, N>(start, &elements, &bad_chunks, &canonical_root),
1995 "tampered pending chunk bytes accepted at k={k}"
1996 );
1997 }
1998 }
1999
2000 #[test_async]
2005 async fn test_pending_to_graftable_transition_at_birth_size() {
2006 type F = mmb::Family;
2007 const N: usize = 1; let hasher = qmdb::hasher::<Sha256>();
2010 let grafting_height = grafting::height::<N>();
2011 assert_eq!(grafting_height, 3);
2012
2013 let birth = (3u64 << (grafting_height - 1)) - 1;
2015 let pre_state_leaves = birth - 1; let post_state_leaves = birth; assert_eq!(pre_state_leaves, 10);
2019 assert_eq!(post_state_leaves, 11);
2020
2021 let graftable_pre = grafting::graftable_chunks::<F>(pre_state_leaves, grafting_height);
2022 let graftable_post = grafting::graftable_chunks::<F>(post_state_leaves, grafting_height);
2023 assert_eq!(graftable_pre, 0);
2024 assert_eq!(graftable_post, 1);
2025
2026 let mut status_pre = BitMap::<N>::new();
2028 for _ in 0..pre_state_leaves {
2029 status_pre.push(true);
2030 }
2031 let ops_pre = build_test_mem(&hasher, mmb::mem::Mmb::new(), pre_state_leaves);
2032 let ops_root_pre = ops_pre.root(&hasher, 0).unwrap();
2033 let grafted_pre = Mem::<F, sha256::Digest>::new();
2034 let storage_pre =
2035 grafting::Storage::<F, Sha256, _, _>::new(&grafted_pre, grafting_height, &ops_pre);
2036 let canonical_pre = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2037 &status_pre,
2038 &storage_pre,
2039 Location::<F>::new(pre_state_leaves),
2040 Location::new(0),
2041 )
2042 .await
2043 .unwrap()
2044 .root::<Sha256>(&ops_root_pre);
2045
2046 let mut status_post = BitMap::<N>::new();
2048 for _ in 0..post_state_leaves {
2049 status_post.push(true);
2050 }
2051 let ops_post = build_test_mem(&hasher, mmb::mem::Mmb::new(), post_state_leaves);
2052 let ops_root_post = ops_post.root(&hasher, 0).unwrap();
2053 let leaf_digests = compute_grafted_leaves::<F, Sha256, Sequential, N>(
2055 &ops_post,
2056 core::iter::once((
2057 0usize,
2058 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status_post, 0),
2059 )),
2060 &Sequential,
2061 )
2062 .await
2063 .unwrap();
2064 assert_eq!(
2065 leaf_digests.len(),
2066 1,
2067 "post-state must have 1 graftable chunk"
2068 );
2069 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2070 let mut grafted_post = Mem::<F, sha256::Digest>::new();
2071 let merkleized = grafted_post
2072 .new_batch()
2073 .add_leaf_digest(leaf_digests[0].1)
2074 .merkleize(&grafted_post, &grafted_hasher);
2075 grafted_post.apply_batch(&merkleized).unwrap();
2076 let storage_post =
2077 grafting::Storage::<F, Sha256, _, _>::new(&grafted_post, grafting_height, &ops_post);
2078
2079 let canonical_post = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2080 &status_post,
2081 &storage_post,
2082 Location::<F>::new(post_state_leaves),
2083 Location::new(0),
2084 )
2085 .await
2086 .unwrap()
2087 .root::<Sha256>(&ops_root_post);
2088
2089 assert_ne!(
2090 canonical_pre, canonical_post,
2091 "canonical root must change when chunk 0 transitions from pending to graftable"
2092 );
2093 }
2094
2095 #[test_async]
2096 async fn test_range_proof_allows_ops_and_grafted_inactive_counts_to_differ() {
2097 type F = mmb::Family;
2098 const N: usize = 1;
2099
2100 let hasher = qmdb::hasher::<Sha256>();
2101 let grafting_height = grafting::height::<N>();
2102 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
2103 let leaf_count = chunk_bits;
2104 let leaves = mmb::Location::new(leaf_count);
2105 let inactivity_floor = mmb::Location::new(chunk_bits - 2);
2106
2107 let ops_inactive_peaks = F::inactive_peaks(leaves, inactivity_floor);
2108 let aligned_inactive =
2109 grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)
2110 .unwrap();
2111 assert_ne!(ops_inactive_peaks, aligned_inactive);
2112
2113 let mut status = BitMap::<N>::new();
2114 for _ in 0..leaf_count {
2115 status.push(true);
2116 }
2117 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
2118
2119 let ops_root = ops.root(&hasher, ops_inactive_peaks).unwrap();
2123
2124 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
2125 *Location::<F>::try_from(ops.size()).unwrap(),
2126 grafting_height,
2127 )
2128 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
2129 as usize;
2130 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
2131 .map(|chunk_idx| {
2132 (
2133 chunk_idx,
2134 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
2135 )
2136 })
2137 .collect();
2138 let mut leaf_digests =
2139 compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
2140 .await
2141 .unwrap();
2142 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
2143
2144 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2145 let mut grafted = Mem::<F, sha256::Digest>::new();
2146 let merkleized = {
2147 let mut batch = grafted.new_batch();
2148 for (_, digest) in leaf_digests {
2149 batch = batch.add_leaf_digest(digest);
2150 }
2151 batch.merkleize(&grafted, &grafted_hasher)
2152 };
2153 grafted.apply_batch(&merkleized).unwrap();
2154
2155 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
2156 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
2157 let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2158 &status,
2159 &storage,
2160 ops_leaves_for_root,
2161 inactivity_floor,
2162 )
2163 .await
2164 .unwrap()
2165 .root::<Sha256>(&ops_root);
2166
2167 let loc = mmb::Location::new(chunk_bits - 1);
2168 let proof = RangeProof::new::<Sha256, _, N>(
2169 &status,
2170 &storage,
2171 inactivity_floor,
2172 loc..loc + 1,
2173 ops_root,
2174 )
2175 .await
2176 .unwrap();
2177 assert_eq!(proof.proof.inactive_peaks, aligned_inactive);
2178
2179 let element = hasher.digest(&(*loc).to_be_bytes());
2180 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
2181 assert!(proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root));
2182 }
2183
2184 fn invalid_chunk_sizes() -> impl Iterator<Item = usize> {
2185 [0, 3, usize::MAX, 1 << (usize::BITS - 1)]
2186 .into_iter()
2187 .chain(usize::try_from(1u64 << 60).ok())
2188 }
2189
2190 async fn check_dynamic_proofs<F: Graftable, const N: usize>() {
2191 let chunk_bits = (N * 8) as u64;
2192 let height = chunk_bits.trailing_zeros() as u64;
2193 for leaves in [
2194 chunk_bits - 1,
2195 chunk_bits,
2196 chunk_bits + 1,
2197 chunk_bits + height,
2198 chunk_bits * 2,
2199 chunk_bits * 2 + 2,
2200 ] {
2201 let start = Location::<F>::new(chunk_bits - 2);
2202 let (_, proof, operations, chunks, root, _) =
2203 current_range_proof_fixture::<F, N>(leaves, start..Location::new(leaves)).await;
2204 assert!(proof.verify::<Sha256, _, N>(start, &operations, &chunks, &root));
2205 let slices = chunks.iter().map(<[u8; N]>::as_slice).collect::<Vec<_>>();
2206 assert!(proof.verify_with_chunk_size::<Sha256, _>(
2207 start,
2208 &operations,
2209 &slices,
2210 N,
2211 &root
2212 ));
2213 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2214 start,
2215 &operations,
2216 &slices,
2217 N,
2218 &Sha256::hash(&[b"wrong root"]),
2219 ));
2220
2221 for loc in [start, Location::new(leaves - 1)] {
2222 let (_, range_proof, operations, chunks, root, _) =
2223 current_range_proof_fixture::<F, N>(leaves, loc..loc + 1).await;
2224 let native = constant::OperationProof::<F, sha256::Digest, N> {
2225 loc,
2226 chunk: chunks[0],
2227 range_proof,
2228 };
2229 assert!(native.verify::<Sha256, _>(operations[0], &root));
2230 let encoded = native.encode();
2231 let max_digests = native.range_proof.proof.digests.len();
2232 let dynamic = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2233 encoded.clone(),
2234 &(N, max_digests),
2235 )
2236 .unwrap();
2237 assert_eq!(dynamic.encode(), encoded);
2238 assert_eq!(dynamic.encode_size(), encoded.len());
2239 assert_eq!(dynamic.loc, loc);
2240 assert_eq!(dynamic.chunk.as_ref(), native.chunk.as_slice());
2241 assert!(dynamic.verify::<Sha256, _>(operations[0], &root));
2242 assert!(!dynamic.verify::<Sha256, _>(Sha256::hash(&[b"wrong operation"]), &root,));
2243 assert!(
2244 !dynamic.verify::<Sha256, _>(operations[0], &Sha256::hash(&[b"wrong root"]),)
2245 );
2246
2247 let mut inactive = dynamic;
2248 let mut chunk = inactive.chunk.to_vec();
2249 let bit = (*loc % chunk_bits) as usize;
2250 chunk[bit / 8] &= !(1 << (bit % 8));
2251 inactive.chunk = Bytes::from(chunk);
2252 assert!(!inactive.verify::<Sha256, _>(operations[0], &root));
2253 }
2254 }
2255 }
2256
2257 #[test_async]
2258 async fn dynamic_proofs_match_native_mmr() {
2259 check_dynamic_proofs::<mmr::Family, 1>().await;
2260 check_dynamic_proofs::<mmr::Family, 32>().await;
2261 check_dynamic_proofs::<mmr::Family, 64>().await;
2262 }
2263
2264 #[test_async]
2265 async fn dynamic_proofs_match_native_mmb() {
2266 check_dynamic_proofs::<mmb::Family, 1>().await;
2267 check_dynamic_proofs::<mmb::Family, 32>().await;
2268 check_dynamic_proofs::<mmb::Family, 64>().await;
2269 }
2270
2271 async fn check_dynamic_range_rejections<F: Graftable>() {
2272 const N: usize = 1;
2273 let start = Location::<F>::new(6);
2274 let (_, proof, operations, chunks, root, _) =
2275 current_range_proof_fixture::<F, N>(18, start..Location::new(18)).await;
2276 let chunks = chunks
2277 .iter()
2278 .map(|chunk| chunk.to_vec())
2279 .collect::<Vec<_>>();
2280 for chunk_size in invalid_chunk_sizes() {
2281 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2282 start,
2283 &operations,
2284 &chunks,
2285 chunk_size,
2286 &root,
2287 ));
2288 }
2289 assert!(!proof.verify_with_chunk_size::<Sha256, _>(start, &operations, &chunks, 2, &root));
2290 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2291 start,
2292 &operations[..0],
2293 &chunks,
2294 N,
2295 &root
2296 ));
2297 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2298 start,
2299 &operations,
2300 &chunks[..0],
2301 N,
2302 &root
2303 ));
2304 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2305 start,
2306 &operations,
2307 &chunks[..chunks.len() - 1],
2308 N,
2309 &root,
2310 ));
2311 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2312 proof.proof.leaves,
2313 &operations,
2314 &chunks,
2315 N,
2316 &root,
2317 ));
2318 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2319 F::MAX_LEAVES,
2320 &operations,
2321 &chunks,
2322 N,
2323 &root,
2324 ));
2325
2326 let mut extra = chunks.clone();
2327 extra.push(chunks[0].clone());
2328 assert!(!proof.verify_with_chunk_size::<Sha256, _>(start, &operations, &extra, N, &root));
2329 for chunk_len in [0, 2, 3] {
2330 let mut malformed = chunks.clone();
2331 malformed[1].resize(chunk_len, 0);
2332 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2333 start,
2334 &operations,
2335 &malformed,
2336 N,
2337 &root,
2338 ));
2339 }
2340
2341 let mut tampered = chunks.clone();
2342 tampered.last_mut().unwrap()[0] ^= 1;
2343 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2344 start,
2345 &operations,
2346 &tampered,
2347 N,
2348 &root
2349 ));
2350 let mut missing_partial = proof.clone();
2351 assert!(missing_partial.partial_chunk_digest.take().is_some());
2352 assert!(!missing_partial.verify_with_chunk_size::<Sha256, _>(
2353 start,
2354 &operations,
2355 &chunks,
2356 N,
2357 &root,
2358 ));
2359 let mut wrong_operations = operations;
2360 wrong_operations[0] = Sha256::hash(&[b"wrong operation"]);
2361 assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2362 start,
2363 &wrong_operations,
2364 &chunks,
2365 N,
2366 &root,
2367 ));
2368 }
2369
2370 #[test_async]
2371 async fn dynamic_range_rejects_malformed_inputs() {
2372 check_dynamic_range_rejections::<mmr::Family>().await;
2373 check_dynamic_range_rejections::<mmb::Family>().await;
2374 }
2375
2376 async fn check_dynamic_operation_codec_rejections<F: Graftable>() {
2377 const N: usize = 32;
2378 let loc = Location::<F>::new(14);
2379 let (_, range_proof, operations, chunks, root, _) =
2380 current_range_proof_fixture::<F, N>(18, loc..loc + 1).await;
2381 let native = constant::OperationProof::<F, sha256::Digest, N> {
2382 loc,
2383 chunk: chunks[0],
2384 range_proof,
2385 };
2386 let encoded = native.encode();
2387 let max_digests = native.range_proof.proof.digests.len();
2388 assert!(max_digests > 0);
2389 assert!(
2390 dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2391 encoded.clone(),
2392 &(N, max_digests - 1),
2393 )
2394 .is_err()
2395 );
2396 for end in 0..encoded.len() {
2397 assert!(
2398 dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2399 encoded.slice(..end),
2400 &(N, max_digests),
2401 )
2402 .is_err(),
2403 "truncated proof decoded at {end}"
2404 );
2405 }
2406 let mut trailing = encoded.to_vec();
2407 trailing.push(0);
2408 assert!(
2409 dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(trailing, &(N, max_digests),)
2410 .is_err()
2411 );
2412 for chunk_size in invalid_chunk_sizes() {
2413 assert!(matches!(
2414 dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2415 encoded.clone(),
2416 &(chunk_size, max_digests),
2417 ),
2418 Err(commonware_codec::Error::Invalid(_, _)),
2419 ));
2420 }
2421 for chunk_size in [1, N / 2, N * 2] {
2422 if let Ok(dynamic) = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2423 encoded.clone(),
2424 &(chunk_size, max_digests),
2425 ) {
2426 assert!(!dynamic.verify::<Sha256, _>(operations[0], &root));
2427 }
2428 }
2429 let dynamic =
2430 dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(encoded, &(N, max_digests))
2431 .unwrap();
2432 for chunk in [Bytes::new(), Bytes::from_static(&[0; 3])] {
2433 let mut malformed = dynamic.clone();
2434 malformed.chunk = chunk;
2435 assert!(!malformed.verify::<Sha256, _>(operations[0], &root));
2436 }
2437 }
2438
2439 #[test_async]
2440 async fn dynamic_operation_codec_rejects_malformed_inputs() {
2441 check_dynamic_operation_codec_rejections::<mmr::Family>().await;
2442 check_dynamic_operation_codec_rejections::<mmb::Family>().await;
2443 }
2444
2445 struct PreloadedBitmap<'a> {
2446 bitmap: &'a BitMap<1>,
2447 chunks: BTreeMap<usize, [u8; 1]>,
2448 reads: Mutex<BTreeSet<usize>>,
2449 }
2450
2451 impl BitmapReadable<1> for PreloadedBitmap<'_> {
2452 fn complete_chunks(&self) -> usize {
2453 self.bitmap.complete_chunks()
2454 }
2455
2456 fn get_chunk(&self, chunk: usize) -> [u8; 1] {
2457 self.reads.lock().insert(chunk);
2458 self.chunks[&chunk]
2459 }
2460
2461 fn last_chunk(&self) -> ([u8; 1], u64) {
2462 let (_, bits) = self.bitmap.last_chunk();
2463 let chunk = BitMap::<1>::to_chunk_index(self.len() - 1);
2464 (self.get_chunk(chunk), bits)
2465 }
2466
2467 fn pruned_chunks(&self) -> usize {
2468 self.bitmap.pruned_chunks()
2469 }
2470
2471 fn len(&self) -> u64 {
2472 self.bitmap.len()
2473 }
2474 }
2475
2476 async fn fixture<F: Graftable>(
2477 leaves: u64,
2478 pruned: u64,
2479 ) -> (BitMap<1>, Mem<F, sha256::Digest>, Mem<F, sha256::Digest>) {
2480 let hasher = qmdb::hasher::<Sha256>();
2481 let height = grafting::height::<1>();
2482 let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaves);
2483 let mut bitmap = BitMap::<1>::new();
2484 for loc in 0..leaves {
2485 bitmap.push(loc >= pruned * 8);
2486 }
2487 let chunks = (0..grafting::graftable_chunks::<F>(leaves, height))
2488 .map(|chunk| (chunk as usize, *bitmap.get_chunk(chunk as usize)));
2489 let mut digests =
2490 compute_grafted_leaves::<F, Sha256, Sequential, 1>(&ops, chunks, &Sequential)
2491 .await
2492 .unwrap();
2493 digests.sort_unstable_by_key(|(chunk, _)| *chunk);
2494 let mut grafted = Mem::<F, sha256::Digest>::new();
2495 if !digests.is_empty() {
2496 let mut batch = grafted.new_batch();
2497 for (_, digest) in digests {
2498 batch = batch.add_leaf_digest(digest);
2499 }
2500 let hasher = grafting::GraftedHasher::<F, _>::new(hasher, height);
2501 let batch = batch.merkleize(&grafted, &hasher);
2502 grafted.apply_batch(&batch).unwrap();
2503 }
2504 bitmap.prune_to_bit(pruned * 8);
2505 (bitmap, ops, grafted)
2506 }
2507
2508 async fn check_constructor_reads<F: Graftable + PartialEq>() {
2509 for leaves in [1, 7, 8, 9, 10, 11, 16, 17, 18, 24, 25] {
2510 let graftable = grafting::graftable_chunks::<F>(leaves, grafting::height::<1>());
2511 for pruned in 0..=graftable {
2512 let floor = Location::<F>::new(pruned * 8);
2513 if *floor == leaves {
2514 continue;
2515 }
2516 let (bitmap, ops, grafted) = fixture::<F>(leaves, pruned).await;
2517 let hasher = qmdb::hasher::<Sha256>();
2518 let ops_root = ops.root(&hasher, 0).unwrap();
2519 let storage = grafting::Storage::<F, Sha256, _, _>::new(
2520 &grafted,
2521 grafting::height::<1>(),
2522 &ops,
2523 );
2524 let root = db::compute_ops_root_witness::<F, Sha256, _, _, 1>(
2525 &bitmap,
2526 &storage,
2527 Location::new(leaves),
2528 floor,
2529 )
2530 .await
2531 .unwrap()
2532 .root::<Sha256>(&ops_root);
2533 for location in core::iter::once(None)
2534 .chain((*floor..leaves).map(|loc| Some(Location::new(loc))))
2535 {
2536 let required = required_chunks::<F, 1>(
2537 Location::new(leaves),
2538 bitmap.len(),
2539 pruned,
2540 location,
2541 )
2542 .unwrap()
2543 .collect::<Vec<_>>();
2544 assert!(required.windows(2).all(|pair| pair[0] < pair[1]));
2545 let ops_leaves = Location::new(leaves);
2546 let planned = location.map_or_else(
2547 || {
2548 merkle::RangePlan::new(ops_leaves, floor..ops_leaves)
2549 .unwrap()
2550 .positions()
2551 },
2552 |loc| {
2553 merkle::ElementPlan::new(ops_leaves, loc)
2554 .unwrap()
2555 .positions()
2556 },
2557 );
2558 let fetched: BTreeMap<_, _> = planned
2559 .iter()
2560 .copied()
2561 .zip(storage.get_nodes(&planned).await.unwrap())
2562 .collect();
2563 let get_node = |pos: Position<F>| fetched.get(&pos).copied();
2564 let preloaded = PreloadedBitmap {
2565 bitmap: &bitmap,
2566 chunks: required
2567 .iter()
2568 .map(|&chunk| (chunk as usize, *bitmap.get_chunk(chunk as usize)))
2569 .collect(),
2570 reads: Mutex::new(BTreeSet::new()),
2571 };
2572 if let Some(loc) = location {
2573 let proof =
2574 constant::OperationProof::<F, sha256::Digest, 1>::new::<Sha256, _>(
2575 &preloaded, &storage, floor, loc, ops_root,
2576 )
2577 .await
2578 .unwrap();
2579 let built =
2580 constant::OperationProof::<F, sha256::Digest, 1>::build::<Sha256>(
2581 &preloaded,
2582 merkle::ElementPlan::new(ops_leaves, loc).unwrap(),
2583 get_node,
2584 floor,
2585 ops_root,
2586 )
2587 .unwrap();
2588 assert_eq!(built, proof);
2589 assert!(
2590 proof.verify::<Sha256, _>(hasher.digest(&(*loc).to_be_bytes()), &root,)
2591 );
2592 let proof = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2593 proof.encode(),
2594 &(1, 64),
2595 )
2596 .unwrap();
2597 assert!(
2598 proof.verify::<Sha256, _>(hasher.digest(&(*loc).to_be_bytes()), &root)
2599 );
2600 } else {
2601 let proof = RangeProof::new::<Sha256, _, 1>(
2602 &preloaded,
2603 &storage,
2604 floor,
2605 floor..Location::new(leaves),
2606 ops_root,
2607 )
2608 .await
2609 .unwrap();
2610 let built = RangeProof::build::<Sha256, 1>(
2611 &preloaded,
2612 merkle::RangePlan::new(ops_leaves, floor..ops_leaves).unwrap(),
2613 get_node,
2614 floor,
2615 ops_root,
2616 )
2617 .unwrap();
2618 assert_eq!(built, proof);
2619 let elements = (*floor..leaves)
2620 .map(|loc| hasher.digest(&loc.to_be_bytes()))
2621 .collect::<Vec<_>>();
2622 let chunks = (pruned..leaves.div_ceil(8))
2623 .map(|chunk| *bitmap.get_chunk(chunk as usize))
2624 .collect::<Vec<_>>();
2625 assert!(proof.verify::<Sha256, _, 1>(floor, &elements, &chunks, &root));
2626 assert!(proof.verify_with_chunk_size::<Sha256, _>(
2627 floor, &elements, &chunks, 1, &root,
2628 ));
2629 }
2630 let read_chunks = preloaded
2631 .reads
2632 .into_inner()
2633 .into_iter()
2634 .map(Widen::widen)
2635 .collect::<Vec<_>>();
2636 assert_eq!(
2637 read_chunks, required,
2638 "leaves={leaves}, pruned={pruned}, location={location:?}",
2639 );
2640 }
2641 }
2642 }
2643 }
2644
2645 #[test_async]
2646 async fn required_chunks_match_constructor_reads() {
2647 check_constructor_reads::<mmr::Family>().await;
2648 check_constructor_reads::<mmb::Family>().await;
2649 }
2650
2651 #[test_async]
2652 async fn operation_proof_build_rejects_pruned_locations() {
2653 type F = mmr::Family;
2654 let (bitmap, ops, _) = fixture::<F>(16, 1).await;
2655 let ops_root = ops.root(&qmdb::hasher::<Sha256>(), 0).unwrap();
2656 let plan = merkle::ElementPlan::new(Location::new(16), Location::new(3)).unwrap();
2657 assert!(matches!(
2658 constant::OperationProof::<F, sha256::Digest, 1>::build::<Sha256>(
2659 &bitmap,
2660 plan,
2661 |_| None,
2662 Location::new(8),
2663 ops_root,
2664 ),
2665 Err(Error::OperationPruned(loc)) if *loc == 3
2666 ));
2667 }
2668
2669 #[test]
2670 fn required_chunks_boundary_sets() {
2671 for (leaves, mmr, mmb) in [
2672 (0, vec![], vec![]),
2673 (1, vec![0], vec![0]),
2674 (7, vec![0], vec![0]),
2675 (8, vec![], vec![0]),
2676 (9, vec![1], vec![0, 1]),
2677 (10, vec![1], vec![0, 1]),
2678 (11, vec![1], vec![1]),
2679 (16, vec![], vec![1]),
2680 (17, vec![2], vec![1, 2]),
2681 (18, vec![2], vec![1, 2]),
2682 (24, vec![], vec![2]),
2683 (25, vec![3], vec![2, 3]),
2684 ] {
2685 assert_eq!(
2686 required_chunks::<mmr::Family, 1>(Location::new(leaves), leaves, 0, None)
2687 .unwrap()
2688 .collect::<Vec<_>>(),
2689 mmr
2690 );
2691 assert_eq!(
2692 required_chunks::<mmb::Family, 1>(Location::new(leaves), leaves, 0, None)
2693 .unwrap()
2694 .collect::<Vec<_>>(),
2695 mmb
2696 );
2697 }
2698 assert!(
2699 required_chunks::<mmr::Family, 1>(Location::new(16), 16, 2, None)
2700 .unwrap()
2701 .next()
2702 .is_none()
2703 );
2704 }
2705
2706 fn check_invalid_metadata<F: Graftable>() {
2707 for (ops_leaves, bitmap_len, loc) in [(0, 0, 0), (16, 16, 16), (8, 9, 8), (9, 8, 8)] {
2708 assert!(matches!(
2709 required_chunks::<F, 1>(Location::new(ops_leaves), bitmap_len, 0, Some(Location::new(loc))),
2710 Err(Error::Merkle(merkle::Error::RangeOutOfBounds(found))) if *found == loc
2711 ));
2712 }
2713 assert!(matches!(
2714 required_chunks::<F, 1>(Location::new(16), 16, 1, Some(Location::new(7))),
2715 Err(Error::OperationPruned(loc)) if *loc == 7
2716 ));
2717 assert!(matches!(
2718 required_chunks::<F, 1>(Location::new(0), 24, 0, None),
2719 Err(Error::DataCorrupted("multiple pending bitmap chunks"))
2720 ));
2721 assert!(matches!(
2722 required_chunks::<F, 1>(Location::new(8), 8, 2, None),
2723 Err(Error::DataCorrupted(
2724 "pruned chunks exceed graftable chunks"
2725 ))
2726 ));
2727 }
2728
2729 #[test]
2730 fn required_chunks_reject_invalid_metadata() {
2731 check_invalid_metadata::<mmr::Family>();
2732 check_invalid_metadata::<mmb::Family>();
2733 assert!(matches!(
2734 required_chunks::<mmb::Family, 1>(Location::new(8), 8, 1, None),
2735 Err(Error::DataCorrupted(
2736 "pruned chunks exceed graftable chunks"
2737 ))
2738 ));
2739 }
2740
2741 #[cfg(feature = "arbitrary")]
2742 mod conformance {
2743 use super::super::{OpsRootWitness, RangeProof, constant, dynamic};
2744 use crate::merkle::{mmb, mmr};
2745 use commonware_codec::conformance::CodecConformance;
2746 use commonware_cryptography::sha256::Digest as Sha256Digest;
2747
2748 commonware_conformance::conformance_tests! {
2749 CodecConformance<OpsRootWitness<mmr::Family, Sha256Digest>>,
2750 CodecConformance<OpsRootWitness<mmb::Family, Sha256Digest>>,
2751 CodecConformance<RangeProof<mmr::Family, Sha256Digest>>,
2752 CodecConformance<RangeProof<mmb::Family, Sha256Digest>>,
2753 CodecConformance<constant::OperationProof<mmr::Family, Sha256Digest, 32>>,
2754 CodecConformance<constant::OperationProof<mmb::Family, Sha256Digest, 32>>,
2755 CodecConformance<dynamic::OperationProof<mmr::Family, Sha256Digest>>,
2756 CodecConformance<dynamic::OperationProof<mmb::Family, Sha256Digest>>,
2757 }
2758 }
2759}