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, hasher::Hasher as _, storage::Storage, Family, Graftable, Location, PendingChunk,
34        Position, Proof,
35    },
36    qmdb::{
37        self,
38        current::{
39            db::{combine_roots, partial_chunk, pending_chunk},
40            grafting,
41        },
42        Error,
43    },
44};
45use bytes::{Buf, BufMut};
46use commonware_codec::{varint::UInt, Codec, EncodeSize, Read, ReadExt as _, Write};
47use commonware_cryptography::{Digest, Hasher};
48use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
49use core::{num::NonZeroU64, ops::Range};
50use futures::future::try_join_all;
51use tracing::debug;
52
53/// Witness that a particular `ops_root` is committed by a `current` canonical root.
54///
55/// See the [Canonical root structure](self#canonical-root-structure) section in the module
56/// documentation for the full layout.
57#[derive(Clone, Eq, PartialEq, Debug)]
58pub struct OpsRootWitness<F: Graftable, D: Digest> {
59    /// The grafted-tree root committed by the canonical root.
60    pub grafted_root: D,
61
62    /// The pending-chunk contribution, if any.
63    pub pending_chunk_digest: F::PendingChunk<D>,
64
65    /// The trailing partial chunk contribution, if the bitmap length is not chunk-aligned:
66    /// `(next_bit, partial_chunk_digest)`.
67    pub partial_chunk: Option<(u64, D)>,
68}
69
70impl<F: Graftable, D: Digest> OpsRootWitness<F, D> {
71    /// Compute the canonical `current` root that commits to `ops_root` through this witness.
72    ///
73    /// See the [Canonical root structure](self#canonical-root-structure) section in the module
74    /// documentation for the full layout.
75    pub fn root<H: Hasher<Digest = D>>(&self, ops_root: &D) -> D {
76        let partial = self.partial_chunk.as_ref().map(|(nb, d)| (*nb, d));
77        combine_roots::<H>(
78            ops_root,
79            &self.grafted_root,
80            self.pending_chunk_digest.as_ref(),
81            partial,
82        )
83    }
84
85    /// Return true if this witness proves that `root` commits to `ops_root`.
86    pub fn verify<H: Hasher<Digest = D>>(&self, ops_root: &D, root: &D) -> bool {
87        self.root::<H>(ops_root) == *root
88    }
89}
90
91impl<F: Graftable, D: Digest> Write for OpsRootWitness<F, D> {
92    fn write(&self, buf: &mut impl BufMut) {
93        self.grafted_root.write(buf);
94        self.pending_chunk_digest.write(buf);
95        self.partial_chunk.is_some().write(buf);
96        if let Some((next_bit, digest)) = &self.partial_chunk {
97            UInt(*next_bit).write(buf);
98            digest.write(buf);
99        }
100    }
101}
102
103impl<F: Graftable, D: Digest> EncodeSize for OpsRootWitness<F, D> {
104    fn encode_size(&self) -> usize {
105        self.grafted_root.encode_size()
106            + self.pending_chunk_digest.encode_size()
107            + self
108                .partial_chunk
109                .as_ref()
110                .map_or(1, |(nb, d)| 1 + UInt(*nb).encode_size() + d.encode_size())
111    }
112}
113
114impl<F: Graftable, D: Digest> Read for OpsRootWitness<F, D> {
115    type Cfg = ();
116
117    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
118        let grafted_root = D::read(buf)?;
119        let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
120        let partial_chunk = if bool::read(buf)? {
121            let next_bit = UInt::<u64>::read(buf)?.into();
122            let digest = D::read(buf)?;
123            Some((next_bit, digest))
124        } else {
125            None
126        };
127        Ok(Self {
128            grafted_root,
129            pending_chunk_digest,
130            partial_chunk,
131        })
132    }
133}
134
135#[cfg(feature = "arbitrary")]
136impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for OpsRootWitness<F, D>
137where
138    D: for<'a> arbitrary::Arbitrary<'a>,
139    F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
140{
141    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
142        Ok(Self {
143            grafted_root: u.arbitrary()?,
144            pending_chunk_digest: u.arbitrary()?,
145            partial_chunk: u.arbitrary()?,
146        })
147    }
148}
149
150/// A proof that a range of operations exist in the database.
151#[derive(Clone, Eq, PartialEq, Debug)]
152pub struct RangeProof<F: Graftable, D: Digest> {
153    /// The Merkle digest material required to verify the proof.
154    pub proof: Proof<F, D>,
155
156    /// The pending-chunk contribution, if any.
157    pub pending_chunk_digest: F::PendingChunk<D>,
158
159    /// Digest of the bitmap's trailing partial chunk, if any.
160    pub partial_chunk_digest: Option<D>,
161
162    /// The ops-tree root digest.
163    pub ops_root: D,
164}
165
166/// Parameters that identify the operation span and snapshot used to build a range proof.
167#[derive(Clone, Copy, Eq, PartialEq, Debug)]
168pub struct RangeProofSpec<F: Family, D: Digest> {
169    /// First operation location to prove.
170    pub start_loc: Location<F>,
171
172    /// Maximum number of operations to include.
173    pub max_ops: NonZeroU64,
174
175    /// Inactivity floor used to fold old inactive peaks.
176    pub inactivity_floor: Location<F>,
177
178    /// The ops-tree root at the time of proof generation.
179    pub ops_root: D,
180}
181
182impl<F: Graftable, D: Digest> RangeProof<F, D> {
183    /// Create a new range proof for the provided `range` of operations.
184    pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>, const N: usize>(
185        status: &impl BitmapReadable<N>,
186        storage: &S,
187        inactivity_floor: Location<F>,
188        range: Range<Location<F>>,
189        ops_root: D,
190    ) -> Result<Self, Error<F>> {
191        // Snapshot ops_leaves once and thread through every derivation that needs it so the
192        // pruned <= graftable <= complete invariant holds across all derivations.
193        let ops_leaves = Location::try_from(storage.size())?;
194        let grafting_height = grafting::height::<N>();
195        let inactive_peaks = grafting::chunk_aligned_inactive_peaks::<F>(
196            ops_leaves,
197            inactivity_floor,
198            grafting_height,
199        )?;
200
201        let hasher = qmdb::hasher::<H>();
202        let proof = merkle::verification::historical_range_proof(
203            &hasher,
204            storage,
205            ops_leaves,
206            range,
207            inactive_peaks,
208        )
209        .await?;
210
211        let partial_chunk_digest =
212            partial_chunk::<_, N>(status).map(|(chunk, _)| hasher.digest(chunk.as_slice()));
213
214        let pending_chunk_digest: F::PendingChunk<D> =
215            pending_chunk::<_, _, N>(status, ops_leaves, grafting_height)?
216                .map(|chunk| hasher.digest(chunk.as_slice()))
217                .try_into()
218                .expect("pending_chunk must be consistent with family");
219
220        Ok(Self {
221            proof,
222            pending_chunk_digest,
223            partial_chunk_digest,
224            ops_root,
225        })
226    }
227
228    /// Returns a proof that the specified range of operations are part of the database, along with
229    /// the operations from the range and their activity status chunks. A truncated range (from
230    /// hitting the max) can be detected by looking at the length of the returned operations vector.
231    ///
232    /// # Errors
233    ///
234    /// Returns [Error::OperationPruned] if `start_loc` falls in a pruned bitmap chunk.
235    /// Returns [`merkle::Error::LocationOverflow`] if `start_loc` > [merkle::Family::MAX_LEAVES].
236    /// Returns [`merkle::Error::RangeOutOfBounds`] if `start_loc` >= number of leaves in the tree.
237    pub async fn new_with_ops<
238        H: Hasher<Digest = D>,
239        C: Contiguous,
240        S: Storage<F, Digest = D>,
241        const N: usize,
242    >(
243        status: &impl BitmapReadable<N>,
244        storage: &S,
245        log: &C,
246        request: RangeProofSpec<F, D>,
247    ) -> Result<(Self, Vec<C::Item>, Vec<[u8; N]>), Error<F>> {
248        // Compute the end location of the range.
249        let leaves = Location::new(status.len());
250        if request.start_loc >= leaves {
251            return Err(merkle::Error::RangeOutOfBounds(request.start_loc).into());
252        }
253
254        // Reject ranges that start in pruned bitmap chunks.
255        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
256        let start = *request.start_loc / chunk_bits;
257        if (start as usize) < status.pruned_chunks() {
258            return Err(Error::OperationPruned(request.start_loc));
259        }
260
261        let max_loc = request.start_loc.saturating_add(request.max_ops.get());
262        let end_loc = core::cmp::min(max_loc, leaves);
263
264        // Generate the proof from the grafted storage.
265        let proof = Self::new::<H, S, N>(
266            status,
267            storage,
268            request.inactivity_floor,
269            request.start_loc..end_loc,
270            request.ops_root,
271        )
272        .await?;
273
274        // Collect the operations necessary to verify the proof.
275        let futures = (*request.start_loc..*end_loc)
276            .map(|i| log.read(i))
277            .collect::<Vec<_>>();
278        let ops = try_join_all(futures).await?;
279
280        // Gather the chunks necessary to verify the proof.
281        let end = (*end_loc - 1) / chunk_bits; // chunk that contains the last bit
282        let chunks = (start..=end)
283            .map(|i| status.get_chunk(i as usize))
284            .collect::<Vec<_>>();
285
286        Ok((proof, ops, chunks))
287    }
288
289    /// Reconstruct the canonical current root, optionally collecting the positioned digests
290    /// required to compute the peaks covering the proven range.
291    fn reconstruct_root<H, O, const N: usize>(
292        &self,
293        start_loc: Location<F>,
294        ops: &[O],
295        chunks: &[[u8; N]],
296        collected: Option<&mut Vec<(Position<F>, D)>>,
297    ) -> Result<D, merkle::Error<F>>
298    where
299        H: Hasher<Digest = D>,
300        O: Codec,
301    {
302        if ops.is_empty() || chunks.is_empty() {
303            debug!("verification failed, empty input");
304            return Err(merkle::Error::InvalidProof);
305        }
306        // Compute the (non-inclusive) end location of the range.
307        let Some(end_loc) = start_loc.checked_add(ops.len() as u64) else {
308            debug!("verification failed, end_loc overflow");
309            return Err(merkle::Error::InvalidProof);
310        };
311
312        let leaves = self.proof.leaves;
313        if end_loc > leaves {
314            debug!(
315                loc = ?end_loc,
316                ?leaves, "verification failed, invalid range"
317            );
318            return Err(merkle::Error::InvalidProof);
319        }
320
321        // Validate the number of input chunks.
322        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
323        let start_chunk = *start_loc / chunk_bits;
324        let end_chunk = (*end_loc - 1) / chunk_bits;
325        let complete_chunks = *leaves / chunk_bits;
326
327        if (end_chunk - start_chunk + 1) != chunks.len() as u64 {
328            debug!("verification failed, chunk metadata length mismatch");
329            return Err(merkle::Error::InvalidProof);
330        }
331
332        let next_bit = *leaves % chunk_bits;
333        let has_partial_chunk = next_bit != 0;
334
335        let elements = ops.iter().map(|op| op.encode()).collect::<Vec<_>>();
336        let chunk_vec = chunks.iter().map(|c| c.as_ref()).collect::<Vec<_>>();
337        let grafting_height = grafting::height::<N>();
338
339        let graftable_chunks =
340            grafting::graftable_chunks::<F>(*leaves, grafting_height).min(complete_chunks);
341        let pending_chunks = complete_chunks - graftable_chunks;
342        if pending_chunks > 1 {
343            debug!(
344                ?complete_chunks,
345                ?graftable_chunks,
346                "verification failed, multiple pending chunks"
347            );
348            return Err(merkle::Error::InvalidProof);
349        }
350        let has_pending_chunk = pending_chunks == 1;
351
352        let grafting_verifier = grafting::Verifier::<F, H>::new(
353            grafting_height,
354            start_chunk,
355            chunk_vec,
356            graftable_chunks,
357        );
358
359        if self.pending_chunk_digest.as_ref().is_some() != has_pending_chunk {
360            debug!(
361                pending_in_proof = self.pending_chunk_digest.as_ref().is_some(),
362                expected = has_pending_chunk,
363                "pending_chunk_digest presence does not match bitmap state"
364            );
365            return Err(merkle::Error::InvalidProof);
366        }
367
368        // For partial chunks, validate the last chunk digest from the proof.
369        if has_partial_chunk {
370            let Some(last_chunk_digest) = self.partial_chunk_digest else {
371                debug!("proof has no partial chunk digest");
372                return Err(merkle::Error::InvalidProof);
373            };
374
375            // If the proof covers an operation in the partial chunk, verify that the
376            // chunk provided by the caller matches the digest embedded in the proof.
377            if end_chunk == complete_chunks {
378                let last_chunk = chunks.last().expect("chunks non-empty");
379                if last_chunk_digest != grafting_verifier.digest(last_chunk) {
380                    debug!("last chunk digest does not match expected value");
381                    return Err(merkle::Error::InvalidProof);
382                }
383            }
384        } else if self.partial_chunk_digest.is_some() {
385            debug!("proof has unexpected partial chunk digest");
386            return Err(merkle::Error::InvalidProof);
387        }
388
389        // For a pending chunk, validate the supplied chunk bytes against the digest in the proof
390        // when the verifier's range includes the pending chunk's index. The pending chunk is at
391        // index `graftable_chunks` (== `complete_chunks - 1` when present).
392        if let Some(pending_digest) = self.pending_chunk_digest.as_ref() {
393            let pending_idx = graftable_chunks;
394            if pending_idx >= start_chunk && pending_idx <= end_chunk {
395                let local = (pending_idx - start_chunk) as usize;
396                // The earlier `chunks.len() == end_chunk - start_chunk + 1` check makes this
397                // index in-bounds for well-formed inputs; treat any mismatch as a malformed
398                // proof (rather than panicking) since `verify` runs against attacker-supplied data.
399                let Some(pending_chunk_bytes) = chunks.get(local) else {
400                    debug!(
401                        ?pending_idx,
402                        chunks_len = chunks.len(),
403                        "pending chunk index out of range in supplied chunks"
404                    );
405                    return Err(merkle::Error::InvalidProof);
406                };
407                if *pending_digest != grafting_verifier.digest(pending_chunk_bytes) {
408                    debug!("pending chunk digest does not match expected value");
409                    return Err(merkle::Error::InvalidProof);
410                }
411            }
412        }
413
414        let merkle_root = match self.proof.reconstruct_root_inner(
415            &grafting_verifier,
416            &elements,
417            start_loc,
418            collected,
419        ) {
420            Ok(root) => root,
421            Err(error) => {
422                debug!(?error, "invalid proof input");
423                return Err(merkle::Error::InvalidProof);
424            }
425        };
426
427        let partial =
428            has_partial_chunk.then(|| (next_bit, self.partial_chunk_digest.as_ref().unwrap()));
429        Ok(combine_roots::<H>(
430            &self.ops_root,
431            &merkle_root,
432            self.pending_chunk_digest.as_ref(),
433            partial,
434        ))
435    }
436
437    /// Return true if the given sequence of `ops` were applied starting at location `start_loc` in
438    /// the db with the provided root, and having the activity status described by `chunks`.
439    pub fn verify<H: Hasher<Digest = D>, O: Codec, const N: usize>(
440        &self,
441        start_loc: Location<F>,
442        ops: &[O],
443        chunks: &[[u8; N]],
444        root: &H::Digest,
445    ) -> bool {
446        matches!(
447            self.reconstruct_root::<H, O, N>(start_loc, ops, chunks, None),
448            Ok(reconstructed_root) if reconstructed_root == *root
449        )
450    }
451}
452
453/// Verify that a [RangeProof] is valid for a range of operations and return the positioned digests
454/// required to compute the peaks covering the proven range.
455pub fn verify_proof_and_extract_digests<F, Op, H, D, const N: usize>(
456    proof: &RangeProof<F, D>,
457    start_loc: Location<F>,
458    operations: &[Op],
459    chunks: &[[u8; N]],
460    target_root: &D,
461) -> Result<Vec<(Position<F>, D)>, merkle::Error<F>>
462where
463    F: Graftable,
464    Op: Codec,
465    H: Hasher<Digest = D>,
466    D: Digest,
467{
468    let mut collected = Vec::new();
469    let reconstructed_root =
470        proof.reconstruct_root::<H, Op, N>(start_loc, operations, chunks, Some(&mut collected))?;
471    if reconstructed_root != *target_root {
472        debug!("verification failed, root mismatch");
473        return Err(merkle::Error::RootMismatch);
474    }
475
476    Ok(collected)
477}
478
479impl<F: Graftable, D: Digest> Write for RangeProof<F, D> {
480    fn write(&self, buf: &mut impl BufMut) {
481        self.proof.write(buf);
482        self.pending_chunk_digest.write(buf);
483        self.partial_chunk_digest.write(buf);
484        self.ops_root.write(buf);
485    }
486}
487
488impl<F: Graftable, D: Digest> EncodeSize for RangeProof<F, D> {
489    fn encode_size(&self) -> usize {
490        self.proof.encode_size()
491            + self.pending_chunk_digest.encode_size()
492            + self.partial_chunk_digest.encode_size()
493            + self.ops_root.encode_size()
494    }
495}
496
497impl<F: Graftable, D: Digest> Read for RangeProof<F, D> {
498    /// The maximum number of digests in the embedded Merkle proof.
499    type Cfg = usize;
500
501    fn read_cfg(
502        buf: &mut impl Buf,
503        max_digests: &Self::Cfg,
504    ) -> Result<Self, commonware_codec::Error> {
505        let proof = Proof::<F, D>::read_cfg(buf, max_digests)?;
506        let pending_chunk_digest = F::PendingChunk::<D>::read(buf)?;
507        let partial_chunk_digest = Option::<D>::read(buf)?;
508        let ops_root = D::read(buf)?;
509        Ok(Self {
510            proof,
511            pending_chunk_digest,
512            partial_chunk_digest,
513            ops_root,
514        })
515    }
516}
517
518#[cfg(feature = "arbitrary")]
519impl<F: Graftable, D: Digest> arbitrary::Arbitrary<'_> for RangeProof<F, D>
520where
521    D: for<'a> arbitrary::Arbitrary<'a>,
522    F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
523{
524    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
525        Ok(Self {
526            proof: u.arbitrary()?,
527            pending_chunk_digest: u.arbitrary()?,
528            partial_chunk_digest: u.arbitrary()?,
529            ops_root: u.arbitrary()?,
530        })
531    }
532}
533
534/// A proof that a specific operation is currently active in the database.
535#[derive(Clone, Eq, PartialEq, Debug)]
536pub struct OperationProof<F: Graftable, D: Digest, const N: usize> {
537    /// The location of the operation in the db.
538    pub loc: Location<F>,
539
540    /// The status bitmap chunk that contains the bit corresponding the operation's location.
541    pub chunk: [u8; N],
542
543    /// The range proof that incorporates activity status for the operation designated by `loc`.
544    pub range_proof: RangeProof<F, D>,
545}
546
547impl<F: Graftable, D: Digest, const N: usize> OperationProof<F, D, N> {
548    /// Return an inclusion proof that incorporates activity status for the operation designated by
549    /// `loc`.
550    ///
551    /// # Errors
552    ///
553    /// Returns [Error::OperationPruned] if `loc` falls in a pruned bitmap chunk.
554    pub async fn new<H: Hasher<Digest = D>, S: Storage<F, Digest = D>>(
555        status: &impl BitmapReadable<N>,
556        storage: &S,
557        inactivity_floor: Location<F>,
558        loc: Location<F>,
559        ops_root: D,
560    ) -> Result<Self, Error<F>> {
561        // Reject locations in pruned bitmap chunks.
562        if BitMap::<N>::to_chunk_index(*loc) < status.pruned_chunks() {
563            return Err(Error::OperationPruned(loc));
564        }
565        let range_proof =
566            RangeProof::new::<H, S, N>(status, storage, inactivity_floor, loc..loc + 1, ops_root)
567                .await?;
568        let chunk = status.get_chunk(BitMap::<N>::to_chunk_index(*loc));
569        Ok(Self {
570            loc,
571            chunk,
572            range_proof,
573        })
574    }
575}
576
577impl<F: Graftable, D: Digest, const N: usize> OperationProof<F, D, N> {
578    /// Verify that the proof proves that `operation` is active in the database with the given
579    /// `root`.
580    pub fn verify<H: Hasher<Digest = D>, O: Codec>(&self, operation: O, root: &D) -> bool {
581        // Make sure that the bit for the operation in the bitmap chunk is actually a 1 (indicating
582        // the operation is indeed active).
583        if !BitMap::<N>::get_bit_from_chunk(&self.chunk, *self.loc) {
584            debug!(
585                ?self.loc,
586                "proof verification failed, operation is inactive"
587            );
588            return false;
589        }
590
591        self.range_proof
592            .verify::<H, O, N>(self.loc, &[operation], &[self.chunk], root)
593    }
594}
595
596impl<F: Graftable, D: Digest, const N: usize> Write for OperationProof<F, D, N> {
597    fn write(&self, buf: &mut impl BufMut) {
598        self.loc.write(buf);
599        self.chunk.write(buf);
600        self.range_proof.write(buf);
601    }
602}
603
604impl<F: Graftable, D: Digest, const N: usize> EncodeSize for OperationProof<F, D, N> {
605    fn encode_size(&self) -> usize {
606        self.loc.encode_size() + self.chunk.encode_size() + self.range_proof.encode_size()
607    }
608}
609
610impl<F: Graftable, D: Digest, const N: usize> Read for OperationProof<F, D, N> {
611    /// The maximum number of digests forwarded to the embedded range proof.
612    type Cfg = usize;
613
614    fn read_cfg(
615        buf: &mut impl Buf,
616        max_digests: &Self::Cfg,
617    ) -> Result<Self, commonware_codec::Error> {
618        let loc = Location::<F>::read(buf)?;
619        let chunk = <[u8; N]>::read(buf)?;
620        let range_proof = RangeProof::<F, D>::read_cfg(buf, max_digests)?;
621        Ok(Self {
622            loc,
623            chunk,
624            range_proof,
625        })
626    }
627}
628
629#[cfg(feature = "arbitrary")]
630impl<F: Graftable, D: Digest, const N: usize> arbitrary::Arbitrary<'_> for OperationProof<F, D, N>
631where
632    D: for<'a> arbitrary::Arbitrary<'a>,
633    F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
634{
635    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
636        Ok(Self {
637            loc: u.arbitrary()?,
638            chunk: u.arbitrary()?,
639            range_proof: u.arbitrary()?,
640        })
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647    use crate::{
648        merkle::{conformance::build_test_mem, hasher::Standard as StandardHasher, mem::Mem},
649        mmb, mmr,
650        qmdb::current::{db, grafting},
651    };
652    use commonware_codec::{Decode as _, DecodeExt as _, Encode as _};
653    use commonware_cryptography::{sha256, Sha256};
654    use commonware_macros::test_async;
655    use commonware_parallel::Sequential;
656    use commonware_utils::bitmap::{Prunable as BitMap, Readable as BitmapReadable};
657    use core::ops::Range;
658
659    #[test]
660    fn test_ops_root_witness_codec_roundtrip() {
661        type F = mmb::Family;
662        for partial_chunk in [
663            None,
664            Some((0u64, Sha256::hash(b"partial-zero"))),
665            Some((123u64, Sha256::hash(b"partial-nonzero"))),
666        ] {
667            let witness: OpsRootWitness<F, _> = OpsRootWitness {
668                grafted_root: Sha256::hash(b"grafted"),
669                pending_chunk_digest: None,
670                partial_chunk,
671            };
672            let encoded = witness.encode();
673            assert_eq!(encoded.len(), witness.encode_size());
674            let decoded = OpsRootWitness::<F, sha256::Digest>::decode(encoded).unwrap();
675            assert_eq!(decoded, witness);
676        }
677    }
678
679    #[test]
680    fn test_ops_root_witness_root_matches_verify() {
681        type F = mmb::Family;
682
683        let ops_root = Sha256::hash(b"ops root");
684        let witness: OpsRootWitness<F, _> = OpsRootWitness {
685            grafted_root: Sha256::hash(b"grafted root"),
686            pending_chunk_digest: Some(Sha256::hash(b"pending chunk")),
687            partial_chunk: Some((13, Sha256::hash(b"partial chunk"))),
688        };
689
690        let root = witness.root::<Sha256>(&ops_root);
691
692        assert!(witness.verify::<Sha256>(&ops_root, &root));
693        assert_ne!(root, ops_root);
694
695        let wrong_ops_root = Sha256::hash(b"wrong ops root");
696        assert!(!witness.verify::<Sha256>(&wrong_ops_root, &root));
697    }
698
699    fn range_proof_digest_count<F: Graftable, D: Digest>(proof: &RangeProof<F, D>) -> usize {
700        proof.proof.digests.len()
701    }
702
703    #[test]
704    fn test_range_proof_codec_roundtrip() {
705        type F = mmb::Family;
706        const MAX_DIGESTS: usize = 64;
707
708        let proof = Proof::<F, sha256::Digest> {
709            leaves: mmb::Location::new(42),
710            inactive_peaks: 0,
711            digests: vec![
712                Sha256::hash(b"d0"),
713                Sha256::hash(b"d1"),
714                Sha256::hash(b"d2"),
715            ],
716        };
717        let ops_root = Sha256::hash(b"ops-root");
718
719        let cases = [
720            // Minimal: no optional fields or prefix/suffix witnesses.
721            RangeProof {
722                proof: proof.clone(),
723                pending_chunk_digest: None,
724                partial_chunk_digest: None,
725                ops_root,
726            },
727            // All optional fields populated.
728            RangeProof {
729                proof,
730                pending_chunk_digest: Some(Sha256::hash(b"pending")),
731                partial_chunk_digest: Some(Sha256::hash(b"partial")),
732                ops_root,
733            },
734            // Default proof with only partial chunk digest.
735            RangeProof {
736                proof: Proof::<F, sha256::Digest>::default(),
737                pending_chunk_digest: None,
738                partial_chunk_digest: Some(Sha256::hash(b"only-partial")),
739                ops_root,
740            },
741        ];
742
743        for proof in cases {
744            let encoded = proof.encode();
745            assert_eq!(encoded.len(), proof.encode_size());
746            let decoded =
747                RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
748            assert_eq!(decoded, proof);
749        }
750    }
751
752    #[test]
753    fn test_range_proof_codec_enforces_merkle_digest_budget() {
754        type F = mmb::Family;
755
756        let proof = RangeProof {
757            proof: Proof::<F, sha256::Digest> {
758                leaves: mmb::Location::new(42),
759                inactive_peaks: 0,
760                digests: vec![Sha256::hash(b"d0")],
761            },
762            pending_chunk_digest: None,
763            partial_chunk_digest: None,
764            ops_root: Sha256::hash(b"ops-root"),
765        };
766
767        let encoded = proof.encode();
768        let total_digests = range_proof_digest_count(&proof);
769
770        let decoded =
771            RangeProof::<F, sha256::Digest>::decode_cfg(encoded.clone(), &total_digests).unwrap();
772        assert_eq!(decoded, proof);
773        assert!(
774            RangeProof::<F, sha256::Digest>::decode_cfg(encoded, &(total_digests - 1)).is_err()
775        );
776    }
777
778    #[test]
779    fn test_range_proof_decode_rejects_pending_for_mmr() {
780        const MAX_DIGESTS: usize = 64;
781
782        let proof = RangeProof {
783            proof: Proof::<mmb::Family, sha256::Digest> {
784                leaves: mmb::Location::new(42),
785                inactive_peaks: 0,
786                digests: vec![Sha256::hash(b"d0")],
787            },
788            pending_chunk_digest: Some(Sha256::hash(b"pending")),
789            partial_chunk_digest: None,
790            ops_root: Sha256::hash(b"ops-root"),
791        };
792        let encoded = proof.encode();
793
794        // MMB allows pending_chunk_digest.
795        assert!(RangeProof::<mmb::Family, sha256::Digest>::decode_cfg(
796            encoded.clone(),
797            &MAX_DIGESTS
798        )
799        .is_ok());
800
801        // MMR rejects it on decode.
802        assert!(
803            RangeProof::<crate::merkle::mmr::Family, sha256::Digest>::decode_cfg(
804                encoded,
805                &MAX_DIGESTS
806            )
807            .is_err()
808        );
809    }
810
811    #[test]
812    fn test_operation_proof_codec_roundtrip() {
813        type F = mmb::Family;
814        const N: usize = 32;
815        const MAX_DIGESTS: usize = 64;
816
817        let range_proof = RangeProof {
818            proof: Proof::<F, sha256::Digest> {
819                leaves: mmb::Location::new(7),
820                inactive_peaks: 0,
821                digests: vec![Sha256::hash(b"sib")],
822            },
823            pending_chunk_digest: None,
824            partial_chunk_digest: None,
825            ops_root: Sha256::hash(b"ops"),
826        };
827
828        let chunk: [u8; N] = core::array::from_fn(|i| i as u8);
829
830        let proof = OperationProof::<F, sha256::Digest, N> {
831            loc: mmb::Location::new(5),
832            chunk,
833            range_proof,
834        };
835
836        let encoded = proof.encode();
837        assert_eq!(encoded.len(), proof.encode_size());
838        let decoded =
839            OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &MAX_DIGESTS).unwrap();
840        assert_eq!(decoded, proof);
841    }
842
843    #[test]
844    fn test_operation_proof_codec_enforces_merkle_digest_budget() {
845        type F = mmb::Family;
846        const N: usize = 32;
847
848        let range_proof = RangeProof {
849            proof: Proof::<F, sha256::Digest> {
850                leaves: mmb::Location::new(7),
851                inactive_peaks: 0,
852                digests: vec![Sha256::hash(b"sib")],
853            },
854            pending_chunk_digest: None,
855            partial_chunk_digest: None,
856            ops_root: Sha256::hash(b"ops"),
857        };
858        let total_digests = range_proof_digest_count(&range_proof);
859        let proof = OperationProof::<F, sha256::Digest, N> {
860            loc: mmb::Location::new(5),
861            chunk: core::array::from_fn(|i| i as u8),
862            range_proof,
863        };
864
865        let encoded = proof.encode();
866        let decoded =
867            OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded.clone(), &total_digests)
868                .unwrap();
869        assert_eq!(decoded, proof);
870        assert!(
871            OperationProof::<F, sha256::Digest, N>::decode_cfg(encoded, &(total_digests - 1))
872                .is_err()
873        );
874    }
875
876    #[test_async]
877    async fn test_range_proof_verifies_for_mmb_multi_peak_chunk() {
878        type F = mmb::Family;
879        const N: usize = 1;
880
881        let hasher = qmdb::hasher::<Sha256>();
882        let grafting_height = grafting::height::<N>();
883
884        let leaf_count = (16..=64u64)
885            .find(|&leaves| {
886                let size = F::location_to_position(mmb::Location::new(leaves));
887                F::chunk_peaks(size, 1, grafting_height).nth(1).is_some()
888            })
889            .expect("expected an MMB size whose second chunk spans multiple peaks");
890
891        let mut status = BitMap::<N>::new();
892        for _ in 0..leaf_count {
893            status.push(true);
894        }
895        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
896        let ops_root = ops.root(&hasher, 0).unwrap();
897
898        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
899            *Location::<F>::try_from(ops.size()).unwrap(),
900            grafting_height,
901        )
902        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
903            as usize;
904        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
905            .map(|chunk_idx| {
906                (
907                    chunk_idx,
908                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
909                )
910            })
911            .collect();
912        let mut leaf_digests =
913            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
914                .await
915                .unwrap();
916        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
917
918        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
919        let mut grafted = Mem::<F, sha256::Digest>::new();
920        let merkleized = {
921            let mut batch = grafted.new_batch();
922            for (_, digest) in leaf_digests {
923                batch = batch.add_leaf_digest(digest);
924            }
925            batch.merkleize(&grafted, &grafted_hasher)
926        };
927        grafted.apply_batch(&merkleized).unwrap();
928
929        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
930        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
931        let root = db::compute_db_root::<F, Sha256, _, _, N>(
932            &status,
933            &storage,
934            ops_leaves_for_root,
935            None,
936            Location::new(0),
937            &ops_root,
938        )
939        .await
940        .unwrap();
941
942        let loc = mmb::Location::new(BitMap::<N>::CHUNK_SIZE_BITS + 4);
943        let proof = RangeProof::new::<Sha256, _, N>(
944            &status,
945            &storage,
946            Location::new(0),
947            loc..loc + 1,
948            ops_root,
949        )
950        .await
951        .unwrap();
952
953        let element = hasher.digest(&(*loc).to_be_bytes());
954        assert!(proof.verify::<Sha256, _, N>(
955            loc,
956            &[element],
957            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 1)],
958            &root,
959        ));
960    }
961
962    #[test_async]
963    async fn test_range_proof_verifies_with_partial_suffix_mmb() {
964        type F = mmb::Family;
965        const N: usize = 1;
966
967        let hasher = qmdb::hasher::<Sha256>();
968        let grafting_height = grafting::height::<N>();
969        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
970
971        let (leaf_count, loc) = (chunk_bits * 2 + 1..=64u64)
972            .find_map(|leaves| {
973                let complete_chunks = leaves / chunk_bits;
974                if complete_chunks < 2 || leaves % chunk_bits == 0 {
975                    return None;
976                }
977
978                let size = F::location_to_position(mmb::Location::new(leaves));
979                F::chunk_peaks(size, 1, grafting_height).nth(1)?;
980                Some((leaves, mmb::Location::new(chunk_bits + 1)))
981            })
982            .expect("expected an MMB proof with a partial trailing suffix chunk");
983
984        let mut status = BitMap::<N>::new();
985        for _ in 0..leaf_count {
986            status.push(true);
987        }
988        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
989        let ops_root = ops.root(&hasher, 0).unwrap();
990
991        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
992            *Location::<F>::try_from(ops.size()).unwrap(),
993            grafting_height,
994        )
995        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
996            as usize;
997        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
998            .map(|chunk_idx| {
999                (
1000                    chunk_idx,
1001                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1002                )
1003            })
1004            .collect();
1005        let mut leaf_digests =
1006            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1007                .await
1008                .unwrap();
1009        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1010
1011        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1012        let mut grafted = Mem::<F, sha256::Digest>::new();
1013        let merkleized = {
1014            let mut batch = grafted.new_batch();
1015            for (_, digest) in leaf_digests {
1016                batch = batch.add_leaf_digest(digest);
1017            }
1018            batch.merkleize(&grafted, &grafted_hasher)
1019        };
1020        grafted.apply_batch(&merkleized).unwrap();
1021
1022        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1023        let partial = {
1024            let (chunk, next_bit) = status.last_chunk();
1025            Some((*chunk, next_bit))
1026        };
1027        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1028        let root = db::compute_db_root::<F, Sha256, _, _, N>(
1029            &status,
1030            &storage,
1031            ops_leaves_for_root,
1032            partial,
1033            Location::new(0),
1034            &ops_root,
1035        )
1036        .await
1037        .unwrap();
1038        let proof = RangeProof::new::<Sha256, _, N>(
1039            &status,
1040            &storage,
1041            Location::new(0),
1042            loc..loc + 1,
1043            ops_root,
1044        )
1045        .await
1046        .unwrap();
1047
1048        let element = hasher.digest(&(*loc).to_be_bytes());
1049        let chunk_idx = (*loc / BitMap::<N>::CHUNK_SIZE_BITS) as usize;
1050        assert!(proof.verify::<Sha256, _, N>(
1051            loc,
1052            &[element],
1053            &[<BitMap<N> as BitmapReadable<N>>::get_chunk(
1054                &status, chunk_idx
1055            )],
1056            &root,
1057        ));
1058    }
1059
1060    #[test_async]
1061    async fn test_range_proof_verifies_when_range_reaches_partial_chunk_mmb() {
1062        type F = mmb::Family;
1063        const N: usize = 1;
1064
1065        let hasher = qmdb::hasher::<Sha256>();
1066        let grafting_height = grafting::height::<N>();
1067        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1068
1069        // Search for an MMB size whose chunk 1 is multi-peak AND whose total leaves
1070        // aren't chunk-aligned (so a partial trailing chunk exists). The proven range
1071        // starts inside chunk 1 and extends to the end (touching the partial chunk).
1072        let (leaf_count, start_loc, complete_chunks) = (17..=128u64)
1073            .find_map(|leaves| {
1074                let complete_chunks = leaves / chunk_bits;
1075                if complete_chunks < 2 || leaves % chunk_bits == 0 {
1076                    return None;
1077                }
1078                let leaves_loc = mmb::Location::new(leaves);
1079                let size = F::location_to_position(leaves_loc);
1080                F::chunk_peaks(size, 1, grafting_height).nth(1)?;
1081                let start_loc = mmb::Location::new(chunk_bits + 1);
1082                Some((leaves, start_loc, complete_chunks))
1083            })
1084            .expect("expected an MMB size with chunk 1 multi-peak and a partial trailing chunk");
1085
1086        let mut status = BitMap::<N>::new();
1087        for _ in 0..leaf_count {
1088            status.push(true);
1089        }
1090        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1091        let ops_root = ops.root(&hasher, 0).unwrap();
1092
1093        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1094            *Location::<F>::try_from(ops.size()).unwrap(),
1095            grafting_height,
1096        )
1097        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1098            as usize;
1099        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1100            .map(|chunk_idx| {
1101                (
1102                    chunk_idx,
1103                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1104                )
1105            })
1106            .collect();
1107        let mut leaf_digests =
1108            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1109                .await
1110                .unwrap();
1111        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1112
1113        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1114        let mut grafted = Mem::<F, sha256::Digest>::new();
1115        let merkleized = {
1116            let mut batch = grafted.new_batch();
1117            for (_, digest) in leaf_digests {
1118                batch = batch.add_leaf_digest(digest);
1119            }
1120            batch.merkleize(&grafted, &grafted_hasher)
1121        };
1122        grafted.apply_batch(&merkleized).unwrap();
1123
1124        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1125        let partial = {
1126            let (chunk, next_bit) = status.last_chunk();
1127            Some((*chunk, next_bit))
1128        };
1129        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1130        let root = db::compute_db_root::<F, Sha256, _, _, N>(
1131            &status,
1132            &storage,
1133            ops_leaves_for_root,
1134            partial,
1135            Location::new(0),
1136            &ops_root,
1137        )
1138        .await
1139        .unwrap();
1140
1141        let leaves_loc = mmb::Location::new(leaf_count);
1142        let proof = RangeProof::new::<Sha256, _, N>(
1143            &status,
1144            &storage,
1145            Location::new(0),
1146            start_loc..leaves_loc,
1147            ops_root,
1148        )
1149        .await
1150        .unwrap();
1151
1152        let elements = (*start_loc..leaf_count)
1153            .map(|idx| hasher.digest(&idx.to_be_bytes()))
1154            .collect::<Vec<_>>();
1155        let start_chunk_idx = (*start_loc / chunk_bits) as usize;
1156        let end_chunk_idx = complete_chunks as usize;
1157        let chunks = (start_chunk_idx..=end_chunk_idx)
1158            .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1159            .collect::<Vec<_>>();
1160        assert!(proof.verify::<Sha256, _, N>(start_loc, &elements, &chunks, &root,));
1161
1162        // Flip a byte in the trailing partial chunk while preserving the window shape.
1163        let mut bad_chunks = chunks;
1164        let last = bad_chunks.last_mut().unwrap();
1165        last[0] ^= 1;
1166        assert!(
1167            !proof.verify::<Sha256, _, N>(start_loc, &elements, &bad_chunks, &root),
1168            "tampered partial chunk bytes should not verify"
1169        );
1170    }
1171
1172    #[test_async]
1173    async fn test_range_proof_rejects_unexpected_partial_chunk_digest() {
1174        type F = mmb::Family;
1175        const N: usize = 1;
1176
1177        let hasher = qmdb::hasher::<Sha256>();
1178        let grafting_height = grafting::height::<N>();
1179        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1180
1181        let leaf_count = chunk_bits * 2; // Perfect chunks, NO partial trailing bits
1182        let mut status = BitMap::<N>::new();
1183        for _ in 0..leaf_count {
1184            status.push(true);
1185        }
1186        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
1187        let ops_root = ops.root(&hasher, 0).unwrap();
1188
1189        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
1190            *Location::<F>::try_from(ops.size()).unwrap(),
1191            grafting_height,
1192        )
1193        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1194            as usize;
1195        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1196            .map(|chunk_idx| {
1197                (
1198                    chunk_idx,
1199                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1200                )
1201            })
1202            .collect();
1203        let mut leaf_digests =
1204            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1205                .await
1206                .unwrap();
1207        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1208
1209        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1210        let mut grafted = Mem::<F, sha256::Digest>::new();
1211        let merkleized = {
1212            let mut batch = grafted.new_batch();
1213            for (_, digest) in leaf_digests {
1214                batch = batch.add_leaf_digest(digest);
1215            }
1216            batch.merkleize(&grafted, &grafted_hasher)
1217        };
1218        grafted.apply_batch(&merkleized).unwrap();
1219
1220        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1221        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
1222        let root = db::compute_db_root::<F, Sha256, _, _, N>(
1223            &status,
1224            &storage,
1225            ops_leaves_for_root,
1226            None,
1227            Location::new(0),
1228            &ops_root,
1229        )
1230        .await
1231        .unwrap();
1232
1233        let loc = mmb::Location::new(0);
1234        let mut proof = RangeProof::new::<Sha256, _, N>(
1235            &status,
1236            &storage,
1237            Location::new(0),
1238            loc..loc + 1,
1239            ops_root,
1240        )
1241        .await
1242        .unwrap();
1243
1244        let element = hasher.digest(&(*loc).to_be_bytes());
1245        let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
1246
1247        // Tamper with the proof by injecting a fake partial chunk digest
1248        let mut tampered = proof.clone();
1249        tampered.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1250        assert!(!tampered.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1251
1252        proof.partial_chunk_digest = Some(hasher.digest(b"fake partial chunk"));
1253        assert!(!proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root,));
1254    }
1255
1256    async fn current_range_proof_fixture<F: Graftable, const N: usize>(
1257        leaf_count: u64,
1258        range: Range<Location<F>>,
1259    ) -> (
1260        StandardHasher<Sha256>,
1261        RangeProof<F, sha256::Digest>,
1262        Vec<sha256::Digest>,
1263        Vec<[u8; N]>,
1264        sha256::Digest,
1265        Mem<F, sha256::Digest>,
1266    ) {
1267        let hasher = qmdb::hasher::<Sha256>();
1268        let grafting_height = grafting::height::<N>();
1269        let chunk_bits = BitMap::<N>::CHUNK_SIZE_BITS;
1270
1271        let mut status = BitMap::<N>::new();
1272        for _ in 0..leaf_count {
1273            status.push(true);
1274        }
1275
1276        let ops = build_test_mem(&hasher, Mem::<F, sha256::Digest>::new(), leaf_count);
1277        let ops_root = ops.root(&hasher, 0).unwrap();
1278        let ops_leaves = Location::<F>::try_from(ops.size()).unwrap();
1279
1280        let graftable_chunks_for_test =
1281            grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1282                .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
1283                as usize;
1284        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
1285            .map(|chunk_idx| {
1286                (
1287                    chunk_idx,
1288                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
1289                )
1290            })
1291            .collect();
1292        let mut leaf_digests =
1293            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
1294                .await
1295                .unwrap();
1296        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
1297
1298        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
1299        let mut grafted = Mem::<F, sha256::Digest>::new();
1300        if !leaf_digests.is_empty() {
1301            let merkleized = {
1302                let mut batch = grafted.new_batch();
1303                for (_, digest) in leaf_digests {
1304                    batch = batch.add_leaf_digest(digest);
1305                }
1306                batch.merkleize(&grafted, &grafted_hasher)
1307            };
1308            grafted.apply_batch(&merkleized).unwrap();
1309        }
1310
1311        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
1312        let root = db::compute_db_root::<F, Sha256, _, _, N>(
1313            &status,
1314            &storage,
1315            ops_leaves,
1316            db::partial_chunk::<_, N>(&status),
1317            Location::new(0),
1318            &ops_root,
1319        )
1320        .await
1321        .unwrap();
1322
1323        let proof = RangeProof::new::<Sha256, _, N>(
1324            &status,
1325            &storage,
1326            Location::new(0),
1327            range.clone(),
1328            ops_root,
1329        )
1330        .await
1331        .unwrap();
1332        let operations = (*range.start..*range.end)
1333            .map(|i| hasher.digest(&i.to_be_bytes()))
1334            .collect::<Vec<_>>();
1335
1336        // Provide every bitmap chunk touched by the proven operation range.
1337        let start_chunk = (*range.start / chunk_bits) as usize;
1338        let end_chunk = ((*range.end - 1) / chunk_bits) as usize;
1339        let chunks = (start_chunk..=end_chunk)
1340            .map(|chunk_idx| <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx))
1341            .collect::<Vec<_>>();
1342
1343        assert!(proof.verify::<Sha256, _, N>(range.start, &operations, &chunks, &root));
1344
1345        (hasher, proof, operations, chunks, root, ops)
1346    }
1347
1348    async fn verify_proof_and_extract_digests_inner<F: Graftable>() {
1349        const N: usize = 1;
1350        let start = Location::<F>::new(14);
1351        let end = Location::<F>::new(18);
1352        let (hasher, proof, operations, chunks, root, ops) =
1353            current_range_proof_fixture::<F, N>(18, start..end).await;
1354
1355        let extracted = verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1356            &proof,
1357            start,
1358            &operations,
1359            &chunks,
1360            &root,
1361        )
1362        .unwrap();
1363        assert!(!extracted.is_empty());
1364
1365        // The extractor should return the authenticated digest for every proven leaf.
1366        for loc in *start..*end {
1367            let pos = F::location_to_position(Location::<F>::new(loc));
1368            let expected = ops.get_node(pos).unwrap();
1369            assert!(
1370                extracted
1371                    .iter()
1372                    .any(|(actual_pos, actual)| *actual_pos == pos && *actual == expected),
1373                "missing extracted leaf digest at {pos:?}",
1374            );
1375        }
1376
1377        // Root mismatches are reported distinctly from malformed proof inputs.
1378        let wrong_root = hasher.digest(b"wrong current root");
1379        assert!(matches!(
1380            verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1381                &proof,
1382                start,
1383                &operations,
1384                &chunks,
1385                &wrong_root,
1386            ),
1387            Err(merkle::Error::RootMismatch)
1388        ));
1389
1390        // Mutating operations or bitmap chunks must invalidate the extracted proof.
1391        let mut wrong_operations = operations.clone();
1392        wrong_operations[0] = hasher.digest(b"wrong operation");
1393        assert!(verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1394            &proof,
1395            start,
1396            &wrong_operations,
1397            &chunks,
1398            &root,
1399        )
1400        .is_err());
1401
1402        let mut bad_chunks = chunks;
1403        bad_chunks.last_mut().unwrap()[0] ^= 1;
1404        assert!(verify_proof_and_extract_digests::<F, _, Sha256, _, N>(
1405            &proof,
1406            start,
1407            &operations,
1408            &bad_chunks,
1409            &root,
1410        )
1411        .is_err());
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 =
2061            F::inactive_peaks(F::location_to_position(leaves), inactivity_floor);
2062        let aligned_inactive =
2063            grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)
2064                .unwrap();
2065        assert_ne!(ops_inactive_peaks, aligned_inactive);
2066
2067        let mut status = BitMap::<N>::new();
2068        for _ in 0..leaf_count {
2069            status.push(true);
2070        }
2071        let ops = build_test_mem(&hasher, mmb::mem::Mmb::new(), leaf_count);
2072
2073        // The ops root is the inner QMDB log root and commits the ops-tree inactive peak count.
2074        // The grafted bitmap root commits the chunk-aligned count, since bitmap chunks are
2075        // the atomic inactive-prefix boundary for the current root.
2076        let ops_root = ops.root(&hasher, ops_inactive_peaks).unwrap();
2077
2078        let graftable_chunks_for_test = grafting::graftable_chunks::<F>(
2079            *Location::<F>::try_from(ops.size()).unwrap(),
2080            grafting_height,
2081        )
2082        .min(<BitMap<N> as BitmapReadable<N>>::complete_chunks(&status) as u64)
2083            as usize;
2084        let chunk_inputs: Vec<_> = (0..graftable_chunks_for_test)
2085            .map(|chunk_idx| {
2086                (
2087                    chunk_idx,
2088                    <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, chunk_idx),
2089                )
2090            })
2091            .collect();
2092        let mut leaf_digests =
2093            db::compute_grafted_leaves::<F, Sha256, Sequential, N>(&ops, chunk_inputs, &Sequential)
2094                .await
2095                .unwrap();
2096        leaf_digests.sort_by_key(|(chunk_idx, _)| *chunk_idx);
2097
2098        let grafted_hasher = grafting::GraftedHasher::<F, _>::new(hasher.clone(), grafting_height);
2099        let mut grafted = Mem::<F, sha256::Digest>::new();
2100        let merkleized = {
2101            let mut batch = grafted.new_batch();
2102            for (_, digest) in leaf_digests {
2103                batch = batch.add_leaf_digest(digest);
2104            }
2105            batch.merkleize(&grafted, &grafted_hasher)
2106        };
2107        grafted.apply_batch(&merkleized).unwrap();
2108
2109        let storage = grafting::Storage::<F, Sha256, _, _>::new(&grafted, grafting_height, &ops);
2110        let ops_leaves_for_root = Location::<F>::try_from(ops.size()).unwrap();
2111        let root = db::compute_db_root::<F, Sha256, _, _, N>(
2112            &status,
2113            &storage,
2114            ops_leaves_for_root,
2115            None,
2116            inactivity_floor,
2117            &ops_root,
2118        )
2119        .await
2120        .unwrap();
2121
2122        let loc = mmb::Location::new(chunk_bits - 1);
2123        let proof = RangeProof::new::<Sha256, _, N>(
2124            &status,
2125            &storage,
2126            inactivity_floor,
2127            loc..loc + 1,
2128            ops_root,
2129        )
2130        .await
2131        .unwrap();
2132        assert_eq!(proof.proof.inactive_peaks, aligned_inactive);
2133
2134        let element = hasher.digest(&(*loc).to_be_bytes());
2135        let chunk = <BitMap<N> as BitmapReadable<N>>::get_chunk(&status, 0);
2136        assert!(proof.verify::<Sha256, _, N>(loc, &[element], &[chunk], &root));
2137    }
2138
2139    #[cfg(feature = "arbitrary")]
2140    mod conformance {
2141        use super::super::{OperationProof, OpsRootWitness, RangeProof};
2142        use crate::merkle::{mmb, mmr};
2143        use commonware_codec::conformance::CodecConformance;
2144        use commonware_cryptography::sha256::Digest as Sha256Digest;
2145
2146        commonware_conformance::conformance_tests! {
2147            CodecConformance<OpsRootWitness<mmr::Family, Sha256Digest>>,
2148            CodecConformance<OpsRootWitness<mmb::Family, Sha256Digest>>,
2149            CodecConformance<RangeProof<mmr::Family, Sha256Digest>>,
2150            CodecConformance<RangeProof<mmb::Family, Sha256Digest>>,
2151            CodecConformance<OperationProof<mmr::Family, Sha256Digest, 32>>,
2152            CodecConformance<OperationProof<mmb::Family, Sha256Digest, 32>>,
2153        }
2154    }
2155}