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