1use crate::{
31 journal::contiguous::Contiguous,
32 merkle::{
33 self, Family, Graftable, Location, PendingChunk, Position, Proof, hasher::Hasher as _,
34 storage::Storage,
35 },
36 qmdb::{
37 self, Error,
38 current::{
39 db::{combine_roots, partial_chunk, pending_chunk},
40 grafting,
41 },
42 },
43};
44use bytes::{Buf, BufMut};
45use commonware_codec::{Codec, EncodeSize, Read, ReadExt as _, Write, varint::UInt};
46use commonware_cryptography::{Digest, Hasher};
47use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
48use core::{num::NonZeroU64, ops::Range};
49use futures::future::try_join_all;
50use tracing::debug;
51
52#[derive(Clone, Eq, PartialEq, Debug)]
57pub struct OpsRootWitness<F: Graftable, D: Digest> {
58 pub grafted_root: D,
60
61 pub pending_chunk_digest: F::PendingChunk<D>,
63
64 pub partial_chunk: Option<(u64, D)>,
67}
68
69impl<F: Graftable, D: Digest> OpsRootWitness<F, D> {
70 pub fn root<H: Hasher<Digest = D>>(&self, ops_root: &D) -> D {
75 let partial = self.partial_chunk.as_ref().map(|(nb, d)| (*nb, d));
76 combine_roots::<H>(
77 ops_root,
78 &self.grafted_root,
79 self.pending_chunk_digest.as_ref(),
80 partial,
81 )
82 }
83
84 pub fn verify<H: Hasher<Digest = D>>(&self, ops_root: &D, root: &D) -> bool {
86 self.root::<H>(ops_root) == *root
87 }
88}
89
90impl<F: Graftable, D: Digest> Write for OpsRootWitness<F, D> {
91 fn write(&self, buf: &mut impl BufMut) {
92 self.grafted_root.write(buf);
93 self.pending_chunk_digest.write(buf);
94 self.partial_chunk.is_some().write(buf);
95 if let Some((next_bit, digest)) = &self.partial_chunk {
96 UInt(*next_bit).write(buf);
97 digest.write(buf);
98 }
99 }
100}
101
102impl<F: Graftable, D: Digest> EncodeSize for OpsRootWitness<F, D> {
103 fn encode_size(&self) -> usize {
104 self.grafted_root.encode_size()
105 + self.pending_chunk_digest.encode_size()
106 + self
107 .partial_chunk
108 .as_ref()
109 .map_or(1, |(nb, d)| 1 + UInt(*nb).encode_size() + d.encode_size())
110 }
111}
112
113impl<F: Graftable, D: Digest> Read for OpsRootWitness<F, D> {
114 type Cfg = ();
115
116 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
117 let grafted_root = D::read(buf)?;
118 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
119 let partial_chunk = if bool::read(buf)? {
120 let next_bit = UInt::<u64>::read(buf)?.into();
121 let digest = D::read(buf)?;
122 Some((next_bit, digest))
123 } else {
124 None
125 };
126 Ok(Self {
127 grafted_root,
128 pending_chunk_digest,
129 partial_chunk,
130 })
131 }
132}
133
134#[cfg(feature = "arbitrary")]
135impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for OpsRootWitness<F, D>
136where
137 D: for<'a> arbitrary::Arbitrary<'a>,
138 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
139{
140 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
141 Ok(Self {
142 grafted_root: u.arbitrary()?,
143 pending_chunk_digest: u.arbitrary()?,
144 partial_chunk: u.arbitrary()?,
145 })
146 }
147}
148
149#[derive(Clone, Eq, PartialEq, Debug)]
151pub struct RangeProof<F: Graftable, D: Digest> {
152 pub proof: Proof<F, D>,
154
155 pub pending_chunk_digest: F::PendingChunk<D>,
157
158 pub partial_chunk_digest: Option<D>,
160
161 pub ops_root: D,
163}
164
165#[derive(Clone, Copy, Eq, PartialEq, Debug)]
167pub struct RangeProofSpec<F: Family, D: Digest> {
168 pub start_loc: Location<F>,
170
171 pub max_ops: NonZeroU64,
173
174 pub inactivity_floor: Location<F>,
176
177 pub ops_root: D,
179}
180
181impl<F: Graftable, D: Digest> RangeProof<F, D> {
182 pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>, const N: usize>(
184 status: &impl BitmapReadable<N>,
185 storage: &S,
186 inactivity_floor: Location<F>,
187 range: Range<Location<F>>,
188 ops_root: D,
189 ) -> Result<Self, Error<F>> {
190 let ops_leaves = Location::try_from(storage.size())?;
193 let grafting_height = grafting::height::<N>();
194 let inactive_peaks = grafting::chunk_aligned_inactive_peaks::<F>(
195 ops_leaves,
196 inactivity_floor,
197 grafting_height,
198 )?;
199
200 let hasher = qmdb::hasher::<H>();
201 let proof = merkle::verification::historical_range_proof(
202 &hasher,
203 storage,
204 ops_leaves,
205 range,
206 inactive_peaks,
207 )
208 .await?;
209
210 let partial_chunk_digest =
211 partial_chunk::<_, N>(status).map(|(chunk, _)| hasher.digest(chunk.as_slice()));
212
213 let pending_chunk_digest: F::PendingChunk<D> =
214 pending_chunk::<_, _, N>(status, ops_leaves, grafting_height)?
215 .map(|chunk| hasher.digest(chunk.as_slice()))
216 .try_into()
217 .expect("pending_chunk must be consistent with family");
218
219 Ok(Self {
220 proof,
221 pending_chunk_digest,
222 partial_chunk_digest,
223 ops_root,
224 })
225 }
226
227 pub async fn new_with_ops<
237 H: Hasher<Digest = D>,
238 C: Contiguous,
239 S: Storage<F, Digest = D>,
240 const N: usize,
241 >(
242 status: &impl BitmapReadable<N>,
243 storage: &S,
244 log: &C,
245 request: RangeProofSpec<F, D>,
246 ) -> Result<(Self, Vec<C::Item>, Vec<[u8; N]>), Error<F>> {
247 let leaves = Location::new(status.len());
249 if request.start_loc >= leaves {
250 return Err(merkle::Error::RangeOutOfBounds(request.start_loc).into());
251 }
252
253 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
255 let start = *request.start_loc / chunk_bits;
256 if (start as usize) < status.pruned_chunks() {
257 return Err(Error::OperationPruned(request.start_loc));
258 }
259
260 let max_loc = request.start_loc.saturating_add(request.max_ops.get());
261 let end_loc = core::cmp::min(max_loc, leaves);
262
263 let proof = Self::new::<H, S, N>(
265 status,
266 storage,
267 request.inactivity_floor,
268 request.start_loc..end_loc,
269 request.ops_root,
270 )
271 .await?;
272
273 let futures = (*request.start_loc..*end_loc)
275 .map(|i| log.read(i))
276 .collect::<Vec<_>>();
277 let ops = try_join_all(futures).await?;
278
279 let end = (*end_loc - 1) / chunk_bits; let chunks = (start..=end)
282 .map(|i| status.get_chunk(i as usize))
283 .collect::<Vec<_>>();
284
285 Ok((proof, ops, chunks))
286 }
287
288 fn reconstruct_root<H, O, const N: usize>(
291 &self,
292 start_loc: Location<F>,
293 ops: &[O],
294 chunks: &[[u8; N]],
295 collected: Option<&mut Vec<(Position<F>, D)>>,
296 ) -> Result<D, merkle::Error<F>>
297 where
298 H: Hasher<Digest = D>,
299 O: Codec,
300 {
301 if ops.is_empty() || chunks.is_empty() {
302 debug!("verification failed, empty input");
303 return Err(merkle::Error::InvalidProof);
304 }
305 let Some(end_loc) = start_loc.checked_add(ops.len() as u64) else {
307 debug!("verification failed, end_loc overflow");
308 return Err(merkle::Error::InvalidProof);
309 };
310
311 let leaves = self.proof.leaves;
312 if end_loc > leaves {
313 debug!(
314 loc = ?end_loc,
315 ?leaves, "verification failed, invalid range"
316 );
317 return Err(merkle::Error::InvalidProof);
318 }
319
320 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
322 let start_chunk = *start_loc / chunk_bits;
323 let end_chunk = (*end_loc - 1) / chunk_bits;
324 let complete_chunks = *leaves / chunk_bits;
325
326 if (end_chunk - start_chunk + 1) != chunks.len() as u64 {
327 debug!("verification failed, chunk metadata length mismatch");
328 return Err(merkle::Error::InvalidProof);
329 }
330
331 let next_bit = *leaves % chunk_bits;
332 let has_partial_chunk = next_bit != 0;
333
334 let elements = ops.iter().map(|op| op.encode()).collect::<Vec<_>>();
335 let chunk_vec = chunks.iter().map(|c| c.as_ref()).collect::<Vec<_>>();
336 let grafting_height = grafting::height::<N>();
337
338 let graftable_chunks =
339 grafting::graftable_chunks::<F>(*leaves, grafting_height).min(complete_chunks);
340 let pending_chunks = complete_chunks - graftable_chunks;
341 if pending_chunks > 1 {
342 debug!(
343 ?complete_chunks,
344 ?graftable_chunks,
345 "verification failed, multiple pending chunks"
346 );
347 return Err(merkle::Error::InvalidProof);
348 }
349 let has_pending_chunk = pending_chunks == 1;
350
351 let grafting_verifier = grafting::Verifier::<F, H>::new(
352 grafting_height,
353 start_chunk,
354 chunk_vec,
355 graftable_chunks,
356 );
357
358 if self.pending_chunk_digest.as_ref().is_some() != has_pending_chunk {
359 debug!(
360 pending_in_proof = self.pending_chunk_digest.as_ref().is_some(),
361 expected = has_pending_chunk,
362 "pending_chunk_digest presence does not match bitmap state"
363 );
364 return Err(merkle::Error::InvalidProof);
365 }
366
367 if has_partial_chunk {
369 let Some(last_chunk_digest) = self.partial_chunk_digest else {
370 debug!("proof has no partial chunk digest");
371 return Err(merkle::Error::InvalidProof);
372 };
373
374 if end_chunk == complete_chunks {
377 let last_chunk = chunks.last().expect("chunks non-empty");
378 if last_chunk_digest != grafting_verifier.digest(last_chunk) {
379 debug!("last chunk digest does not match expected value");
380 return Err(merkle::Error::InvalidProof);
381 }
382 }
383 } else if self.partial_chunk_digest.is_some() {
384 debug!("proof has unexpected partial chunk digest");
385 return Err(merkle::Error::InvalidProof);
386 }
387
388 if let Some(pending_digest) = self.pending_chunk_digest.as_ref() {
392 let pending_idx = graftable_chunks;
393 if pending_idx >= start_chunk && pending_idx <= end_chunk {
394 let local = (pending_idx - start_chunk) as usize;
395 let Some(pending_chunk_bytes) = chunks.get(local) else {
399 debug!(
400 ?pending_idx,
401 chunks_len = chunks.len(),
402 "pending chunk index out of range in supplied chunks"
403 );
404 return Err(merkle::Error::InvalidProof);
405 };
406 if *pending_digest != grafting_verifier.digest(pending_chunk_bytes) {
407 debug!("pending chunk digest does not match expected value");
408 return Err(merkle::Error::InvalidProof);
409 }
410 }
411 }
412
413 let merkle_root = match self.proof.reconstruct_root_inner(
414 &grafting_verifier,
415 &elements,
416 start_loc,
417 collected,
418 ) {
419 Ok(root) => root,
420 Err(error) => {
421 debug!(?error, "invalid proof input");
422 return Err(merkle::Error::InvalidProof);
423 }
424 };
425
426 let partial =
427 has_partial_chunk.then(|| (next_bit, self.partial_chunk_digest.as_ref().unwrap()));
428 Ok(combine_roots::<H>(
429 &self.ops_root,
430 &merkle_root,
431 self.pending_chunk_digest.as_ref(),
432 partial,
433 ))
434 }
435
436 pub fn verify<H: Hasher<Digest = D>, O: Codec, const N: usize>(
439 &self,
440 start_loc: Location<F>,
441 ops: &[O],
442 chunks: &[[u8; N]],
443 root: &H::Digest,
444 ) -> bool {
445 matches!(
446 self.reconstruct_root::<H, O, N>(start_loc, ops, chunks, None),
447 Ok(reconstructed_root) if reconstructed_root == *root
448 )
449 }
450}
451
452pub fn verify_proof_and_extract_digests<F, Op, H, D, const N: usize>(
455 proof: &RangeProof<F, D>,
456 start_loc: Location<F>,
457 operations: &[Op],
458 chunks: &[[u8; N]],
459 target_root: &D,
460) -> Result<Vec<(Position<F>, D)>, merkle::Error<F>>
461where
462 F: Graftable,
463 Op: Codec,
464 H: Hasher<Digest = D>,
465 D: Digest,
466{
467 let mut collected = Vec::new();
468 let reconstructed_root =
469 proof.reconstruct_root::<H, Op, N>(start_loc, operations, chunks, Some(&mut collected))?;
470 if reconstructed_root != *target_root {
471 debug!("verification failed, root mismatch");
472 return Err(merkle::Error::RootMismatch);
473 }
474
475 Ok(collected)
476}
477
478impl<F: Graftable, D: Digest> Write for RangeProof<F, D> {
479 fn write(&self, buf: &mut impl BufMut) {
480 self.proof.write(buf);
481 self.pending_chunk_digest.write(buf);
482 self.partial_chunk_digest.write(buf);
483 self.ops_root.write(buf);
484 }
485}
486
487impl<F: Graftable, D: Digest> EncodeSize for RangeProof<F, D> {
488 fn encode_size(&self) -> usize {
489 self.proof.encode_size()
490 + self.pending_chunk_digest.encode_size()
491 + self.partial_chunk_digest.encode_size()
492 + self.ops_root.encode_size()
493 }
494}
495
496impl<F: Graftable, D: Digest> Read for RangeProof<F, D> {
497 type Cfg = usize;
499
500 fn read_cfg(
501 buf: &mut impl Buf,
502 max_digests: &Self::Cfg,
503 ) -> Result<Self, commonware_codec::Error> {
504 let proof = Proof::<F, D>::read_cfg(buf, max_digests)?;
505 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
506 let partial_chunk_digest = Option::<D>::read(buf)?;
507 let ops_root = D::read(buf)?;
508 Ok(Self {
509 proof,
510 pending_chunk_digest,
511 partial_chunk_digest,
512 ops_root,
513 })
514 }
515}
516
517#[cfg(feature = "arbitrary")]
518impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for RangeProof<F, D>
519where
520 D: for<'a> arbitrary::Arbitrary<'a>,
521 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
522{
523 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
524 Ok(Self {
525 proof: u.arbitrary()?,
526 pending_chunk_digest: u.arbitrary()?,
527 partial_chunk_digest: u.arbitrary()?,
528 ops_root: u.arbitrary()?,
529 })
530 }
531}
532
533#[derive(Clone, Eq, PartialEq, Debug)]
535pub struct OperationProof<F: Graftable, D: Digest, const N: usize> {
536 pub loc: Location<F>,
538
539 pub chunk: [u8; N],
541
542 pub range_proof: RangeProof<F, D>,
544}
545
546impl<F: Graftable, D: Digest, const N: usize> OperationProof<F, D, N> {
547 pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>>(
554 status: &impl BitmapReadable<N>,
555 storage: &S,
556 inactivity_floor: Location<F>,
557 loc: Location<F>,
558 ops_root: D,
559 ) -> Result<Self, Error<F>> {
560 if BitMap::<N>::to_chunk_index(*loc) < status.pruned_chunks() {
562 return Err(Error::OperationPruned(loc));
563 }
564 let range_proof =
565 RangeProof::new::<H, S, N>(status, storage, inactivity_floor, loc..loc + 1, ops_root)
566 .await?;
567 let chunk = status.get_chunk(BitMap::<N>::to_chunk_index(*loc));
568 Ok(Self {
569 loc,
570 chunk,
571 range_proof,
572 })
573 }
574
575 pub fn verify<H: Hasher<Digest = D>, O: Codec>(&self, operation: O, root: &D) -> bool {
578 if !BitMap::<N>::get_bit_from_chunk(&self.chunk, *self.loc) {
581 debug!(
582 ?self.loc,
583 "proof verification failed, operation is inactive"
584 );
585 return false;
586 }
587
588 self.range_proof
589 .verify::<H, O, N>(self.loc, &[operation], &[self.chunk], root)
590 }
591}
592
593impl<F: Graftable, D: Digest, const N: usize> Write for OperationProof<F, D, N> {
594 fn write(&self, buf: &mut impl BufMut) {
595 self.loc.write(buf);
596 self.chunk.write(buf);
597 self.range_proof.write(buf);
598 }
599}
600
601impl<F: Graftable, D: Digest, const N: usize> EncodeSize for OperationProof<F, D, N> {
602 fn encode_size(&self) -> usize {
603 self.loc.encode_size() + self.chunk.encode_size() + self.range_proof.encode_size()
604 }
605}
606
607impl<F: Graftable, D: Digest, const N: usize> Read for OperationProof<F, D, N> {
608 type Cfg = usize;
610
611 fn read_cfg(
612 buf: &mut impl Buf,
613 max_digests: &Self::Cfg,
614 ) -> Result<Self, commonware_codec::Error> {
615 let loc = Location::<F>::read(buf)?;
616 let chunk = <[u8; N]>::read(buf)?;
617 let range_proof = RangeProof::<F, D>::read_cfg(buf, max_digests)?;
618 Ok(Self {
619 loc,
620 chunk,
621 range_proof,
622 })
623 }
624}
625
626#[cfg(feature = "arbitrary")]
627impl<F: Graftable, D: Digest, const N: usize> arbitrary::Arbitrary<'_> for OperationProof<F, D, N>
628where
629 D: for<'a> arbitrary::Arbitrary<'a>,
630 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
631{
632 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
633 Ok(Self {
634 loc: u.arbitrary()?,
635 chunk: u.arbitrary()?,
636 range_proof: u.arbitrary()?,
637 })
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use crate::{
645 merkle::{conformance::build_test_mem, hasher::Standard as StandardHasher, mem::Mem},
646 mmb, mmr,
647 qmdb::current::{db, grafting},
648 };
649 use commonware_codec::{Decode as _, DecodeExt as _, Encode as _};
650 use commonware_cryptography::{Sha256, sha256};
651 use commonware_macros::test_async;
652 use commonware_parallel::Sequential;
653 use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
654 use core::ops::Range;
655
656 #[test]
657 fn test_ops_root_witness_codec_roundtrip() {
658 type F = mmb::Family;
659 for partial_chunk in [
660 None,
661 Some((0u64, Sha256::hash(&[b"partial-zero"]))),
662 Some((123u64, Sha256::hash(&[b"partial-nonzero"]))),
663 ] {
664 let witness: OpsRootWitness<F, _> = OpsRootWitness {
665 grafted_root: Sha256::hash(&[b"grafted"]),
666 pending_chunk_digest: None,
667 partial_chunk,
668 };
669 let encoded = witness.encode();
670 assert_eq!(encoded.len(), witness.encode_size());
671 let decoded = OpsRootWitness::<F, sha256::Digest>::decode(encoded).unwrap();
672 assert_eq!(decoded, witness);
673 }
674 }
675
676 #[test]
677 fn test_ops_root_witness_root_matches_verify() {
678 type F = mmb::Family;
679
680 let ops_root = Sha256::hash(&[b"ops root"]);
681 let witness: OpsRootWitness<F, _> = OpsRootWitness {
682 grafted_root: Sha256::hash(&[b"grafted root"]),
683 pending_chunk_digest: Some(Sha256::hash(&[b"pending chunk"])),
684 partial_chunk: Some((13, Sha256::hash(&[b"partial chunk"]))),
685 };
686
687 let root = witness.root::<Sha256>(&ops_root);
688
689 assert!(witness.verify::<Sha256>(&ops_root, &root));
690 assert_ne!(root, ops_root);
691
692 let wrong_ops_root = Sha256::hash(&[b"wrong ops root"]);
693 assert!(!witness.verify::<Sha256>(&wrong_ops_root, &root));
694 }
695
696 fn range_proof_digest_count<F: Graftable, D: Digest>(proof: &RangeProof<F, D>) -> usize {
697 proof.proof.digests.len()
698 }
699
700 #[test]
701 fn test_range_proof_codec_roundtrip() {
702 type F = mmb::Family;
703 const MAX_DIGESTS: usize = 64;
704
705 let proof = Proof::<F, sha256::Digest> {
706 leaves: mmb::Location::new(42),
707 inactive_peaks: 0,
708 digests: vec![
709 Sha256::hash(&[b"d0"]),
710 Sha256::hash(&[b"d1"]),
711 Sha256::hash(&[b"d2"]),
712 ],
713 };
714 let ops_root = Sha256::hash(&[b"ops-root"]);
715
716 let cases = [
717 RangeProof {
719 proof: proof.clone(),
720 pending_chunk_digest: None,
721 partial_chunk_digest: None,
722 ops_root,
723 },
724 RangeProof {
726 proof,
727 pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
728 partial_chunk_digest: Some(Sha256::hash(&[b"partial"])),
729 ops_root,
730 },
731 RangeProof {
733 proof: Proof::<F, sha256::Digest>::default(),
734 pending_chunk_digest: None,
735 partial_chunk_digest: Some(Sha256::hash(&[b"only-partial"])),
736 ops_root,
737 },
738 ];
739
740 for proof in cases {
741 let encoded = proof.encode();
742 assert_eq!(encoded.len(), proof.encode_size());
743 let decoded =
744 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
745 assert_eq!(decoded, proof);
746 }
747 }
748
749 #[test]
750 fn test_range_proof_codec_enforces_merkle_digest_budget() {
751 type F = mmb::Family;
752
753 let proof = RangeProof {
754 proof: Proof::<F, sha256::Digest> {
755 leaves: mmb::Location::new(42),
756 inactive_peaks: 0,
757 digests: vec![Sha256::hash(&[b"d0"])],
758 },
759 pending_chunk_digest: None,
760 partial_chunk_digest: None,
761 ops_root: Sha256::hash(&[b"ops-root"]),
762 };
763
764 let encoded = proof.encode();
765 let total_digests = range_proof_digest_count(&proof);
766
767 let decoded =
768 RangeProof::<F, sha256::Digest>::decode_cfg(encoded.clone(), &total_digests).unwrap();
769 assert_eq!(decoded, proof);
770 assert!(
771 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &(total_digests - 1)).is_err()
772 );
773 }
774
775 #[test]
776 fn test_range_proof_decode_rejects_pending_for_mmr() {
777 const MAX_DIGESTS: usize = 64;
778
779 let proof = RangeProof {
780 proof: Proof::<mmb::Family, sha256::Digest> {
781 leaves: mmb::Location::new(42),
782 inactive_peaks: 0,
783 digests: vec![Sha256::hash(&[b"d0"])],
784 },
785 pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
786 partial_chunk_digest: None,
787 ops_root: Sha256::hash(&[b"ops-root"]),
788 };
789 let encoded = proof.encode();
790
791 assert!(
793 RangeProof::<mmb::Family, sha256::Digest>::decode_cfg(encoded.clone(), &MAX_DIGESTS)
794 .is_ok()
795 );
796
797 assert!(
799 RangeProof::<crate::merkle::mmr::Family, sha256::Digest>::decode_cfg(
800 encoded,
801 &MAX_DIGESTS
802 )
803 .is_err()
804 );
805 }
806
807 #[test]
808 fn test_operation_proof_codec_roundtrip() {
809 type F = mmb::Family;
810 const N: usize = 32;
811 const MAX_DIGESTS: usize = 64;
812
813 let range_proof = RangeProof {
814 proof: Proof::<F, sha256::Digest> {
815 leaves: mmb::Location::new(7),
816 inactive_peaks: 0,
817 digests: vec![Sha256::hash(&[b"sib"])],
818 },
819 pending_chunk_digest: None,
820 partial_chunk_digest: None,
821 ops_root: Sha256::hash(&[b"ops"]),
822 };
823
824 let chunk: [u8; N] = core::array::from_fn(|i| i as u8);
825
826 let proof = OperationProof::<F, sha256::Digest, N> {
827 loc: mmb::Location::new(5),
828 chunk,
829 range_proof,
830 };
831
832 let encoded = proof.encode();
833 assert_eq!(encoded.len(), proof.encode_size());
834 let decoded =
835 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
836 assert_eq!(decoded, proof);
837 }
838
839 #[test]
840 fn test_operation_proof_codec_enforces_merkle_digest_budget() {
841 type F = mmb::Family;
842 const N: usize = 32;
843
844 let range_proof = RangeProof {
845 proof: Proof::<F, sha256::Digest> {
846 leaves: mmb::Location::new(7),
847 inactive_peaks: 0,
848 digests: vec![Sha256::hash(&[b"sib"])],
849 },
850 pending_chunk_digest: None,
851 partial_chunk_digest: None,
852 ops_root: Sha256::hash(&[b"ops"]),
853 };
854 let total_digests = range_proof_digest_count(&range_proof);
855 let proof = OperationProof::<F, sha256::Digest, N> {
856 loc: mmb::Location::new(5),
857 chunk: core::array::from_fn(|i| i as u8),
858 range_proof,
859 };
860
861 let encoded = proof.encode();
862 let decoded =
863 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded.clone(), &total_digests)
864 .unwrap();
865 assert_eq!(decoded, proof);
866 assert!(
867 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &(total_digests - 1))
868 .is_err()
869 );
870 }
871
872 #[test_async]
873 async fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
874 type F = mmb::Family;
875 const N: usize = 1;
876
877 let hasher = qmdb::hasher::<Sha256>();
878 let grafting_height = grafting::height::<N>();
879
880 let leaf_count = (16..=64u64)
881 .find(|&leaves| {
882 let size = F::location_to_position(mmb::Location::new(leaves));
883 F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
884 })
885 .expect("expected an MMB size whose second chunk spans multiple peaks");
886
887 let mut status = BitMap::<N>::new();
888 for _ in 0..leaf_count {
889 status.push(true);
890 }
891 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
892 let ops_root = ops.root(&hasher, 0).unwrap();
893
894 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
895 *Location::<F>::try_from(ops.size()).unwrap(),
896 grafting_height,
897 )
898 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
899 as usize;
900 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
901 .map(|chunk_idx| {
902 (
903 chunk_idx,
904 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
905 )
906 })
907 .collect();
908 let mut leaf_digests =
909 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
910 .await
911 .unwrap();
912 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
913
914 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
915 let mut grafted = Mem::<F, sha256::Digest>::new();
916 let merkleized = {
917 let mut batch = grafted.new_batch();
918 for (_, digest) in leaf_digests {
919 batch = batch.add_leaf_digest(digest);
920 }
921 batch.merkleize(&grafted, &grafted_hasher)
922 };
923 grafted.apply_batch(&merkleized).unwrap();
924
925 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
926 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
927 let root = db::compute_db_root::<F, Sha256, _, _, N>(
928 &status,
929 &storage,
930 ops_leaves_for_root,
931 None,
932 Location::new(0),
933 &ops_root,
934 )
935 .await
936 .unwrap();
937
938 let loc = mmb::Location::new(BitMap::<N>::CHUNK_SIZE_BITS + 4);
939 let proof = RangeProof::new::<Sha256, _, N>(
940 &status,
941 &storage,
942 Location::new(0),
943 loc..loc + 1,
944 ops_root,
945 )
946 .await
947 .unwrap();
948
949 let element = hasher.digest(&(*loc).to_be_bytes());
950 assert!(proof.verify::<Sha256, _, N>(
951 loc,
952 &[element],
953 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 1)],
954 &root,
955 ));
956 }
957
958 #[test_async]
959 async fn test_range_proof_verifies_with_partial_suffix_mmb() {
960 type F = mmb::Family;
961 const N: usize = 1;
962
963 let hasher = qmdb::hasher::<Sha256>();
964 let grafting_height = grafting::height::<N>();
965 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
966
967 let (leaf_count, loc) = (chunk_bits * 2 + 1..=64u64)
968 .find_map(|leaves| {
969 let complete_chunks = leaves / chunk_bits;
970 if complete_chunks < 2 || leaves % chunk_bits == 0 {
971 return None;
972 }
973
974 let size = F::location_to_position(mmb::Location::new(leaves));
975 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
976 Some((leaves, mmb::Location::new(chunk_bits + 1)))
977 })
978 .expect("expected an MMB proof with a partial trailing suffix chunk");
979
980 let mut status = BitMap::<N>::new();
981 for _ in 0..leaf_count {
982 status.push(true);
983 }
984 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
985 let ops_root = ops.root(&hasher, 0).unwrap();
986
987 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
988 *Location::<F>::try_from(ops.size()).unwrap(),
989 grafting_height,
990 )
991 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
992 as usize;
993 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
994 .map(|chunk_idx| {
995 (
996 chunk_idx,
997 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
998 )
999 })
1000 .collect();
1001 let mut leaf_digests =
1002 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1003 .await
1004 .unwrap();
1005 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1006
1007 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1008 let mut grafted = Mem::<F, sha256::Digest>::new();
1009 let merkleized = {
1010 let mut batch = grafted.new_batch();
1011 for (_, digest) in leaf_digests {
1012 batch = batch.add_leaf_digest(digest);
1013 }
1014 batch.merkleize(&grafted, &grafted_hasher)
1015 };
1016 grafted.apply_batch(&merkleized).unwrap();
1017
1018 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1019 let partial = {
1020 let (chunk, next_bit) = status.last_chunk();
1021 Some((*chunk, next_bit))
1022 };
1023 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1024 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1025 &status,
1026 &storage,
1027 ops_leaves_for_root,
1028 partial,
1029 Location::new(0),
1030 &ops_root,
1031 )
1032 .await
1033 .unwrap();
1034 let proof = RangeProof::new::<Sha256, _, N>(
1035 &status,
1036 &storage,
1037 Location::new(0),
1038 loc..loc + 1,
1039 ops_root,
1040 )
1041 .await
1042 .unwrap();
1043
1044 let element = hasher.digest(&(*loc).to_be_bytes());
1045 let chunk_idx = (*loc / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
1046 assert!(proof.verify::<Sha256, _, N>(
1047 loc,
1048 &[element],
1049 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1050 &status, chunk_idx
1051 )],
1052 &root,
1053 ));
1054 }
1055
1056 #[test_async]
1057 async fn test_range_proof_verifies_when_range_reaches_partial_chunk_mmb() {
1058 type F = mmb::Family;
1059 const N: usize = 1;
1060
1061 let hasher = qmdb::hasher::<Sha256>();
1062 let grafting_height = grafting::height::<N>();
1063 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1064
1065 let (leaf_count, start_loc, complete_chunks) = (17..=128u64)
1069 .find_map(|leaves| {
1070 let complete_chunks = leaves / chunk_bits;
1071 if complete_chunks < 2 || leaves % chunk_bits == 0 {
1072 return None;
1073 }
1074 let leaves_loc = mmb::Location::new(leaves);
1075 let size = F::location_to_position(leaves_loc);
1076 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1077 let start_loc = mmb::Location::new(chunk_bits + 1);
1078 Some((leaves, start_loc, complete_chunks))
1079 })
1080 .expect("expected an MMB size with chunk 1 multi-peak and a partial trailing chunk");
1081
1082 let mut status = BitMap::<N>::new();
1083 for _ in 0..leaf_count {
1084 status.push(true);
1085 }
1086 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1087 let ops_root = ops.root(&hasher, 0).unwrap();
1088
1089 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1090 *Location::<F>::try_from(ops.size()).unwrap(),
1091 grafting_height,
1092 )
1093 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1094 as usize;
1095 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1096 .map(|chunk_idx| {
1097 (
1098 chunk_idx,
1099 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1100 )
1101 })
1102 .collect();
1103 let mut leaf_digests =
1104 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1105 .await
1106 .unwrap();
1107 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1108
1109 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1110 let mut grafted = Mem::<F, sha256::Digest>::new();
1111 let merkleized = {
1112 let mut batch = grafted.new_batch();
1113 for (_, digest) in leaf_digests {
1114 batch = batch.add_leaf_digest(digest);
1115 }
1116 batch.merkleize(&grafted, &grafted_hasher)
1117 };
1118 grafted.apply_batch(&merkleized).unwrap();
1119
1120 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1121 let partial = {
1122 let (chunk, next_bit) = status.last_chunk();
1123 Some((*chunk, next_bit))
1124 };
1125 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1126 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1127 &status,
1128 &storage,
1129 ops_leaves_for_root,
1130 partial,
1131 Location::new(0),
1132 &ops_root,
1133 )
1134 .await
1135 .unwrap();
1136
1137 let leaves_loc = mmb::Location::new(leaf_count);
1138 let proof = RangeProof::new::<Sha256, _, N>(
1139 &status,
1140 &storage,
1141 Location::new(0),
1142 start_loc..leaves_loc,
1143 ops_root,
1144 )
1145 .await
1146 .unwrap();
1147
1148 let elements = (*start_loc..leaf_count)
1149 .map(|idx| hasher.digest(&idx.to_be_bytes()))
1150 .collect::<Vec<_>>();
1151 let start_chunk_idx = (*start_loc / chunk_bits) as usize;
1152 let end_chunk_idx = complete_chunks as usize;
1153 let chunks = (start_chunk_idx..=end_chunk_idx)
1154 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1155 .collect::<Vec<_>>();
1156 assert!(proof.verify::<Sha256, _, N>(start_loc, &elements, &chunks, &root,));
1157
1158 let mut bad_chunks = chunks;
1160 let last = bad_chunks.last_mut().unwrap();
1161 last[0] ^= 1;
1162 assert!(
1163 !proof.verify::<Sha256, _, N>(start_loc, &elements, &bad_chunks, &root),
1164 "tampered partial chunk bytes should not verify"
1165 );
1166 }
1167
1168 #[test_async]
1169 async fn test_range_proof_rejects_unexpected_partial_chunk_digest() {
1170 type F = mmb::Family;
1171 const N: usize = 1;
1172
1173 let hasher = qmdb::hasher::<Sha256>();
1174 let grafting_height = grafting::height::<N>();
1175 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1176
1177 let leaf_count = chunk_bits * 2; let mut status = BitMap::<N>::new();
1179 for _ in 0..leaf_count {
1180 status.push(true);
1181 }
1182 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1183 let ops_root = ops.root(&hasher, 0).unwrap();
1184
1185 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1186 *Location::<F>::try_from(ops.size()).unwrap(),
1187 grafting_height,
1188 )
1189 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1190 as usize;
1191 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1192 .map(|chunk_idx| {
1193 (
1194 chunk_idx,
1195 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1196 )
1197 })
1198 .collect();
1199 let mut leaf_digests =
1200 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1201 .await
1202 .unwrap();
1203 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1204
1205 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1206 let mut grafted = Mem::<F, sha256::Digest>::new();
1207 let merkleized = {
1208 let mut batch = grafted.new_batch();
1209 for (_, digest) in leaf_digests {
1210 batch = batch.add_leaf_digest(digest);
1211 }
1212 batch.merkleize(&grafted, &grafted_hasher)
1213 };
1214 grafted.apply_batch(&merkleized).unwrap();
1215
1216 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1217 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1218 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1219 &status,
1220 &storage,
1221 ops_leaves_for_root,
1222 None,
1223 Location::new(0),
1224 &ops_root,
1225 )
1226 .await
1227 .unwrap();
1228
1229 let loc = mmb::Location::new(0);
1230 let mut proof = RangeProof::new::<Sha256, _, N>(
1231 &status,
1232 &storage,
1233 Location::new(0),
1234 loc..loc + 1,
1235 ops_root,
1236 )
1237 .await
1238 .unwrap();
1239
1240 let element = hasher.digest(&(*loc).to_be_bytes());
1241 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
1242
1243 let mut tampered = proof.clone();
1245 tampered.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1246 assert!(!tampered.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1247
1248 proof.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1249 assert!(!proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1250 }
1251
1252 async fn current_range_proof_fixture<F: Graftable, const N: usize>(
1253 leaf_count: u64,
1254 range: Range<Location<F>>,
1255 ) -> (
1256 StandardHasher<Sha256>,
1257 RangeProof<F, sha256::Digest>,
1258 Vec<sha256::Digest>,
1259 Vec<[u8; N]>,
1260 sha256::Digest,
1261 Mem<F, sha256::Digest>,
1262 ) {
1263 let hasher = qmdb::hasher::<Sha256>();
1264 let grafting_height = grafting::height::<N>();
1265 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1266
1267 let mut status = BitMap::<N>::new();
1268 for _ in 0..leaf_count {
1269 status.push(true);
1270 }
1271
1272 let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaf_count);
1273 let ops_root = ops.root(&hasher, 0).unwrap();
1274 let ops_leaves = Location::<F>::try_from(ops.size()).unwrap();
1275
1276 let graftable_chunks_for_test =
1277 grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1278 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1279 as usize;
1280 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1281 .map(|chunk_idx| {
1282 (
1283 chunk_idx,
1284 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1285 )
1286 })
1287 .collect();
1288 let mut leaf_digests =
1289 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1290 .await
1291 .unwrap();
1292 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1293
1294 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1295 let mut grafted = Mem::<F, sha256::Digest>::new();
1296 if !leaf_digests.is_empty() {
1297 let merkleized = {
1298 let mut batch = grafted.new_batch();
1299 for (_, digest) in leaf_digests {
1300 batch = batch.add_leaf_digest(digest);
1301 }
1302 batch.merkleize(&grafted, &grafted_hasher)
1303 };
1304 grafted.apply_batch(&merkleized).unwrap();
1305 }
1306
1307 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1308 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1309 &status,
1310 &storage,
1311 ops_leaves,
1312 db::partial_chunk::<_, N>(&status),
1313 Location::new(0),
1314 &ops_root,
1315 )
1316 .await
1317 .unwrap();
1318
1319 let proof = RangeProof::new::<Sha256, _, N>(
1320 &status,
1321 &storage,
1322 Location::new(0),
1323 range.clone(),
1324 ops_root,
1325 )
1326 .await
1327 .unwrap();
1328 let operations = (*range.start..*range.end)
1329 .map(|i| hasher.digest(&i.to_be_bytes()))
1330 .collect::<Vec<_>>();
1331
1332 let start_chunk = (*range.start / chunk_bits) as usize;
1334 let end_chunk = ((*range.end - 1) / chunk_bits) as usize;
1335 let chunks = (start_chunk..=end_chunk)
1336 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1337 .collect::<Vec<_>>();
1338
1339 assert!(proof.verify::<Sha256, _, N>(range.start, &operations, &chunks, &root));
1340
1341 (hasher, proof, operations, chunks, root, ops)
1342 }
1343
1344 async fn verify_proof_and_extract_digests_inner<F: Graftable>() {
1345 const N: usize = 1;
1346 let start = Location::<F>::new(14);
1347 let end = Location::<F>::new(18);
1348 let (hasher, proof, operations, chunks, root, ops) =
1349 current_range_proof_fixture::<F, N>(18, start..end).await;
1350
1351 let extracted = verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1352 &proof,
1353 start,
1354 &operations,
1355 &chunks,
1356 &root,
1357 )
1358 .unwrap();
1359 assert!(!extracted.is_empty());
1360
1361 for loc in *start..*end {
1363 let pos = F::location_to_position(Location::<F>::new(loc));
1364 let expected = ops.get_node(pos).unwrap();
1365 assert!(
1366 extracted
1367 .iter()
1368 .any(|(actual_pos, actual)| *actual_pos == pos && *actual == expected),
1369 "missing extracted leaf digest at {pos:?}",
1370 );
1371 }
1372
1373 let wrong_root = hasher.digest(b"wrong current root");
1375 assert!(matches!(
1376 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1377 &proof,
1378 start,
1379 &operations,
1380 &chunks,
1381 &wrong_root,
1382 ),
1383 Err(merkle::Error::RootMismatch)
1384 ));
1385
1386 let mut wrong_operations = operations.clone();
1388 wrong_operations[0] = hasher.digest(b"wrong operation");
1389 assert!(
1390 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1391 &proof,
1392 start,
1393 &wrong_operations,
1394 &chunks,
1395 &root,
1396 )
1397 .is_err()
1398 );
1399
1400 let mut bad_chunks = chunks;
1401 bad_chunks.last_mut().unwrap()[0] ^= 1;
1402 assert!(
1403 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1404 &proof,
1405 start,
1406 &operations,
1407 &bad_chunks,
1408 &root,
1409 )
1410 .is_err()
1411 );
1412 }
1413
1414 #[test_async]
1415 async fn test_verify_proof_and_extract_digests_handles_no_grafted_chunks_mmb() {
1416 type F = mmb::Family;
1417 const N: usize = 1;
1418 let start = Location::<F>::new(2);
1419 let end = Location::<F>::new(4);
1420 let (_, proof, operations, chunks, root, _ops) =
1421 current_range_proof_fixture::<F, N>(6, start..end).await;
1422
1423 let extracted = verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1424 &proof,
1425 start,
1426 &operations,
1427 &chunks,
1428 &root,
1429 )
1430 .unwrap();
1431
1432 assert!(!extracted.is_empty());
1433 }
1434
1435 #[test_async]
1436 async fn test_verify_proof_and_extract_digests_rejects_malformed_inputs_mmb() {
1437 type F = mmb::Family;
1438 const N: usize = 1;
1439 let start = Location::<F>::new(14);
1440 let end = Location::<F>::new(18);
1441 let (_, proof, operations, chunks, root, _ops) =
1442 current_range_proof_fixture::<F, N>(18, start..end).await;
1443
1444 let no_operations = Vec::<sha256::Digest>::new();
1445 assert!(matches!(
1446 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1447 &proof,
1448 start,
1449 &no_operations,
1450 &chunks,
1451 &root,
1452 ),
1453 Err(merkle::Error::InvalidProof)
1454 ));
1455
1456 let no_chunks = Vec::<[u8; N]>::new();
1457 assert!(matches!(
1458 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1459 &proof,
1460 start,
1461 &operations[..1],
1462 &no_chunks,
1463 &root,
1464 ),
1465 Err(merkle::Error::InvalidProof)
1466 ));
1467
1468 assert!(matches!(
1469 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1470 &proof,
1471 F::MAX_LEAVES,
1472 &operations[..1],
1473 &chunks,
1474 &root,
1475 ),
1476 Err(merkle::Error::InvalidProof)
1477 ));
1478
1479 assert!(matches!(
1480 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1481 &proof,
1482 proof.proof.leaves,
1483 &operations[..1],
1484 &chunks,
1485 &root,
1486 ),
1487 Err(merkle::Error::InvalidProof)
1488 ));
1489
1490 assert!(matches!(
1491 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1492 &proof,
1493 start,
1494 &operations,
1495 &chunks[..chunks.len() - 1],
1496 &root,
1497 ),
1498 Err(merkle::Error::InvalidProof)
1499 ));
1500
1501 let mut missing_partial = proof.clone();
1502 missing_partial.partial_chunk_digest = None;
1503 assert!(matches!(
1504 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1505 &missing_partial,
1506 start,
1507 &operations,
1508 &chunks,
1509 &root,
1510 ),
1511 Err(merkle::Error::InvalidProof)
1512 ));
1513
1514 let mut broken_merkle = proof;
1515 assert!(!broken_merkle.proof.digests.is_empty());
1516 broken_merkle.proof.digests.clear();
1517 assert!(matches!(
1518 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1519 &broken_merkle,
1520 start,
1521 &operations,
1522 &chunks,
1523 &root,
1524 ),
1525 Err(merkle::Error::InvalidProof)
1526 ));
1527 }
1528
1529 #[test_async]
1530 async fn test_verify_proof_and_extract_digests_rejects_metadata_mismatches_mmb() {
1531 type F = mmb::Family;
1532 const N: usize = 1;
1533 let start = Location::<F>::new(14);
1534 let end = Location::<F>::new(18);
1535 let (hasher, proof, operations, chunks, root, _ops) =
1536 current_range_proof_fixture::<F, N>(18, start..end).await;
1537
1538 assert!(proof.pending_chunk_digest.is_some());
1539
1540 let mut missing_pending = proof.clone();
1541 missing_pending.pending_chunk_digest = None;
1542 assert!(matches!(
1543 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1544 &missing_pending,
1545 start,
1546 &operations,
1547 &chunks,
1548 &root,
1549 ),
1550 Err(merkle::Error::InvalidProof)
1551 ));
1552
1553 let mut wrong_pending = proof.clone();
1554 wrong_pending.pending_chunk_digest = Some(hasher.digest(b"wrong pending"));
1555 assert!(matches!(
1556 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1557 &wrong_pending,
1558 start,
1559 &operations,
1560 &chunks,
1561 &root,
1562 ),
1563 Err(merkle::Error::InvalidProof)
1564 ));
1565
1566 let aligned_start = Location::<F>::new(8);
1567 let aligned_end = Location::<F>::new(12);
1568 let (hasher, proof, operations, chunks, root, _ops) =
1569 current_range_proof_fixture::<F, N>(16, aligned_start..aligned_end).await;
1570 assert!(proof.partial_chunk_digest.is_none());
1571
1572 let mut unexpected_partial = proof;
1574 unexpected_partial.partial_chunk_digest = Some(hasher.digest(b"unexpected partial"));
1575 assert!(matches!(
1576 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1577 &unexpected_partial,
1578 aligned_start,
1579 &operations,
1580 &chunks,
1581 &root,
1582 ),
1583 Err(merkle::Error::InvalidProof)
1584 ));
1585 }
1586
1587 #[test_async]
1588 async fn test_verify_proof_and_extract_digests_mmr() {
1589 verify_proof_and_extract_digests_inner::<mmr::Family>().await;
1590 }
1591
1592 #[test_async]
1593 async fn test_verify_proof_and_extract_digests_mmb() {
1594 verify_proof_and_extract_digests_inner::<mmb::Family>().await;
1595 }
1596
1597 #[test_async]
1602 async fn test_graftable_chunks_always_single_peak_at_pending_sizes() {
1603 type F = mmb::Family;
1604 const N: usize = 1;
1605
1606 let hasher = qmdb::hasher::<Sha256>();
1607 let grafting_height = grafting::height::<N>();
1608 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1609
1610 let mut found_any_pending = false;
1611 for leaves in chunk_bits * 3..=128u64 {
1612 let leaves_loc = mmb::Location::new(leaves);
1613 let leaves_count = *leaves_loc;
1614 let complete = leaves_count / chunk_bits;
1615 let graftable =
1616 grafting::graftable_chunks::<F>(leaves_count, grafting_height).min(complete);
1617 if graftable == complete {
1618 continue; }
1620 found_any_pending = true;
1621
1622 let size = F::location_to_position(leaves_loc);
1625 for chunk_idx in 0..graftable {
1626 let count = F::chunk_peaks(size, chunk_idx, grafting_height).count();
1627 assert_eq!(
1628 count, 1,
1629 "graftable chunk {chunk_idx} has {count} peaks (leaves={leaves_count}, graftable={graftable}, complete={complete})"
1630 );
1631 }
1632 }
1633 assert!(
1634 found_any_pending,
1635 "expected at least one MMB size in [{}, 128] with a pending chunk",
1636 chunk_bits * 3
1637 );
1638
1639 let leaf_count = (chunk_bits * 2..=256u64)
1642 .filter(|leaves| leaves % chunk_bits == 0)
1643 .find(|&leaves| {
1644 let size = F::location_to_position(mmb::Location::new(leaves));
1645 F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
1646 })
1647 .expect("expected a chunk-aligned MMB size whose chunk 1 is multi-peak");
1648 let loc = mmb::Location::new(chunk_bits + 1);
1649
1650 let mut status = BitMap::<N>::new();
1651 for _ in 0..leaf_count {
1652 status.push(true);
1653 }
1654 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1655 let ops_root = ops.root(&hasher, 0).unwrap();
1656
1657 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1658 *Location::<F>::try_from(ops.size()).unwrap(),
1659 grafting_height,
1660 )
1661 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1662 as usize;
1663 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1664 .map(|chunk_idx| {
1665 (
1666 chunk_idx,
1667 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1668 )
1669 })
1670 .collect();
1671 let mut leaf_digests =
1672 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1673 .await
1674 .unwrap();
1675 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1676
1677 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1678 let mut grafted = Mem::<F, sha256::Digest>::new();
1679 let merkleized = {
1680 let mut batch = grafted.new_batch();
1681 for (_, digest) in leaf_digests {
1682 batch = batch.add_leaf_digest(digest);
1683 }
1684 batch.merkleize(&grafted, &grafted_hasher)
1685 };
1686 grafted.apply_batch(&merkleized).unwrap();
1687
1688 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1689 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1690 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1691 &status,
1692 &storage,
1693 ops_leaves_for_root,
1694 None,
1695 Location::new(0),
1696 &ops_root,
1697 )
1698 .await
1699 .unwrap();
1700 let proof = RangeProof::new::<Sha256, _, N>(
1701 &status,
1702 &storage,
1703 Location::new(0),
1704 loc..loc + 1,
1705 ops_root,
1706 )
1707 .await
1708 .unwrap();
1709
1710 let element = hasher.digest(&(*loc).to_be_bytes());
1711 let chunk_idx = (*loc / chunk_bits) as usize;
1712 assert!(proof.verify::<Sha256, _, N>(
1713 loc,
1714 &[element],
1715 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1716 &status, chunk_idx
1717 )],
1718 &root,
1719 ));
1720
1721 let mut tampered = proof.clone();
1722 tampered.proof.inactive_peaks = 1;
1723 assert!(!tampered.verify::<Sha256, _, N>(
1724 loc,
1725 &[element],
1726 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1727 &status, chunk_idx
1728 )],
1729 &root,
1730 ));
1731
1732 let mut tampered = proof.clone();
1733 tampered.proof.inactive_peaks = usize::MAX;
1734 assert!(!tampered.verify::<Sha256, _, N>(
1735 loc,
1736 &[element],
1737 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1738 &status, chunk_idx
1739 )],
1740 &root,
1741 ));
1742
1743 let mut tampered = proof;
1744 assert!(!tampered.proof.digests.is_empty());
1745 tampered.proof.digests[0] = hasher.digest(b"fake generic sibling");
1746 assert!(!tampered.verify::<Sha256, _, N>(
1747 loc,
1748 &[element],
1749 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1750 &status, chunk_idx
1751 )],
1752 &root,
1753 ));
1754 }
1755
1756 #[test_async]
1762 async fn test_pending_and_partial_coexist_at_g_3() {
1763 type F = mmb::Family;
1764 const N: usize = 1; let hasher = qmdb::hasher::<Sha256>();
1767 let grafting_height = grafting::height::<N>();
1768 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1769 assert_eq!(grafting_height, 3);
1770 assert_eq!(chunk_bits, 8);
1771
1772 for k in 1u64..=2 {
1775 let leaf_count = chunk_bits + k;
1776 let mut status = BitMap::<N>::new();
1777 for _ in 0..leaf_count {
1778 status.push(true);
1779 }
1780 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1781 let ops_root = ops.root(&hasher, 0).unwrap();
1782
1783 let complete = <BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64;
1784 let graftable =
1785 grafting::graftable_chunks::<F>(leaf_count, grafting_height).min(complete);
1786 let next_bit = leaf_count % chunk_bits;
1787 assert_eq!(complete, 1);
1788 assert_eq!(graftable, 0);
1789 assert!(next_bit > 0, "expected partial chunk for k={k}");
1790
1791 let chunk_inputs: Vec<_> = (0..graftable as usize)
1794 .map(|chunk_idx| {
1795 (
1796 chunk_idx,
1797 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1798 )
1799 })
1800 .collect();
1801 let leaf_digests = db::compute_grafted_leaves::<F, Sha256, Sequential, N>(
1802 &ops,
1803 chunk_inputs,
1804 &Sequential,
1805 )
1806 .await
1807 .unwrap();
1808 let grafted_hasher =
1809 grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1810 let mut grafted = Mem::<F, sha256::Digest>::new();
1811 if !leaf_digests.is_empty() {
1812 let merkleized = {
1813 let mut batch = grafted.new_batch();
1814 for (_, digest) in leaf_digests {
1815 batch = batch.add_leaf_digest(digest);
1816 }
1817 batch.merkleize(&grafted, &grafted_hasher)
1818 };
1819 grafted.apply_batch(&merkleized).unwrap();
1820 }
1821 let storage =
1822 grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1823
1824 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1825 let canonical_root = db::compute_db_root::<F, Sha256, _, _, N>(
1826 &status,
1827 &storage,
1828 ops_leaves_for_root,
1829 db::partial_chunk::<_, N>(&status),
1830 Location::new(0),
1831 &ops_root,
1832 )
1833 .await
1834 .unwrap();
1835
1836 let pending_chunk_digest =
1838 db::pending_chunk::<F, _, N>(&status, ops_leaves_for_root, grafting_height)
1839 .unwrap()
1840 .map(|c| hasher.digest(&c));
1841 let partial_digest =
1842 db::partial_chunk::<_, N>(&status).map(|(c, nb)| (nb, hasher.digest(&c)));
1843 let grafted_root = db::compute_grafted_root::<F, Sha256, _, _, N>(
1844 &status,
1845 &storage,
1846 ops_leaves_for_root,
1847 Location::new(0),
1848 )
1849 .await
1850 .unwrap();
1851 let witness: OpsRootWitness<F, _> = OpsRootWitness {
1852 grafted_root,
1853 pending_chunk_digest,
1854 partial_chunk: partial_digest,
1855 };
1856 assert!(
1857 witness.verify::<Sha256>(&ops_root, &canonical_root),
1858 "OpsRootWitness verify failed at k={k}"
1859 );
1860 assert!(
1861 pending_chunk_digest.is_some(),
1862 "expected pending chunk at k={k}"
1863 );
1864 assert!(
1865 witness.partial_chunk.is_some(),
1866 "expected partial chunk at k={k}"
1867 );
1868
1869 let start = mmb::Location::new(0);
1871 let end = mmb::Location::new(leaf_count);
1872 let proof = RangeProof::new::<Sha256, _, N>(
1873 &status,
1874 &storage,
1875 Location::new(0),
1876 start..end,
1877 ops_root,
1878 )
1879 .await
1880 .unwrap();
1881 assert!(
1882 proof.pending_chunk_digest.is_some(),
1883 "expected RangeProof pending_chunk_digest at k={k}"
1884 );
1885 assert!(
1886 proof.partial_chunk_digest.is_some(),
1887 "expected RangeProof partial_chunk_digest at k={k}"
1888 );
1889
1890 let elements: Vec<sha256::Digest> = (0..leaf_count)
1891 .map(|i| hasher.digest(&i.to_be_bytes()))
1892 .collect();
1893 let chunks: Vec<[u8; N]> = (0..=1)
1895 .map(|i| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, i))
1896 .collect();
1897 assert!(
1898 proof.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1899 "RangeProof verify failed at k={k}"
1900 );
1901
1902 let pending_loc = mmb::Location::new(3);
1903 let pending_proof = RangeProof::new::<Sha256, _, N>(
1904 &status,
1905 &storage,
1906 Location::new(0),
1907 pending_loc..pending_loc + 1,
1908 ops_root,
1909 )
1910 .await
1911 .unwrap();
1912 assert!(
1913 pending_proof.pending_chunk_digest.is_some(),
1914 "expected single-element proof to carry pending chunk digest at k={k}"
1915 );
1916 let pending_element = hasher.digest(&(*pending_loc).to_be_bytes());
1917 assert!(
1918 pending_proof.verify::<Sha256, _, N>(
1919 pending_loc,
1920 &[pending_element],
1921 &[chunks[0]],
1922 &canonical_root,
1923 ),
1924 "single-element proof inside pending chunk failed at k={k}"
1925 );
1926
1927 let mut tampered = proof.clone();
1929 tampered.pending_chunk_digest = Some(hasher.digest(b"fake pending"));
1930 assert!(
1931 !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1932 "tampered pending digest accepted at k={k}"
1933 );
1934
1935 let mut tampered = proof.clone();
1936 tampered.pending_chunk_digest = None;
1937 assert!(
1938 !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1939 "missing pending digest accepted at k={k}"
1940 );
1941
1942 let mut bad_chunks = chunks.clone();
1943 bad_chunks[0][0] ^= 1;
1944 assert!(
1945 !proof.verify::<Sha256, _, N>(start, &elements, &bad_chunks, &canonical_root),
1946 "tampered pending chunk bytes accepted at k={k}"
1947 );
1948 }
1949 }
1950
1951 #[test_async]
1956 async fn test_pending_to_graftable_transition_at_birth_size() {
1957 type F = mmb::Family;
1958 const N: usize = 1; let hasher = qmdb::hasher::<Sha256>();
1961 let grafting_height = grafting::height::<N>();
1962 assert_eq!(grafting_height, 3);
1963
1964 let birth = (3u64 << (grafting_height - 1)) - 1;
1966 let pre_state_leaves = birth - 1; let post_state_leaves = birth; assert_eq!(pre_state_leaves, 10);
1970 assert_eq!(post_state_leaves, 11);
1971
1972 let graftable_pre = grafting::graftable_chunks::<F>(pre_state_leaves, grafting_height);
1973 let graftable_post = grafting::graftable_chunks::<F>(post_state_leaves, grafting_height);
1974 assert_eq!(graftable_pre, 0);
1975 assert_eq!(graftable_post, 1);
1976
1977 let mut status_pre = BitMap::<N>::new();
1979 for _ in 0..pre_state_leaves {
1980 status_pre.push(true);
1981 }
1982 let ops_pre = build_test_mem(&hasher, mmb::mem::Mmb::new(), pre_state_leaves);
1983 let ops_root_pre = ops_pre.root(&hasher, 0).unwrap();
1984 let grafted_pre = Mem::<F, sha256::Digest>::new();
1985 let storage_pre =
1986 grafting::Storage::<F, Sha256, _, _>::new(&grafted_pre, grafting_height, &ops_pre);
1987 let canonical_pre = db::compute_db_root::<F, Sha256, _, _, N>(
1988 &status_pre,
1989 &storage_pre,
1990 Location::<F>::new(pre_state_leaves),
1991 db::partial_chunk::<_, N>(&status_pre),
1992 Location::new(0),
1993 &ops_root_pre,
1994 )
1995 .await
1996 .unwrap();
1997
1998 let mut status_post = BitMap::<N>::new();
2000 for _ in 0..post_state_leaves {
2001 status_post.push(true);
2002 }
2003 let ops_post = build_test_mem(&hasher, mmb::mem::Mmb::new(), post_state_leaves);
2004 let ops_root_post = ops_post.root(&hasher, 0).unwrap();
2005 let leaf_digests = db::compute_grafted_leaves::<F, Sha256, Sequential, N>(
2007 &ops_post,
2008 core::iter::once((
2009 0usize,
2010 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status_post, 0),
2011 )),
2012 &Sequential,
2013 )
2014 .await
2015 .unwrap();
2016 assert_eq!(
2017 leaf_digests.len(),
2018 1,
2019 "post-state must have 1 graftable chunk"
2020 );
2021 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2022 let mut grafted_post = Mem::<F, sha256::Digest>::new();
2023 let merkleized = grafted_post
2024 .new_batch()
2025 .add_leaf_digest(leaf_digests[0].1)
2026 .merkleize(&grafted_post, &grafted_hasher);
2027 grafted_post.apply_batch(&merkleized).unwrap();
2028 let storage_post =
2029 grafting::Storage::<F, Sha256, _, _>::new(&grafted_post, grafting_height, &ops_post);
2030
2031 let canonical_post = db::compute_db_root::<F, Sha256, _, _, N>(
2032 &status_post,
2033 &storage_post,
2034 Location::<F>::new(post_state_leaves),
2035 db::partial_chunk::<_, N>(&status_post),
2036 Location::new(0),
2037 &ops_root_post,
2038 )
2039 .await
2040 .unwrap();
2041
2042 assert_ne!(
2043 canonical_pre, canonical_post,
2044 "canonical root must change when chunk 0 transitions from pending to graftable"
2045 );
2046 }
2047
2048 #[test_async]
2049 async fn test_range_proof_allows_ops_and_grafted_inactive_counts_to_differ() {
2050 type F = mmb::Family;
2051 const N: usize = 1;
2052
2053 let hasher = qmdb::hasher::<Sha256>();
2054 let grafting_height = grafting::height::<N>();
2055 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
2056 let leaf_count = chunk_bits;
2057 let leaves = mmb::Location::new(leaf_count);
2058 let inactivity_floor = mmb::Location::new(chunk_bits - 2);
2059
2060 let ops_inactive_peaks = F::inactive_peaks(leaves, inactivity_floor);
2061 let aligned_inactive =
2062 grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)
2063 .unwrap();
2064 assert_ne!(ops_inactive_peaks, aligned_inactive);
2065
2066 let mut status = BitMap::<N>::new();
2067 for _ in 0..leaf_count {
2068 status.push(true);
2069 }
2070 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
2071
2072 let ops_root = ops.root(&hasher, ops_inactive_peaks).unwrap();
2076
2077 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
2078 *Location::<F>::try_from(ops.size()).unwrap(),
2079 grafting_height,
2080 )
2081 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
2082 as usize;
2083 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
2084 .map(|chunk_idx| {
2085 (
2086 chunk_idx,
2087 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
2088 )
2089 })
2090 .collect();
2091 let mut leaf_digests =
2092 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
2093 .await
2094 .unwrap();
2095 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
2096
2097 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2098 let mut grafted = Mem::<F, sha256::Digest>::new();
2099 let merkleized = {
2100 let mut batch = grafted.new_batch();
2101 for (_, digest) in leaf_digests {
2102 batch = batch.add_leaf_digest(digest);
2103 }
2104 batch.merkleize(&grafted, &grafted_hasher)
2105 };
2106 grafted.apply_batch(&merkleized).unwrap();
2107
2108 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
2109 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
2110 let root = db::compute_db_root::<F, Sha256, _, _, N>(
2111 &status,
2112 &storage,
2113 ops_leaves_for_root,
2114 None,
2115 inactivity_floor,
2116 &ops_root,
2117 )
2118 .await
2119 .unwrap();
2120
2121 let loc = mmb::Location::new(chunk_bits - 1);
2122 let proof = RangeProof::new::<Sha256, _, N>(
2123 &status,
2124 &storage,
2125 inactivity_floor,
2126 loc..loc + 1,
2127 ops_root,
2128 )
2129 .await
2130 .unwrap();
2131 assert_eq!(proof.proof.inactive_peaks, aligned_inactive);
2132
2133 let element = hasher.digest(&(*loc).to_be_bytes());
2134 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
2135 assert!(proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root));
2136 }
2137
2138 #[cfg(feature = "arbitrary")]
2139 mod conformance {
2140 use super::super::{OperationProof, OpsRootWitness, RangeProof};
2141 use crate::merkle::{mmb, mmr};
2142 use commonware_codec::conformance::CodecConformance;
2143 use commonware_cryptography::sha256::Digest as Sha256Digest;
2144
2145 commonware_conformance::conformance_tests! {
2146 CodecConformance<OpsRootWitness<mmr::Family, Sha256Digest>>,
2147 CodecConformance<OpsRootWitness<mmb::Family, Sha256Digest>>,
2148 CodecConformance<RangeProof<mmr::Family, Sha256Digest>>,
2149 CodecConformance<RangeProof<mmb::Family, Sha256Digest>>,
2150 CodecConformance<OperationProof<mmr::Family, Sha256Digest, 32>>,
2151 CodecConformance<OperationProof<mmb::Family, Sha256Digest, 32>>,
2152 }
2153 }
2154}