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