1use crate::{
31 journal::contiguous::Contiguous,
32 merkle::{
33 self, hasher::Hasher as _, storage::Storage, Family, Graftable, Location, PendingChunk,
34 Position, Proof,
35 },
36 qmdb::{
37 self,
38 current::{
39 db::{combine_roots, partial_chunk, pending_chunk},
40 grafting,
41 },
42 Error,
43 },
44};
45use bytes::{Buf, BufMut};
46use commonware_codec::{varint::UInt, Codec, EncodeSize, Read, ReadExt as _, Write};
47use commonware_cryptography::{Digest, Hasher};
48use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
49use core::{num::NonZeroU64, ops::Range};
50use futures::future::try_join_all;
51use tracing::debug;
52
53#[derive(Clone, Eq, PartialEq, Debug)]
58pub struct OpsRootWitness<F: Graftable, D: Digest> {
59 pub grafted_root: D,
61
62 pub pending_chunk_digest: F::PendingChunk<D>,
64
65 pub partial_chunk: Option<(u64, D)>,
68}
69
70impl<F: Graftable, D: Digest> OpsRootWitness<F, D> {
71 pub fn root<H: Hasher<Digest = D>>(&self, ops_root: &D) -> D {
76 let partial = self.partial_chunk.as_ref().map(|(nb, d)| (*nb, d));
77 combine_roots::<H>(
78 ops_root,
79 &self.grafted_root,
80 self.pending_chunk_digest.as_ref(),
81 partial,
82 )
83 }
84
85 pub fn verify<H: Hasher<Digest = D>>(&self, ops_root: &D, root: &D) -> bool {
87 self.root::<H>(ops_root) == *root
88 }
89}
90
91impl<F: Graftable, D: Digest> Write for OpsRootWitness<F, D> {
92 fn write(&self, buf: &mut impl BufMut) {
93 self.grafted_root.write(buf);
94 self.pending_chunk_digest.write(buf);
95 self.partial_chunk.is_some().write(buf);
96 if let Some((next_bit, digest)) = &self.partial_chunk {
97 UInt(*next_bit).write(buf);
98 digest.write(buf);
99 }
100 }
101}
102
103impl<F: Graftable, D: Digest> EncodeSize for OpsRootWitness<F, D> {
104 fn encode_size(&self) -> usize {
105 self.grafted_root.encode_size()
106 + self.pending_chunk_digest.encode_size()
107 + self
108 .partial_chunk
109 .as_ref()
110 .map_or(1, |(nb, d)| 1 + UInt(*nb).encode_size() + d.encode_size())
111 }
112}
113
114impl<F: Graftable, D: Digest> Read for OpsRootWitness<F, D> {
115 type Cfg = ();
116
117 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
118 let grafted_root = D::read(buf)?;
119 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
120 let partial_chunk = if bool::read(buf)? {
121 let next_bit = UInt::<u64>::read(buf)?.into();
122 let digest = D::read(buf)?;
123 Some((next_bit, digest))
124 } else {
125 None
126 };
127 Ok(Self {
128 grafted_root,
129 pending_chunk_digest,
130 partial_chunk,
131 })
132 }
133}
134
135#[cfg(feature = "arbitrary")]
136impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for OpsRootWitness<F, D>
137where
138 D: for<'a> arbitrary::Arbitrary<'a>,
139 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
140{
141 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
142 Ok(Self {
143 grafted_root: u.arbitrary()?,
144 pending_chunk_digest: u.arbitrary()?,
145 partial_chunk: u.arbitrary()?,
146 })
147 }
148}
149
150#[derive(Clone, Eq, PartialEq, Debug)]
152pub struct RangeProof<F: Graftable, D: Digest> {
153 pub proof: Proof<F, D>,
155
156 pub pending_chunk_digest: F::PendingChunk<D>,
158
159 pub partial_chunk_digest: Option<D>,
161
162 pub ops_root: D,
164}
165
166#[derive(Clone, Copy, Eq, PartialEq, Debug)]
168pub struct RangeProofSpec<F: Family, D: Digest> {
169 pub start_loc: Location<F>,
171
172 pub max_ops: NonZeroU64,
174
175 pub inactivity_floor: Location<F>,
177
178 pub ops_root: D,
180}
181
182impl<F: Graftable, D: Digest> RangeProof<F, D> {
183 pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>, const N: usize>(
185 status: &impl BitmapReadable<N>,
186 storage: &S,
187 inactivity_floor: Location<F>,
188 range: Range<Location<F>>,
189 ops_root: D,
190 ) -> Result<Self, Error<F>> {
191 let ops_leaves = Location::try_from(storage.size())?;
194 let grafting_height = grafting::height::<N>();
195 let inactive_peaks = grafting::chunk_aligned_inactive_peaks::<F>(
196 ops_leaves,
197 inactivity_floor,
198 grafting_height,
199 )?;
200
201 let hasher = qmdb::hasher::<H>();
202 let proof = merkle::verification::historical_range_proof(
203 &hasher,
204 storage,
205 ops_leaves,
206 range,
207 inactive_peaks,
208 )
209 .await?;
210
211 let partial_chunk_digest =
212 partial_chunk::<_, N>(status).map(|(chunk, _)| hasher.digest(chunk.as_slice()));
213
214 let pending_chunk_digest: F::PendingChunk<D> =
215 pending_chunk::<_, _, N>(status, ops_leaves, grafting_height)?
216 .map(|chunk| hasher.digest(chunk.as_slice()))
217 .try_into()
218 .expect("pending_chunk must be consistent with family");
219
220 Ok(Self {
221 proof,
222 pending_chunk_digest,
223 partial_chunk_digest,
224 ops_root,
225 })
226 }
227
228 pub async fn new_with_ops<
238 H: Hasher<Digest = D>,
239 C: Contiguous,
240 S: Storage<F, Digest = D>,
241 const N: usize,
242 >(
243 status: &impl BitmapReadable<N>,
244 storage: &S,
245 log: &C,
246 request: RangeProofSpec<F, D>,
247 ) -> Result<(Self, Vec<C::Item>, Vec<[u8; N]>), Error<F>> {
248 let leaves = Location::new(status.len());
250 if request.start_loc >= leaves {
251 return Err(merkle::Error::RangeOutOfBounds(request.start_loc).into());
252 }
253
254 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
256 let start = *request.start_loc / chunk_bits;
257 if (start as usize) < status.pruned_chunks() {
258 return Err(Error::OperationPruned(request.start_loc));
259 }
260
261 let max_loc = request.start_loc.saturating_add(request.max_ops.get());
262 let end_loc = core::cmp::min(max_loc, leaves);
263
264 let proof = Self::new::<H, S, N>(
266 status,
267 storage,
268 request.inactivity_floor,
269 request.start_loc..end_loc,
270 request.ops_root,
271 )
272 .await?;
273
274 let futures = (*request.start_loc..*end_loc)
276 .map(|i| log.read(i))
277 .collect::<Vec<_>>();
278 let ops = try_join_all(futures).await?;
279
280 let end = (*end_loc - 1) / chunk_bits; let chunks = (start..=end)
283 .map(|i| status.get_chunk(i as usize))
284 .collect::<Vec<_>>();
285
286 Ok((proof, ops, chunks))
287 }
288
289 fn reconstruct_root<H, O, const N: usize>(
292 &self,
293 start_loc: Location<F>,
294 ops: &[O],
295 chunks: &[[u8; N]],
296 collected: Option<&mut Vec<(Position<F>, D)>>,
297 ) -> Result<D, merkle::Error<F>>
298 where
299 H: Hasher<Digest = D>,
300 O: Codec,
301 {
302 if ops.is_empty() || chunks.is_empty() {
303 debug!("verification failed, empty input");
304 return Err(merkle::Error::InvalidProof);
305 }
306 let Some(end_loc) = start_loc.checked_add(ops.len() as u64) else {
308 debug!("verification failed, end_loc overflow");
309 return Err(merkle::Error::InvalidProof);
310 };
311
312 let leaves = self.proof.leaves;
313 if end_loc > leaves {
314 debug!(
315 loc = ?end_loc,
316 ?leaves, "verification failed, invalid range"
317 );
318 return Err(merkle::Error::InvalidProof);
319 }
320
321 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
323 let start_chunk = *start_loc / chunk_bits;
324 let end_chunk = (*end_loc - 1) / chunk_bits;
325 let complete_chunks = *leaves / chunk_bits;
326
327 if (end_chunk - start_chunk + 1) != chunks.len() as u64 {
328 debug!("verification failed, chunk metadata length mismatch");
329 return Err(merkle::Error::InvalidProof);
330 }
331
332 let next_bit = *leaves % chunk_bits;
333 let has_partial_chunk = next_bit != 0;
334
335 let elements = ops.iter().map(|op| op.encode()).collect::<Vec<_>>();
336 let chunk_vec = chunks.iter().map(|c| c.as_ref()).collect::<Vec<_>>();
337 let grafting_height = grafting::height::<N>();
338
339 let graftable_chunks =
340 grafting::graftable_chunks::<F>(*leaves, grafting_height).min(complete_chunks);
341 let pending_chunks = complete_chunks - graftable_chunks;
342 if pending_chunks > 1 {
343 debug!(
344 ?complete_chunks,
345 ?graftable_chunks,
346 "verification failed, multiple pending chunks"
347 );
348 return Err(merkle::Error::InvalidProof);
349 }
350 let has_pending_chunk = pending_chunks == 1;
351
352 let grafting_verifier = grafting::Verifier::<F, H>::new(
353 grafting_height,
354 start_chunk,
355 chunk_vec,
356 graftable_chunks,
357 );
358
359 if self.pending_chunk_digest.as_ref().is_some() != has_pending_chunk {
360 debug!(
361 pending_in_proof = self.pending_chunk_digest.as_ref().is_some(),
362 expected = has_pending_chunk,
363 "pending_chunk_digest presence does not match bitmap state"
364 );
365 return Err(merkle::Error::InvalidProof);
366 }
367
368 if has_partial_chunk {
370 let Some(last_chunk_digest) = self.partial_chunk_digest else {
371 debug!("proof has no partial chunk digest");
372 return Err(merkle::Error::InvalidProof);
373 };
374
375 if end_chunk == complete_chunks {
378 let last_chunk = chunks.last().expect("chunks non-empty");
379 if last_chunk_digest != grafting_verifier.digest(last_chunk) {
380 debug!("last chunk digest does not match expected value");
381 return Err(merkle::Error::InvalidProof);
382 }
383 }
384 } else if self.partial_chunk_digest.is_some() {
385 debug!("proof has unexpected partial chunk digest");
386 return Err(merkle::Error::InvalidProof);
387 }
388
389 if let Some(pending_digest) = self.pending_chunk_digest.as_ref() {
393 let pending_idx = graftable_chunks;
394 if pending_idx >= start_chunk && pending_idx <= end_chunk {
395 let local = (pending_idx - start_chunk) as usize;
396 let Some(pending_chunk_bytes) = chunks.get(local) else {
400 debug!(
401 ?pending_idx,
402 chunks_len = chunks.len(),
403 "pending chunk index out of range in supplied chunks"
404 );
405 return Err(merkle::Error::InvalidProof);
406 };
407 if *pending_digest != grafting_verifier.digest(pending_chunk_bytes) {
408 debug!("pending chunk digest does not match expected value");
409 return Err(merkle::Error::InvalidProof);
410 }
411 }
412 }
413
414 let merkle_root = match self.proof.reconstruct_root_inner(
415 &grafting_verifier,
416 &elements,
417 start_loc,
418 collected,
419 ) {
420 Ok(root) => root,
421 Err(error) => {
422 debug!(?error, "invalid proof input");
423 return Err(merkle::Error::InvalidProof);
424 }
425 };
426
427 let partial =
428 has_partial_chunk.then(|| (next_bit, self.partial_chunk_digest.as_ref().unwrap()));
429 Ok(combine_roots::<H>(
430 &self.ops_root,
431 &merkle_root,
432 self.pending_chunk_digest.as_ref(),
433 partial,
434 ))
435 }
436
437 pub fn verify<H: Hasher<Digest = D>, O: Codec, const N: usize>(
440 &self,
441 start_loc: Location<F>,
442 ops: &[O],
443 chunks: &[[u8; N]],
444 root: &H::Digest,
445 ) -> bool {
446 matches!(
447 self.reconstruct_root::<H, O, N>(start_loc, ops, chunks, None),
448 Ok(reconstructed_root) if reconstructed_root == *root
449 )
450 }
451}
452
453pub fn verify_proof_and_extract_digests<F, Op, H, D, const N: usize>(
456 proof: &RangeProof<F, D>,
457 start_loc: Location<F>,
458 operations: &[Op],
459 chunks: &[[u8; N]],
460 target_root: &D,
461) -> Result<Vec<(Position<F>, D)>, merkle::Error<F>>
462where
463 F: Graftable,
464 Op: Codec,
465 H: Hasher<Digest = D>,
466 D: Digest,
467{
468 let mut collected = Vec::new();
469 let reconstructed_root =
470 proof.reconstruct_root::<H, Op, N>(start_loc, operations, chunks, Some(&mut collected))?;
471 if reconstructed_root != *target_root {
472 debug!("verification failed, root mismatch");
473 return Err(merkle::Error::RootMismatch);
474 }
475
476 Ok(collected)
477}
478
479impl<F: Graftable, D: Digest> Write for RangeProof<F, D> {
480 fn write(&self, buf: &mut impl BufMut) {
481 self.proof.write(buf);
482 self.pending_chunk_digest.write(buf);
483 self.partial_chunk_digest.write(buf);
484 self.ops_root.write(buf);
485 }
486}
487
488impl<F: Graftable, D: Digest> EncodeSize for RangeProof<F, D> {
489 fn encode_size(&self) -> usize {
490 self.proof.encode_size()
491 + self.pending_chunk_digest.encode_size()
492 + self.partial_chunk_digest.encode_size()
493 + self.ops_root.encode_size()
494 }
495}
496
497impl<F: Graftable, D: Digest> Read for RangeProof<F, D> {
498 type Cfg = usize;
500
501 fn read_cfg(
502 buf: &mut impl Buf,
503 max_digests: &Self::Cfg,
504 ) -> Result<Self, commonware_codec::Error> {
505 let proof = Proof::<F, D>::read_cfg(buf, max_digests)?;
506 let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
507 let partial_chunk_digest = Option::<D>::read(buf)?;
508 let ops_root = D::read(buf)?;
509 Ok(Self {
510 proof,
511 pending_chunk_digest,
512 partial_chunk_digest,
513 ops_root,
514 })
515 }
516}
517
518#[cfg(feature = "arbitrary")]
519impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for RangeProof<F, D>
520where
521 D: for<'a> arbitrary::Arbitrary<'a>,
522 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
523{
524 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
525 Ok(Self {
526 proof: u.arbitrary()?,
527 pending_chunk_digest: u.arbitrary()?,
528 partial_chunk_digest: u.arbitrary()?,
529 ops_root: u.arbitrary()?,
530 })
531 }
532}
533
534#[derive(Clone, Eq, PartialEq, Debug)]
536pub struct OperationProof<F: Graftable, D: Digest, const N: usize> {
537 pub loc: Location<F>,
539
540 pub chunk: [u8; N],
542
543 pub range_proof: RangeProof<F, D>,
545}
546
547impl<F: Graftable, D: Digest, const N: usize> OperationProof<F, D, N> {
548 pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>>(
555 status: &impl BitmapReadable<N>,
556 storage: &S,
557 inactivity_floor: Location<F>,
558 loc: Location<F>,
559 ops_root: D,
560 ) -> Result<Self, Error<F>> {
561 if BitMap::<N>::to_chunk_index(*loc) < status.pruned_chunks() {
563 return Err(Error::OperationPruned(loc));
564 }
565 let range_proof =
566 RangeProof::new::<H, S, N>(status, storage, inactivity_floor, loc..loc + 1, ops_root)
567 .await?;
568 let chunk = status.get_chunk(BitMap::<N>::to_chunk_index(*loc));
569 Ok(Self {
570 loc,
571 chunk,
572 range_proof,
573 })
574 }
575}
576
577impl<F: Graftable, D: Digest, const N: usize> OperationProof<F, D, N> {
578 pub fn verify<H: Hasher<Digest = D>, O: Codec>(&self, operation: O, root: &D) -> bool {
581 if !BitMap::<N>::get_bit_from_chunk(&self.chunk, *self.loc) {
584 debug!(
585 ?self.loc,
586 "proof verification failed, operation is inactive"
587 );
588 return false;
589 }
590
591 self.range_proof
592 .verify::<H, O, N>(self.loc, &[operation], &[self.chunk], root)
593 }
594}
595
596impl<F: Graftable, D: Digest, const N: usize> Write for OperationProof<F, D, N> {
597 fn write(&self, buf: &mut impl BufMut) {
598 self.loc.write(buf);
599 self.chunk.write(buf);
600 self.range_proof.write(buf);
601 }
602}
603
604impl<F: Graftable, D: Digest, const N: usize> EncodeSize for OperationProof<F, D, N> {
605 fn encode_size(&self) -> usize {
606 self.loc.encode_size() + self.chunk.encode_size() + self.range_proof.encode_size()
607 }
608}
609
610impl<F: Graftable, D: Digest, const N: usize> Read for OperationProof<F, D, N> {
611 type Cfg = usize;
613
614 fn read_cfg(
615 buf: &mut impl Buf,
616 max_digests: &Self::Cfg,
617 ) -> Result<Self, commonware_codec::Error> {
618 let loc = Location::<F>::read(buf)?;
619 let chunk = <[u8; N]>::read(buf)?;
620 let range_proof = RangeProof::<F, D>::read_cfg(buf, max_digests)?;
621 Ok(Self {
622 loc,
623 chunk,
624 range_proof,
625 })
626 }
627}
628
629#[cfg(feature = "arbitrary")]
630impl<F: Graftable, D: Digest, const N: usize> arbitrary::Arbitrary<'_> for OperationProof<F, D, N>
631where
632 D: for<'a> arbitrary::Arbitrary<'a>,
633 F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
634{
635 fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
636 Ok(Self {
637 loc: u.arbitrary()?,
638 chunk: u.arbitrary()?,
639 range_proof: u.arbitrary()?,
640 })
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647 use crate::{
648 merkle::{conformance::build_test_mem, hasher::Standard as StandardHasher, mem::Mem},
649 mmb, mmr,
650 qmdb::current::{db, grafting},
651 };
652 use commonware_codec::{Decode as _, DecodeExt as _, Encode as _};
653 use commonware_cryptography::{sha256, Sha256};
654 use commonware_macros::test_async;
655 use commonware_parallel::Sequential;
656 use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
657 use core::ops::Range;
658
659 #[test]
660 fn test_ops_root_witness_codec_roundtrip() {
661 type F = mmb::Family;
662 for partial_chunk in [
663 None,
664 Some((0u64, Sha256::hash(b"partial-zero"))),
665 Some((123u64, Sha256::hash(b"partial-nonzero"))),
666 ] {
667 let witness: OpsRootWitness<F, _> = OpsRootWitness {
668 grafted_root: Sha256::hash(b"grafted"),
669 pending_chunk_digest: None,
670 partial_chunk,
671 };
672 let encoded = witness.encode();
673 assert_eq!(encoded.len(), witness.encode_size());
674 let decoded = OpsRootWitness::<F, sha256::Digest>::decode(encoded).unwrap();
675 assert_eq!(decoded, witness);
676 }
677 }
678
679 #[test]
680 fn test_ops_root_witness_root_matches_verify() {
681 type F = mmb::Family;
682
683 let ops_root = Sha256::hash(b"ops root");
684 let witness: OpsRootWitness<F, _> = OpsRootWitness {
685 grafted_root: Sha256::hash(b"grafted root"),
686 pending_chunk_digest: Some(Sha256::hash(b"pending chunk")),
687 partial_chunk: Some((13, Sha256::hash(b"partial chunk"))),
688 };
689
690 let root = witness.root::<Sha256>(&ops_root);
691
692 assert!(witness.verify::<Sha256>(&ops_root, &root));
693 assert_ne!(root, ops_root);
694
695 let wrong_ops_root = Sha256::hash(b"wrong ops root");
696 assert!(!witness.verify::<Sha256>(&wrong_ops_root, &root));
697 }
698
699 fn range_proof_digest_count<F: Graftable, D: Digest>(proof: &RangeProof<F, D>) -> usize {
700 proof.proof.digests.len()
701 }
702
703 #[test]
704 fn test_range_proof_codec_roundtrip() {
705 type F = mmb::Family;
706 const MAX_DIGESTS: usize = 64;
707
708 let proof = Proof::<F, sha256::Digest> {
709 leaves: mmb::Location::new(42),
710 inactive_peaks: 0,
711 digests: vec![
712 Sha256::hash(b"d0"),
713 Sha256::hash(b"d1"),
714 Sha256::hash(b"d2"),
715 ],
716 };
717 let ops_root = Sha256::hash(b"ops-root");
718
719 let cases = [
720 RangeProof {
722 proof: proof.clone(),
723 pending_chunk_digest: None,
724 partial_chunk_digest: None,
725 ops_root,
726 },
727 RangeProof {
729 proof,
730 pending_chunk_digest: Some(Sha256::hash(b"pending")),
731 partial_chunk_digest: Some(Sha256::hash(b"partial")),
732 ops_root,
733 },
734 RangeProof {
736 proof: Proof::<F, sha256::Digest>::default(),
737 pending_chunk_digest: None,
738 partial_chunk_digest: Some(Sha256::hash(b"only-partial")),
739 ops_root,
740 },
741 ];
742
743 for proof in cases {
744 let encoded = proof.encode();
745 assert_eq!(encoded.len(), proof.encode_size());
746 let decoded =
747 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
748 assert_eq!(decoded, proof);
749 }
750 }
751
752 #[test]
753 fn test_range_proof_codec_enforces_merkle_digest_budget() {
754 type F = mmb::Family;
755
756 let proof = RangeProof {
757 proof: Proof::<F, sha256::Digest> {
758 leaves: mmb::Location::new(42),
759 inactive_peaks: 0,
760 digests: vec![Sha256::hash(b"d0")],
761 },
762 pending_chunk_digest: None,
763 partial_chunk_digest: None,
764 ops_root: Sha256::hash(b"ops-root"),
765 };
766
767 let encoded = proof.encode();
768 let total_digests = range_proof_digest_count(&proof);
769
770 let decoded =
771 RangeProof::<F, sha256::Digest>::decode_cfg(encoded.clone(), &total_digests).unwrap();
772 assert_eq!(decoded, proof);
773 assert!(
774 RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &(total_digests - 1)).is_err()
775 );
776 }
777
778 #[test]
779 fn test_range_proof_decode_rejects_pending_for_mmr() {
780 const MAX_DIGESTS: usize = 64;
781
782 let proof = RangeProof {
783 proof: Proof::<mmb::Family, sha256::Digest> {
784 leaves: mmb::Location::new(42),
785 inactive_peaks: 0,
786 digests: vec![Sha256::hash(b"d0")],
787 },
788 pending_chunk_digest: Some(Sha256::hash(b"pending")),
789 partial_chunk_digest: None,
790 ops_root: Sha256::hash(b"ops-root"),
791 };
792 let encoded = proof.encode();
793
794 assert!(RangeProof::<mmb::Family, sha256::Digest>::decode_cfg(
796 encoded.clone(),
797 &MAX_DIGESTS
798 )
799 .is_ok());
800
801 assert!(
803 RangeProof::<crate::merkle::mmr::Family, sha256::Digest>::decode_cfg(
804 encoded,
805 &MAX_DIGESTS
806 )
807 .is_err()
808 );
809 }
810
811 #[test]
812 fn test_operation_proof_codec_roundtrip() {
813 type F = mmb::Family;
814 const N: usize = 32;
815 const MAX_DIGESTS: usize = 64;
816
817 let range_proof = RangeProof {
818 proof: Proof::<F, sha256::Digest> {
819 leaves: mmb::Location::new(7),
820 inactive_peaks: 0,
821 digests: vec![Sha256::hash(b"sib")],
822 },
823 pending_chunk_digest: None,
824 partial_chunk_digest: None,
825 ops_root: Sha256::hash(b"ops"),
826 };
827
828 let chunk: [u8; N] = core::array::from_fn(|i| i as u8);
829
830 let proof = OperationProof::<F, sha256::Digest, N> {
831 loc: mmb::Location::new(5),
832 chunk,
833 range_proof,
834 };
835
836 let encoded = proof.encode();
837 assert_eq!(encoded.len(), proof.encode_size());
838 let decoded =
839 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
840 assert_eq!(decoded, proof);
841 }
842
843 #[test]
844 fn test_operation_proof_codec_enforces_merkle_digest_budget() {
845 type F = mmb::Family;
846 const N: usize = 32;
847
848 let range_proof = RangeProof {
849 proof: Proof::<F, sha256::Digest> {
850 leaves: mmb::Location::new(7),
851 inactive_peaks: 0,
852 digests: vec![Sha256::hash(b"sib")],
853 },
854 pending_chunk_digest: None,
855 partial_chunk_digest: None,
856 ops_root: Sha256::hash(b"ops"),
857 };
858 let total_digests = range_proof_digest_count(&range_proof);
859 let proof = OperationProof::<F, sha256::Digest, N> {
860 loc: mmb::Location::new(5),
861 chunk: core::array::from_fn(|i| i as u8),
862 range_proof,
863 };
864
865 let encoded = proof.encode();
866 let decoded =
867 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded.clone(), &total_digests)
868 .unwrap();
869 assert_eq!(decoded, proof);
870 assert!(
871 OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &(total_digests - 1))
872 .is_err()
873 );
874 }
875
876 #[test_async]
877 async fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
878 type F = mmb::Family;
879 const N: usize = 1;
880
881 let hasher = qmdb::hasher::<Sha256>();
882 let grafting_height = grafting::height::<N>();
883
884 let leaf_count = (16..=64u64)
885 .find(|&leaves| {
886 let size = F::location_to_position(mmb::Location::new(leaves));
887 F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
888 })
889 .expect("expected an MMB size whose second chunk spans multiple peaks");
890
891 let mut status = BitMap::<N>::new();
892 for _ in 0..leaf_count {
893 status.push(true);
894 }
895 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
896 let ops_root = ops.root(&hasher, 0).unwrap();
897
898 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
899 *Location::<F>::try_from(ops.size()).unwrap(),
900 grafting_height,
901 )
902 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
903 as usize;
904 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
905 .map(|chunk_idx| {
906 (
907 chunk_idx,
908 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
909 )
910 })
911 .collect();
912 let mut leaf_digests =
913 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
914 .await
915 .unwrap();
916 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
917
918 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
919 let mut grafted = Mem::<F, sha256::Digest>::new();
920 let merkleized = {
921 let mut batch = grafted.new_batch();
922 for (_, digest) in leaf_digests {
923 batch = batch.add_leaf_digest(digest);
924 }
925 batch.merkleize(&grafted, &grafted_hasher)
926 };
927 grafted.apply_batch(&merkleized).unwrap();
928
929 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
930 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
931 let root = db::compute_db_root::<F, Sha256, _, _, N>(
932 &status,
933 &storage,
934 ops_leaves_for_root,
935 None,
936 Location::new(0),
937 &ops_root,
938 )
939 .await
940 .unwrap();
941
942 let loc = mmb::Location::new(BitMap::<N>::CHUNK_SIZE_BITS + 4);
943 let proof = RangeProof::new::<Sha256, _, N>(
944 &status,
945 &storage,
946 Location::new(0),
947 loc..loc + 1,
948 ops_root,
949 )
950 .await
951 .unwrap();
952
953 let element = hasher.digest(&(*loc).to_be_bytes());
954 assert!(proof.verify::<Sha256, _, N>(
955 loc,
956 &[element],
957 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 1)],
958 &root,
959 ));
960 }
961
962 #[test_async]
963 async fn test_range_proof_verifies_with_partial_suffix_mmb() {
964 type F = mmb::Family;
965 const N: usize = 1;
966
967 let hasher = qmdb::hasher::<Sha256>();
968 let grafting_height = grafting::height::<N>();
969 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
970
971 let (leaf_count, loc) = (chunk_bits * 2 + 1..=64u64)
972 .find_map(|leaves| {
973 let complete_chunks = leaves / chunk_bits;
974 if complete_chunks < 2 || leaves % chunk_bits == 0 {
975 return None;
976 }
977
978 let size = F::location_to_position(mmb::Location::new(leaves));
979 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
980 Some((leaves, mmb::Location::new(chunk_bits + 1)))
981 })
982 .expect("expected an MMB proof with a partial trailing suffix chunk");
983
984 let mut status = BitMap::<N>::new();
985 for _ in 0..leaf_count {
986 status.push(true);
987 }
988 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
989 let ops_root = ops.root(&hasher, 0).unwrap();
990
991 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
992 *Location::<F>::try_from(ops.size()).unwrap(),
993 grafting_height,
994 )
995 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
996 as usize;
997 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
998 .map(|chunk_idx| {
999 (
1000 chunk_idx,
1001 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1002 )
1003 })
1004 .collect();
1005 let mut leaf_digests =
1006 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1007 .await
1008 .unwrap();
1009 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1010
1011 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1012 let mut grafted = Mem::<F, sha256::Digest>::new();
1013 let merkleized = {
1014 let mut batch = grafted.new_batch();
1015 for (_, digest) in leaf_digests {
1016 batch = batch.add_leaf_digest(digest);
1017 }
1018 batch.merkleize(&grafted, &grafted_hasher)
1019 };
1020 grafted.apply_batch(&merkleized).unwrap();
1021
1022 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1023 let partial = {
1024 let (chunk, next_bit) = status.last_chunk();
1025 Some((*chunk, next_bit))
1026 };
1027 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1028 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1029 &status,
1030 &storage,
1031 ops_leaves_for_root,
1032 partial,
1033 Location::new(0),
1034 &ops_root,
1035 )
1036 .await
1037 .unwrap();
1038 let proof = RangeProof::new::<Sha256, _, N>(
1039 &status,
1040 &storage,
1041 Location::new(0),
1042 loc..loc + 1,
1043 ops_root,
1044 )
1045 .await
1046 .unwrap();
1047
1048 let element = hasher.digest(&(*loc).to_be_bytes());
1049 let chunk_idx = (*loc / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
1050 assert!(proof.verify::<Sha256, _, N>(
1051 loc,
1052 &[element],
1053 &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1054 &status, chunk_idx
1055 )],
1056 &root,
1057 ));
1058 }
1059
1060 #[test_async]
1061 async fn test_range_proof_verifies_when_range_reaches_partial_chunk_mmb() {
1062 type F = mmb::Family;
1063 const N: usize = 1;
1064
1065 let hasher = qmdb::hasher::<Sha256>();
1066 let grafting_height = grafting::height::<N>();
1067 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1068
1069 let (leaf_count, start_loc, complete_chunks) = (17..=128u64)
1073 .find_map(|leaves| {
1074 let complete_chunks = leaves / chunk_bits;
1075 if complete_chunks < 2 || leaves % chunk_bits == 0 {
1076 return None;
1077 }
1078 let leaves_loc = mmb::Location::new(leaves);
1079 let size = F::location_to_position(leaves_loc);
1080 F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1081 let start_loc = mmb::Location::new(chunk_bits + 1);
1082 Some((leaves, start_loc, complete_chunks))
1083 })
1084 .expect("expected an MMB size with chunk 1 multi-peak and a partial trailing chunk");
1085
1086 let mut status = BitMap::<N>::new();
1087 for _ in 0..leaf_count {
1088 status.push(true);
1089 }
1090 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1091 let ops_root = ops.root(&hasher, 0).unwrap();
1092
1093 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1094 *Location::<F>::try_from(ops.size()).unwrap(),
1095 grafting_height,
1096 )
1097 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1098 as usize;
1099 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1100 .map(|chunk_idx| {
1101 (
1102 chunk_idx,
1103 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1104 )
1105 })
1106 .collect();
1107 let mut leaf_digests =
1108 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1109 .await
1110 .unwrap();
1111 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1112
1113 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1114 let mut grafted = Mem::<F, sha256::Digest>::new();
1115 let merkleized = {
1116 let mut batch = grafted.new_batch();
1117 for (_, digest) in leaf_digests {
1118 batch = batch.add_leaf_digest(digest);
1119 }
1120 batch.merkleize(&grafted, &grafted_hasher)
1121 };
1122 grafted.apply_batch(&merkleized).unwrap();
1123
1124 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1125 let partial = {
1126 let (chunk, next_bit) = status.last_chunk();
1127 Some((*chunk, next_bit))
1128 };
1129 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1130 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1131 &status,
1132 &storage,
1133 ops_leaves_for_root,
1134 partial,
1135 Location::new(0),
1136 &ops_root,
1137 )
1138 .await
1139 .unwrap();
1140
1141 let leaves_loc = mmb::Location::new(leaf_count);
1142 let proof = RangeProof::new::<Sha256, _, N>(
1143 &status,
1144 &storage,
1145 Location::new(0),
1146 start_loc..leaves_loc,
1147 ops_root,
1148 )
1149 .await
1150 .unwrap();
1151
1152 let elements = (*start_loc..leaf_count)
1153 .map(|idx| hasher.digest(&idx.to_be_bytes()))
1154 .collect::<Vec<_>>();
1155 let start_chunk_idx = (*start_loc / chunk_bits) as usize;
1156 let end_chunk_idx = complete_chunks as usize;
1157 let chunks = (start_chunk_idx..=end_chunk_idx)
1158 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1159 .collect::<Vec<_>>();
1160 assert!(proof.verify::<Sha256, _, N>(start_loc, &elements, &chunks, &root,));
1161
1162 let mut bad_chunks = chunks;
1164 let last = bad_chunks.last_mut().unwrap();
1165 last[0] ^= 1;
1166 assert!(
1167 !proof.verify::<Sha256, _, N>(start_loc, &elements, &bad_chunks, &root),
1168 "tampered partial chunk bytes should not verify"
1169 );
1170 }
1171
1172 #[test_async]
1173 async fn test_range_proof_rejects_unexpected_partial_chunk_digest() {
1174 type F = mmb::Family;
1175 const N: usize = 1;
1176
1177 let hasher = qmdb::hasher::<Sha256>();
1178 let grafting_height = grafting::height::<N>();
1179 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1180
1181 let leaf_count = chunk_bits * 2; let mut status = BitMap::<N>::new();
1183 for _ in 0..leaf_count {
1184 status.push(true);
1185 }
1186 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1187 let ops_root = ops.root(&hasher, 0).unwrap();
1188
1189 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1190 *Location::<F>::try_from(ops.size()).unwrap(),
1191 grafting_height,
1192 )
1193 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1194 as usize;
1195 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1196 .map(|chunk_idx| {
1197 (
1198 chunk_idx,
1199 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1200 )
1201 })
1202 .collect();
1203 let mut leaf_digests =
1204 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1205 .await
1206 .unwrap();
1207 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1208
1209 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1210 let mut grafted = Mem::<F, sha256::Digest>::new();
1211 let merkleized = {
1212 let mut batch = grafted.new_batch();
1213 for (_, digest) in leaf_digests {
1214 batch = batch.add_leaf_digest(digest);
1215 }
1216 batch.merkleize(&grafted, &grafted_hasher)
1217 };
1218 grafted.apply_batch(&merkleized).unwrap();
1219
1220 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1221 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1222 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1223 &status,
1224 &storage,
1225 ops_leaves_for_root,
1226 None,
1227 Location::new(0),
1228 &ops_root,
1229 )
1230 .await
1231 .unwrap();
1232
1233 let loc = mmb::Location::new(0);
1234 let mut proof = RangeProof::new::<Sha256, _, N>(
1235 &status,
1236 &storage,
1237 Location::new(0),
1238 loc..loc + 1,
1239 ops_root,
1240 )
1241 .await
1242 .unwrap();
1243
1244 let element = hasher.digest(&(*loc).to_be_bytes());
1245 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
1246
1247 let mut tampered = proof.clone();
1249 tampered.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1250 assert!(!tampered.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1251
1252 proof.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1253 assert!(!proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1254 }
1255
1256 async fn current_range_proof_fixture<F: Graftable, const N: usize>(
1257 leaf_count: u64,
1258 range: Range<Location<F>>,
1259 ) -> (
1260 StandardHasher<Sha256>,
1261 RangeProof<F, sha256::Digest>,
1262 Vec<sha256::Digest>,
1263 Vec<[u8; N]>,
1264 sha256::Digest,
1265 Mem<F, sha256::Digest>,
1266 ) {
1267 let hasher = qmdb::hasher::<Sha256>();
1268 let grafting_height = grafting::height::<N>();
1269 let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1270
1271 let mut status = BitMap::<N>::new();
1272 for _ in 0..leaf_count {
1273 status.push(true);
1274 }
1275
1276 let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaf_count);
1277 let ops_root = ops.root(&hasher, 0).unwrap();
1278 let ops_leaves = Location::<F>::try_from(ops.size()).unwrap();
1279
1280 let graftable_chunks_for_test =
1281 grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1282 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1283 as usize;
1284 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1285 .map(|chunk_idx| {
1286 (
1287 chunk_idx,
1288 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1289 )
1290 })
1291 .collect();
1292 let mut leaf_digests =
1293 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1294 .await
1295 .unwrap();
1296 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1297
1298 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1299 let mut grafted = Mem::<F, sha256::Digest>::new();
1300 if !leaf_digests.is_empty() {
1301 let merkleized = {
1302 let mut batch = grafted.new_batch();
1303 for (_, digest) in leaf_digests {
1304 batch = batch.add_leaf_digest(digest);
1305 }
1306 batch.merkleize(&grafted, &grafted_hasher)
1307 };
1308 grafted.apply_batch(&merkleized).unwrap();
1309 }
1310
1311 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1312 let root = db::compute_db_root::<F, Sha256, _, _, N>(
1313 &status,
1314 &storage,
1315 ops_leaves,
1316 db::partial_chunk::<_, N>(&status),
1317 Location::new(0),
1318 &ops_root,
1319 )
1320 .await
1321 .unwrap();
1322
1323 let proof = RangeProof::new::<Sha256, _, N>(
1324 &status,
1325 &storage,
1326 Location::new(0),
1327 range.clone(),
1328 ops_root,
1329 )
1330 .await
1331 .unwrap();
1332 let operations = (*range.start..*range.end)
1333 .map(|i| hasher.digest(&i.to_be_bytes()))
1334 .collect::<Vec<_>>();
1335
1336 let start_chunk = (*range.start / chunk_bits) as usize;
1338 let end_chunk = ((*range.end - 1) / chunk_bits) as usize;
1339 let chunks = (start_chunk..=end_chunk)
1340 .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1341 .collect::<Vec<_>>();
1342
1343 assert!(proof.verify::<Sha256, _, N>(range.start, &operations, &chunks, &root));
1344
1345 (hasher, proof, operations, chunks, root, ops)
1346 }
1347
1348 async fn verify_proof_and_extract_digests_inner<F: Graftable>() {
1349 const N: usize = 1;
1350 let start = Location::<F>::new(14);
1351 let end = Location::<F>::new(18);
1352 let (hasher, proof, operations, chunks, root, ops) =
1353 current_range_proof_fixture::<F, N>(18, start..end).await;
1354
1355 let extracted = verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1356 &proof,
1357 start,
1358 &operations,
1359 &chunks,
1360 &root,
1361 )
1362 .unwrap();
1363 assert!(!extracted.is_empty());
1364
1365 for loc in *start..*end {
1367 let pos = F::location_to_position(Location::<F>::new(loc));
1368 let expected = ops.get_node(pos).unwrap();
1369 assert!(
1370 extracted
1371 .iter()
1372 .any(|(actual_pos, actual)| *actual_pos == pos && *actual == expected),
1373 "missing extracted leaf digest at {pos:?}",
1374 );
1375 }
1376
1377 let wrong_root = hasher.digest(b"wrong current root");
1379 assert!(matches!(
1380 verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1381 &proof,
1382 start,
1383 &operations,
1384 &chunks,
1385 &wrong_root,
1386 ),
1387 Err(merkle::Error::RootMismatch)
1388 ));
1389
1390 let mut wrong_operations = operations.clone();
1392 wrong_operations[0] = hasher.digest(b"wrong operation");
1393 assert!(verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1394 &proof,
1395 start,
1396 &wrong_operations,
1397 &chunks,
1398 &root,
1399 )
1400 .is_err());
1401
1402 let mut bad_chunks = chunks;
1403 bad_chunks.last_mut().unwrap()[0] ^= 1;
1404 assert!(verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1405 &proof,
1406 start,
1407 &operations,
1408 &bad_chunks,
1409 &root,
1410 )
1411 .is_err());
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 =
2061 F::inactive_peaks(F::location_to_position(leaves), inactivity_floor);
2062 let aligned_inactive =
2063 grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)
2064 .unwrap();
2065 assert_ne!(ops_inactive_peaks, aligned_inactive);
2066
2067 let mut status = BitMap::<N>::new();
2068 for _ in 0..leaf_count {
2069 status.push(true);
2070 }
2071 let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
2072
2073 let ops_root = ops.root(&hasher, ops_inactive_peaks).unwrap();
2077
2078 let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
2079 *Location::<F>::try_from(ops.size()).unwrap(),
2080 grafting_height,
2081 )
2082 .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
2083 as usize;
2084 let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
2085 .map(|chunk_idx| {
2086 (
2087 chunk_idx,
2088 <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
2089 )
2090 })
2091 .collect();
2092 let mut leaf_digests =
2093 db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
2094 .await
2095 .unwrap();
2096 leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
2097
2098 let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2099 let mut grafted = Mem::<F, sha256::Digest>::new();
2100 let merkleized = {
2101 let mut batch = grafted.new_batch();
2102 for (_, digest) in leaf_digests {
2103 batch = batch.add_leaf_digest(digest);
2104 }
2105 batch.merkleize(&grafted, &grafted_hasher)
2106 };
2107 grafted.apply_batch(&merkleized).unwrap();
2108
2109 let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
2110 let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
2111 let root = db::compute_db_root::<F, Sha256, _, _, N>(
2112 &status,
2113 &storage,
2114 ops_leaves_for_root,
2115 None,
2116 inactivity_floor,
2117 &ops_root,
2118 )
2119 .await
2120 .unwrap();
2121
2122 let loc = mmb::Location::new(chunk_bits - 1);
2123 let proof = RangeProof::new::<Sha256, _, N>(
2124 &status,
2125 &storage,
2126 inactivity_floor,
2127 loc..loc + 1,
2128 ops_root,
2129 )
2130 .await
2131 .unwrap();
2132 assert_eq!(proof.proof.inactive_peaks, aligned_inactive);
2133
2134 let element = hasher.digest(&(*loc).to_be_bytes());
2135 let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
2136 assert!(proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root));
2137 }
2138
2139 #[cfg(feature = "arbitrary")]
2140 mod conformance {
2141 use super::super::{OperationProof, OpsRootWitness, RangeProof};
2142 use crate::merkle::{mmb, mmr};
2143 use commonware_codec::conformance::CodecConformance;
2144 use commonware_cryptography::sha256::Digest as Sha256Digest;
2145
2146 commonware_conformance::conformance_tests! {
2147 CodecConformance<OpsRootWitness<mmr::Family, Sha256Digest>>,
2148 CodecConformance<OpsRootWitness<mmb::Family, Sha256Digest>>,
2149 CodecConformance<RangeProof<mmr::Family, Sha256Digest>>,
2150 CodecConformance<RangeProof<mmb::Family, Sha256Digest>>,
2151 CodecConformance<OperationProof<mmr::Family, Sha256Digest, 32>>,
2152 CodecConformance<OperationProof<mmb::Family, Sha256Digest, 32>>,
2153 }
2154 }
2155}