Skip to main content

commonware_storage/qmdb/current/proof/
mod.rs

1//! Proof types for [crate::qmdb::current] authenticated databases.
2//!
3//! This module provides:
4//! - [OpsRootWitness]: Authenticates an ops root against a canonical `current` root.
5//! - [RangeProof]: Proves a range of operations exist in the database.
6//! - [constant::OperationProof]: Proves a specific operation is active, with a fixed-size bitmap chunk.
7//! - [dynamic::OperationProof]: Uses a bitmap chunk size supplied when decoding.
8//!
9//! # Canonical root structure
10//!
11//! ```text
12//! canonical_root = hash(
13//!     ops_root
14//!     || grafted_root
15//!     [|| pending_chunk_digest]
16//!     [|| next_bit_be || partial_chunk_digest]
17//! )
18//! ```
19//!
20//! - `ops_root` is the root of the operations tree (MMR or MMB).
21//! - `grafted_root` commits to the activity bitmap's **graftable** chunks (chunks whose height-G
22//!   ancestor has been born in the ops tree).
23//! - `pending_chunk_digest` is `H(pending_bytes)` if a chunk is bit-complete in the bitmap but its
24//!   h=G ancestor has not yet been born; absent otherwise.
25//! - `(next_bit_be, partial_chunk_digest)` covers the trailing partial chunk when the bitmap length
26//!   is not chunk-aligned; both elements are absent when it is.
27//!
28//! Both `ops_root` and `grafted_root` are computed by
29//! [`Hasher::root`](crate::merkle::hasher::Hasher::root), so each commits the ops tree's family and
30//! bagging policy.
31//!
32//! Pending and partial slots are independent: at gh >= 3 they can coexist. When both are present,
33//! pending hashes in before partial.
34
35use crate::{
36    journal::contiguous::Contiguous,
37    merkle::{
38        self, Family, Graftable, Location, PendingChunk, Position, Proof, hasher::Hasher as _,
39        storage::Storage,
40    },
41    qmdb::{
42        self, Error,
43        current::{
44            db::{combine_roots, graftable_chunk_window, partial_chunk, pending_chunk},
45            grafting,
46        },
47    },
48};
49use ahash::AHashMap;
50use bytes::BufMut;
51use commonware_codec::{Buf, Encode, EncodeSize, Read, ReadExt as _, Write, varint::UInt};
52use commonware_cryptography::{Digest, Hasher};
53use commonware_utils::{
54    Widen,
55    bitmap::{Prunable as BitMap, Readable as BitmapReadable},
56};
57use core::{num::NonZeroU64, ops::Range};
58use tracing::debug;
59
60pub mod operation;
61
62/// Validate the chunk width before deriving bitmap indices or Merkle heights.
63pub(super) fn chunk_bits(chunk_size: usize) -> Result<u64, commonware_codec::Error> {
64    chunk_size
65        .checked_mul(8)
66        .map(|bits| -> u64 { Widen::widen(bits) })
67        .filter(|bits| bits.is_power_of_two() && bits.trailing_zeros() < 63)
68        .ok_or(commonware_codec::Error::Invalid(
69            "current proof",
70            "invalid bitmap chunk size",
71        ))
72}
73
74/// Bitmap chunk indices read by [RangeProof::new] or [constant::OperationProof::new].
75///
76/// Pass `None` for a range proof or the queried operation location for an operation proof.
77/// The returned indices are unique and increasing. At most three chunks are needed: the
78/// trailing partial chunk, a complete chunk awaiting its ops-tree ancestor, and the queried
79/// operation's chunk. This allows a storage adapter to preload bitmap bytes before exposing a
80/// synchronous [BitmapReadable] implementation.
81///
82/// `ops_leaves`, `bitmap_len`, and `pruned_chunks` must describe one consistent snapshot.
83/// `N` is the bitmap chunk size in bytes and must be a nonzero power of two.
84///
85/// # Errors
86///
87/// Returns [Error::DataCorrupted] if more than one complete chunk awaits its ancestor or
88/// pruning exceeds the graftable prefix. Returns [Error::OperationPruned] for a queried
89/// operation in a pruned chunk, and [merkle::Error::RangeOutOfBounds] for a queried operation
90/// outside the snapshot.
91pub fn required_chunks<F: Graftable, const N: usize>(
92    ops_leaves: Location<F>,
93    bitmap_len: u64,
94    pruned_chunks: u64,
95    location: Option<Location<F>>,
96) -> Result<impl Iterator<Item = u64>, Error<F>> {
97    const { assert!(N.is_power_of_two() && N <= usize::MAX / 8) };
98    let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
99    let complete = bitmap_len / chunk_bits;
100    let graftable =
101        graftable_chunk_window(ops_leaves, complete, pruned_chunks, grafting::height::<N>())?;
102    let queried = if let Some(loc) = location {
103        if loc >= ops_leaves || *loc >= bitmap_len {
104            return Err(merkle::Error::RangeOutOfBounds(loc).into());
105        }
106        let chunk = *loc / chunk_bits;
107        if chunk < pruned_chunks {
108            return Err(Error::OperationPruned(loc));
109        }
110        Some(chunk)
111    } else {
112        None
113    };
114    let mut chunks = [
115        (!bitmap_len.is_multiple_of(chunk_bits)).then_some(complete),
116        (complete > graftable).then_some(graftable),
117        queried,
118    ];
119    chunks.sort_unstable();
120    let mut previous = None;
121    Ok(chunks.into_iter().flatten().filter(move |&chunk| {
122        let distinct = previous != Some(chunk);
123        previous = Some(chunk);
124        distinct
125    }))
126}
127
128/// Witness that a particular `ops_root` is committed by a `current` canonical root.
129///
130/// See the [Canonical root structure](self#canonical-root-structure) section in the module
131/// documentation for the full layout.
132#[derive(Clone, Eq, PartialEq, Debug)]
133pub struct OpsRootWitness<F: Graftable, D: Digest> {
134    /// The grafted-tree root committed by the canonical root.
135    pub grafted_root: D,
136
137    /// The pending-chunk contribution, if any.
138    pub pending_chunk_digest: F::PendingChunk<D>,
139
140    /// The trailing partial chunk contribution, if the bitmap length is not chunk-aligned:
141    /// `(next_bit, partial_chunk_digest)`.
142    pub partial_chunk: Option<(u64, D)>,
143}
144
145impl<F: Graftable, D: Digest> OpsRootWitness<F, D> {
146    /// Compute the canonical `current` root that commits to `ops_root` through this witness.
147    ///
148    /// See the [Canonical root structure](self#canonical-root-structure) section in the module
149    /// documentation for the full layout.
150    pub fn root<H: Hasher<Digest = D>>(&self, ops_root: &D) -> D {
151        let partial = self.partial_chunk.as_ref().map(|(nb, d)| (*nb, d));
152        combine_roots::<H>(
153            ops_root,
154            &self.grafted_root,
155            self.pending_chunk_digest.as_ref(),
156            partial,
157        )
158    }
159
160    /// Return true if this witness proves that `root` commits to `ops_root`.
161    pub fn verify<H: Hasher<Digest = D>>(&self, ops_root: &D, root: &D) -> bool {
162        self.root::<H>(ops_root) == *root
163    }
164}
165
166impl<F: Graftable, D: Digest> Write for OpsRootWitness<F, D> {
167    fn write(&self, buf: &mut impl BufMut) {
168        self.grafted_root.write(buf);
169        self.pending_chunk_digest.write(buf);
170        self.partial_chunk.is_some().write(buf);
171        if let Some((next_bit, digest)) = &self.partial_chunk {
172            UInt(*next_bit).write(buf);
173            digest.write(buf);
174        }
175    }
176}
177
178impl<F: Graftable, D: Digest> EncodeSize for OpsRootWitness<F, D> {
179    fn encode_size(&self) -> usize {
180        self.grafted_root.encode_size()
181            + self.pending_chunk_digest.encode_size()
182            + self
183                .partial_chunk
184                .as_ref()
185                .map_or(1, |(nb, d)| 1 + UInt(*nb).encode_size() + d.encode_size())
186    }
187}
188
189impl<F: Graftable, D: Digest> Read for OpsRootWitness<F, D> {
190    type Cfg = ();
191
192    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
193        let grafted_root = D::read(buf)?;
194        let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
195        let partial_chunk = if bool::read(buf)? {
196            let next_bit = UInt::<u64>::read(buf)?.into();
197            let digest = D::read(buf)?;
198            Some((next_bit, digest))
199        } else {
200            None
201        };
202        Ok(Self {
203            grafted_root,
204            pending_chunk_digest,
205            partial_chunk,
206        })
207    }
208}
209
210#[cfg(feature = "arbitrary")]
211impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for OpsRootWitness<F, D>
212where
213    D: for<'a> arbitrary::Arbitrary<'a>,
214    F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
215{
216    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
217        Ok(Self {
218            grafted_root: u.arbitrary()?,
219            pending_chunk_digest: u.arbitrary()?,
220            partial_chunk: u.arbitrary()?,
221        })
222    }
223}
224
225/// A proof that a range of operations exist in the database.
226#[derive(Clone, Eq, PartialEq, Debug)]
227pub struct RangeProof<F: Graftable, D: Digest> {
228    /// The Merkle digest material required to verify the proof.
229    pub proof: Proof<F, D>,
230
231    /// The pending-chunk contribution, if any.
232    pub pending_chunk_digest: F::PendingChunk<D>,
233
234    /// Digest of the bitmap's trailing partial chunk, if any.
235    pub partial_chunk_digest: Option<D>,
236
237    /// The ops-tree root digest.
238    pub ops_root: D,
239}
240
241/// Parameters that identify the operation span and snapshot used to build a range proof.
242#[derive(Clone, Copy, Eq, PartialEq, Debug)]
243pub struct RangeProofSpec<F: Family, D: Digest> {
244    /// First operation location to prove.
245    pub start_loc: Location<F>,
246
247    /// Maximum number of operations to include.
248    pub max_ops: NonZeroU64,
249
250    /// Inactivity floor used to fold old inactive peaks.
251    pub inactivity_floor: Location<F>,
252
253    /// The ops-tree root at the time of proof generation.
254    pub ops_root: D,
255}
256
257impl<F: Graftable, D: Digest> RangeProof<F, D> {
258    /// Create a new range proof for the provided `range` of operations, fetching the digests of
259    /// its [merkle::RangePlan] from `storage` and building with [Self::build].
260    pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>, const N: usize>(
261        status: &impl BitmapReadable<N>,
262        storage: &S,
263        inactivity_floor: Location<F>,
264        range: Range<Location<F>>,
265        ops_root: D,
266    ) -> Result<Self, Error<F>> {
267        let ops_leaves = Location::try_from(storage.size())?;
268        let plan = merkle::RangePlan::new(ops_leaves, range)?;
269        let positions = plan.positions();
270        let digests = storage.get_nodes(&positions).await?;
271        let fetched: AHashMap<_, _> = positions.into_iter().zip(digests).collect();
272        Self::build::<H, N>(
273            status,
274            plan,
275            |pos| fetched.get(&pos).copied(),
276            inactivity_floor,
277            ops_root,
278        )
279    }
280
281    /// Build the range proof planned by `plan` from digests already in memory.
282    ///
283    /// `get_node` supplies the digest at each of the plan's positions as the grafted view of the
284    /// ops tree serves them, like [crate::qmdb::current::db::Db::grafted_storage]: plain ops-tree
285    /// digests below the grafting height and bitmap-authenticated grafted digests at and above it.
286    /// `status` supplies the bitmap chunks of [required_chunks]. The plan's leaf count, the digests,
287    /// `status`, `inactivity_floor`, and `ops_root` must all describe one snapshot.
288    ///
289    /// # Errors
290    ///
291    /// Returns the errors of [merkle::RangePlan::build], and [Error::DataCorrupted] if the bitmap
292    /// and the plan's leaf count imply more than one pending chunk or pruning beyond the graftable
293    /// chunks.
294    pub fn build<H: Hasher<Digest = D>, const N: usize>(
295        status: &impl BitmapReadable<N>,
296        plan: merkle::RangePlan<F>,
297        get_node: impl Fn(Position<F>) -> Option<D>,
298        inactivity_floor: Location<F>,
299        ops_root: D,
300    ) -> Result<Self, Error<F>> {
301        let ops_leaves = plan.leaves();
302        let grafting_height = grafting::height::<N>();
303        let inactive_peaks = grafting::chunk_aligned_inactive_peaks::<F>(
304            ops_leaves,
305            inactivity_floor,
306            grafting_height,
307        )?;
308
309        let hasher = qmdb::hasher::<H>();
310        let proof = plan.build(&hasher, inactive_peaks, get_node)?;
311
312        let partial_chunk_digest =
313            partial_chunk::<_, N>(status).map(|(chunk, _)| hasher.digest(chunk.as_slice()));
314
315        let pending_chunk_digest: F::PendingChunk<D> =
316            pending_chunk::<_, _, N>(status, ops_leaves, grafting_height)?
317                .map(|chunk| hasher.digest(chunk.as_slice()))
318                .try_into()
319                .expect("pending_chunk must be consistent with family");
320
321        Ok(Self {
322            proof,
323            pending_chunk_digest,
324            partial_chunk_digest,
325            ops_root,
326        })
327    }
328
329    /// Returns a proof that the specified range of operations are part of the database, along with
330    /// the operations from the range and their activity status chunks. A truncated range (from
331    /// hitting the max) can be detected by looking at the length of the returned operations vector.
332    ///
333    /// # Errors
334    ///
335    /// Returns [Error::OperationPruned] if `start_loc` falls in a pruned bitmap chunk.
336    /// Returns [`merkle::Error::LocationOverflow`] if `start_loc` > [merkle::Family::MAX_LEAVES].
337    /// Returns [`merkle::Error::RangeOutOfBounds`] if `start_loc` >= number of leaves in the tree.
338    pub async fn new_with_ops<
339        H: Hasher<Digest = D>,
340        C: Contiguous,
341        S: Storage<F, Digest = D>,
342        const N: usize,
343    >(
344        status: &impl BitmapReadable<N>,
345        storage: &S,
346        log: &C,
347        request: RangeProofSpec<F, D>,
348    ) -> Result<(Self, Vec<C::Item>, Vec<[u8; N]>), Error<F>> {
349        // Compute the end location of the range.
350        let leaves = Location::new(status.len());
351        if request.start_loc >= leaves {
352            return Err(merkle::Error::RangeOutOfBounds(request.start_loc).into());
353        }
354
355        // Reject ranges that start in pruned bitmap chunks.
356        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
357        let start = *request.start_loc / chunk_bits;
358        if (start as usize) < status.pruned_chunks() {
359            return Err(Error::OperationPruned(request.start_loc));
360        }
361
362        let max_loc = request.start_loc.saturating_add(request.max_ops.get());
363        let end_loc = core::cmp::min(max_loc, leaves);
364
365        // Generate the proof from the grafted storage.
366        let proof = Self::new::<H, S, N>(
367            status,
368            storage,
369            request.inactivity_floor,
370            request.start_loc..end_loc,
371            request.ops_root,
372        )
373        .await?;
374
375        // Collect the operations necessary to verify the proof.
376        let positions: Vec<u64> = (*request.start_loc..*end_loc).collect();
377        let ops = log.read_many(&positions).await?;
378
379        // Gather the chunks necessary to verify the proof.
380        let end = (*end_loc - 1) / chunk_bits; // chunk that contains the last bit
381        let chunks = (start..=end)
382            .map(|i| status.get_chunk(i as usize))
383            .collect::<Vec<_>>();
384
385        Ok((proof, ops, chunks))
386    }
387
388    /// Reconstruct the canonical current root, optionally collecting the positioned digests
389    /// required to compute the peaks covering the proven range.
390    fn reconstruct_root<H, O>(
391        &self,
392        start_loc: Location<F>,
393        ops: &[O],
394        chunks: &[impl AsRef<[u8]>],
395        chunk_size: usize,
396        collected: Option<&mut Vec<(Position<F>, D)>>,
397    ) -> Result<D, merkle::Error<F>>
398    where
399        H: Hasher<Digest = D>,
400        O: Encode,
401    {
402        let chunk_bits = chunk_bits(chunk_size).map_err(|_| merkle::Error::InvalidProof)?;
403        if ops.is_empty() || chunks.is_empty() {
404            debug!("verification failed, empty input");
405            return Err(merkle::Error::InvalidProof);
406        }
407        // Compute the (non-inclusive) end location of the range.
408        let Some(end_loc) = start_loc.checked_add(ops.len() as u64) else {
409            debug!("verification failed, end_loc overflow");
410            return Err(merkle::Error::InvalidProof);
411        };
412
413        let leaves = self.proof.leaves;
414        if end_loc > leaves {
415            debug!(
416                loc = ?end_loc,
417                ?leaves, "verification failed, invalid range"
418            );
419            return Err(merkle::Error::InvalidProof);
420        }
421
422        // Validate the number of input chunks.
423        let start_chunk = *start_loc / chunk_bits;
424        let end_chunk = (*end_loc - 1) / chunk_bits;
425        let complete_chunks = *leaves / chunk_bits;
426
427        if (end_chunk - start_chunk + 1) != chunks.len() as u64 {
428            debug!("verification failed, chunk metadata length mismatch");
429            return Err(merkle::Error::InvalidProof);
430        }
431        if chunks
432            .iter()
433            .any(|chunk| chunk.as_ref().len() != chunk_size)
434        {
435            debug!("verification failed, chunk size mismatch");
436            return Err(merkle::Error::InvalidProof);
437        }
438
439        let next_bit = *leaves % chunk_bits;
440        let has_partial_chunk = next_bit != 0;
441
442        let elements = ops.iter().map(|op| op.encode()).collect::<Vec<_>>();
443        let chunk_vec = chunks.iter().map(|c| c.as_ref()).collect::<Vec<_>>();
444        let grafting_height = chunk_bits.trailing_zeros();
445
446        let graftable_chunks =
447            grafting::graftable_chunks::<F>(*leaves, grafting_height).min(complete_chunks);
448        let pending_chunks = complete_chunks - graftable_chunks;
449        if pending_chunks > 1 {
450            debug!(
451                ?complete_chunks,
452                ?graftable_chunks,
453                "verification failed, multiple pending chunks"
454            );
455            return Err(merkle::Error::InvalidProof);
456        }
457        let has_pending_chunk = pending_chunks == 1;
458
459        let grafting_verifier = grafting::Verifier::<F, H>::new(
460            grafting_height,
461            start_chunk,
462            chunk_vec,
463            graftable_chunks,
464        );
465
466        if self.pending_chunk_digest.as_ref().is_some() != has_pending_chunk {
467            debug!(
468                pending_in_proof = self.pending_chunk_digest.as_ref().is_some(),
469                expected = has_pending_chunk,
470                "pending_chunk_digest presence does not match bitmap state"
471            );
472            return Err(merkle::Error::InvalidProof);
473        }
474
475        // For partial chunks, validate the last chunk digest from the proof.
476        if has_partial_chunk {
477            let Some(last_chunk_digest) = self.partial_chunk_digest else {
478                debug!("proof has no partial chunk digest");
479                return Err(merkle::Error::InvalidProof);
480            };
481
482            // If the proof covers an operation in the partial chunk, verify that the
483            // chunk provided by the caller matches the digest embedded in the proof.
484            if end_chunk == complete_chunks {
485                let last_chunk = chunks.last().expect("chunks non-empty");
486                if last_chunk_digest != grafting_verifier.digest(last_chunk.as_ref()) {
487                    debug!("last chunk digest does not match expected value");
488                    return Err(merkle::Error::InvalidProof);
489                }
490            }
491        } else if self.partial_chunk_digest.is_some() {
492            debug!("proof has unexpected partial chunk digest");
493            return Err(merkle::Error::InvalidProof);
494        }
495
496        // For a pending chunk, validate the supplied chunk bytes against the digest in the proof
497        // when the verifier's range includes the pending chunk's index. The pending chunk is at
498        // index `graftable_chunks` (== `complete_chunks - 1` when present).
499        if let Some(pending_digest) = self.pending_chunk_digest.as_ref() {
500            let pending_idx = graftable_chunks;
501            if pending_idx >= start_chunk && pending_idx <= end_chunk {
502                let local = (pending_idx - start_chunk) as usize;
503                // The earlier `chunks.len() == end_chunk - start_chunk + 1` check makes this
504                // index in-bounds for well-formed inputs; treat any mismatch as a malformed
505                // proof (rather than panicking) since `verify` runs against attacker-supplied data.
506                let Some(pending_chunk_bytes) = chunks.get(local) else {
507                    debug!(
508                        ?pending_idx,
509                        chunks_len = chunks.len(),
510                        "pending chunk index out of range in supplied chunks"
511                    );
512                    return Err(merkle::Error::InvalidProof);
513                };
514                if *pending_digest != grafting_verifier.digest(pending_chunk_bytes.as_ref()) {
515                    debug!("pending chunk digest does not match expected value");
516                    return Err(merkle::Error::InvalidProof);
517                }
518            }
519        }
520
521        let merkle_root = match self.proof.reconstruct_root_inner(
522            &grafting_verifier,
523            &elements,
524            start_loc,
525            collected,
526        ) {
527            Ok(root) => root,
528            Err(error) => {
529                debug!(?error, "invalid proof input");
530                return Err(merkle::Error::InvalidProof);
531            }
532        };
533
534        let partial =
535            has_partial_chunk.then(|| (next_bit, self.partial_chunk_digest.as_ref().unwrap()));
536        Ok(combine_roots::<H>(
537            &self.ops_root,
538            &merkle_root,
539            self.pending_chunk_digest.as_ref(),
540            partial,
541        ))
542    }
543
544    /// Return true if the given sequence of `ops` were applied starting at location `start_loc` in
545    /// the db with the provided root, and having the activity status described by `chunks`.
546    pub fn verify<H: Hasher<Digest = D>, O: Encode, const N: usize>(
547        &self,
548        start_loc: Location<F>,
549        ops: &[O],
550        chunks: &[[u8; N]],
551        root: &H::Digest,
552    ) -> bool {
553        self.verify_with_chunk_size::<H, O>(start_loc, ops, chunks, N, root)
554    }
555
556    /// Verify an operation range using bitmap chunks whose size is configured at runtime.
557    ///
558    /// `chunk_size` must match the source database. It must be a nonzero power of two,
559    /// its bit width must fit in `usize` and `u64`, and its grafting height must be below 63.
560    /// Every supplied chunk must contain exactly `chunk_size` bytes. Invalid sizes and
561    /// malformed proofs return `false`.
562    ///
563    /// This verifies the same proof and wire encoding as [Self::verify], accepting arrays,
564    /// byte slices, or owned byte buffers without copying their contents.
565    pub fn verify_with_chunk_size<H: Hasher<Digest = D>, O: Encode>(
566        &self,
567        start_loc: Location<F>,
568        ops: &[O],
569        chunks: &[impl AsRef<[u8]>],
570        chunk_size: usize,
571        root: &D,
572    ) -> bool {
573        matches!(
574            self.reconstruct_root::<H, O>(start_loc, ops, chunks, chunk_size, None),
575            Ok(reconstructed_root) if reconstructed_root == *root
576        )
577    }
578}
579
580/// Verify that a [RangeProof] is valid for a range of operations and return the positioned digests
581/// required to compute the peaks covering the proven range.
582pub fn verify_proof_and_extract_digests<F, Op, H, const N: usize>(
583    proof: &RangeProof<F, H::Digest>,
584    start_loc: Location<F>,
585    operations: &[Op],
586    chunks: &[[u8; N]],
587    target_root: &H::Digest,
588) -> Result<qmdb::verify::ExtractedDigests<F, H>, merkle::Error<F>>
589where
590    F: Graftable,
591    Op: Encode,
592    H: Hasher,
593{
594    let mut collected = Vec::new();
595    let reconstructed_root =
596        proof.reconstruct_root::<H, Op>(start_loc, operations, chunks, N, Some(&mut collected))?;
597    if reconstructed_root != *target_root {
598        debug!("verification failed, root mismatch");
599        return Err(merkle::Error::RootMismatch);
600    }
601
602    Ok(collected)
603}
604
605impl<F: Graftable, D: Digest> Write for RangeProof<F, D> {
606    fn write(&self, buf: &mut impl BufMut) {
607        self.proof.write(buf);
608        self.pending_chunk_digest.write(buf);
609        self.partial_chunk_digest.write(buf);
610        self.ops_root.write(buf);
611    }
612}
613
614impl<F: Graftable, D: Digest> EncodeSize for RangeProof<F, D> {
615    fn encode_size(&self) -> usize {
616        self.proof.encode_size()
617            + self.pending_chunk_digest.encode_size()
618            + self.partial_chunk_digest.encode_size()
619            + self.ops_root.encode_size()
620    }
621}
622
623impl<F: Graftable, D: Digest> Read for RangeProof<F, D> {
624    /// The maximum number of digests in the embedded Merkle proof.
625    type Cfg = usize;
626
627    fn read_cfg(
628        buf: &mut impl Buf,
629        max_digests: &Self::Cfg,
630    ) -> Result<Self, commonware_codec::Error> {
631        let proof = Proof::<F, D>::read_cfg(buf, max_digests)?;
632        let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
633        let partial_chunk_digest = Option::<D>::read(buf)?;
634        let ops_root = D::read(buf)?;
635        Ok(Self {
636            proof,
637            pending_chunk_digest,
638            partial_chunk_digest,
639            ops_root,
640        })
641    }
642}
643
644#[cfg(feature = "arbitrary")]
645impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for RangeProof<F, D>
646where
647    D: for<'a> arbitrary::Arbitrary<'a>,
648    F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
649{
650    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
651        Ok(Self {
652            proof: u.arbitrary()?,
653            pending_chunk_digest: u.arbitrary()?,
654            partial_chunk_digest: u.arbitrary()?,
655            ops_root: u.arbitrary()?,
656        })
657    }
658}
659
660/// Proofs with fixed-size bitmap chunks.
661pub mod constant {
662    /// A proof that a specific operation is active, with a fixed-size bitmap chunk.
663    pub type OperationProof<F, D, const N: usize> = super::operation::Proof<F, D, [u8; N]>;
664}
665
666/// Proofs with runtime-sized bitmap chunks.
667pub mod dynamic {
668    /// An operation proof with a runtime-sized bitmap chunk.
669    ///
670    /// Decoding takes `(chunk_size, max_digests)` as configuration. The chunk size must match
671    /// the source database and satisfy [super::RangeProof::verify_with_chunk_size]'s size
672    /// requirements. The bitmap chunk has no length prefix, so its wire encoding matches
673    /// [super::constant::OperationProof].
674    /// The operation supplied to verification must use the source database's key and value codecs.
675    ///
676    /// # Examples
677    ///
678    /// ```
679    /// use commonware_codec::{Decode, Encode, Input};
680    /// use commonware_cryptography::{Sha256, sha256::Digest};
681    /// use commonware_storage::{merkle::mmr, qmdb::current::proof::dynamic::OperationProof};
682    ///
683    /// fn verify<O: Encode>(
684    ///     encoded: impl Input,
685    ///     chunk_size: usize,
686    ///     max_digests: usize,
687    ///     operation: O,
688    ///     root: &Digest,
689    /// ) -> bool {
690    ///     let Ok(proof) = OperationProof::<mmr::Family, Digest>::decode_cfg(
691    ///         encoded,
692    ///         &(chunk_size, max_digests),
693    ///     ) else {
694    ///         return false;
695    ///     };
696    ///     proof.verify::<Sha256, _>(operation, root)
697    /// }
698    /// ```
699    pub type OperationProof<F, D> = super::operation::Proof<F, D, bytes::Bytes>;
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use crate::{
706        merkle::{conformance::build_test_mem, hasher::Standard as StandardHasher, mem::Mem},
707        mmb, mmr,
708        qmdb::current::{db, grafting},
709    };
710    use bytes::Bytes;
711    use commonware_codec::{Decode as _, DecodeExt as _};
712    use commonware_cryptography::{Sha256, sha256};
713    use commonware_macros::test_async;
714    use commonware_parallel::{Sequential, Strategy};
715    use commonware_utils::{
716        Widen,
717        bitmap::{Prunable as BitMap, Readable as BitmapReadable},
718        sync::Mutex,
719    };
720    use core::ops::Range;
721    use std::collections::{BTreeMap, BTreeSet};
722
723    /// Compute grafted leaf digests for the given bitmap chunks as `(chunk_idx, digest)` pairs.
724    ///
725    /// See [`db::read_graft_inputs`] for the chunk requirements. The grafted leaf digest is
726    /// `hash(chunk || ops_h_G_node)`, except that an all-zero chunk's leaf is the ops digest
727    /// itself (zero-chunk identity).
728    async fn compute_grafted_leaves<
729        F: merkle::Graftable,
730        H: Hasher,
731        S: Strategy,
732        const N: usize,
733    >(
734        ops_tree: &impl Storage<F, Digest = H::Digest>,
735        chunks: impl IntoIterator<Item = (usize, [u8; N])>,
736        strategy: &S,
737    ) -> Result<Vec<(usize, H::Digest)>, Error<F>> {
738        let inputs = db::read_graft_inputs::<F, _, N>(ops_tree, chunks).await?;
739        Ok(grafting::graft_chunk_digests::<H, _, N>(strategy, inputs))
740    }
741
742    #[test]
743    fn test_ops_root_witness_codec_roundtrip() {
744        type F = mmb::Family;
745        for partial_chunk in [
746            None,
747            Some((0u64, Sha256::hash(&[b"partial-zero"]))),
748            Some((123u64, Sha256::hash(&[b"partial-nonzero"]))),
749        ] {
750            let witness: OpsRootWitness<F, _> = OpsRootWitness {
751                grafted_root: Sha256::hash(&[b"grafted"]),
752                pending_chunk_digest: None,
753                partial_chunk,
754            };
755            let encoded = witness.encode();
756            assert_eq!(encoded.len(), witness.encode_size());
757            let decoded = OpsRootWitness::<F, sha256::Digest>::decode(encoded).unwrap();
758            assert_eq!(decoded, witness);
759        }
760    }
761
762    #[test]
763    fn test_ops_root_witness_root_matches_verify() {
764        type F = mmb::Family;
765
766        let ops_root = Sha256::hash(&[b"ops root"]);
767        let witness: OpsRootWitness<F, _> = OpsRootWitness {
768            grafted_root: Sha256::hash(&[b"grafted root"]),
769            pending_chunk_digest: Some(Sha256::hash(&[b"pending chunk"])),
770            partial_chunk: Some((13, Sha256::hash(&[b"partial chunk"]))),
771        };
772
773        let root = witness.root::<Sha256>(&ops_root);
774
775        assert!(witness.verify::<Sha256>(&ops_root, &root));
776        assert_ne!(root, ops_root);
777
778        let wrong_ops_root = Sha256::hash(&[b"wrong ops root"]);
779        assert!(!witness.verify::<Sha256>(&wrong_ops_root, &root));
780    }
781
782    fn range_proof_digest_count<F: Graftable, D: Digest>(proof: &RangeProof<F, D>) -> usize {
783        proof.proof.digests.len()
784    }
785
786    #[test]
787    fn test_range_proof_codec_roundtrip() {
788        type F = mmb::Family;
789        const MAX_DIGESTS: usize = 64;
790
791        let proof = Proof::<F, sha256::Digest> {
792            leaves: mmb::Location::new(42),
793            inactive_peaks: 0,
794            digests: vec![
795                Sha256::hash(&[b"d0"]),
796                Sha256::hash(&[b"d1"]),
797                Sha256::hash(&[b"d2"]),
798            ],
799        };
800        let ops_root = Sha256::hash(&[b"ops-root"]);
801
802        let cases = [
803            // Minimal: no optional fields or prefix/suffix witnesses.
804            RangeProof {
805                proof: proof.clone(),
806                pending_chunk_digest: None,
807                partial_chunk_digest: None,
808                ops_root,
809            },
810            // All optional fields populated.
811            RangeProof {
812                proof,
813                pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
814                partial_chunk_digest: Some(Sha256::hash(&[b"partial"])),
815                ops_root,
816            },
817            // Default proof with only partial chunk digest.
818            RangeProof {
819                proof: Proof::<F, sha256::Digest>::default(),
820                pending_chunk_digest: None,
821                partial_chunk_digest: Some(Sha256::hash(&[b"only-partial"])),
822                ops_root,
823            },
824        ];
825
826        for proof in cases {
827            let encoded = proof.encode();
828            assert_eq!(encoded.len(), proof.encode_size());
829            let decoded =
830                RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
831            assert_eq!(decoded, proof);
832        }
833    }
834
835    #[test]
836    fn test_range_proof_codec_enforces_merkle_digest_budget() {
837        type F = mmb::Family;
838
839        let proof = RangeProof {
840            proof: Proof::<F, sha256::Digest> {
841                leaves: mmb::Location::new(42),
842                inactive_peaks: 0,
843                digests: vec![Sha256::hash(&[b"d0"])],
844            },
845            pending_chunk_digest: None,
846            partial_chunk_digest: None,
847            ops_root: Sha256::hash(&[b"ops-root"]),
848        };
849
850        let encoded = proof.encode();
851        let total_digests = range_proof_digest_count(&proof);
852
853        let decoded =
854            RangeProof::<F, sha256::Digest>::decode_cfg(encoded.clone(), &total_digests).unwrap();
855        assert_eq!(decoded, proof);
856        assert!(
857            RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &(total_digests - 1)).is_err()
858        );
859    }
860
861    #[test]
862    fn test_range_proof_decode_rejects_pending_for_mmr() {
863        const MAX_DIGESTS: usize = 64;
864
865        let proof = RangeProof {
866            proof: Proof::<mmb::Family, sha256::Digest> {
867                leaves: mmb::Location::new(42),
868                inactive_peaks: 0,
869                digests: vec![Sha256::hash(&[b"d0"])],
870            },
871            pending_chunk_digest: Some(Sha256::hash(&[b"pending"])),
872            partial_chunk_digest: None,
873            ops_root: Sha256::hash(&[b"ops-root"]),
874        };
875        let encoded = proof.encode();
876
877        // MMB allows pending_chunk_digest.
878        assert!(
879            RangeProof::<mmb::Family, sha256::Digest>::decode_cfg(encoded.clone(), &MAX_DIGESTS)
880                .is_ok()
881        );
882
883        // MMR rejects it on decode.
884        assert!(
885            RangeProof::<crate::merkle::mmr::Family, sha256::Digest>::decode_cfg(
886                encoded,
887                &MAX_DIGESTS
888            )
889            .is_err()
890        );
891    }
892
893    #[test]
894    fn test_operation_proof_codec_roundtrip() {
895        type F = mmb::Family;
896        const N: usize = 32;
897        const MAX_DIGESTS: usize = 64;
898
899        let range_proof = RangeProof {
900            proof: Proof::<F, sha256::Digest> {
901                leaves: mmb::Location::new(7),
902                inactive_peaks: 0,
903                digests: vec![Sha256::hash(&[b"sib"])],
904            },
905            pending_chunk_digest: None,
906            partial_chunk_digest: None,
907            ops_root: Sha256::hash(&[b"ops"]),
908        };
909
910        let chunk: [u8; N] = core::array::from_fn(|i| i as u8);
911
912        let proof = constant::OperationProof::<F, sha256::Digest, N> {
913            loc: mmb::Location::new(5),
914            chunk,
915            range_proof,
916        };
917
918        let encoded = proof.encode();
919        assert_eq!(encoded.len(), proof.encode_size());
920        let decoded =
921            constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &MAX_DIGESTS)
922                .unwrap();
923        assert_eq!(decoded, proof);
924    }
925
926    #[test]
927    fn test_operation_proof_codec_enforces_merkle_digest_budget() {
928        type F = mmb::Family;
929        const N: usize = 32;
930
931        let range_proof = RangeProof {
932            proof: Proof::<F, sha256::Digest> {
933                leaves: mmb::Location::new(7),
934                inactive_peaks: 0,
935                digests: vec![Sha256::hash(&[b"sib"])],
936            },
937            pending_chunk_digest: None,
938            partial_chunk_digest: None,
939            ops_root: Sha256::hash(&[b"ops"]),
940        };
941        let total_digests = range_proof_digest_count(&range_proof);
942        let proof = constant::OperationProof::<F, sha256::Digest, N> {
943            loc: mmb::Location::new(5),
944            chunk: core::array::from_fn(|i| i as u8),
945            range_proof,
946        };
947
948        let encoded = proof.encode();
949        let decoded = constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(
950            encoded.clone(),
951            &total_digests,
952        )
953        .unwrap();
954        assert_eq!(decoded, proof);
955        assert!(
956            constant::OperationProof::<F, sha256::Digest, N>::decode_cfg(
957                encoded,
958                &(total_digests - 1)
959            )
960            .is_err()
961        );
962    }
963
964    #[test_async]
965    async fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
966        type F = mmb::Family;
967        const N: usize = 1;
968
969        let hasher = qmdb::hasher::<Sha256>();
970        let grafting_height = grafting::height::<N>();
971
972        let leaf_count = (16..=64u64)
973            .find(|&leaves| {
974                let size = F::location_to_position(mmb::Location::new(leaves));
975                F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
976            })
977            .expect("expected an MMB size whose second chunk spans multiple peaks");
978
979        let mut status = BitMap::<N>::new();
980        for _ in 0..leaf_count {
981            status.push(true);
982        }
983        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
984        let ops_root = ops.root(&hasher, 0).unwrap();
985
986        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
987            *Location::<F>::try_from(ops.size()).unwrap(),
988            grafting_height,
989        )
990        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
991            as usize;
992        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
993            .map(|chunk_idx| {
994                (
995                    chunk_idx,
996                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
997                )
998            })
999            .collect();
1000        let mut leaf_digests =
1001            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1002                .await
1003                .unwrap();
1004        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1005
1006        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1007        let mut grafted = Mem::<F, sha256::Digest>::new();
1008        let merkleized = {
1009            let mut batch = grafted.new_batch();
1010            for (_, digest) in leaf_digests {
1011                batch = batch.add_leaf_digest(digest);
1012            }
1013            batch.merkleize(&grafted, &grafted_hasher)
1014        };
1015        grafted.apply_batch(&merkleized).unwrap();
1016
1017        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1018        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1019        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1020            &status,
1021            &storage,
1022            ops_leaves_for_root,
1023            Location::new(0),
1024        )
1025        .await
1026        .unwrap()
1027        .root::<Sha256>(&ops_root);
1028
1029        let loc = mmb::Location::new(BitMap::<N>::CHUNK_SIZE_BITS + 4);
1030        let proof = RangeProof::new::<Sha256, _, N>(
1031            &status,
1032            &storage,
1033            Location::new(0),
1034            loc..loc + 1,
1035            ops_root,
1036        )
1037        .await
1038        .unwrap();
1039
1040        let element = hasher.digest(&(*loc).to_be_bytes());
1041        assert!(proof.verify::<Sha256, _, N>(
1042            loc,
1043            &[element],
1044            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 1)],
1045            &root,
1046        ));
1047    }
1048
1049    #[test_async]
1050    async fn test_range_proof_verifies_with_partial_suffix_mmb() {
1051        type F = mmb::Family;
1052        const N: usize = 1;
1053
1054        let hasher = qmdb::hasher::<Sha256>();
1055        let grafting_height = grafting::height::<N>();
1056        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1057
1058        let (leaf_count, loc) = (chunk_bits * 2 + 1..=64u64)
1059            .find_map(|leaves| {
1060                let complete_chunks = leaves / chunk_bits;
1061                if complete_chunks < 2 || leaves % chunk_bits == 0 {
1062                    return None;
1063                }
1064
1065                let size = F::location_to_position(mmb::Location::new(leaves));
1066                F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1067                Some((leaves, mmb::Location::new(chunk_bits + 1)))
1068            })
1069            .expect("expected an MMB proof with a partial trailing suffix chunk");
1070
1071        let mut status = BitMap::<N>::new();
1072        for _ in 0..leaf_count {
1073            status.push(true);
1074        }
1075        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1076        let ops_root = ops.root(&hasher, 0).unwrap();
1077
1078        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1079            *Location::<F>::try_from(ops.size()).unwrap(),
1080            grafting_height,
1081        )
1082        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1083            as usize;
1084        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1085            .map(|chunk_idx| {
1086                (
1087                    chunk_idx,
1088                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1089                )
1090            })
1091            .collect();
1092        let mut leaf_digests =
1093            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1094                .await
1095                .unwrap();
1096        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1097
1098        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1099        let mut grafted = Mem::<F, sha256::Digest>::new();
1100        let merkleized = {
1101            let mut batch = grafted.new_batch();
1102            for (_, digest) in leaf_digests {
1103                batch = batch.add_leaf_digest(digest);
1104            }
1105            batch.merkleize(&grafted, &grafted_hasher)
1106        };
1107        grafted.apply_batch(&merkleized).unwrap();
1108
1109        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1110        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1111        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1112            &status,
1113            &storage,
1114            ops_leaves_for_root,
1115            Location::new(0),
1116        )
1117        .await
1118        .unwrap()
1119        .root::<Sha256>(&ops_root);
1120        let proof = RangeProof::new::<Sha256, _, N>(
1121            &status,
1122            &storage,
1123            Location::new(0),
1124            loc..loc + 1,
1125            ops_root,
1126        )
1127        .await
1128        .unwrap();
1129
1130        let element = hasher.digest(&(*loc).to_be_bytes());
1131        let chunk_idx = (*loc / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
1132        assert!(proof.verify::<Sha256, _, N>(
1133            loc,
1134            &[element],
1135            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1136                &status, chunk_idx
1137            )],
1138            &root,
1139        ));
1140    }
1141
1142    #[test_async]
1143    async fn test_range_proof_verifies_when_range_reaches_partial_chunk_mmb() {
1144        type F = mmb::Family;
1145        const N: usize = 1;
1146
1147        let hasher = qmdb::hasher::<Sha256>();
1148        let grafting_height = grafting::height::<N>();
1149        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1150
1151        // Search for an MMB size whose chunk 1 is multi-peak AND whose total leaves
1152        // aren't chunk-aligned (so a partial trailing chunk exists). The proven range
1153        // starts inside chunk 1 and extends to the end (touching the partial chunk).
1154        let (leaf_count, start_loc, complete_chunks) = (17..=128u64)
1155            .find_map(|leaves| {
1156                let complete_chunks = leaves / chunk_bits;
1157                if complete_chunks < 2 || leaves % chunk_bits == 0 {
1158                    return None;
1159                }
1160                let leaves_loc = mmb::Location::new(leaves);
1161                let size = F::location_to_position(leaves_loc);
1162                F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1163                let start_loc = mmb::Location::new(chunk_bits + 1);
1164                Some((leaves, start_loc, complete_chunks))
1165            })
1166            .expect("expected an MMB size with chunk 1 multi-peak and a partial trailing chunk");
1167
1168        let mut status = BitMap::<N>::new();
1169        for _ in 0..leaf_count {
1170            status.push(true);
1171        }
1172        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1173        let ops_root = ops.root(&hasher, 0).unwrap();
1174
1175        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1176            *Location::<F>::try_from(ops.size()).unwrap(),
1177            grafting_height,
1178        )
1179        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1180            as usize;
1181        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1182            .map(|chunk_idx| {
1183                (
1184                    chunk_idx,
1185                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1186                )
1187            })
1188            .collect();
1189        let mut leaf_digests =
1190            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1191                .await
1192                .unwrap();
1193        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1194
1195        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1196        let mut grafted = Mem::<F, sha256::Digest>::new();
1197        let merkleized = {
1198            let mut batch = grafted.new_batch();
1199            for (_, digest) in leaf_digests {
1200                batch = batch.add_leaf_digest(digest);
1201            }
1202            batch.merkleize(&grafted, &grafted_hasher)
1203        };
1204        grafted.apply_batch(&merkleized).unwrap();
1205
1206        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1207        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1208        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1209            &status,
1210            &storage,
1211            ops_leaves_for_root,
1212            Location::new(0),
1213        )
1214        .await
1215        .unwrap()
1216        .root::<Sha256>(&ops_root);
1217
1218        let leaves_loc = mmb::Location::new(leaf_count);
1219        let proof = RangeProof::new::<Sha256, _, N>(
1220            &status,
1221            &storage,
1222            Location::new(0),
1223            start_loc..leaves_loc,
1224            ops_root,
1225        )
1226        .await
1227        .unwrap();
1228
1229        let elements = (*start_loc..leaf_count)
1230            .map(|idx| hasher.digest(&idx.to_be_bytes()))
1231            .collect::<Vec<_>>();
1232        let start_chunk_idx = (*start_loc / chunk_bits) as usize;
1233        let end_chunk_idx = complete_chunks as usize;
1234        let chunks = (start_chunk_idx..=end_chunk_idx)
1235            .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1236            .collect::<Vec<_>>();
1237        assert!(proof.verify::<Sha256, _, N>(start_loc, &elements, &chunks, &root,));
1238
1239        // Flip a byte in the trailing partial chunk while preserving the window shape.
1240        let mut bad_chunks = chunks;
1241        let last = bad_chunks.last_mut().unwrap();
1242        last[0] ^= 1;
1243        assert!(
1244            !proof.verify::<Sha256, _, N>(start_loc, &elements, &bad_chunks, &root),
1245            "tampered partial chunk bytes should not verify"
1246        );
1247    }
1248
1249    #[test_async]
1250    async fn test_range_proof_rejects_unexpected_partial_chunk_digest() {
1251        type F = mmb::Family;
1252        const N: usize = 1;
1253
1254        let hasher = qmdb::hasher::<Sha256>();
1255        let grafting_height = grafting::height::<N>();
1256        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1257
1258        let leaf_count = chunk_bits * 2; // Perfect chunks, NO partial trailing bits
1259        let mut status = BitMap::<N>::new();
1260        for _ in 0..leaf_count {
1261            status.push(true);
1262        }
1263        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1264        let ops_root = ops.root(&hasher, 0).unwrap();
1265
1266        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1267            *Location::<F>::try_from(ops.size()).unwrap(),
1268            grafting_height,
1269        )
1270        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1271            as usize;
1272        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1273            .map(|chunk_idx| {
1274                (
1275                    chunk_idx,
1276                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1277                )
1278            })
1279            .collect();
1280        let mut leaf_digests =
1281            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1282                .await
1283                .unwrap();
1284        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1285
1286        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1287        let mut grafted = Mem::<F, sha256::Digest>::new();
1288        let merkleized = {
1289            let mut batch = grafted.new_batch();
1290            for (_, digest) in leaf_digests {
1291                batch = batch.add_leaf_digest(digest);
1292            }
1293            batch.merkleize(&grafted, &grafted_hasher)
1294        };
1295        grafted.apply_batch(&merkleized).unwrap();
1296
1297        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1298        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1299        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1300            &status,
1301            &storage,
1302            ops_leaves_for_root,
1303            Location::new(0),
1304        )
1305        .await
1306        .unwrap()
1307        .root::<Sha256>(&ops_root);
1308
1309        let loc = mmb::Location::new(0);
1310        let mut proof = RangeProof::new::<Sha256, _, N>(
1311            &status,
1312            &storage,
1313            Location::new(0),
1314            loc..loc + 1,
1315            ops_root,
1316        )
1317        .await
1318        .unwrap();
1319
1320        let element = hasher.digest(&(*loc).to_be_bytes());
1321        let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
1322
1323        // Tamper with the proof by injecting a fake partial chunk digest
1324        let mut tampered = proof.clone();
1325        tampered.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1326        assert!(!tampered.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1327
1328        proof.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1329        assert!(!proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1330    }
1331
1332    async fn current_range_proof_fixture<F: Graftable, const N: usize>(
1333        leaf_count: u64,
1334        range: Range<Location<F>>,
1335    ) -> (
1336        StandardHasher<Sha256>,
1337        RangeProof<F, sha256::Digest>,
1338        Vec<sha256::Digest>,
1339        Vec<[u8; N]>,
1340        sha256::Digest,
1341        Mem<F, sha256::Digest>,
1342    ) {
1343        let hasher = qmdb::hasher::<Sha256>();
1344        let grafting_height = grafting::height::<N>();
1345        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1346
1347        let mut status = BitMap::<N>::new();
1348        for _ in 0..leaf_count {
1349            status.push(true);
1350        }
1351
1352        let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaf_count);
1353        let ops_root = ops.root(&hasher, 0).unwrap();
1354        let ops_leaves = Location::<F>::try_from(ops.size()).unwrap();
1355
1356        let graftable_chunks_for_test =
1357            grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1358                .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1359                as usize;
1360        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1361            .map(|chunk_idx| {
1362                (
1363                    chunk_idx,
1364                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1365                )
1366            })
1367            .collect();
1368        let mut leaf_digests =
1369            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1370                .await
1371                .unwrap();
1372        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1373
1374        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1375        let mut grafted = Mem::<F, sha256::Digest>::new();
1376        if !leaf_digests.is_empty() {
1377            let merkleized = {
1378                let mut batch = grafted.new_batch();
1379                for (_, digest) in leaf_digests {
1380                    batch = batch.add_leaf_digest(digest);
1381                }
1382                batch.merkleize(&grafted, &grafted_hasher)
1383            };
1384            grafted.apply_batch(&merkleized).unwrap();
1385        }
1386
1387        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1388        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1389            &status,
1390            &storage,
1391            ops_leaves,
1392            Location::new(0),
1393        )
1394        .await
1395        .unwrap()
1396        .root::<Sha256>(&ops_root);
1397
1398        let proof = RangeProof::new::<Sha256, _, N>(
1399            &status,
1400            &storage,
1401            Location::new(0),
1402            range.clone(),
1403            ops_root,
1404        )
1405        .await
1406        .unwrap();
1407        let operations = (*range.start..*range.end)
1408            .map(|i| hasher.digest(&i.to_be_bytes()))
1409            .collect::<Vec<_>>();
1410
1411        // Provide every bitmap chunk touched by the proven operation range.
1412        let start_chunk = (*range.start / chunk_bits) as usize;
1413        let end_chunk = ((*range.end - 1) / chunk_bits) as usize;
1414        let chunks = (start_chunk..=end_chunk)
1415            .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1416            .collect::<Vec<_>>();
1417
1418        assert!(proof.verify::<Sha256, _, N>(range.start, &operations, &chunks, &root));
1419
1420        (hasher, proof, operations, chunks, root, ops)
1421    }
1422
1423    async fn verify_proof_and_extract_digests_inner<F: Graftable>() {
1424        const N: usize = 1;
1425        let start = Location::<F>::new(14);
1426        let end = Location::<F>::new(18);
1427        let (hasher, proof, operations, chunks, root, ops) =
1428            current_range_proof_fixture::<F, N>(18, start..end).await;
1429
1430        let extracted = verify_proof_and_extract_digests::<F, _, Sha256, N>(
1431            &proof,
1432            start,
1433            &operations,
1434            &chunks,
1435            &root,
1436        )
1437        .unwrap();
1438        assert!(!extracted.is_empty());
1439
1440        // The extractor should return the authenticated digest for every proven leaf.
1441        for loc in *start..*end {
1442            let pos = F::location_to_position(Location::<F>::new(loc));
1443            let expected = ops.get_node(pos).unwrap();
1444            assert!(
1445                extracted
1446                    .iter()
1447                    .any(|(actual_pos, actual)| *actual_pos == pos && *actual == expected),
1448                "missing extracted leaf digest at {pos:?}",
1449            );
1450        }
1451
1452        // Root mismatches are reported distinctly from malformed proof inputs.
1453        let wrong_root = hasher.digest(b"wrong current root");
1454        assert!(matches!(
1455            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1456                &proof,
1457                start,
1458                &operations,
1459                &chunks,
1460                &wrong_root,
1461            ),
1462            Err(merkle::Error::RootMismatch)
1463        ));
1464
1465        // Mutating operations or bitmap chunks must invalidate the extracted proof.
1466        let mut wrong_operations = operations.clone();
1467        wrong_operations[0] = hasher.digest(b"wrong operation");
1468        assert!(
1469            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1470                &proof,
1471                start,
1472                &wrong_operations,
1473                &chunks,
1474                &root,
1475            )
1476            .is_err()
1477        );
1478
1479        let mut bad_chunks = chunks;
1480        bad_chunks.last_mut().unwrap()[0] ^= 1;
1481        assert!(
1482            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1483                &proof,
1484                start,
1485                &operations,
1486                &bad_chunks,
1487                &root,
1488            )
1489            .is_err()
1490        );
1491    }
1492
1493    #[test_async]
1494    async fn test_verify_proof_and_extract_digests_handles_no_grafted_chunks_mmb() {
1495        type F = mmb::Family;
1496        const N: usize = 1;
1497        let start = Location::<F>::new(2);
1498        let end = Location::<F>::new(4);
1499        let (_, proof, operations, chunks, root, _ops) =
1500            current_range_proof_fixture::<F, N>(6, start..end).await;
1501
1502        let extracted = verify_proof_and_extract_digests::<F, _, Sha256, N>(
1503            &proof,
1504            start,
1505            &operations,
1506            &chunks,
1507            &root,
1508        )
1509        .unwrap();
1510
1511        assert!(!extracted.is_empty());
1512    }
1513
1514    #[test_async]
1515    async fn test_verify_proof_and_extract_digests_rejects_malformed_inputs_mmb() {
1516        type F = mmb::Family;
1517        const N: usize = 1;
1518        let start = Location::<F>::new(14);
1519        let end = Location::<F>::new(18);
1520        let (_, proof, operations, chunks, root, _ops) =
1521            current_range_proof_fixture::<F, N>(18, start..end).await;
1522
1523        let no_operations = Vec::<sha256::Digest>::new();
1524        assert!(matches!(
1525            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1526                &proof,
1527                start,
1528                &no_operations,
1529                &chunks,
1530                &root,
1531            ),
1532            Err(merkle::Error::InvalidProof)
1533        ));
1534
1535        let no_chunks = Vec::<[u8; N]>::new();
1536        assert!(matches!(
1537            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1538                &proof,
1539                start,
1540                &operations[..1],
1541                &no_chunks,
1542                &root,
1543            ),
1544            Err(merkle::Error::InvalidProof)
1545        ));
1546
1547        assert!(matches!(
1548            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1549                &proof,
1550                F::MAX_LEAVES,
1551                &operations[..1],
1552                &chunks,
1553                &root,
1554            ),
1555            Err(merkle::Error::InvalidProof)
1556        ));
1557
1558        assert!(matches!(
1559            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1560                &proof,
1561                proof.proof.leaves,
1562                &operations[..1],
1563                &chunks,
1564                &root,
1565            ),
1566            Err(merkle::Error::InvalidProof)
1567        ));
1568
1569        assert!(matches!(
1570            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1571                &proof,
1572                start,
1573                &operations,
1574                &chunks[..chunks.len() - 1],
1575                &root,
1576            ),
1577            Err(merkle::Error::InvalidProof)
1578        ));
1579
1580        let mut missing_partial = proof.clone();
1581        missing_partial.partial_chunk_digest = None;
1582        assert!(matches!(
1583            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1584                &missing_partial,
1585                start,
1586                &operations,
1587                &chunks,
1588                &root,
1589            ),
1590            Err(merkle::Error::InvalidProof)
1591        ));
1592
1593        let mut broken_merkle = proof;
1594        assert!(!broken_merkle.proof.digests.is_empty());
1595        broken_merkle.proof.digests.clear();
1596        assert!(matches!(
1597            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1598                &broken_merkle,
1599                start,
1600                &operations,
1601                &chunks,
1602                &root,
1603            ),
1604            Err(merkle::Error::InvalidProof)
1605        ));
1606    }
1607
1608    #[test_async]
1609    async fn test_verify_proof_and_extract_digests_rejects_metadata_mismatches_mmb() {
1610        type F = mmb::Family;
1611        const N: usize = 1;
1612        let start = Location::<F>::new(14);
1613        let end = Location::<F>::new(18);
1614        let (hasher, proof, operations, chunks, root, _ops) =
1615            current_range_proof_fixture::<F, N>(18, start..end).await;
1616
1617        assert!(proof.pending_chunk_digest.is_some());
1618
1619        let mut missing_pending = proof.clone();
1620        missing_pending.pending_chunk_digest = None;
1621        assert!(matches!(
1622            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1623                &missing_pending,
1624                start,
1625                &operations,
1626                &chunks,
1627                &root,
1628            ),
1629            Err(merkle::Error::InvalidProof)
1630        ));
1631
1632        let mut wrong_pending = proof.clone();
1633        wrong_pending.pending_chunk_digest = Some(hasher.digest(b"wrong pending"));
1634        assert!(matches!(
1635            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1636                &wrong_pending,
1637                start,
1638                &operations,
1639                &chunks,
1640                &root,
1641            ),
1642            Err(merkle::Error::InvalidProof)
1643        ));
1644
1645        let aligned_start = Location::<F>::new(8);
1646        let aligned_end = Location::<F>::new(12);
1647        let (hasher, proof, operations, chunks, root, _ops) =
1648            current_range_proof_fixture::<F, N>(16, aligned_start..aligned_end).await;
1649        assert!(proof.partial_chunk_digest.is_none());
1650
1651        // A chunk-aligned proof must not carry partial metadata.
1652        let mut unexpected_partial = proof;
1653        unexpected_partial.partial_chunk_digest = Some(hasher.digest(b"unexpected partial"));
1654        assert!(matches!(
1655            verify_proof_and_extract_digests::<F, _, Sha256, N>(
1656                &unexpected_partial,
1657                aligned_start,
1658                &operations,
1659                &chunks,
1660                &root,
1661            ),
1662            Err(merkle::Error::InvalidProof)
1663        ));
1664    }
1665
1666    #[test_async]
1667    async fn test_verify_proof_and_extract_digests_mmr() {
1668        verify_proof_and_extract_digests_inner::<mmr::Family>().await;
1669    }
1670
1671    #[test_async]
1672    async fn test_verify_proof_and_extract_digests_mmb() {
1673        verify_proof_and_extract_digests_inner::<mmb::Family>().await;
1674    }
1675
1676    /// Active chunks always have a single h=G peak; multi-peak structure can only appear
1677    /// at the pending-chunk index. This test exhaustively scans MMB sizes that have a
1678    /// pending chunk (the only configuration where multi-peak chunks ever existed) and
1679    /// asserts that every graftable chunk has exactly one peak.
1680    #[test_async]
1681    async fn test_graftable_chunks_always_single_peak_at_pending_sizes() {
1682        type F = mmb::Family;
1683        const N: usize = 1;
1684
1685        let hasher = qmdb::hasher::<Sha256>();
1686        let grafting_height = grafting::height::<N>();
1687        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1688
1689        let mut found_any_pending = false;
1690        for leaves in chunk_bits * 3..=128u64 {
1691            let leaves_loc = mmb::Location::new(leaves);
1692            let leaves_count = *leaves_loc;
1693            let complete = leaves_count / chunk_bits;
1694            let graftable =
1695                grafting::graftable_chunks::<F>(leaves_count, grafting_height).min(complete);
1696            if graftable == complete {
1697                continue; // no pending chunk at this size
1698            }
1699            found_any_pending = true;
1700
1701            // Pending chunks (index >= graftable) are allowed multi-peak; their digests
1702            // are hashed into the canonical root separately.
1703            let size = F::location_to_position(leaves_loc);
1704            for chunk_idx in 0..graftable {
1705                let count = F::chunk_peaks(size, chunk_idx, grafting_height).count();
1706                assert_eq!(
1707                    count, 1,
1708                    "graftable chunk {chunk_idx} has {count} peaks (leaves={leaves_count}, graftable={graftable}, complete={complete})"
1709                );
1710            }
1711        }
1712        assert!(
1713            found_any_pending,
1714            "expected at least one MMB size in [{}, 128] with a pending chunk",
1715            chunk_bits * 3
1716        );
1717
1718        // End-to-end: build a proof for an op in a chunk-aligned MMB whose chunk 1 is
1719        // multi-peak, and confirm the proof has only the standard digest material.
1720        let leaf_count = (chunk_bits * 2..=256u64)
1721            .filter(|leaves| leaves % chunk_bits == 0)
1722            .find(|&leaves| {
1723                let size = F::location_to_position(mmb::Location::new(leaves));
1724                F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
1725            })
1726            .expect("expected a chunk-aligned MMB size whose chunk 1 is multi-peak");
1727        let loc = mmb::Location::new(chunk_bits + 1);
1728
1729        let mut status = BitMap::<N>::new();
1730        for _ in 0..leaf_count {
1731            status.push(true);
1732        }
1733        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1734        let ops_root = ops.root(&hasher, 0).unwrap();
1735
1736        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1737            *Location::<F>::try_from(ops.size()).unwrap(),
1738            grafting_height,
1739        )
1740        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1741            as usize;
1742        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1743            .map(|chunk_idx| {
1744                (
1745                    chunk_idx,
1746                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1747                )
1748            })
1749            .collect();
1750        let mut leaf_digests =
1751            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1752                .await
1753                .unwrap();
1754        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1755
1756        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1757        let mut grafted = Mem::<F, sha256::Digest>::new();
1758        let merkleized = {
1759            let mut batch = grafted.new_batch();
1760            for (_, digest) in leaf_digests {
1761                batch = batch.add_leaf_digest(digest);
1762            }
1763            batch.merkleize(&grafted, &grafted_hasher)
1764        };
1765        grafted.apply_batch(&merkleized).unwrap();
1766
1767        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1768        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1769        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1770            &status,
1771            &storage,
1772            ops_leaves_for_root,
1773            Location::new(0),
1774        )
1775        .await
1776        .unwrap()
1777        .root::<Sha256>(&ops_root);
1778        let proof = RangeProof::new::<Sha256, _, N>(
1779            &status,
1780            &storage,
1781            Location::new(0),
1782            loc..loc + 1,
1783            ops_root,
1784        )
1785        .await
1786        .unwrap();
1787
1788        let element = hasher.digest(&(*loc).to_be_bytes());
1789        let chunk_idx = (*loc / chunk_bits) as usize;
1790        assert!(proof.verify::<Sha256, _, N>(
1791            loc,
1792            &[element],
1793            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1794                &status, chunk_idx
1795            )],
1796            &root,
1797        ));
1798
1799        let mut tampered = proof.clone();
1800        tampered.proof.inactive_peaks = 1;
1801        assert!(!tampered.verify::<Sha256, _, N>(
1802            loc,
1803            &[element],
1804            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1805                &status, chunk_idx
1806            )],
1807            &root,
1808        ));
1809
1810        let mut tampered = proof.clone();
1811        tampered.proof.inactive_peaks = usize::MAX;
1812        assert!(!tampered.verify::<Sha256, _, N>(
1813            loc,
1814            &[element],
1815            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1816                &status, chunk_idx
1817            )],
1818            &root,
1819        ));
1820
1821        let mut tampered = proof;
1822        assert!(!tampered.proof.digests.is_empty());
1823        tampered.proof.digests[0] = hasher.digest(b"fake generic sibling");
1824        assert!(!tampered.verify::<Sha256, _, N>(
1825            loc,
1826            &[element],
1827            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1828                &status, chunk_idx
1829            )],
1830            &root,
1831        ));
1832    }
1833
1834    /// Pending and partial chunks coexist when the bitmap has both (1) a chunk whose bits
1835    /// are complete but whose h=G ancestor isn't yet born, AND (2) an in-progress trailing
1836    /// chunk. At G=3 (N=1) chunk 0 is pending for ops_leaves in [8, 11), and any ops_leaves
1837    /// strictly in (8, 11) also has a partial trailing chunk. This test builds those states
1838    /// and round-trips a `RangeProof` that spans both regions.
1839    #[test_async]
1840    async fn test_pending_and_partial_coexist_at_g_3() {
1841        type F = mmb::Family;
1842        const N: usize = 1; // G = 3, chunk_bits = 8
1843
1844        let hasher = qmdb::hasher::<Sha256>();
1845        let grafting_height = grafting::height::<N>();
1846        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1847        assert_eq!(grafting_height, 3);
1848        assert_eq!(chunk_bits, 8);
1849
1850        // For G=3, chunk 0 is pending while ops_leaves is in [8, 11). Pending+partial
1851        // coexistence holds for k in [1, 2] (k=3 transitions chunk 0 to graftable).
1852        for k in 1u64..=2 {
1853            let leaf_count = chunk_bits + k;
1854            let mut status = BitMap::<N>::new();
1855            for _ in 0..leaf_count {
1856                status.push(true);
1857            }
1858            let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1859            let ops_root = ops.root(&hasher, 0).unwrap();
1860
1861            let complete = <BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64;
1862            let graftable =
1863                grafting::graftable_chunks::<F>(leaf_count, grafting_height).min(complete);
1864            let next_bit = leaf_count % chunk_bits;
1865            assert_eq!(complete, 1);
1866            assert_eq!(graftable, 0);
1867            assert!(next_bit > 0, "expected partial chunk for k={k}");
1868
1869            // Build a grafted tree from the (zero) graftable chunks and a Storage covering
1870            // the post-state.
1871            let chunk_inputs: Vec<_> = (0..graftable as usize)
1872                .map(|chunk_idx| {
1873                    (
1874                        chunk_idx,
1875                        <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1876                    )
1877                })
1878                .collect();
1879            let leaf_digests =
1880                compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
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 =
1897                grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1898
1899            let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1900            let witness = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
1901                &status,
1902                &storage,
1903                ops_leaves_for_root,
1904                Location::new(0),
1905            )
1906            .await
1907            .unwrap();
1908            let canonical_root = witness.root::<Sha256>(&ops_root);
1909            assert!(
1910                witness.pending_chunk_digest.is_some(),
1911                "expected pending chunk at k={k}"
1912            );
1913            assert!(
1914                witness.partial_chunk.is_some(),
1915                "expected partial chunk at k={k}"
1916            );
1917
1918            // Range proof spanning the pending chunk into the partial bits
1919            let start = mmb::Location::new(0);
1920            let end = mmb::Location::new(leaf_count);
1921            let proof = RangeProof::new::<Sha256, _, N>(
1922                &status,
1923                &storage,
1924                Location::new(0),
1925                start..end,
1926                ops_root,
1927            )
1928            .await
1929            .unwrap();
1930            assert!(
1931                proof.pending_chunk_digest.is_some(),
1932                "expected RangeProof pending_chunk_digest at k={k}"
1933            );
1934            assert!(
1935                proof.partial_chunk_digest.is_some(),
1936                "expected RangeProof partial_chunk_digest at k={k}"
1937            );
1938
1939            let elements: Vec<sha256::Digest> = (0..leaf_count)
1940                .map(|i| hasher.digest(&i.to_be_bytes()))
1941                .collect();
1942            // Range covers chunks 0..=1: chunk 0 is pending, chunk 1 is partial. Provide both.
1943            let chunks: Vec<[u8; N]> = (0..=1)
1944                .map(|i| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, i))
1945                .collect();
1946            assert!(
1947                proof.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1948                "RangeProof verify failed at k={k}"
1949            );
1950
1951            let pending_loc = mmb::Location::new(3);
1952            let pending_proof = RangeProof::new::<Sha256, _, N>(
1953                &status,
1954                &storage,
1955                Location::new(0),
1956                pending_loc..pending_loc + 1,
1957                ops_root,
1958            )
1959            .await
1960            .unwrap();
1961            assert!(
1962                pending_proof.pending_chunk_digest.is_some(),
1963                "expected single-element proof to carry pending chunk digest at k={k}"
1964            );
1965            let pending_element = hasher.digest(&(*pending_loc).to_be_bytes());
1966            assert!(
1967                pending_proof.verify::<Sha256, _, N>(
1968                    pending_loc,
1969                    &[pending_element],
1970                    &[chunks[0]],
1971                    &canonical_root,
1972                ),
1973                "single-element proof inside pending chunk failed at k={k}"
1974            );
1975
1976            // Tamper with the pending chunk digest or its supplied bytes.
1977            let mut tampered = proof.clone();
1978            tampered.pending_chunk_digest = Some(hasher.digest(b"fake pending"));
1979            assert!(
1980                !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1981                "tampered pending digest accepted at k={k}"
1982            );
1983
1984            let mut tampered = proof.clone();
1985            tampered.pending_chunk_digest = None;
1986            assert!(
1987                !tampered.verify::<Sha256, _, N>(start, &elements, &chunks, &canonical_root),
1988                "missing pending digest accepted at k={k}"
1989            );
1990
1991            let mut bad_chunks = chunks.clone();
1992            bad_chunks[0][0] ^= 1;
1993            assert!(
1994                !proof.verify::<Sha256, _, N>(start, &elements, &bad_chunks, &canonical_root),
1995                "tampered pending chunk bytes accepted at k={k}"
1996            );
1997        }
1998    }
1999
2000    /// Appending one op at the exact birth size of a pending chunk's h=G ancestor causes
2001    /// the chunk to transition from pending to graftable. The canonical root must change, and
2002    /// a freshly-rebuilt grafted tree from the post-state must contain the now-graftable
2003    /// chunk's leaf.
2004    #[test_async]
2005    async fn test_pending_to_graftable_transition_at_birth_size() {
2006        type F = mmb::Family;
2007        const N: usize = 1; // G = 3, chunk_bits = 8
2008
2009        let hasher = qmdb::hasher::<Sha256>();
2010        let grafting_height = grafting::height::<N>();
2011        assert_eq!(grafting_height, 3);
2012
2013        // chunk 0's h=G ancestor: birth = 3*2^(G-1) - 1 = 11 for G=3.
2014        let birth = (3u64 << (grafting_height - 1)) - 1;
2015        let pre_state_leaves = birth - 1; // = 10: chunk 0 still pending
2016        let post_state_leaves = birth; // = 11: chunk 0 just graftable
2017
2018        assert_eq!(pre_state_leaves, 10);
2019        assert_eq!(post_state_leaves, 11);
2020
2021        let graftable_pre = grafting::graftable_chunks::<F>(pre_state_leaves, grafting_height);
2022        let graftable_post = grafting::graftable_chunks::<F>(post_state_leaves, grafting_height);
2023        assert_eq!(graftable_pre, 0);
2024        assert_eq!(graftable_post, 1);
2025
2026        // Pre-state canonical root: chunk 0 is pending; grafted tree empty.
2027        let mut status_pre = BitMap::<N>::new();
2028        for _ in 0..pre_state_leaves {
2029            status_pre.push(true);
2030        }
2031        let ops_pre = build_test_mem(&hasher, mmb::mem::Mmb::new(), pre_state_leaves);
2032        let ops_root_pre = ops_pre.root(&hasher, 0).unwrap();
2033        let grafted_pre = Mem::<F, sha256::Digest>::new();
2034        let storage_pre =
2035            grafting::Storage::<F, Sha256, _, _>::new(&grafted_pre, grafting_height, &ops_pre);
2036        let canonical_pre = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2037            &status_pre,
2038            &storage_pre,
2039            Location::<F>::new(pre_state_leaves),
2040            Location::new(0),
2041        )
2042        .await
2043        .unwrap()
2044        .root::<Sha256>(&ops_root_pre);
2045
2046        // Post-state canonical root.
2047        let mut status_post = BitMap::<N>::new();
2048        for _ in 0..post_state_leaves {
2049            status_post.push(true);
2050        }
2051        let ops_post = build_test_mem(&hasher, mmb::mem::Mmb::new(), post_state_leaves);
2052        let ops_root_post = ops_post.root(&hasher, 0).unwrap();
2053        // After transition chunk 0 has a single h=G ancestor; build the grafted tree.
2054        let leaf_digests = compute_grafted_leaves::<F, Sha256, Sequential, N>(
2055            &ops_post,
2056            core::iter::once((
2057                0usize,
2058                <BitMap<N> as BitmapReadable<N>>::get_chunk(&status_post, 0),
2059            )),
2060            &Sequential,
2061        )
2062        .await
2063        .unwrap();
2064        assert_eq!(
2065            leaf_digests.len(),
2066            1,
2067            "post-state must have 1 graftable chunk"
2068        );
2069        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2070        let mut grafted_post = Mem::<F, sha256::Digest>::new();
2071        let merkleized = grafted_post
2072            .new_batch()
2073            .add_leaf_digest(leaf_digests[0].1)
2074            .merkleize(&grafted_post, &grafted_hasher);
2075        grafted_post.apply_batch(&merkleized).unwrap();
2076        let storage_post =
2077            grafting::Storage::<F, Sha256, _, _>::new(&grafted_post, grafting_height, &ops_post);
2078
2079        let canonical_post = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2080            &status_post,
2081            &storage_post,
2082            Location::<F>::new(post_state_leaves),
2083            Location::new(0),
2084        )
2085        .await
2086        .unwrap()
2087        .root::<Sha256>(&ops_root_post);
2088
2089        assert_ne!(
2090            canonical_pre, canonical_post,
2091            "canonical root must change when chunk 0 transitions from pending to graftable"
2092        );
2093    }
2094
2095    #[test_async]
2096    async fn test_range_proof_allows_ops_and_grafted_inactive_counts_to_differ() {
2097        type F = mmb::Family;
2098        const N: usize = 1;
2099
2100        let hasher = qmdb::hasher::<Sha256>();
2101        let grafting_height = grafting::height::<N>();
2102        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
2103        let leaf_count = chunk_bits;
2104        let leaves = mmb::Location::new(leaf_count);
2105        let inactivity_floor = mmb::Location::new(chunk_bits - 2);
2106
2107        let ops_inactive_peaks = F::inactive_peaks(leaves, inactivity_floor);
2108        let aligned_inactive =
2109            grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)
2110                .unwrap();
2111        assert_ne!(ops_inactive_peaks, aligned_inactive);
2112
2113        let mut status = BitMap::<N>::new();
2114        for _ in 0..leaf_count {
2115            status.push(true);
2116        }
2117        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
2118
2119        // The ops root is the inner QMDB log root and commits the ops-tree inactive peak count.
2120        // The grafted bitmap root commits the chunk-aligned count, since bitmap chunks are
2121        // the atomic inactive-prefix boundary for the current root.
2122        let ops_root = ops.root(&hasher, ops_inactive_peaks).unwrap();
2123
2124        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
2125            *Location::<F>::try_from(ops.size()).unwrap(),
2126            grafting_height,
2127        )
2128        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
2129            as usize;
2130        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
2131            .map(|chunk_idx| {
2132                (
2133                    chunk_idx,
2134                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
2135                )
2136            })
2137            .collect();
2138        let mut leaf_digests =
2139            compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
2140                .await
2141                .unwrap();
2142        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
2143
2144        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2145        let mut grafted = Mem::<F, sha256::Digest>::new();
2146        let merkleized = {
2147            let mut batch = grafted.new_batch();
2148            for (_, digest) in leaf_digests {
2149                batch = batch.add_leaf_digest(digest);
2150            }
2151            batch.merkleize(&grafted, &grafted_hasher)
2152        };
2153        grafted.apply_batch(&merkleized).unwrap();
2154
2155        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
2156        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
2157        let root = db::compute_ops_root_witness::<F, Sha256, _, _, N>(
2158            &status,
2159            &storage,
2160            ops_leaves_for_root,
2161            inactivity_floor,
2162        )
2163        .await
2164        .unwrap()
2165        .root::<Sha256>(&ops_root);
2166
2167        let loc = mmb::Location::new(chunk_bits - 1);
2168        let proof = RangeProof::new::<Sha256, _, N>(
2169            &status,
2170            &storage,
2171            inactivity_floor,
2172            loc..loc + 1,
2173            ops_root,
2174        )
2175        .await
2176        .unwrap();
2177        assert_eq!(proof.proof.inactive_peaks, aligned_inactive);
2178
2179        let element = hasher.digest(&(*loc).to_be_bytes());
2180        let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
2181        assert!(proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root));
2182    }
2183
2184    fn invalid_chunk_sizes() -> impl Iterator<Item = usize> {
2185        [0, 3, usize::MAX, 1 << (usize::BITS - 1)]
2186            .into_iter()
2187            .chain(usize::try_from(1u64 << 60).ok())
2188    }
2189
2190    async fn check_dynamic_proofs<F: Graftable, const N: usize>() {
2191        let chunk_bits = (N * 8) as u64;
2192        let height = chunk_bits.trailing_zeros() as u64;
2193        for leaves in [
2194            chunk_bits - 1,
2195            chunk_bits,
2196            chunk_bits + 1,
2197            chunk_bits + height,
2198            chunk_bits * 2,
2199            chunk_bits * 2 + 2,
2200        ] {
2201            let start = Location::<F>::new(chunk_bits - 2);
2202            let (_, proof, operations, chunks, root, _) =
2203                current_range_proof_fixture::<F, N>(leaves, start..Location::new(leaves)).await;
2204            assert!(proof.verify::<Sha256, _, N>(start, &operations, &chunks, &root));
2205            let slices = chunks.iter().map(<[u8; N]>::as_slice).collect::<Vec<_>>();
2206            assert!(proof.verify_with_chunk_size::<Sha256, _>(
2207                start,
2208                &operations,
2209                &slices,
2210                N,
2211                &root
2212            ));
2213            assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2214                start,
2215                &operations,
2216                &slices,
2217                N,
2218                &Sha256::hash(&[b"wrong root"]),
2219            ));
2220
2221            for loc in [start, Location::new(leaves - 1)] {
2222                let (_, range_proof, operations, chunks, root, _) =
2223                    current_range_proof_fixture::<F, N>(leaves, loc..loc + 1).await;
2224                let native = constant::OperationProof::<F, sha256::Digest, N> {
2225                    loc,
2226                    chunk: chunks[0],
2227                    range_proof,
2228                };
2229                assert!(native.verify::<Sha256, _>(operations[0], &root));
2230                let encoded = native.encode();
2231                let max_digests = native.range_proof.proof.digests.len();
2232                let dynamic = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2233                    encoded.clone(),
2234                    &(N, max_digests),
2235                )
2236                .unwrap();
2237                assert_eq!(dynamic.encode(), encoded);
2238                assert_eq!(dynamic.encode_size(), encoded.len());
2239                assert_eq!(dynamic.loc, loc);
2240                assert_eq!(dynamic.chunk.as_ref(), native.chunk.as_slice());
2241                assert!(dynamic.verify::<Sha256, _>(operations[0], &root));
2242                assert!(!dynamic.verify::<Sha256, _>(Sha256::hash(&[b"wrong operation"]), &root,));
2243                assert!(
2244                    !dynamic.verify::<Sha256, _>(operations[0], &Sha256::hash(&[b"wrong root"]),)
2245                );
2246
2247                let mut inactive = dynamic;
2248                let mut chunk = inactive.chunk.to_vec();
2249                let bit = (*loc % chunk_bits) as usize;
2250                chunk[bit / 8] &= !(1 << (bit % 8));
2251                inactive.chunk = Bytes::from(chunk);
2252                assert!(!inactive.verify::<Sha256, _>(operations[0], &root));
2253            }
2254        }
2255    }
2256
2257    #[test_async]
2258    async fn dynamic_proofs_match_native_mmr() {
2259        check_dynamic_proofs::<mmr::Family, 1>().await;
2260        check_dynamic_proofs::<mmr::Family, 32>().await;
2261        check_dynamic_proofs::<mmr::Family, 64>().await;
2262    }
2263
2264    #[test_async]
2265    async fn dynamic_proofs_match_native_mmb() {
2266        check_dynamic_proofs::<mmb::Family, 1>().await;
2267        check_dynamic_proofs::<mmb::Family, 32>().await;
2268        check_dynamic_proofs::<mmb::Family, 64>().await;
2269    }
2270
2271    async fn check_dynamic_range_rejections<F: Graftable>() {
2272        const N: usize = 1;
2273        let start = Location::<F>::new(6);
2274        let (_, proof, operations, chunks, root, _) =
2275            current_range_proof_fixture::<F, N>(18, start..Location::new(18)).await;
2276        let chunks = chunks
2277            .iter()
2278            .map(|chunk| chunk.to_vec())
2279            .collect::<Vec<_>>();
2280        for chunk_size in invalid_chunk_sizes() {
2281            assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2282                start,
2283                &operations,
2284                &chunks,
2285                chunk_size,
2286                &root,
2287            ));
2288        }
2289        assert!(!proof.verify_with_chunk_size::<Sha256, _>(start, &operations, &chunks, 2, &root));
2290        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2291            start,
2292            &operations[..0],
2293            &chunks,
2294            N,
2295            &root
2296        ));
2297        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2298            start,
2299            &operations,
2300            &chunks[..0],
2301            N,
2302            &root
2303        ));
2304        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2305            start,
2306            &operations,
2307            &chunks[..chunks.len() - 1],
2308            N,
2309            &root,
2310        ));
2311        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2312            proof.proof.leaves,
2313            &operations,
2314            &chunks,
2315            N,
2316            &root,
2317        ));
2318        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2319            F::MAX_LEAVES,
2320            &operations,
2321            &chunks,
2322            N,
2323            &root,
2324        ));
2325
2326        let mut extra = chunks.clone();
2327        extra.push(chunks[0].clone());
2328        assert!(!proof.verify_with_chunk_size::<Sha256, _>(start, &operations, &extra, N, &root));
2329        for chunk_len in [0, 2, 3] {
2330            let mut malformed = chunks.clone();
2331            malformed[1].resize(chunk_len, 0);
2332            assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2333                start,
2334                &operations,
2335                &malformed,
2336                N,
2337                &root,
2338            ));
2339        }
2340
2341        let mut tampered = chunks.clone();
2342        tampered.last_mut().unwrap()[0] ^= 1;
2343        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2344            start,
2345            &operations,
2346            &tampered,
2347            N,
2348            &root
2349        ));
2350        let mut missing_partial = proof.clone();
2351        assert!(missing_partial.partial_chunk_digest.take().is_some());
2352        assert!(!missing_partial.verify_with_chunk_size::<Sha256, _>(
2353            start,
2354            &operations,
2355            &chunks,
2356            N,
2357            &root,
2358        ));
2359        let mut wrong_operations = operations;
2360        wrong_operations[0] = Sha256::hash(&[b"wrong operation"]);
2361        assert!(!proof.verify_with_chunk_size::<Sha256, _>(
2362            start,
2363            &wrong_operations,
2364            &chunks,
2365            N,
2366            &root,
2367        ));
2368    }
2369
2370    #[test_async]
2371    async fn dynamic_range_rejects_malformed_inputs() {
2372        check_dynamic_range_rejections::<mmr::Family>().await;
2373        check_dynamic_range_rejections::<mmb::Family>().await;
2374    }
2375
2376    async fn check_dynamic_operation_codec_rejections<F: Graftable>() {
2377        const N: usize = 32;
2378        let loc = Location::<F>::new(14);
2379        let (_, range_proof, operations, chunks, root, _) =
2380            current_range_proof_fixture::<F, N>(18, loc..loc + 1).await;
2381        let native = constant::OperationProof::<F, sha256::Digest, N> {
2382            loc,
2383            chunk: chunks[0],
2384            range_proof,
2385        };
2386        let encoded = native.encode();
2387        let max_digests = native.range_proof.proof.digests.len();
2388        assert!(max_digests > 0);
2389        assert!(
2390            dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2391                encoded.clone(),
2392                &(N, max_digests - 1),
2393            )
2394            .is_err()
2395        );
2396        for end in 0..encoded.len() {
2397            assert!(
2398                dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2399                    encoded.slice(..end),
2400                    &(N, max_digests),
2401                )
2402                .is_err(),
2403                "truncated proof decoded at {end}"
2404            );
2405        }
2406        let mut trailing = encoded.to_vec();
2407        trailing.push(0);
2408        assert!(
2409            dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(trailing, &(N, max_digests),)
2410                .is_err()
2411        );
2412        for chunk_size in invalid_chunk_sizes() {
2413            assert!(matches!(
2414                dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2415                    encoded.clone(),
2416                    &(chunk_size, max_digests),
2417                ),
2418                Err(commonware_codec::Error::Invalid(_, _)),
2419            ));
2420        }
2421        for chunk_size in [1, N / 2, N * 2] {
2422            if let Ok(dynamic) = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2423                encoded.clone(),
2424                &(chunk_size, max_digests),
2425            ) {
2426                assert!(!dynamic.verify::<Sha256, _>(operations[0], &root));
2427            }
2428        }
2429        let dynamic =
2430            dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(encoded, &(N, max_digests))
2431                .unwrap();
2432        for chunk in [Bytes::new(), Bytes::from_static(&[0; 3])] {
2433            let mut malformed = dynamic.clone();
2434            malformed.chunk = chunk;
2435            assert!(!malformed.verify::<Sha256, _>(operations[0], &root));
2436        }
2437    }
2438
2439    #[test_async]
2440    async fn dynamic_operation_codec_rejects_malformed_inputs() {
2441        check_dynamic_operation_codec_rejections::<mmr::Family>().await;
2442        check_dynamic_operation_codec_rejections::<mmb::Family>().await;
2443    }
2444
2445    struct PreloadedBitmap<'a> {
2446        bitmap: &'a BitMap<1>,
2447        chunks: BTreeMap<usize, [u8; 1]>,
2448        reads: Mutex<BTreeSet<usize>>,
2449    }
2450
2451    impl BitmapReadable<1> for PreloadedBitmap<'_> {
2452        fn complete_chunks(&self) -> usize {
2453            self.bitmap.complete_chunks()
2454        }
2455
2456        fn get_chunk(&self, chunk: usize) -> [u8; 1] {
2457            self.reads.lock().insert(chunk);
2458            self.chunks[&chunk]
2459        }
2460
2461        fn last_chunk(&self) -> ([u8; 1], u64) {
2462            let (_, bits) = self.bitmap.last_chunk();
2463            let chunk = BitMap::<1>::to_chunk_index(self.len() - 1);
2464            (self.get_chunk(chunk), bits)
2465        }
2466
2467        fn pruned_chunks(&self) -> usize {
2468            self.bitmap.pruned_chunks()
2469        }
2470
2471        fn len(&self) -> u64 {
2472            self.bitmap.len()
2473        }
2474    }
2475
2476    async fn fixture<F: Graftable>(
2477        leaves: u64,
2478        pruned: u64,
2479    ) -> (BitMap<1>, Mem<F, sha256::Digest>, Mem<F, sha256::Digest>) {
2480        let hasher = qmdb::hasher::<Sha256>();
2481        let height = grafting::height::<1>();
2482        let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaves);
2483        let mut bitmap = BitMap::<1>::new();
2484        for loc in 0..leaves {
2485            bitmap.push(loc >= pruned * 8);
2486        }
2487        let chunks = (0..grafting::graftable_chunks::<F>(leaves, height))
2488            .map(|chunk| (chunk as usize, *bitmap.get_chunk(chunk as usize)));
2489        let mut digests =
2490            compute_grafted_leaves::<F, Sha256, Sequential, 1>(&ops, chunks, &Sequential)
2491                .await
2492                .unwrap();
2493        digests.sort_unstable_by_key(|(chunk, _)| *chunk);
2494        let mut grafted = Mem::<F, sha256::Digest>::new();
2495        if !digests.is_empty() {
2496            let mut batch = grafted.new_batch();
2497            for (_, digest) in digests {
2498                batch = batch.add_leaf_digest(digest);
2499            }
2500            let hasher = grafting::GraftedHasher::<F, _>::new(hasher, height);
2501            let batch = batch.merkleize(&grafted, &hasher);
2502            grafted.apply_batch(&batch).unwrap();
2503        }
2504        bitmap.prune_to_bit(pruned * 8);
2505        (bitmap, ops, grafted)
2506    }
2507
2508    async fn check_constructor_reads<F: Graftable + PartialEq>() {
2509        for leaves in [1, 7, 8, 9, 10, 11, 16, 17, 18, 24, 25] {
2510            let graftable = grafting::graftable_chunks::<F>(leaves, grafting::height::<1>());
2511            for pruned in 0..=graftable {
2512                let floor = Location::<F>::new(pruned * 8);
2513                if *floor == leaves {
2514                    continue;
2515                }
2516                let (bitmap, ops, grafted) = fixture::<F>(leaves, pruned).await;
2517                let hasher = qmdb::hasher::<Sha256>();
2518                let ops_root = ops.root(&hasher, 0).unwrap();
2519                let storage = grafting::Storage::<F, Sha256, _, _>::new(
2520                    &grafted,
2521                    grafting::height::<1>(),
2522                    &ops,
2523                );
2524                let root = db::compute_ops_root_witness::<F, Sha256, _, _, 1>(
2525                    &bitmap,
2526                    &storage,
2527                    Location::new(leaves),
2528                    floor,
2529                )
2530                .await
2531                .unwrap()
2532                .root::<Sha256>(&ops_root);
2533                for location in core::iter::once(None)
2534                    .chain((*floor..leaves).map(|loc| Some(Location::new(loc))))
2535                {
2536                    let required = required_chunks::<F, 1>(
2537                        Location::new(leaves),
2538                        bitmap.len(),
2539                        pruned,
2540                        location,
2541                    )
2542                    .unwrap()
2543                    .collect::<Vec<_>>();
2544                    assert!(required.windows(2).all(|pair| pair[0] < pair[1]));
2545                    let ops_leaves = Location::new(leaves);
2546                    let planned = location.map_or_else(
2547                        || {
2548                            merkle::RangePlan::new(ops_leaves, floor..ops_leaves)
2549                                .unwrap()
2550                                .positions()
2551                        },
2552                        |loc| {
2553                            merkle::ElementPlan::new(ops_leaves, loc)
2554                                .unwrap()
2555                                .positions()
2556                        },
2557                    );
2558                    let fetched: BTreeMap<_, _> = planned
2559                        .iter()
2560                        .copied()
2561                        .zip(storage.get_nodes(&planned).await.unwrap())
2562                        .collect();
2563                    let get_node = |pos: Position<F>| fetched.get(&pos).copied();
2564                    let preloaded = PreloadedBitmap {
2565                        bitmap: &bitmap,
2566                        chunks: required
2567                            .iter()
2568                            .map(|&chunk| (chunk as usize, *bitmap.get_chunk(chunk as usize)))
2569                            .collect(),
2570                        reads: Mutex::new(BTreeSet::new()),
2571                    };
2572                    if let Some(loc) = location {
2573                        let proof =
2574                            constant::OperationProof::<F, sha256::Digest, 1>::new::<Sha256, _>(
2575                                &preloaded, &storage, floor, loc, ops_root,
2576                            )
2577                            .await
2578                            .unwrap();
2579                        let built =
2580                            constant::OperationProof::<F, sha256::Digest, 1>::build::<Sha256>(
2581                                &preloaded,
2582                                merkle::ElementPlan::new(ops_leaves, loc).unwrap(),
2583                                get_node,
2584                                floor,
2585                                ops_root,
2586                            )
2587                            .unwrap();
2588                        assert_eq!(built, proof);
2589                        assert!(
2590                            proof.verify::<Sha256, _>(hasher.digest(&(*loc).to_be_bytes()), &root,)
2591                        );
2592                        let proof = dynamic::OperationProof::<F, sha256::Digest>::decode_cfg(
2593                            proof.encode(),
2594                            &(1, 64),
2595                        )
2596                        .unwrap();
2597                        assert!(
2598                            proof.verify::<Sha256, _>(hasher.digest(&(*loc).to_be_bytes()), &root)
2599                        );
2600                    } else {
2601                        let proof = RangeProof::new::<Sha256, _, 1>(
2602                            &preloaded,
2603                            &storage,
2604                            floor,
2605                            floor..Location::new(leaves),
2606                            ops_root,
2607                        )
2608                        .await
2609                        .unwrap();
2610                        let built = RangeProof::build::<Sha256, 1>(
2611                            &preloaded,
2612                            merkle::RangePlan::new(ops_leaves, floor..ops_leaves).unwrap(),
2613                            get_node,
2614                            floor,
2615                            ops_root,
2616                        )
2617                        .unwrap();
2618                        assert_eq!(built, proof);
2619                        let elements = (*floor..leaves)
2620                            .map(|loc| hasher.digest(&loc.to_be_bytes()))
2621                            .collect::<Vec<_>>();
2622                        let chunks = (pruned..leaves.div_ceil(8))
2623                            .map(|chunk| *bitmap.get_chunk(chunk as usize))
2624                            .collect::<Vec<_>>();
2625                        assert!(proof.verify::<Sha256, _, 1>(floor, &elements, &chunks, &root));
2626                        assert!(proof.verify_with_chunk_size::<Sha256, _>(
2627                            floor, &elements, &chunks, 1, &root,
2628                        ));
2629                    }
2630                    let read_chunks = preloaded
2631                        .reads
2632                        .into_inner()
2633                        .into_iter()
2634                        .map(Widen::widen)
2635                        .collect::<Vec<_>>();
2636                    assert_eq!(
2637                        read_chunks, required,
2638                        "leaves={leaves}, pruned={pruned}, location={location:?}",
2639                    );
2640                }
2641            }
2642        }
2643    }
2644
2645    #[test_async]
2646    async fn required_chunks_match_constructor_reads() {
2647        check_constructor_reads::<mmr::Family>().await;
2648        check_constructor_reads::<mmb::Family>().await;
2649    }
2650
2651    #[test_async]
2652    async fn operation_proof_build_rejects_pruned_locations() {
2653        type F = mmr::Family;
2654        let (bitmap, ops, _) = fixture::<F>(16, 1).await;
2655        let ops_root = ops.root(&qmdb::hasher::<Sha256>(), 0).unwrap();
2656        let plan = merkle::ElementPlan::new(Location::new(16), Location::new(3)).unwrap();
2657        assert!(matches!(
2658            constant::OperationProof::<F, sha256::Digest, 1>::build::<Sha256>(
2659                &bitmap,
2660                plan,
2661                |_| None,
2662                Location::new(8),
2663                ops_root,
2664            ),
2665            Err(Error::OperationPruned(loc)) if *loc == 3
2666        ));
2667    }
2668
2669    #[test]
2670    fn required_chunks_boundary_sets() {
2671        for (leaves, mmr, mmb) in [
2672            (0, vec![], vec![]),
2673            (1, vec![0], vec![0]),
2674            (7, vec![0], vec![0]),
2675            (8, vec![], vec![0]),
2676            (9, vec![1], vec![0, 1]),
2677            (10, vec![1], vec![0, 1]),
2678            (11, vec![1], vec![1]),
2679            (16, vec![], vec![1]),
2680            (17, vec![2], vec![1, 2]),
2681            (18, vec![2], vec![1, 2]),
2682            (24, vec![], vec![2]),
2683            (25, vec![3], vec![2, 3]),
2684        ] {
2685            assert_eq!(
2686                required_chunks::<mmr::Family, 1>(Location::new(leaves), leaves, 0, None)
2687                    .unwrap()
2688                    .collect::<Vec<_>>(),
2689                mmr
2690            );
2691            assert_eq!(
2692                required_chunks::<mmb::Family, 1>(Location::new(leaves), leaves, 0, None)
2693                    .unwrap()
2694                    .collect::<Vec<_>>(),
2695                mmb
2696            );
2697        }
2698        assert!(
2699            required_chunks::<mmr::Family, 1>(Location::new(16), 16, 2, None)
2700                .unwrap()
2701                .next()
2702                .is_none()
2703        );
2704    }
2705
2706    fn check_invalid_metadata<F: Graftable>() {
2707        for (ops_leaves, bitmap_len, loc) in [(0, 0, 0), (16, 16, 16), (8, 9, 8), (9, 8, 8)] {
2708            assert!(matches!(
2709                required_chunks::<F, 1>(Location::new(ops_leaves), bitmap_len, 0, Some(Location::new(loc))),
2710                Err(Error::Merkle(merkle::Error::RangeOutOfBounds(found))) if *found == loc
2711            ));
2712        }
2713        assert!(matches!(
2714            required_chunks::<F, 1>(Location::new(16), 16, 1, Some(Location::new(7))),
2715            Err(Error::OperationPruned(loc)) if *loc == 7
2716        ));
2717        assert!(matches!(
2718            required_chunks::<F, 1>(Location::new(0), 24, 0, None),
2719            Err(Error::DataCorrupted("multiple pending bitmap chunks"))
2720        ));
2721        assert!(matches!(
2722            required_chunks::<F, 1>(Location::new(8), 8, 2, None),
2723            Err(Error::DataCorrupted(
2724                "pruned chunks exceed graftable chunks"
2725            ))
2726        ));
2727    }
2728
2729    #[test]
2730    fn required_chunks_reject_invalid_metadata() {
2731        check_invalid_metadata::<mmr::Family>();
2732        check_invalid_metadata::<mmb::Family>();
2733        assert!(matches!(
2734            required_chunks::<mmb::Family, 1>(Location::new(8), 8, 1, None),
2735            Err(Error::DataCorrupted(
2736                "pruned chunks exceed graftable chunks"
2737            ))
2738        ));
2739    }
2740
2741    #[cfg(feature = "arbitrary")]
2742    mod conformance {
2743        use super::super::{OpsRootWitness, RangeProof, constant, dynamic};
2744        use crate::merkle::{mmb, mmr};
2745        use commonware_codec::conformance::CodecConformance;
2746        use commonware_cryptography::sha256::Digest as Sha256Digest;
2747
2748        commonware_conformance::conformance_tests! {
2749            CodecConformance<OpsRootWitness<mmr::Family, Sha256Digest>>,
2750            CodecConformance<OpsRootWitness<mmb::Family, Sha256Digest>>,
2751            CodecConformance<RangeProof<mmr::Family, Sha256Digest>>,
2752            CodecConformance<RangeProof<mmb::Family, Sha256Digest>>,
2753            CodecConformance<constant::OperationProof<mmr::Family, Sha256Digest, 32>>,
2754            CodecConformance<constant::OperationProof<mmb::Family, Sha256Digest, 32>>,
2755            CodecConformance<dynamic::OperationProof<mmr::Family, Sha256Digest>>,
2756            CodecConformance<dynamic::OperationProof<mmb::Family, Sha256Digest>>,
2757        }
2758    }
2759}