1use crate::{
64 merkle::{
65 self, hasher::Hasher as HasherTrait, storage::Storage as StorageTrait, Family, Graftable,
66 Location, Position, Readable,
67 },
68 qmdb,
69};
70use commonware_cryptography::Hasher;
71use commonware_parallel::Strategy;
72use commonware_utils::bitmap::BitMap;
73use core::{cmp::Ordering, marker::PhantomData};
74use tracing::debug;
75
76pub const fn height<const N: usize>() -> u32 {
78 BitMap::<N>::CHUNK_SIZE_BITS.trailing_zeros()
79}
80
81pub(super) const fn hasher<F: Graftable, H: Hasher>(
83 grafting_height: u32,
84) -> GraftedHasher<F, merkle::hasher::Standard<H>> {
85 GraftedHasher::new(qmdb::hasher(), grafting_height)
86}
87
88pub fn graftable_chunks<F: Graftable>(ops_leaves: u64, grafting_height: u32) -> u64 {
97 assert!(grafting_height >= 1, "grafting_height must be >= 1");
98 let pos = F::subtree_root_position(Location::<F>::new(0), grafting_height);
99 let birth_chunk_0 = F::peak_birth_size(pos, grafting_height);
100 if ops_leaves < birth_chunk_0 {
101 return 0;
102 }
103 let chunk_size = 1u64 << grafting_height;
104 (ops_leaves - birth_chunk_0) / chunk_size + 1
105}
106
107pub(super) fn graft_chunk_digests<H: Hasher, S: Strategy, const N: usize>(
111 strategy: &S,
112 inputs: Vec<(usize, H::Digest, [u8; N])>,
113) -> Vec<(usize, H::Digest)> {
114 strategy.map_init_collect_vec(
115 inputs,
116 || qmdb::hasher::<H>(),
117 |h, (chunk_idx, chunk_ops_digest, chunk)| {
118 if chunk == BitMap::<N>::EMPTY_CHUNK {
119 (chunk_idx, chunk_ops_digest)
120 } else {
121 (
122 chunk_idx,
123 h.hash([chunk.as_slice(), chunk_ops_digest.as_ref()]),
124 )
125 }
126 },
127 )
128}
129
130pub(super) fn chunk_aligned_inactive_peaks<F: Family>(
138 leaves: Location<F>,
139 inactivity_floor: Location<F>,
140 grafting_height: u32,
141) -> Result<usize, merkle::Error<F>> {
142 let size = F::location_to_position(leaves);
143 let chunk_size = 1u64 << grafting_height;
144 let floor = *inactivity_floor;
145 let mut leaf_end = 0u64;
146 let mut aligned_count = 0usize;
147
148 for (idx, (_pos, height)) in F::peaks(size).enumerate() {
149 let next_leaf_end = leaf_end + (1u64 << height);
150 if next_leaf_end > floor {
151 break;
152 }
153 leaf_end = next_leaf_end;
154 if leaf_end.is_multiple_of(chunk_size) {
155 aligned_count = idx + 1;
156 }
157 }
158
159 Ok(aligned_count)
160}
161
162pub fn ops_to_grafted_pos<F: Graftable>(ops_pos: Position<F>, grafting_height: u32) -> Position<F> {
185 let ops_height = F::pos_to_height(ops_pos);
186 assert!(
187 ops_height >= grafting_height,
188 "position height {ops_height} < grafting height {grafting_height}"
189 );
190 let grafted_height = ops_height - grafting_height;
191
192 let ops_leaf_loc = F::leftmost_leaf(ops_pos, ops_height);
193 let chunk_idx = *ops_leaf_loc >> grafting_height;
194 let grafted_leaf_loc = Location::<F>::new(chunk_idx);
195 F::subtree_root_position(grafted_leaf_loc, grafted_height)
196}
197
198pub fn grafted_to_ops_pos<F: Graftable>(
201 grafted_pos: Position<F>,
202 grafting_height: u32,
203) -> Position<F> {
204 let grafted_height = F::pos_to_height(grafted_pos);
205 let grafted_leaf = F::leftmost_leaf(grafted_pos, grafted_height);
206 let ops_leaf_start = Location::<F>::new(*grafted_leaf << grafting_height);
207 let ops_height = grafted_height + grafting_height;
208 F::subtree_root_position(ops_leaf_start, ops_height)
209}
210
211#[derive(Clone)]
219pub(super) struct GraftedHasher<F: Graftable, H: HasherTrait<F>> {
220 inner: H,
221 grafting_height: u32,
222 _family: PhantomData<F>,
223}
224
225impl<F: Graftable, H: HasherTrait<F>> GraftedHasher<F, H> {
226 pub(super) const fn new(inner: H, grafting_height: u32) -> Self {
227 Self {
228 inner,
229 grafting_height,
230 _family: PhantomData,
231 }
232 }
233}
234
235impl<F: Graftable, H: HasherTrait<F>> HasherTrait<F> for GraftedHasher<F, H> {
236 type Digest = H::Digest;
237
238 fn hash<'a>(&self, parts: impl IntoIterator<Item = &'a [u8]>) -> Self::Digest {
239 self.inner.hash(parts)
240 }
241
242 fn root_bagging(&self) -> merkle::Bagging {
243 self.inner.root_bagging()
244 }
245
246 fn node_digest(
247 &self,
248 pos: Position<F>,
249 left: &Self::Digest,
250 right: &Self::Digest,
251 ) -> Self::Digest {
252 let ops_pos = grafted_to_ops_pos::<F>(pos, self.grafting_height);
253 self.inner.node_digest(ops_pos, left, right)
254 }
255}
256
257#[derive(Clone)]
270pub struct Verifier<'a, F: Graftable, H: Hasher> {
271 hasher: merkle::hasher::Standard<H>,
272 grafting_height: u32,
273
274 chunks: Vec<&'a [u8]>,
276
277 start_chunk_index: u64,
279
280 graftable_chunks: u64,
282
283 _ops_family: PhantomData<F>,
284}
285
286impl<'a, F: Graftable, H: Hasher> Verifier<'a, F, H> {
287 pub const fn new(
294 grafting_height: u32,
295 start_chunk_index: u64,
296 chunks: Vec<&'a [u8]>,
297 graftable_chunks: u64,
298 ) -> Self {
299 Self {
300 hasher: qmdb::hasher::<H>(),
301 grafting_height,
302 chunks,
303 start_chunk_index,
304 graftable_chunks,
305 _ops_family: PhantomData,
306 }
307 }
308}
309
310impl<F: Graftable, H: Hasher> HasherTrait<F> for Verifier<'_, F, H> {
311 type Digest = H::Digest;
312
313 fn hash<'a>(&self, parts: impl IntoIterator<Item = &'a [u8]>) -> H::Digest {
314 self.hasher.hash(parts)
315 }
316
317 fn root_bagging(&self) -> merkle::Bagging {
318 <merkle::hasher::Standard<H> as HasherTrait<F>>::root_bagging(&self.hasher)
319 }
320
321 fn node_digest(
322 &self,
323 pos: merkle::Position<F>,
324 left_digest: &H::Digest,
325 right_digest: &H::Digest,
326 ) -> H::Digest {
327 match F::pos_to_height(pos).cmp(&self.grafting_height) {
328 Ordering::Less | Ordering::Greater => {
329 self.hasher.node_digest(pos, left_digest, right_digest)
331 }
332 Ordering::Equal => {
333 let ops_subtree_root = self.hasher.node_digest(pos, left_digest, right_digest);
335
336 let loc = F::leftmost_leaf(pos, self.grafting_height);
338 let chunk_idx = *loc >> self.grafting_height;
339
340 if chunk_idx >= self.graftable_chunks {
343 debug!(?chunk_idx, "skipping pending chunk");
344 return ops_subtree_root;
345 }
346
347 let Some(local) = chunk_idx
348 .checked_sub(self.start_chunk_index)
349 .filter(|&l| l < self.chunks.len() as u64)
350 .map(|l| l as usize)
351 else {
352 debug!(?pos, "chunk not available for grafted leaf");
353 return ops_subtree_root;
354 };
355
356 let chunk = self.chunks[local];
359 if chunk.iter().all(|&b| b == 0) {
360 ops_subtree_root
361 } else {
362 self.hash([chunk, ops_subtree_root.as_ref()])
363 }
364 }
365 }
366 }
367}
368
369pub(super) struct Storage<
379 'a,
380 F: Graftable,
381 H: Hasher,
382 G: Readable<Family = F, Digest = H::Digest, Error = merkle::Error<F>>,
383 S: StorageTrait<F, Digest = H::Digest>,
384> {
385 grafted_tree: &'a G,
386 grafting_height: u32,
387 ops_tree: &'a S,
388 grafted_hasher: GraftedHasher<F, merkle::hasher::Standard<H>>,
389}
390
391impl<
392 'a,
393 F: Graftable,
394 H: Hasher,
395 G: Readable<Family = F, Digest = H::Digest, Error = merkle::Error<F>>,
396 S: StorageTrait<F, Digest = H::Digest>,
397 > Storage<'a, F, H, G, S>
398{
399 pub(super) const fn new(grafted_tree: &'a G, grafting_height: u32, ops_tree: &'a S) -> Self {
401 Self {
402 grafted_tree,
403 grafting_height,
404 ops_tree,
405 grafted_hasher: hasher::<F, H>(grafting_height),
406 }
407 }
408
409 fn reconstruct_grafted_node(&self, pos: Position<F>) -> Option<H::Digest> {
426 if let Some(node) = self.grafted_tree.get_node(pos) {
427 return Some(node);
428 }
429
430 let height = F::pos_to_height(pos);
431 if height == 0 {
432 return None;
433 }
434 let (left, right) = F::children(pos, height);
435 let left_digest = self.reconstruct_grafted_node(left)?;
436 let right_digest = self.reconstruct_grafted_node(right)?;
437 Some(
438 self.grafted_hasher
439 .node_digest(pos, &left_digest, &right_digest),
440 )
441 }
442}
443
444impl<
445 F: Graftable,
446 H: Hasher,
447 G: Readable<Family = F, Digest = H::Digest, Error = merkle::Error<F>>,
448 S: StorageTrait<F, Digest = H::Digest>,
449 > StorageTrait<F> for Storage<'_, F, H, G, S>
450{
451 type Digest = H::Digest;
452
453 fn size(&self) -> Position<F> {
454 self.ops_tree.size()
455 }
456
457 async fn get_node(&self, pos: Position<F>) -> Result<Option<H::Digest>, merkle::Error<F>> {
458 let ops_height = F::pos_to_height(pos);
459 if ops_height < self.grafting_height {
460 return self.ops_tree.get_node(pos).await;
461 }
462 let grafted_pos = ops_to_grafted_pos::<F>(pos, self.grafting_height);
463 Ok(self.reconstruct_grafted_node(grafted_pos))
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::{
471 merkle::conformance::build_test_mmr,
472 mmb, mmr,
473 mmr::{
474 iterator::{pos_to_height, PeakIterator},
475 mem::Mmr,
476 verification, Location, Position, StandardHasher,
477 },
478 };
479 use commonware_cryptography::{sha256, Sha256};
480 use commonware_macros::test_traced;
481 use commonware_runtime::{deterministic, Runner};
482
483 const ALL_CHUNKS_GRAFTABLE: u64 = u64::MAX;
487
488 fn count_single_peak_chunks<F: Graftable>(ops_leaves: u64, grafting_height: u32) -> u64 {
492 let chunk_size = 1u64 << grafting_height;
493 let total_complete = ops_leaves / chunk_size;
494 if total_complete == 0 {
495 return 0;
496 }
497 let size = F::location_to_position(crate::merkle::Location::<F>::new(ops_leaves));
498 let mut count = 0u64;
499 for chunk_idx in 0..total_complete {
500 let mut iter = F::chunk_peaks(size, chunk_idx, grafting_height);
502 let _first = iter.next();
503 if iter.next().is_none() {
504 count += 1;
505 }
506 }
507 count
508 }
509
510 fn graftable_chunks_matches_oracle<F: Graftable>() {
511 for grafting_height in 1u32..=8 {
512 let chunk_size = 1u64 << grafting_height;
513 let n_max = (chunk_size * 80).min(2000);
514 for ops_leaves in 0u64..=n_max {
515 let graftable = graftable_chunks::<F>(ops_leaves, grafting_height);
516 let oracle = count_single_peak_chunks::<F>(ops_leaves, grafting_height);
517 assert_eq!(
518 graftable, oracle,
519 "mismatch: family graftable={graftable}, oracle={oracle}, ops_leaves={ops_leaves}, G={grafting_height}"
520 );
521
522 let complete = ops_leaves / chunk_size;
523 assert!(
524 graftable <= complete,
525 "graftable {graftable} exceeded complete {complete} (ops_leaves={ops_leaves}, G={grafting_height})"
526 );
527 assert!(
528 complete - graftable <= 1,
529 "complete-graftable gap > 1: complete={complete}, graftable={graftable}, ops_leaves={ops_leaves}, G={grafting_height}"
530 );
531 }
532 }
533 }
534
535 #[test]
536 fn test_graftable_chunks_mmr_matches_chunk_peaks() {
537 graftable_chunks_matches_oracle::<mmr::Family>();
538 }
539
540 #[test]
541 fn test_graftable_chunks_mmb_matches_chunk_peaks() {
542 graftable_chunks_matches_oracle::<mmb::Family>();
543 }
544
545 #[test]
547 fn test_graftable_chunks_mmr_no_pending() {
548 for grafting_height in 1u32..=8 {
549 let chunk_size = 1u64 << grafting_height;
550 let n_max = (chunk_size * 80).min(2000);
551 for ops_leaves in 0u64..=n_max {
552 let graftable = graftable_chunks::<mmr::Family>(ops_leaves, grafting_height);
553 let complete = ops_leaves / chunk_size;
554 assert_eq!(
555 graftable, complete,
556 "MMR has unexpected pending state: ops_leaves={ops_leaves}, G={grafting_height}, graftable={graftable}, complete={complete}"
557 );
558 }
559 }
560 }
561
562 #[test]
565 fn test_graftable_chunks_mmb_at_most_one_pending() {
566 for grafting_height in 1u32..=8 {
567 let chunk_size = 1u64 << grafting_height;
568 let n_max = (chunk_size * 80).min(2000);
569 for ops_leaves in 0u64..=n_max {
570 let graftable = graftable_chunks::<mmb::Family>(ops_leaves, grafting_height);
571 let complete = ops_leaves / chunk_size;
572 let pending = complete.saturating_sub(graftable);
573 assert!(
574 pending <= 1,
575 "MMB has {pending} pending chunks (ops_leaves={ops_leaves}, G={grafting_height}); should be <= 1"
576 );
577 }
578 }
579 }
580
581 #[test]
582 #[should_panic(expected = "grafting_height must be >= 1")]
583 fn test_graftable_chunks_rejects_height_zero() {
584 let _ = graftable_chunks::<mmr::Family>(100, 0);
585 }
586
587 #[test]
591 fn test_verifier_graftable_chunks_zero_skips_chunk_combine() {
592 const GH: u32 = 1;
593 let chunk: [u8; 1] = [0xAB];
594 let left = Sha256::fill(0x01);
595 let right = Sha256::fill(0x02);
596
597 let pos_at_g = mmr::Family::subtree_root_position(Location::new(0), GH);
600
601 let standard = qmdb::hasher::<Sha256>();
602 let expected_no_combine = <StandardHasher<Sha256> as HasherTrait<mmr::Family>>::node_digest(
603 &standard, pos_at_g, &left, &right,
604 );
605
606 let v = Verifier::<mmr::Family, Sha256>::new(GH, 0, vec![&chunk], 0);
607 let got = <Verifier<'_, mmr::Family, Sha256> as HasherTrait<mmr::Family>>::node_digest(
608 &v, pos_at_g, &left, &right,
609 );
610 assert_eq!(
611 got, expected_no_combine,
612 "graftable_chunks=0 must skip chunk combination at the grafting height"
613 );
614
615 let v_graftable = Verifier::<mmr::Family, Sha256>::new(GH, 0, vec![&chunk], 1);
617 let got_graftable =
618 <Verifier<'_, mmr::Family, Sha256> as HasherTrait<mmr::Family>>::node_digest(
619 &v_graftable,
620 pos_at_g,
621 &left,
622 &right,
623 );
624 assert_ne!(got, got_graftable);
625 }
626
627 fn ops_pos_to_chunk_idx(ops_pos: Position, grafting_height: u32) -> u64 {
629 let loc = mmr::Family::leftmost_leaf(ops_pos, grafting_height);
630 *loc >> grafting_height
631 }
632
633 fn chunk_idx_to_ops_pos(chunk_idx: u64, grafting_height: u32) -> Position {
635 let first_leaf_loc = Location::new(chunk_idx << grafting_height);
636 mmr::Family::subtree_root_position(first_leaf_loc, grafting_height)
637 }
638
639 fn build_test_grafted_mmr(
644 standard: &StandardHasher<Sha256>,
645 ops_mmr: &Mmr<sha256::Digest>,
646 chunks: &[sha256::Digest],
647 grafting_height: u32,
648 ) -> Mmr<sha256::Digest> {
649 let grafted_hasher =
650 GraftedHasher::<mmr::Family, _>::new(standard.clone(), grafting_height);
651 let mut grafted_mmr = Mmr::new();
652 if !chunks.is_empty() {
653 let leaf_hasher = qmdb::hasher::<Sha256>();
656 let batch = {
657 let mut batch = grafted_mmr.new_batch();
658 for (i, chunk) in chunks.iter().enumerate() {
659 let ops_pos = chunk_idx_to_ops_pos(i as u64, grafting_height);
660 let ops_subtree_root = ops_mmr
661 .get_node(ops_pos)
662 .expect("ops tree missing node at mapped position");
663 batch = batch.add_leaf_digest(
664 leaf_hasher.hash([chunk.as_ref(), ops_subtree_root.as_ref()]),
665 );
666 }
667 batch.merkleize(&grafted_mmr, &grafted_hasher)
668 };
669 grafted_mmr.apply_batch(&batch).unwrap();
670 }
671 grafted_mmr
672 }
673
674 #[test_traced]
675 fn test_chunk_idx_to_ops_pos_roundtrip() {
676 for grafting_height in 1..10 {
677 for chunk_idx in 0..1000u64 {
678 let ops_pos = chunk_idx_to_ops_pos(chunk_idx, grafting_height);
679 assert_eq!(
680 pos_to_height(ops_pos),
681 grafting_height,
682 "chunk_idx_to_ops_pos should return a position at the grafting height"
683 );
684 let back = ops_pos_to_chunk_idx(ops_pos, grafting_height);
685 assert_eq!(chunk_idx, back);
686 }
687 }
688 }
689
690 #[test_traced]
691 fn test_ops_to_grafted_pos_leaves() {
692 for grafting_height in 1..8 {
695 for chunk_idx in 0..200u64 {
696 let ops_pos = chunk_idx_to_ops_pos(chunk_idx, grafting_height);
697 let grafted_pos = ops_to_grafted_pos(ops_pos, grafting_height);
698 let expected = *Position::try_from(Location::new(chunk_idx)).unwrap();
699 assert_eq!(
700 grafted_pos, expected,
701 "leaf mismatch: chunk_idx={chunk_idx}, gh={grafting_height}"
702 );
703 }
704 }
705 }
706
707 #[test_traced]
708 fn test_ops_grafted_roundtrip() {
709 for grafting_height in 1..6 {
711 for chunk_idx in 0..100u64 {
713 let ops_pos = chunk_idx_to_ops_pos(chunk_idx, grafting_height);
714 let grafted_pos = ops_to_grafted_pos(ops_pos, grafting_height);
715 let back = grafted_to_ops_pos(grafted_pos, grafting_height);
716 assert_eq!(
717 ops_pos, back,
718 "leaf roundtrip failed: chunk={chunk_idx}, gh={grafting_height}"
719 );
720 }
721
722 for chunk_idx in (0..100u64).step_by(2) {
724 let left_ops = chunk_idx_to_ops_pos(chunk_idx, grafting_height);
725 let parent_ops = Position::new(*left_ops + (1u64 << (grafting_height + 1)));
727 if pos_to_height(parent_ops) < grafting_height {
728 continue;
729 }
730 let grafted_pos = ops_to_grafted_pos(parent_ops, grafting_height);
731 let back = grafted_to_ops_pos(grafted_pos, grafting_height);
732 assert_eq!(
733 parent_ops, back,
734 "internal roundtrip failed: chunk={chunk_idx}, gh={grafting_height}"
735 );
736 }
737 }
738 }
739
740 #[test_traced]
741 fn test_ops_to_grafted_pos_known_values() {
742 assert_eq!(ops_to_grafted_pos(Position::new(2), 1), 0);
745 assert_eq!(ops_to_grafted_pos(Position::new(5), 1), 1);
747 assert_eq!(ops_to_grafted_pos(Position::new(6), 1), 2);
749 assert_eq!(ops_to_grafted_pos(Position::new(9), 1), 3);
751 assert_eq!(ops_to_grafted_pos(Position::new(12), 1), 4);
753 assert_eq!(ops_to_grafted_pos(Position::new(13), 1), 5);
755 assert_eq!(ops_to_grafted_pos(Position::new(14), 1), 6);
757 }
758
759 #[test_traced]
760 fn test_grafted_leaf_computation() {
761 let executor = deterministic::Runner::default();
762 executor.start(|_| async move {
763 const NUM_ELEMENTS: u64 = 200;
764
765 let standard = qmdb::hasher::<Sha256>();
766 let mmr = Mmr::new();
767 let ops_mmr = build_test_mmr(&standard, mmr, NUM_ELEMENTS);
768
769 let elements: Vec<_> = (0..NUM_ELEMENTS)
771 .map(|i| standard.digest(&i.to_be_bytes()))
772 .collect();
773
774 {
776 assert_eq!(chunk_idx_to_ops_pos(0, 0), Position::new(0));
777 assert_eq!(chunk_idx_to_ops_pos(1, 0), Position::new(1));
778
779 let grafted = build_test_grafted_mmr(&standard, &ops_mmr, &elements, 0);
780 let gp = ops_to_grafted_pos(chunk_idx_to_ops_pos(0, 0), 0);
781 assert!(grafted.get_node(gp).is_some());
782 }
783
784 let ops_mmr = build_test_mmr(&standard, ops_mmr, NUM_ELEMENTS);
786 {
787 assert_eq!(chunk_idx_to_ops_pos(0, 1), Position::new(2));
789 assert_eq!(chunk_idx_to_ops_pos(1, 1), Position::new(5));
790 assert_eq!(chunk_idx_to_ops_pos(2, 1), Position::new(9));
791 assert_eq!(chunk_idx_to_ops_pos(3, 1), Position::new(12));
792 assert_eq!(chunk_idx_to_ops_pos(4, 1), Position::new(17));
793
794 let grafted = build_test_grafted_mmr(&standard, &ops_mmr, &elements, 1);
795 let gp = ops_to_grafted_pos(chunk_idx_to_ops_pos(0, 1), 1);
796 assert!(grafted.get_node(gp).is_some());
797 }
798
799 assert_eq!(chunk_idx_to_ops_pos(0, 2), Position::new(6));
801 assert_eq!(chunk_idx_to_ops_pos(1, 2), Position::new(13));
802 assert_eq!(chunk_idx_to_ops_pos(0, 3), Position::new(14));
803 });
804 }
805
806 #[test_traced]
807 fn test_merkleize_grafted() {
808 let standard = qmdb::hasher::<Sha256>();
809 let grafting_height = 1u32;
810
811 let mut ops_mmr = Mmr::new();
813 let batch = {
814 let mut batch = ops_mmr.new_batch();
815 for i in 0u8..4 {
816 batch = batch.add(&standard, &Sha256::fill(i));
817 }
818 batch.merkleize(&ops_mmr, &standard)
819 };
820 ops_mmr.apply_batch(&batch).unwrap();
821
822 let c1 = Sha256::fill(0xF1);
823 let c2 = Sha256::fill(0xF2);
824
825 let grafted_hasher = GraftedHasher::<mmr::Family, _>::new(standard, grafting_height);
827 let mut grafted = Mmr::new();
828 let pos0 = chunk_idx_to_ops_pos(0, grafting_height);
829 let pos1 = chunk_idx_to_ops_pos(1, grafting_height);
830
831 let batch = {
832 let leaf_hasher = qmdb::hasher::<Sha256>();
833 let sub0 = ops_mmr.get_node(pos0).unwrap();
834 let batch = grafted
835 .new_batch()
836 .add_leaf_digest(leaf_hasher.hash([c1.as_ref(), sub0.as_ref()]));
837
838 let sub1 = ops_mmr.get_node(pos1).unwrap();
839 batch
840 .add_leaf_digest(leaf_hasher.hash([c2.as_ref(), sub1.as_ref()]))
841 .merkleize(&grafted, &grafted_hasher)
842 };
843 grafted.apply_batch(&batch).unwrap();
844
845 let gp0 = ops_to_grafted_pos(pos0, grafting_height);
848 let gp1 = ops_to_grafted_pos(pos1, grafting_height);
849 let gp_root = ops_to_grafted_pos(Position::new(6), grafting_height);
850 assert!(grafted.get_node(gp0).is_some());
851 assert!(grafted.get_node(gp1).is_some());
852 assert!(grafted.get_node(gp_root).is_some());
853 }
854
855 #[test_traced]
857 fn test_grafted_storage_proofs() {
858 let executor = deterministic::Runner::default();
859 const GRAFTING_HEIGHT: u32 = 1;
860 executor.start(|_| async move {
861 let b1 = Sha256::fill(0x01);
862 let b2 = Sha256::fill(0x02);
863 let b3 = Sha256::fill(0x03);
864 let b4 = Sha256::fill(0x04);
865 let hasher = qmdb::hasher::<Sha256>();
866
867 let mut ops_mmr = Mmr::new();
869 let batch = {
870 let mut batch = ops_mmr.new_batch();
871 batch = batch.add(&hasher, &b1);
872 batch = batch.add(&hasher, &b2);
873 batch = batch.add(&hasher, &b3);
874 batch = batch.add(&hasher, &b4);
875 batch.merkleize(&ops_mmr, &hasher)
876 };
877
878 ops_mmr.apply_batch(&batch).unwrap();
879
880 let c1 = Sha256::fill(0xF1);
882 let c2 = Sha256::fill(0xF2);
883
884 let grafted = build_test_grafted_mmr(&hasher, &ops_mmr, &[c1, c2], GRAFTING_HEIGHT);
887
888 let ops_root = ops_mmr.root(&hasher, 0).unwrap();
889
890 {
891 let combined =
892 Storage::<mmr::Family, Sha256, _, _>::new(&grafted, GRAFTING_HEIGHT, &ops_mmr);
893 assert_eq!(combined.size(), ops_mmr.size());
894
895 let grafted_root = {
897 let ops_size = ops_mmr.size();
898 let ops_leaves = Location::try_from(ops_size).unwrap();
899 let mut peaks = Vec::new();
900 for (peak_pos, peak_height) in PeakIterator::new(ops_size) {
901 if peak_height >= GRAFTING_HEIGHT {
902 let gp = ops_to_grafted_pos(peak_pos, GRAFTING_HEIGHT);
903 peaks.push(grafted.get_node(gp).unwrap());
904 } else {
905 peaks.push(combined.get_node(peak_pos).await.unwrap().unwrap());
906 }
907 }
908 hasher
909 .root(ops_leaves, 0, peaks.iter())
910 .expect("zero inactive peaks is always valid")
911 };
912 assert_ne!(grafted_root, ops_root);
913
914 {
916 let loc = Location::new(0);
917 let proof = verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
918 .await
919 .unwrap();
920
921 let verifier = Verifier::<mmr::Family, Sha256>::new(
922 GRAFTING_HEIGHT,
923 0,
924 vec![&c1],
925 ALL_CHUNKS_GRAFTABLE,
926 );
927 assert!(proof.verify_element_inclusion(&verifier, &b1, loc, &grafted_root));
928
929 let loc = Location::new(1);
930 let proof = verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
931 .await
932 .unwrap();
933 assert!(proof.verify_element_inclusion(&verifier, &b2, loc, &grafted_root));
934
935 let loc = Location::new(2);
936 let proof = verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
937 .await
938 .unwrap();
939 let verifier = Verifier::<mmr::Family, Sha256>::new(
940 GRAFTING_HEIGHT,
941 1,
942 vec![&c2],
943 ALL_CHUNKS_GRAFTABLE,
944 );
945 assert!(proof.verify_element_inclusion(&verifier, &b3, loc, &grafted_root));
946
947 let loc = Location::new(3);
948 let proof = verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
949 .await
950 .unwrap();
951 assert!(proof.verify_element_inclusion(&verifier, &b4, loc, &grafted_root));
952 }
953
954 {
956 let loc = Location::new(3);
957 let proof = verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
958 .await
959 .unwrap();
960 let verifier = Verifier::<mmr::Family, Sha256>::new(
961 GRAFTING_HEIGHT,
962 1,
963 vec![&c2],
964 ALL_CHUNKS_GRAFTABLE,
965 );
966 assert!(proof.verify_element_inclusion(&verifier, &b4, loc, &grafted_root));
967
968 assert!(!proof.verify_element_inclusion(&verifier, &b3, loc, &grafted_root));
970
971 assert!(!proof.verify_element_inclusion(&verifier, &b4, loc, &ops_root));
973
974 assert!(!proof.verify_element_inclusion(
976 &verifier,
977 &b4,
978 loc + 1,
979 &grafted_root,
980 ));
981
982 let verifier = Verifier::<mmr::Family, Sha256>::new(
984 GRAFTING_HEIGHT,
985 0,
986 vec![&c1],
987 ALL_CHUNKS_GRAFTABLE,
988 );
989 assert!(!proof.verify_element_inclusion(&verifier, &b4, loc, &grafted_root));
990
991 let verifier = Verifier::<mmr::Family, Sha256>::new(
993 GRAFTING_HEIGHT,
994 2,
995 vec![&c2],
996 ALL_CHUNKS_GRAFTABLE,
997 );
998 assert!(!proof.verify_element_inclusion(&verifier, &b4, loc, &grafted_root));
999 }
1000
1001 {
1003 let proof = verification::range_proof(
1004 &hasher,
1005 &combined,
1006 Location::new(0)..Location::new(4),
1007 0,
1008 )
1009 .await
1010 .unwrap();
1011 let range = vec![&b1, &b2, &b3, &b4];
1012 let verifier = Verifier::<mmr::Family, Sha256>::new(
1013 GRAFTING_HEIGHT,
1014 0,
1015 vec![&c1, &c2],
1016 ALL_CHUNKS_GRAFTABLE,
1017 );
1018 assert!(proof.verify_range_inclusion(
1019 &verifier,
1020 &range,
1021 Location::new(0),
1022 &grafted_root,
1023 ));
1024
1025 let verifier = Verifier::<mmr::Family, Sha256>::new(
1027 GRAFTING_HEIGHT,
1028 0,
1029 vec![&c1],
1030 ALL_CHUNKS_GRAFTABLE,
1031 );
1032 assert!(!proof.verify_range_inclusion(
1033 &verifier,
1034 &range,
1035 Location::new(0),
1036 &grafted_root,
1037 ));
1038 }
1039 }
1040
1041 let b5 = Sha256::fill(0x05);
1044 let batch = {
1045 let mut batch = ops_mmr.new_batch();
1046 batch = batch.add(&hasher, &b5);
1047 batch.merkleize(&ops_mmr, &hasher)
1048 };
1049
1050 ops_mmr.apply_batch(&batch).unwrap();
1051
1052 let combined =
1053 Storage::<mmr::Family, Sha256, _, _>::new(&grafted, GRAFTING_HEIGHT, &ops_mmr);
1054 assert_eq!(combined.size(), ops_mmr.size());
1055
1056 let grafted_root = {
1058 let ops_size = ops_mmr.size();
1059 let ops_leaves = Location::try_from(ops_size).unwrap();
1060 let mut peaks = Vec::new();
1061 for (peak_pos, peak_height) in PeakIterator::new(ops_size) {
1062 if peak_height >= GRAFTING_HEIGHT {
1063 let gp = ops_to_grafted_pos(peak_pos, GRAFTING_HEIGHT);
1064 peaks.push(grafted.get_node(gp).unwrap());
1065 } else {
1066 peaks.push(combined.get_node(peak_pos).await.unwrap().unwrap());
1067 }
1068 }
1069 hasher
1070 .root(ops_leaves, 0, peaks.iter())
1071 .expect("zero inactive peaks is always valid")
1072 };
1073
1074 let loc = Location::new(0);
1076 let proof = merkle::verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
1077 .await
1078 .unwrap();
1079
1080 let verifier = Verifier::<mmr::Family, Sha256>::new(
1081 GRAFTING_HEIGHT,
1082 0,
1083 vec![&c1],
1084 ALL_CHUNKS_GRAFTABLE,
1085 );
1086 assert!(proof.verify_element_inclusion(&verifier, &b1, loc, &grafted_root));
1087
1088 let verifier = Verifier::<mmr::Family, Sha256>::new(
1089 GRAFTING_HEIGHT,
1090 0,
1091 vec![],
1092 ALL_CHUNKS_GRAFTABLE,
1093 );
1094 let loc = Location::new(4);
1095 let proof = merkle::verification::range_proof(&hasher, &combined, loc..loc + 1, 0)
1096 .await
1097 .unwrap();
1098 assert!(proof.verify_element_inclusion(&verifier, &b5, loc, &grafted_root));
1099 });
1100 }
1101
1102 #[test_traced]
1103 fn test_grafted_mmr_basic() {
1104 let grafting_height = 1u32;
1105 let standard = qmdb::hasher::<Sha256>();
1106
1107 let d0 = Sha256::fill(0x01);
1109 let d1 = Sha256::fill(0x02);
1110 let grafted_hasher = GraftedHasher::<mmr::Family, _>::new(standard, grafting_height);
1111 let mut grafted = Mmr::new();
1112 let batch = grafted
1113 .new_batch()
1114 .add_leaf_digest(d0)
1115 .add_leaf_digest(d1)
1116 .merkleize(&grafted, &grafted_hasher);
1117 grafted.apply_batch(&batch).unwrap();
1118
1119 let ops_pos_0 = chunk_idx_to_ops_pos(0, grafting_height);
1121 let ops_pos_1 = chunk_idx_to_ops_pos(1, grafting_height);
1122 let gp0 = ops_to_grafted_pos(ops_pos_0, grafting_height);
1123 let gp1 = ops_to_grafted_pos(ops_pos_1, grafting_height);
1124 assert_eq!(grafted.get_node(gp0), Some(d0));
1125 assert_eq!(grafted.get_node(gp1), Some(d1));
1126
1127 let gp_root = ops_to_grafted_pos(Position::new(6), grafting_height);
1129 assert!(grafted.get_node(gp_root).is_some());
1130
1131 let gp_far = ops_to_grafted_pos(chunk_idx_to_ops_pos(5, grafting_height), grafting_height);
1133 assert_eq!(grafted.get_node(gp_far), None);
1134 }
1135
1136 #[test_traced]
1137 fn test_grafted_mmr_with_pruning() {
1138 let grafting_height = 1u32;
1139 let standard = qmdb::hasher::<Sha256>();
1140
1141 let pinned_digest = Sha256::fill(0xAA);
1144 let grafted_pruning_boundary = Location::new(4);
1145 assert_eq!(*Position::try_from(grafted_pruning_boundary).unwrap(), 7);
1146
1147 let d4 = Sha256::fill(0xBB);
1149 let grafted_hasher = GraftedHasher::<mmr::Family, _>::new(standard, grafting_height);
1150 let mut grafted =
1151 Mmr::from_components(Vec::new(), grafted_pruning_boundary, vec![pinned_digest])
1152 .unwrap();
1153 let batch = grafted
1154 .new_batch()
1155 .add_leaf_digest(d4)
1156 .merkleize(&grafted, &grafted_hasher);
1157 grafted.apply_batch(&batch).unwrap();
1158
1159 assert_eq!(grafted.get_node(Position::new(6)), Some(pinned_digest));
1161
1162 let ops_pos_4 = chunk_idx_to_ops_pos(4, grafting_height);
1164 let gp4 = ops_to_grafted_pos(ops_pos_4, grafting_height);
1165 assert_eq!(grafted.get_node(gp4), Some(d4));
1166 }
1167
1168 #[test_traced]
1174 fn test_graftable_chunks_always_single_peak() {
1175 type F = mmb::Family;
1176 let grafting_height = 2u32;
1177 let mut exercised = 0usize;
1178
1179 for leaf_count in 1..=200u64 {
1180 let size = F::location_to_position(mmb::Location::new(leaf_count));
1181 let complete_chunks = leaf_count / (1u64 << grafting_height);
1182 let graftable_chunks =
1183 graftable_chunks::<F>(leaf_count, grafting_height).min(complete_chunks);
1184 for chunk_idx in 0..graftable_chunks {
1185 let count = F::chunk_peaks(size, chunk_idx, grafting_height).count();
1186 assert_eq!(
1187 count, 1,
1188 "graftable chunk {chunk_idx} has {count} peaks (leaf_count={leaf_count}, graftable={graftable_chunks}, complete={complete_chunks})"
1189 );
1190 exercised += 1;
1191 }
1192 }
1193 assert!(exercised > 0);
1194 }
1195
1196 #[test_traced]
1201 fn test_only_pending_chunk_can_be_multi_peak() {
1202 type F = mmb::Family;
1203 let grafting_height = 2u32;
1204 let mut exercised_pending = 0usize;
1205
1206 for leaf_count in 1..=200u64 {
1207 let size = F::location_to_position(mmb::Location::new(leaf_count));
1208 let complete_chunks = leaf_count / (1u64 << grafting_height);
1209 let graftable_chunks =
1210 graftable_chunks::<F>(leaf_count, grafting_height).min(complete_chunks);
1211 for chunk_idx in 0..complete_chunks {
1212 let count = F::chunk_peaks(size, chunk_idx, grafting_height).count();
1213 if chunk_idx < graftable_chunks {
1214 assert_eq!(
1215 count, 1,
1216 "graftable chunk {chunk_idx} has {count} peaks (leaf_count={leaf_count})"
1217 );
1218 } else {
1219 assert!(
1222 count >= 1,
1223 "pending chunk {chunk_idx} has {count} peaks (leaf_count={leaf_count})"
1224 );
1225 if count > 1 {
1226 exercised_pending += 1;
1227 }
1228 }
1229 }
1230 }
1231
1232 assert!(
1233 exercised_pending > 0,
1234 "expected to exercise at least one multi-peak pending chunk"
1235 );
1236 }
1237}