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