Skip to main content

commonware_storage/qmdb/current/
grafting.rs

1//! Verifier and storage for grafting bitmap chunks onto an operations tree.
2//!
3//! ## Overview
4//!
5//! An operations tree is built over a log of operations, and a bitmap tracks the activity
6//! status of each operation. To authenticate both structures efficiently, we combine them: each
7//! _graftable_ chunk of the bitmap is hashed together with the corresponding subtree root from the
8//! ops tree to produce a single "grafted leaf" digest. These digests, along with their ancestor
9//! nodes, are stored in an in-memory Merkle structure (using grafted-space positions internally,
10//! with ops-space positions in hash pre-images).
11//!
12//! A chunk is _graftable_ once its height-`G` ancestor exists in the ops tree as a single node. In
13//! MMR every complete chunk is immediately graftable. MMB's delayed merges allow at most one chunk
14//! per database to be bit-complete but not yet graftable ("pending").
15//!
16//! ```text
17//!   Why MMB has a pending state, illustrated at G = 2 (chunk size = 4 leaves):
18//!
19//!   At ops_leaves N = 4, chunk 0's four bits are complete, so we want to graft it.
20//!
21//!     MMR:  N = 4 forms a perfect 4-leaf subtree, so chunk 0 has a single
22//!           h=2 ancestor -- graft directly as hash(chunk_0 || h=2_root).
23//!
24//!     MMB:  N = 4 has no h=2 node yet. Chunk 0's leaves are split across
25//!           several h<2 peaks; there is no single ops node to graft onto.
26//!           The h=2 ancestor is not born until N reaches birth_chunk_0 = 5
27//!           (= 3 * 2^(G-1) - 1 for MMB at G=2). Until then, chunk 0's digest
28//!           is hashed directly into the canonical root; once the ancestor is
29//!           born, the chunk migrates into the grafted tree.
30//! ```
31//!
32//! This is more efficient than maintaining two independent authenticated structures. An inclusion
33//! proof for an operation and its activity status only requires one branch (which embeds the bitmap
34//! chunk) plus the sub-branch from the ops tree below the grafting point, reducing proof size by up
35//! to a factor of 2.
36//!
37//! ## Grafting height
38//!
39//! Each grafted leaf covers `2^h` ops-tree leaves, where `h` is the grafting height
40//! (`log2(chunk_size_bits)`). For example, given an ops tree over 8 operations with grafting height
41//! 2 (chunk size = 4 bits):
42//!
43//! ```text
44//!    Height
45//!      3              14
46//!                   /    \
47//!                  /      \
48//!                 /        \
49//!                /          \
50//!      2        6            13       <-- grafting height: grafted leaf positions
51//!             /   \        /    \
52//!      1     2     5      9     12
53//!           / \   / \    / \   /  \
54//!      0   0   1 3   4  7   8 10  11
55//! ```
56//!
57//! Nodes at the grafting height (positions 6 and 13) are "grafted leaves" whose digests combine the
58//! bitmap chunk with the ops subtree root: `hash(chunk || ops_subtree_root)`. Nodes above the
59//! grafting height (position 14) use standard hashing with ops-space positions.
60//!
61//! The grafted tree is incrementally maintained when grafted leaves change.
62
63use 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
76/// Get the grafting height for a bitmap with chunk size determined by N.
77pub const fn height<const N: usize>() -> u32 {
78    BitMap::<N>::CHUNK_SIZE_BITS.trailing_zeros()
79}
80
81/// Return a [GraftedHasher] over QMDB's fixed Merkle hasher configuration.
82pub(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
88/// Return the number of bitmap chunks that have a corresponding height-G ancestor in the ops tree.
89///
90/// - For MMR, this is always the same as the number of complete_chunks.
91/// - For MMB, this is either complete_chunks or complete_chunks - 1.
92///
93/// # Panics
94///
95/// Panics if `grafting_height == 0`.
96pub 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
107/// Compute grafted leaf digests for resolved `(chunk_idx, chunk_ops_digest, chunk)` triples:
108/// each leaf is `hash(chunk || chunk_ops_digest)`, except all-zero chunks preserve
109/// `chunk_ops_digest` directly (zero-chunk identity).
110pub(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
130/// Return the number of root peaks whose covered leaves end on or before `inactivity_floor`, while
131/// keeping the resulting boundary aligned to a complete bitmap chunk.
132///
133/// Current grafted roots treat bitmap chunks as the atomic grafting unit. If the exact inactivity
134/// floor falls inside a root peak or inside a multi-peak chunk group, the partially covered chunk
135/// stays in the graftable region and the inactive peak count is rounded down to the latest chunk
136/// boundary expressible by whole root peaks.
137pub(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
162// --- Coordinate conversion ---
163//
164// These functions convert between three coordinate spaces:
165//
166// 1. **Chunk index**: Sequential index (0, 1, 2, ...) of (complete) bitmap chunks.
167// 2. **Ops position**: Position in the full operations tree.
168// 3. **Grafted position**: Position in the grafted tree, whose leaves correspond 1:1 with chunks.
169//
170// All conversions rely on a single family identity: given the leftmost leaf at position P of a
171// perfect subtree, the subtree root at height h is at `P + 2^(h+1) - 2`, and conversely the
172// leftmost leaf under a subtree root at position P and height h is at `P + 2 - 2^(h+1)`.
173
174/// Convert an ops-family position (at or above the grafting height) to a grafted-tree position.
175///
176/// An ops node at height `ops_h` maps to a grafted node at height `ops_h - grafting_height`.
177/// The conversion descends to the leftmost ops leaf, divides by 2^h to get the chunk index
178/// (= grafted leaf location), then climbs back up to the grafted height. The result always
179/// lives in grafted-space, which is a Merkle tree over chunk indices.
180///
181/// # Panics
182///
183/// Panics if `ops_pos` is below the grafting height.
184pub 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
198/// Convert a grafted position to the ops-family position whose subtree covers the same ops-leaf
199/// range.
200pub 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/// A hasher adapter that maps grafted-structure positions to ops-structure positions.
212///
213/// Both the grafted structure and ops structure use the same family `F`. The grafted
214/// structure's leaves correspond 1:1 with bitmap chunks. This adapter intercepts
215/// [`HasherTrait::node_digest`] to convert each grafted position to the corresponding
216/// ops-space position via [`Graftable::leftmost_leaf`] and [`Graftable::subtree_root_position`],
217/// ensuring hash pre-images use ops-space positions for domain separation.
218#[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/// A [`merkle::hasher::Hasher`] implementation used for verifying proofs over grafted storage.
258///
259/// The ops structure uses family `F`, so this implements [`merkle::hasher::Hasher`] for that
260/// family to match the proof.
261/// Proof verification walks the tree from leaves to root, recomputing digests at each node.
262/// Since a proof path crosses the grafting boundary (from ops leaves up through grafted peaks),
263/// two different hashing behaviors are needed depending on the node's height relative to the
264/// grafting height:
265///
266/// - **Below or above**: standard hash using ops-space positions (`F`).
267/// - **At**: the children form an ops subtree root, which is combined with a bitmap chunk element
268///   to reconstruct the grafted leaf digest.
269#[derive(Clone)]
270pub struct Verifier<'a, F: Graftable, H: Hasher> {
271    hasher: merkle::hasher::Standard<H>,
272    grafting_height: u32,
273
274    /// Bitmap chunks needed for grafted leaf reconstruction at the boundary.
275    chunks: Vec<&'a [u8]>,
276
277    /// The chunk index of `chunks[0]`.
278    start_chunk_index: u64,
279
280    /// Number of chunks with a corresponding height-G ancestor in the ops tree.
281    graftable_chunks: u64,
282
283    _ops_family: PhantomData<F>,
284}
285
286impl<'a, F: Graftable, H: Hasher> Verifier<'a, F, H> {
287    /// Create a new verifier using QMDB's fixed Merkle hasher configuration.
288    ///
289    /// `start_chunk_index` is the chunk index corresponding to `chunks[0]`.
290    /// `graftable_chunks` is the number of chunks committed by the grafted tree; any chunk index
291    /// in `chunks` at or beyond this boundary is treated as pending and **not** combined with
292    /// the ops subtree root at the grafting height.
293    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                // Below or above grafting height: standard hash with ops-space position.
330                self.hasher.node_digest(pos, left_digest, right_digest)
331            }
332            Ordering::Equal => {
333                // At grafting height: compute ops subtree root, then combine with bitmap chunk.
334                let ops_subtree_root = self.hasher.node_digest(pos, left_digest, right_digest);
335
336                // Convert the F-family position to a chunk index using F's leftmost_leaf.
337                let loc = F::leftmost_leaf(pos, self.grafting_height);
338                let chunk_idx = *loc >> self.grafting_height;
339
340                // Skip pending chunks. These will be incorporated in the root via the pending_chunk
341                // digest field.
342                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                // For all-zero chunks, the grafted leaf is the ops subtree root (identity).
357                // For non-zero chunks: grafted_leaf = hash(chunk || ops_subtree_root).
358                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
369/// A virtual [StorageTrait] that presents a grafted tree and ops tree as a single combined Merkle
370/// structure.
371///
372/// Nodes below the grafting height are served from the ops tree. Nodes at or above the grafting
373/// height are served from the grafted tree (with ops-to-grafted position conversion). This allows
374/// standard proof generation to work transparently over the combined structure.
375///
376/// Both the ops structure and the grafted structure use the same [Family] `F`. The combined storage
377/// presents as `StorageTrait<F>` so that callers generic over `F` can use it transparently.
378pub(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    /// Creates a new [Storage] instance.
400    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    /// Reconstruct a grafted node that is missing from the pruned grafted tree.
410    ///
411    /// After pruning, only the grafted tree's pinned peaks and retained nodes are available.
412    /// As the ops tree grows, delayed merges create new ops peaks that map to grafted nodes
413    /// above the pinned peaks (ancestors). This function reconstructs those ancestors by
414    /// recursing into their children and hashing upward until it reaches available nodes
415    /// (pinned peaks or retained nodes).
416    ///
417    /// Recursion depth is bounded by the height difference between the queried node and the
418    /// nearest available descendant (a pinned peak or retained node). In practice it remains
419    /// small because the settlement guard limits how far ahead the ops tree can grow before
420    /// bitmap pruning advances.
421    ///
422    /// Returns `None` at height 0 (a grafted leaf), since leaves encode bitmap data and
423    /// cannot be recomputed from the tree structure alone. The settlement guard in
424    /// [`super::db::Db::sync_boundary`] ensures this case is unreachable for pruned chunks.
425    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    /// MMR has no pending state, so every supplied chunk should be treated as graftable during
484    /// verification. Tests use this sentinel to avoid threading the actual chunk count through
485    /// every call site.
486    const ALL_CHUNKS_GRAFTABLE: u64 = u64::MAX;
487
488    /// Count chunks of `2^G` leaves in `0..ops_leaves` that have a single covering peak at height G.
489    ///
490    /// Used as the oracle for `graftable_chunks` property tests.
491    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            // chunk has a single covering peak iff chunk_peaks returns exactly one entry.
501            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    /// MMR has no pending state: graftable_chunks always equals complete_chunks.
546    #[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    /// Sanity check: the MMB pending window has width `2^(G-1) - 1` per chunk, strictly less than
563    /// `2^G`, so at most one chunk is pending at any moment.
564    #[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    /// When `graftable_chunks: 0` is passed to a `Verifier`, every chunk index is treated as
588    /// pending, so `node_digest` at height G returns the raw ops subtree root, never the
589    /// chunk-combined digest. This is the boundary case for the graftable/pending check.
590    #[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        // Position at h=G with chunk_idx 0; with graftable_chunks=0 the verifier must NOT
598        // combine the chunk (chunk_idx >= graftable_chunks).
599        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        // Sanity: with graftable_chunks=1 the chunk IS combined, so the digest differs.
616        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    /// Convert an ops-tree position at the grafting height back to its chunk index.
628    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    /// Convert a chunk index to the ops position of the subtree root.
634    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    /// Precompute grafted leaf digests and return an MMR-based grafted test tree.
640    ///
641    /// Each grafted leaf is `hash(chunk || ops_subtree_root)` where `ops_subtree_root` is the ops
642    /// tree node at the mapped position.
643    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            // Use a separate hasher for leaf digest computation to avoid borrow conflict
654            // with grafted_hasher (which borrows standard via fork()).
655            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 leaves (grafted height 0), ops_to_grafted_pos should agree with
693        // ops_pos_to_chunk_idx -> Position::try_from(Location(chunk_idx)).
694        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        // Test roundtrip: ops -> grafted -> ops for positions at various heights.
710        for grafting_height in 1..6 {
711            // Build grafted leaves first, then walk up to test internal nodes.
712            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            // Test internal nodes: parent of adjacent grafted leaves.
723            for chunk_idx in (0..100u64).step_by(2) {
724                let left_ops = chunk_idx_to_ops_pos(chunk_idx, grafting_height);
725                // Parent in ops-space: left + (1 << (grafting_height + 1))
726                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        // Grafting height 1: each grafted leaf covers 2 ops leaves.
743        // ops_pos=2 (chunk 0) -> grafted leaf 0 -> grafted pos 0
744        assert_eq!(ops_to_grafted_pos(Position::new(2), 1), 0);
745        // ops_pos=5 (chunk 1) -> grafted leaf 1 -> grafted pos 1
746        assert_eq!(ops_to_grafted_pos(Position::new(5), 1), 1);
747        // ops_pos=6 (internal, height 2) -> grafted internal at height 1 -> grafted pos 2
748        assert_eq!(ops_to_grafted_pos(Position::new(6), 1), 2);
749        // ops_pos=9 (chunk 2) -> grafted leaf 2 -> grafted pos 3
750        assert_eq!(ops_to_grafted_pos(Position::new(9), 1), 3);
751        // ops_pos=12 (chunk 3) -> grafted leaf 3 -> grafted pos 4
752        assert_eq!(ops_to_grafted_pos(Position::new(12), 1), 4);
753        // ops_pos=13 (internal, height 2) -> grafted internal at height 1 -> grafted pos 5
754        assert_eq!(ops_to_grafted_pos(Position::new(13), 1), 5);
755        // ops_pos=14 (root, height 3) -> grafted root at height 2 -> grafted pos 6
756        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            // Generate the elements that build_test_mmr uses: sha256(i.to_be_bytes()).
770            let elements: Vec<_> = (0..NUM_ELEMENTS)
771                .map(|i| standard.digest(&i.to_be_bytes()))
772                .collect();
773
774            // Height 0 grafting (1:1 mapping).
775            {
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            // Height 1 grafting (each grafted leaf covers 2 ops leaves).
785            let ops_mmr = build_test_mmr(&standard, ops_mmr, NUM_ELEMENTS);
786            {
787                // Confirm chunk_idx_to_ops_pos mappings at height 1.
788                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            // Height 2 and 3 checks.
800            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        // Build ops MMR with 4 leaves.
812        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        // Build grafted MMR with 2 leaves.
826        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        // With 4 ops leaves and grafting height 1, the grafted tree has 2 leaves and 1 root.
846        // All 3 nodes should be retrievable (via grafted-space positions).
847        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    /// Builds a small grafted structure, then generates and verifies proofs over it.
856    #[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            // Build an ops MMR with 4 leaves.
868            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            // Bitmap chunk elements (one per grafted leaf).
881            let c1 = Sha256::fill(0xF1);
882            let c2 = Sha256::fill(0xF2);
883
884            // With grafting height 1, each grafted leaf covers 2 ops leaves, so 4 ops leaves
885            // yield 2 grafted leaves.
886            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                // Compute the grafted root by iterating ops peaks.
896                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                // Verify inclusion proofs for each of the 4 ops leaves.
915                {
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                // Verify that manipulated inputs cause proof verification to fail.
955                {
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                    // Wrong leaf element.
969                    assert!(!proof.verify_element_inclusion(&verifier, &b3, loc, &grafted_root));
970
971                    // Wrong root.
972                    assert!(!proof.verify_element_inclusion(&verifier, &b4, loc, &ops_root));
973
974                    // Wrong position.
975                    assert!(!proof.verify_element_inclusion(
976                        &verifier,
977                        &b4,
978                        loc + 1,
979                        &grafted_root,
980                    ));
981
982                    // Wrong chunk element in the verifier.
983                    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                    // Wrong chunk index in the verifier.
992                    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                // Verify range proofs.
1002                {
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                    // Fails with incomplete chunk elements.
1026                    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            // Add a 5th ops leaf that has no corresponding grafted leaf (it falls below
1042            // the grafting height boundary since there's no complete chunk for it yet).
1043            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            // Compute the grafted root.
1057            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            // Verify inclusion proofs still work for both covered and uncovered ops leaves.
1075            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        // Build a grafted MMR with 2 leaves.
1108        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        // Check that grafted leaves are retrievable via grafted-space positions.
1120        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        // Internal node (grafted root) should also exist.
1128        let gp_root = ops_to_grafted_pos(Position::new(6), grafting_height);
1129        assert!(grafted.get_node(gp_root).is_some());
1130
1131        // Non-existent position returns None.
1132        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        // Simulate pruning 4 chunks. The pruned sub-MMR has 4 grafted leaves,
1142        // mmr_size(4) = 7, with one peak at grafted position 6.
1143        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        // Build a grafted MMR from pruned components + one new leaf.
1148        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        // The pinned peak should be at grafted position 6.
1160        assert_eq!(grafted.get_node(Position::new(6)), Some(pinned_digest));
1161
1162        // The new leaf at chunk 4 (grafted pos 7) should be retrievable.
1163        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    /// For every `(leaf_count, chunk_idx)` with `chunk_idx < graftable_chunks(leaf_count, G)`,
1169    /// the chunk has exactly one h=G peak in the ops MMB. The pending chunk (at index
1170    /// `graftable_chunks`, if present) is the only chunk that may have multi-peak structure;
1171    /// its digest is hashed directly into the canonical root rather than being folded into the
1172    /// grafted root.
1173    #[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    /// At every MMB size, the chunk index `complete_chunks - 1` (the most recently completed
1197    /// chunk) is multi-peak iff it is also the pending chunk, i.e. iff
1198    /// `graftable_chunks < complete_chunks`. Earlier chunks are always single-peak (graftable) and
1199    /// never appear in a multi-peak state.
1200    #[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                    // chunk_idx is the pending chunk; multi-peak is allowed (and expected
1220                    // when this branch is taken, since graftable < complete).
1221                    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}