Skip to main content

commonware_storage/qmdb/current/
db.rs

1//! A shared, generic implementation of the _Current_ QMDB.
2//!
3//! The impl blocks in this file define shared functionality across all Current QMDB variants.
4
5use crate::{
6    Context,
7    index::Unordered as UnorderedIndex,
8    journal::{
9        Error as JournalError,
10        contiguous::{Contiguous, Mutable},
11    },
12    merkle::{
13        self, Graftable, Location, Position, hasher::Hasher as _, mem::Mem,
14        storage::Storage as MerkleStorage,
15    },
16    metadata::{Config as MConfig, Metadata},
17    qmdb::{
18        self, Error,
19        any::{
20            self,
21            operation::{Operation, update::Update},
22        },
23        current::{
24            batch::BitmapBatch,
25            grafting,
26            proof::{OperationProof, OpsRootWitness, RangeProof, RangeProofSpec},
27        },
28        operation::Floored as _,
29    },
30};
31use commonware_codec::{Codec, CodecShared, DecodeExt};
32use commonware_cryptography::{Digest, DigestOf, Hasher};
33use commonware_macros::boxed;
34use commonware_parallel::Strategy;
35use commonware_runtime::{
36    Handle,
37    telemetry::metrics::{
38        Counter, Gauge, GaugeExt as _, MetricsExt as _,
39        histogram::{ScopedTimer, Timed},
40    },
41};
42use commonware_utils::{
43    bitmap::{self, Readable as _},
44    sequence::prefixed_u64::U64,
45};
46use core::{num::NonZeroU64, ops::Range};
47use std::{collections::BTreeMap, sync::Arc};
48use tracing::{error, warn};
49
50/// Prefix used for the metadata key for grafted tree pinned nodes.
51const NODE_PREFIX: u8 = 0;
52
53/// Prefix used for the metadata key for the number of pruned bitmap chunks.
54const PRUNED_CHUNKS_PREFIX: u8 = 1;
55
56/// `(position, digest)` pairs for a grafted tree's pinned nodes, in `Family::nodes_to_pin` order.
57type GraftedPinnedNodes<F, D> = Vec<(Position<F>, D)>;
58
59/// Metrics for the Current layer.
60pub(crate) struct Metrics<E: Context> {
61    /// Pruned bitmap chunks.
62    pruned_chunks: Gauge,
63    /// Most recent safe sync/prune boundary location.
64    sync_boundary: Gauge,
65    /// Current-layer apply-batch calls.
66    pub apply_batch_calls: Counter,
67    /// Duration of Current-layer apply-batch calls.
68    apply_batch_duration: Timed,
69    /// Current-layer sync calls.
70    pub sync_calls: Counter,
71    /// Duration of Current-layer sync calls.
72    sync_duration: Timed,
73    /// Current-layer prune calls.
74    pub prune_calls: Counter,
75    /// Duration of Current-layer prune calls.
76    prune_duration: Timed,
77    /// Clock used by the duration timers.
78    clock: Arc<E>,
79}
80
81impl<E: Context> Metrics<E> {
82    /// Register the full metric set under `context`, retaining it as the timers' clock.
83    pub fn new(context: E) -> Self {
84        Self {
85            pruned_chunks: context.gauge("pruned_chunks", "Number of pruned bitmap chunks"),
86            sync_boundary: context
87                .gauge("sync_boundary", "Most recent safe sync boundary location"),
88            apply_batch_calls: context.counter("apply_batch_calls", "Number of apply-batch calls"),
89            apply_batch_duration: Timed::register(
90                &context,
91                "apply_batch_duration",
92                "Duration of apply-batch calls",
93            ),
94            sync_calls: context.counter("sync_calls", "Number of sync calls"),
95            sync_duration: Timed::register(&context, "sync_duration", "Duration of sync calls"),
96            prune_calls: context.counter("prune_calls", "Number of prune calls"),
97            prune_duration: Timed::register(&context, "prune_duration", "Duration of prune calls"),
98            clock: Arc::new(context),
99        }
100    }
101
102    pub fn apply_batch_timer(&self) -> ScopedTimer<E> {
103        self.apply_batch_duration.scoped(&self.clock)
104    }
105
106    pub fn sync_timer(&self) -> ScopedTimer<E> {
107        self.sync_duration.scoped(&self.clock)
108    }
109
110    pub fn prune_timer(&self) -> ScopedTimer<E> {
111        self.prune_duration.scoped(&self.clock)
112    }
113
114    /// Update Current-specific state gauges.
115    pub fn update(&self, pruned_chunks: u64, sync_boundary: u64) {
116        let _ = self.pruned_chunks.try_set(pruned_chunks);
117        let _ = self.sync_boundary.try_set(sync_boundary);
118    }
119}
120
121/// A Current QMDB implementation generic over ordered/unordered keys and variable/fixed values.
122pub struct Db<
123    F: merkle::Graftable,
124    E: Context,
125    C: Contiguous<Item: CodecShared>,
126    I: UnorderedIndex<Value = Location<F>>,
127    H: Hasher,
128    U: Send + Sync,
129    const N: usize,
130    S: Strategy,
131> {
132    /// An authenticated database that provides the ability to prove whether a key ever had a
133    /// specific value. Owns the activity-status bitmap (`any.bitmap`) that this layer reads to
134    /// install grafted-tree updates and serve proofs.
135    pub(super) any: any::db::Db<F, E, C, I, H, U, N, S>,
136
137    /// Each leaf corresponds to a complete bitmap chunk at the grafting height.
138    /// See the [grafted leaf formula](super) in the module documentation.
139    ///
140    /// Internal nodes are hashed using their position in the ops tree rather than their
141    /// grafted position.
142    ///
143    /// Held in an [`Arc`] so merkleize can hand a zero-copy, immutable snapshot to the
144    /// grafted-layer hashing job running off the calling task. Mutations go through
145    /// [`Arc::make_mut`]: they are in-place while no snapshot is alive and copy-on-write
146    /// otherwise, so a snapshot never observes later mutations.
147    pub(super) grafted_tree: Arc<Mem<F, H::Digest>>,
148
149    /// Persists:
150    /// - The number of pruned bitmap chunks at key [PRUNED_CHUNKS_PREFIX]
151    /// - The grafted tree pinned nodes at key [NODE_PREFIX]
152    pub(super) metadata: Metadata<E, U64, Vec<u8>>,
153
154    /// Strategy used to parallelize batch operations across the ops tree, the grafted tree,
155    /// and grafted leaf computation.
156    pub(super) strategy: S,
157
158    /// The cached canonical root.
159    /// See the [Root structure](super) section in the module documentation.
160    pub(super) root: DigestOf<H>,
161
162    /// Metrics for the Current layer.
163    pub(super) metrics: Metrics<E>,
164
165    /// Test-only: park [Self::prune] after the pruning-metadata sync, before the log prune,
166    /// so tests can drop the pending future at that exact point.
167    #[cfg(test)]
168    pub(super) halt_before_prune_log: bool,
169}
170
171impl<F, E, C, I, H, U, const N: usize, S> std::fmt::Debug for Db<F, E, C, I, H, U, N, S>
172where
173    F: merkle::Graftable,
174    E: Context,
175    C: Contiguous<Item = Operation<F, U>>,
176    I: UnorderedIndex<Value = Location<F>>,
177    H: Hasher,
178    U: Update,
179    S: Strategy,
180    Operation<F, U>: Codec,
181{
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        f.debug_struct("Db")
184            .field("bounds", &self.bounds())
185            .field("inactivity_floor_loc", &self.any.inactivity_floor_loc)
186            .finish_non_exhaustive()
187    }
188}
189
190// Shared read-only functionality.
191impl<F, E, C, I, H, U, const N: usize, S> Db<F, E, C, I, H, U, N, S>
192where
193    F: merkle::Graftable,
194    E: Context,
195    C: Contiguous<Item = Operation<F, U>>,
196    I: UnorderedIndex<Value = Location<F>>,
197    H: Hasher,
198    U: Update,
199    S: Strategy,
200    Operation<F, U>: Codec,
201{
202    /// Return the inactivity floor location. This is the location before which all operations are
203    /// known to be inactive.
204    #[cfg(any(test, feature = "test-traits"))]
205    pub(crate) const fn inactivity_floor_loc(&self) -> Location<F> {
206        self.any.inactivity_floor_loc()
207    }
208
209    /// Whether the snapshot currently has no active keys.
210    pub const fn is_empty(&self) -> bool {
211        self.any.is_empty()
212    }
213
214    /// Get the metadata associated with the last commit.
215    pub async fn get_metadata(&self) -> Result<Option<U::Value>, Error<F>> {
216        self.any.get_metadata().await
217    }
218
219    /// Batch read multiple keys, returning results in the same order as the input keys.
220    pub async fn get_many(&self, keys: &[&U::Key]) -> Result<Vec<Option<U::Value>>, Error<F>> {
221        self.any.get_many(keys).await
222    }
223
224    /// Return [start, end) where `start` and `end - 1` are the Locations of the oldest and newest
225    /// retained operations respectively.
226    pub fn bounds(&self) -> std::ops::Range<Location<F>> {
227        self.any.bounds()
228    }
229
230    /// Returns a read-only view of the activity bitmap.
231    ///
232    /// Pruning does not renumber the retained chunks. Only chunks at or after `pruned_chunks()`
233    /// that contain a bit below `len()` are readable. Calling `get_chunk()` or `get_bit()` for a
234    /// pruned or out-of-bounds location panics.
235    pub fn bitmap(&self) -> &impl bitmap::Readable<N> {
236        self.any.bitmap.as_ref()
237    }
238
239    /// Return true if the given sequence of `ops` were applied starting at location `start_loc`
240    /// in the log with the provided `root`, having the activity status described by `chunks`.
241    pub fn verify_range_proof(
242        proof: &RangeProof<F, H::Digest>,
243        start_loc: Location<F>,
244        ops: &[Operation<F, U>],
245        chunks: &[[u8; N]],
246        root: &H::Digest,
247    ) -> bool {
248        proof.verify::<H, _, N>(start_loc, ops, chunks, root)
249    }
250
251    /// Returns a virtual [`crate::merkle::storage::Storage`] view over the grafted tree and ops
252    /// tree.
253    ///
254    /// Positions and `size()` use ops-tree coordinates. Positions at or above the grafting height
255    /// return bitmap-authenticated grafted nodes, while positions below it use the ops tree.
256    pub fn grafted_storage(&self) -> impl MerkleStorage<F, Digest = H::Digest> + '_ {
257        grafting::Storage::<F, H, _, _>::new(
258            &self.grafted_tree,
259            grafting::height::<N>(),
260            &self.any.log.merkle,
261        )
262    }
263
264    /// Returns the canonical root.
265    /// See the [Root structure](super) section in the module documentation.
266    pub const fn root(&self) -> H::Digest {
267        self.root
268    }
269
270    /// Return a reference to the merkleization strategy.
271    pub const fn strategy(&self) -> &S {
272        &self.strategy
273    }
274
275    /// Returns the ops tree root.
276    ///
277    /// This is the root of the raw operations log, without the activity bitmap. It is used as the
278    /// sync target because the sync engine verifies batches against the ops root, not the canonical
279    /// root.
280    ///
281    /// External consumers that receive a trusted canonical `current` root should use
282    /// [`Self::ops_root_witness`] to authenticate this ops root against it.
283    ///
284    /// See the [Root structure](super) section in the module documentation.
285    pub const fn ops_root(&self) -> H::Digest {
286        self.any.root()
287    }
288
289    /// Returns a witness that this database's canonical root commits to its ops root.
290    ///
291    /// This can be used to authenticate an ops root against a trusted canonical `current` root.
292    pub async fn ops_root_witness(&self) -> Result<OpsRootWitness<F, H::Digest>, Error<F>> {
293        let storage = self.grafted_storage();
294        let ops_size = storage.size();
295        let ops_leaves = Location::<F>::try_from(ops_size)?;
296        let grafted_root = compute_grafted_root::<F, H, _, _, N>(
297            self.any.bitmap.as_ref(),
298            &storage,
299            ops_leaves,
300            self.any.inactivity_floor_loc,
301        )
302        .await?;
303        let hasher = qmdb::hasher::<H>();
304        let partial_chunk = partial_chunk::<_, N>(self.any.bitmap.as_ref())
305            .map(|(chunk, next_bit)| (next_bit, hasher.digest(chunk.as_slice())));
306        let pending_chunk_digest: F::PendingChunk<H::Digest> = pending_chunk::<F, _, N>(
307            self.any.bitmap.as_ref(),
308            ops_leaves,
309            grafting::height::<N>(),
310        )?
311        .map(|chunk| hasher.digest(chunk.as_slice()))
312        .try_into()
313        .expect("pending_chunk must be consistent with family");
314        Ok(OpsRootWitness {
315            grafted_root,
316            pending_chunk_digest,
317            partial_chunk,
318        })
319    }
320
321    /// Snapshot of the grafted tree for use in batch chains.
322    pub(super) fn grafted_snapshot(&self) -> Arc<merkle::batch::MerkleizedBatch<F, H::Digest, S>> {
323        merkle::batch::MerkleizedBatch::from_mem_with_strategy(
324            &self.grafted_tree,
325            self.strategy.clone(),
326        )
327    }
328
329    /// Create a new speculative batch of operations with this database as its parent.
330    pub fn new_batch(&self) -> super::batch::UnmerkleizedBatch<F, H, U, N, S> {
331        super::batch::UnmerkleizedBatch::new(
332            self.any.new_batch(),
333            self.grafted_snapshot(),
334            BitmapBatch::Base(Arc::clone(&self.any.bitmap)),
335        )
336    }
337
338    /// Returns a proof for the operation at `loc`.
339    pub(super) async fn operation_proof(
340        &self,
341        loc: Location<F>,
342    ) -> Result<OperationProof<F, H::Digest, N>, Error<F>> {
343        let storage = self.grafted_storage();
344        let ops_root = self.any.root();
345        OperationProof::new::<H, _>(
346            self.any.bitmap.as_ref(),
347            &storage,
348            self.any.inactivity_floor_loc,
349            loc,
350            ops_root,
351        )
352        .await
353    }
354
355    /// Returns a proof that the specified range of operations are part of the database, along with
356    /// the operations from the range. A truncated range (from hitting the max) can be detected by
357    /// looking at the length of the returned operations vector. Also returns the bitmap chunks
358    /// required to verify the proof.
359    ///
360    /// # Errors
361    ///
362    /// Returns [Error::OperationPruned] if `start_loc` falls in a pruned bitmap chunk. Returns
363    /// [`crate::merkle::Error::LocationOverflow`] if `start_loc` >
364    /// [`crate::merkle::Family::MAX_LEAVES`]. Returns [`crate::merkle::Error::RangeOutOfBounds`] if
365    /// `start_loc` >= number of leaves in the tree.
366    #[allow(clippy::type_complexity)]
367    #[tracing::instrument(
368        name = "qmdb.current.db.range_proof",
369        level = "info",
370        skip_all,
371        fields(
372            start_loc = *start_loc,
373            max_ops = max_ops.get(),
374        ),
375    )]
376    pub async fn range_proof(
377        &self,
378        start_loc: Location<F>,
379        max_ops: NonZeroU64,
380    ) -> Result<(RangeProof<F, H::Digest>, Vec<Operation<F, U>>, Vec<[u8; N]>), Error<F>> {
381        let storage = self.grafted_storage();
382        let ops_root = self.any.root();
383        RangeProof::new_with_ops::<H, _, _, N>(
384            self.any.bitmap.as_ref(),
385            &storage,
386            &self.any.log,
387            RangeProofSpec {
388                start_loc,
389                max_ops,
390                inactivity_floor: self.any.inactivity_floor_loc,
391                ops_root,
392            },
393        )
394        .await
395    }
396}
397
398// Functionality requiring a mutable journal.
399impl<F, E, C, I, H, U, const N: usize, S> Db<F, E, C, I, H, U, N, S>
400where
401    F: merkle::Graftable,
402    E: Context,
403    C: Mutable<Item = Operation<F, U>>,
404    I: UnorderedIndex<Value = Location<F>>,
405    H: Hasher,
406    U: Update,
407    S: Strategy,
408    Operation<F, U>: Codec,
409{
410    /// Returns an ops-level historical proof for the specified range.
411    ///
412    /// Unlike [`range_proof`](Self::range_proof) which returns grafted proofs incorporating the
413    /// activity bitmap, this returns ops-tree Merkle proofs suitable for state sync. Direct
414    /// verifiers should use [`crate::qmdb::verify_proof`].
415    pub async fn ops_historical_proof(
416        &self,
417        historical_size: Location<F>,
418        start_loc: Location<F>,
419        max_ops: NonZeroU64,
420    ) -> Result<(merkle::Proof<F, H::Digest>, Vec<Operation<F, U>>), Error<F>> {
421        self.any
422            .historical_proof(historical_size, start_loc, max_ops)
423            .await
424    }
425
426    /// Return the pinned nodes for a lower operation boundary of `loc`.
427    pub async fn pinned_nodes_at(&self, loc: Location<F>) -> Result<Vec<H::Digest>, Error<F>> {
428        self.any.pinned_nodes_at(loc).await
429    }
430
431    /// Returns the most recent location from which this database can safely be synced, and the
432    /// upper bound on [`Self::prune`]'s `prune_loc`.
433    ///
434    /// Callers constructing a sync [`Target`](crate::qmdb::sync::Target) may use this value, or
435    /// any earlier retained location, as `range.start`. Values *above* this boundary are unsafe:
436    /// the receiver's grafted-pin derivation requires absorption-settled state for every fully
437    /// pruned chunk, which this value guarantees.
438    ///
439    /// # Computation
440    ///
441    /// Starts from the inactivity floor (the most chunks we could possibly prune) and walks
442    /// backward until two conditions hold for the youngest chunk that would be pruned:
443    ///
444    /// 1. **Settled**: the chunk's ops subtree root at height `gh` has been born in the ops
445    ///    tree (its `peak_birth_size <= ops_leaves`).
446    ///
447    /// 2. **Absorbed**: the chunk-pair parent at height `gh+1` has been born. This guarantees
448    ///    that the ops tree has no individual height-`gh` peaks for pruned chunks, so
449    ///    `compute_grafted_root` never queries a discarded grafted leaf.
450    ///
451    /// Because older chunk-pairs have strictly earlier birth times, checking only the youngest
452    /// pair is sufficient: if the youngest pair's parent is born, all older pairs' parents are
453    /// too. In the worst case the loop decrements twice (once past the unsettled chunk, once
454    /// to land on the older pair boundary).
455    ///
456    /// For families without delayed merges (e.g. MMR), `peak_birth_size` at height `gh` equals
457    /// the chunk's last leaf, so condition (1) always holds and the function returns the
458    /// inactivity floor rounded down to the nearest chunk boundary.
459    pub fn sync_boundary(&self) -> Location<F> {
460        sync_boundary::<F, N>(
461            *self.any.inactivity_floor_loc / bitmap::Prunable::<N>::CHUNK_SIZE_BITS,
462            *self.any.last_commit_loc + 1,
463        )
464    }
465
466    /// Update Current-specific state gauges.
467    pub(super) fn update_metrics(&self) {
468        self.metrics.update(
469            self.any.bitmap.pruned_chunks() as u64,
470            *self.sync_boundary(),
471        );
472    }
473
474    /// Returns the minimum rewind target that keeps delayed-merge grafting queries valid
475    /// for the current bitmap pruning boundary.
476    ///
477    /// This is the same absorption threshold used by [`Self::sync_boundary`]: the
478    /// `peak_birth_size` of the youngest pruned chunk-pair's height-(gh+1) parent.
479    /// Rewinding below this size would put the ops tree in a state where the parent has not
480    /// been born, re-exposing individual height-`gh` ops peaks for pruned chunks whose
481    /// grafted leaves are no longer available.
482    ///
483    /// Returns `None` for families without delayed merges.
484    fn delayed_merge_rewind_floor(&self) -> Option<u64> {
485        pair_absorption_threshold::<F, N>(self.any.bitmap.pruned_chunks() as u64)
486    }
487
488    /// Read the grafted tree's pinned-node digests for pruning boundary `loc`, in
489    /// `Family::nodes_to_pin` order, as `(position, digest)` pairs.
490    ///
491    /// Errors with [`Error::DataCorrupted`] if any pinned node is absent from the grafted tree.
492    fn grafted_pinned_nodes(
493        &self,
494        loc: Location<F>,
495    ) -> Result<GraftedPinnedNodes<F, H::Digest>, Error<F>> {
496        F::nodes_to_pin(loc)
497            .map(|pos| {
498                let digest = self
499                    .grafted_tree
500                    .get_node(pos)
501                    .ok_or(Error::<F>::DataCorrupted("missing grafted pinned node"))?;
502                Ok((pos, digest))
503            })
504            .collect()
505    }
506
507    /// Prune the grafted tree to match the committed bitmap's pruned chunks.
508    fn prune_grafted_tree_to_bitmap(&mut self) -> Result<(), Error<F>> {
509        let pruned_chunks = self.any.bitmap.pruned_chunks() as u64;
510        if pruned_chunks == 0 {
511            return Ok(());
512        }
513
514        let prune_loc = Location::<F>::new(pruned_chunks);
515        if prune_loc <= self.grafted_tree.bounds().start {
516            return Ok(());
517        }
518
519        let prune_pos = Position::try_from(prune_loc)
520            .map_err(|_| Error::<F>::DataCorrupted("prune location overflow"))?;
521        let size = self.grafted_tree.size();
522
523        let pinned: BTreeMap<_, _> = self.grafted_pinned_nodes(prune_loc)?.into_iter().collect();
524
525        let mut retained = Vec::with_capacity((*size - *prune_pos) as usize);
526        for p in *prune_pos..*size {
527            let digest = self
528                .grafted_tree
529                .get_node(Position::new(p))
530                .ok_or(Error::<F>::DataCorrupted("missing retained grafted node"))?;
531            retained.push(digest);
532        }
533
534        self.grafted_tree = Arc::new(Mem::from_pruned_with_retained(prune_pos, pinned, retained));
535        Ok(())
536    }
537
538    /// Prunes historical operations prior to `prune_loc`. This does not affect the db's root or
539    /// snapshot.
540    ///
541    /// `prune` requires no prior commit. After a crash, the database remains recoverable;
542    /// uncommitted operations are not guaranteed to survive.
543    ///
544    /// `prune_loc` must be at most [`Self::sync_boundary`]: the ops log's lower bound must not
545    /// advance past the point where the grafting overlay has been pruned. The bitmap and grafted
546    /// tree advance to the sync boundary regardless of `prune_loc`.
547    ///
548    /// # Errors
549    ///
550    /// - Returns [Error::PruneBeyondMinRequired] if `prune_loc` > [`Self::sync_boundary`].
551    /// - Returns [`crate::merkle::Error::LocationOverflow`] if `prune_loc` >
552    ///   [crate::merkle::Family::MAX_LEAVES].
553    /// - Returns [Error::DataCorrupted] if internal grafted-tree state is inconsistent (a pinned
554    ///   or retained node is missing, or the prune location overflows a [Position]).
555    #[tracing::instrument(name = "qmdb.current.db.prune", level = "info", skip_all)]
556    #[boxed]
557    pub async fn prune(mut self, prune_loc: Location<F>) -> Result<Self, Error<F>> {
558        let _timer = self.metrics.prune_timer();
559        self.metrics.prune_calls.inc();
560        let sync_boundary = self.sync_boundary();
561        if prune_loc > sync_boundary {
562            return Err(Error::PruneBeyondMinRequired(prune_loc, sync_boundary));
563        }
564
565        // The sync boundary may be advanced by applied-but-uncommitted operations, and the
566        // pruning metadata persisted below durably records it. Commit the log first so
567        // recovery can replay to that boundary: otherwise a crash before the log prune
568        // recovers the older durable floor alongside newer pruning metadata and fails to
569        // initialize the bitmap.
570        self.any.log = self.any.log.commit().await?;
571
572        // Prune the bitmap to the sync boundary (most aggressive safe location).
573        self.any.prune_bitmap(sync_boundary);
574        self.prune_grafted_tree_to_bitmap()?;
575
576        // Persist grafted tree pruning state before pruning the ops log. If the subsequent
577        // `any.prune_log` fails, the metadata is ahead of the log, which is safe: on recovery,
578        // `build_grafted_tree` will recompute from the (un-pruned) log and the metadata
579        // simply records peaks that haven't been pruned yet. The reverse order would be unsafe:
580        // a pruned log with stale metadata would lose peak digests permanently.
581        self = self.sync_metadata().await?;
582
583        #[cfg(test)]
584        if self.halt_before_prune_log {
585            std::future::pending::<()>().await;
586        }
587
588        (self.any, _) = self.any.prune_log(prune_loc).await?;
589        self.any.update_metrics();
590        self.update_metrics();
591        Ok(self)
592    }
593
594    /// Rewind the database to `size` operations, where `size` is the location of the next append.
595    ///
596    /// This rewinds the underlying Any database and rebuilds the Current overlay state (bitmap,
597    /// grafted tree, and canonical root) for the rewound size.
598    ///
599    /// # Errors
600    ///
601    /// Returns an error when:
602    /// - `size` is not a valid rewind target
603    /// - the target's required logical range is not fully retained (for Current, this includes the
604    ///   underlying Any inactivity-floor boundary and bitmap pruning boundary)
605    /// - `size - 1` is not a commit operation
606    /// - `size` is below the bitmap pruning boundary
607    ///
608    /// Any error from this method is fatal for this handle. Rewind may mutate state in the
609    /// underlying Any database before this Current overlay finishes rebuilding. Callers must drop
610    /// this database handle after any `Err` from `rewind` and reopen from storage.
611    ///
612    /// A successful rewind is not restart-stable until a subsequent [`Db::commit`] or
613    /// [`Db::sync`] completes, or until the handle returned by a subsequent [`Db::start_sync`]
614    /// completes.
615    #[tracing::instrument(name = "qmdb.current.db.rewind", level = "info", skip_all)]
616    #[boxed]
617    pub async fn rewind(mut self, size: Location<F>) -> Result<Self, Error<F>> {
618        let rewind_size = *size;
619        let current_size = *self.any.last_commit_loc + 1;
620        // No-op short-circuit. Avoids the post-rewind grafted-tree rebuild and the validation
621        // and journal-read overhead below. Validation runs after this on the non-no-op path.
622        if rewind_size == current_size {
623            return Ok(self);
624        }
625        // Reject zero / out-of-range up front: lines below compute `rewind_size - 1`, which
626        // underflows when `rewind_size == 0`. `any::Db::rewind` would catch these, but it isn't
627        // called until after those subtractions.
628        if rewind_size == 0 || rewind_size > current_size {
629            return Err(Error::Journal(JournalError::InvalidRewind(rewind_size)));
630        }
631
632        let pruned_chunks = self.any.bitmap.pruned_chunks();
633        let pruned_bits = (pruned_chunks as u64)
634            .checked_mul(bitmap::Prunable::<N>::CHUNK_SIZE_BITS)
635            .ok_or_else(|| Error::DataCorrupted("pruned ops leaves overflow"))?;
636        if rewind_size < pruned_bits {
637            return Err(Error::Journal(JournalError::ItemPruned(rewind_size - 1)));
638        }
639        if let Some(rewind_floor) = self.delayed_merge_rewind_floor()
640            && rewind_size < rewind_floor
641        {
642            return Err(Error::Journal(JournalError::ItemPruned(rewind_size - 1)));
643        }
644
645        // Ensure the target commit's logical range is fully representable with the current
646        // bitmap pruning boundary. Even if the ops log still retains older entries, rewinding
647        // to a commit with floor below `pruned_bits` would require bitmap chunks we've already
648        // discarded.
649        {
650            let rewind_last_loc = Location::<F>::new(rewind_size - 1);
651            let rewind_last_op = self.any.log.read(*rewind_last_loc).await?;
652            let Some(rewind_floor) = rewind_last_op.has_floor() else {
653                return Err(Error::<F>::UnexpectedData(rewind_last_loc));
654            };
655            if *rewind_floor < pruned_bits {
656                return Err(Error::<F>::Journal(JournalError::ItemPruned(*rewind_floor)));
657            }
658        }
659
660        // Extract pinned nodes for the existing pruning boundary from the in-memory grafted tree.
661        let pinned_nodes: Vec<H::Digest> = if pruned_chunks > 0 {
662            let grafted_leaves = Location::<F>::new(pruned_chunks as u64);
663            self.grafted_pinned_nodes(grafted_leaves)?
664                .into_iter()
665                .map(|(_, digest)| digest)
666                .collect()
667        } else {
668            Vec::new()
669        };
670
671        // `any.rewind` rewinds the log and patches the shared bitmap (truncate + restore active
672        // bits + set the rewound tail's CommitFloor). Live pre-rewind batches must be dropped by
673        // the caller; reads through them now return inconsistent data.
674        self.any = self.any.rewind(size).await?;
675
676        // Rebuild the grafted tree and canonical root from the rewound `any` state.
677        let (grafted_tree, root) = rebuild_grafted_tree::<F, H, S, N>(
678            self.any.bitmap.as_ref(),
679            &pinned_nodes,
680            &self.any.log.merkle,
681            self.any.inactivity_floor_loc,
682            self.any.root(),
683            &self.strategy,
684        )
685        .await?;
686
687        self.grafted_tree = Arc::new(grafted_tree);
688        self.root = root;
689        self.update_metrics();
690
691        Ok(self)
692    }
693
694    /// Sync the metadata to disk.
695    pub(crate) async fn sync_metadata(mut self) -> Result<Self, Error<F>> {
696        self.metadata.clear();
697
698        // Snapshot the pruning boundary under the read lock; the guard drops before any await.
699        let pruned_chunks_u64 = self.any.bitmap.pruned_chunks() as u64;
700
701        // Write the number of pruned chunks.
702        let key = U64::new(PRUNED_CHUNKS_PREFIX, 0);
703        self.metadata
704            .put(key, pruned_chunks_u64.to_be_bytes().to_vec());
705
706        // Write the pinned nodes of the grafted tree.
707        let pruned_chunks = Location::<F>::new(pruned_chunks_u64);
708        for (i, (_, digest)) in self
709            .grafted_pinned_nodes(pruned_chunks)?
710            .into_iter()
711            .enumerate()
712        {
713            let key = U64::new(NODE_PREFIX, i as u64);
714            self.metadata.put(key, digest.to_vec());
715        }
716
717        self.metadata = self.metadata.sync().await?;
718
719        Ok(self)
720    }
721
722    /// Begin durably persisting the journal state published by prior [`Db::apply_batch`] calls.
723    ///
724    /// Awaiting the returned [Handle] provides the same durability guarantee as [Self::commit],
725    /// plus a best-effort attempt to bound the recovery needed on startup.
726    /// Bitmap metadata is not persisted by this call or by [Self::commit]. A new sync waits for
727    /// the prior sync before starting. Failures surface as described on
728    /// [`any::Db::start_sync`](crate::qmdb::any::db::Db::start_sync).
729    #[tracing::instrument(name = "qmdb.current.db.start_sync", level = "info", skip_all)]
730    #[boxed]
731    pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error<F>> {
732        let (any, handle) = self.any.start_sync().await?;
733        self.any = any;
734        Ok((self, handle))
735    }
736
737    /// Durably commit the journal state published by prior [`Db::apply_batch`]
738    /// calls.
739    #[tracing::instrument(name = "qmdb.current.db.commit", level = "info", skip_all)]
740    #[boxed]
741    pub async fn commit(mut self) -> Result<Self, Error<F>> {
742        self.any = self.any.commit().await?;
743        Ok(self)
744    }
745
746    /// Sync all database state to disk.
747    #[tracing::instrument(name = "qmdb.current.db.sync", level = "info", skip_all)]
748    #[boxed]
749    pub async fn sync(mut self) -> Result<Self, Error<F>> {
750        let _timer = self.metrics.sync_timer();
751        self.metrics.sync_calls.inc();
752        self.any = self.any.sync().await?;
753
754        // Write the bitmap pruning boundary to disk so that next startup doesn't have to
755        // re-Merkleize the inactive portion up to the inactivity floor.
756        self = self.sync_metadata().await?;
757        self.update_metrics();
758        Ok(self)
759    }
760
761    /// Destroy the db, removing all data from disk.
762    #[boxed]
763    pub async fn destroy(self) -> Result<(), Error<F>> {
764        // Destructure before the await boundary to avoid stack growth from
765        // retaining the entire `self` in the future.
766        let Self { any, metadata, .. } = self;
767        metadata.destroy().await?;
768        any.destroy().await
769    }
770
771    /// Check that `batch` can be applied to the database in its current state, without
772    /// applying it.
773    ///
774    /// [`Self::apply_batch`] runs the same validation but consumes the database when it
775    /// fails; callers that want to reject a bad batch and keep the handle can check first.
776    pub fn validate_batch(
777        &self,
778        batch: &super::batch::MerkleizedBatch<F, H::Digest, U, N, S>,
779    ) -> Result<(), Error<F>> {
780        self.any.validate_batch(&batch.inner)
781    }
782
783    /// Apply a batch to the database, returning the range of written operations.
784    ///
785    /// A batch is valid only if every batch applied to the database since this batch's
786    /// ancestor chain was created is an ancestor of this batch. Applying a batch from a
787    /// different fork returns [`Error::StaleBatch`] (see [`crate::qmdb::batch_chain`] for
788    /// more details).
789    ///
790    /// This publishes the batch to the in-memory Current view and appends it to the journal. Call
791    /// [`Db::commit`] or [`Db::sync`], or await the handle returned by [`Db::start_sync`], to make
792    /// the applied state durable.
793    #[tracing::instrument(name = "qmdb.current.db.apply_batch", level = "info", skip_all)]
794    #[boxed]
795    pub async fn apply_batch(
796        mut self,
797        batch: Arc<super::batch::MerkleizedBatch<F, H::Digest, U, N, S>>,
798    ) -> Result<(Self, Range<Location<F>>), Error<F>> {
799        let _timer = self.metrics.apply_batch_timer();
800        self.metrics.apply_batch_calls.inc();
801        let range;
802        (self.any, range) = self.any.apply_batch(Arc::clone(&batch.inner)).await?;
803        Arc::make_mut(&mut self.grafted_tree).apply_batch(&batch.grafted)?;
804        self.root = batch.canonical_root;
805        self.update_metrics();
806        Ok((self, range))
807    }
808}
809
810/// Compute the safe sync boundary from the chunk-aligned inactivity floor and the current
811/// ops-tree size.
812///
813/// `floor_chunks` is the inactivity floor expressed in bitmap chunks (`floor / CHUNK_SIZE_BITS`),
814/// not the number of physically pruned chunks. Shared by the live DB and speculative batch
815/// wrappers, which both derive it from the inactivity floor so they report the same range start.
816pub(crate) fn sync_boundary<F: Graftable, const N: usize>(
817    mut floor_chunks: u64,
818    ops_leaves: u64,
819) -> Location<F> {
820    let chunk_bits = bitmap::Prunable::<N>::CHUNK_SIZE_BITS;
821    let grafting_height = grafting::height::<N>();
822
823    while floor_chunks > 0 {
824        let required_ops = pair_absorption_threshold::<F, N>(floor_chunks).unwrap_or_else(|| {
825            let youngest_start = (floor_chunks - 1) * chunk_bits;
826            let pos = F::subtree_root_position(Location::<F>::new(youngest_start), grafting_height);
827            F::peak_birth_size(pos, grafting_height)
828        });
829
830        if ops_leaves >= required_ops {
831            break;
832        }
833        floor_chunks -= 1;
834    }
835
836    Location::new(floor_chunks * chunk_bits)
837}
838
839/// For the youngest of `chunk_count` chunks, return the `peak_birth_size` of its
840/// chunk-pair parent at height `gh+1`. Returns `None` for families without delayed merges
841/// (where `peak_birth_size` at height `gh` equals the chunk boundary).
842fn pair_absorption_threshold<F: Graftable, const N: usize>(chunk_count: u64) -> Option<u64> {
843    if chunk_count == 0 {
844        return None;
845    }
846
847    let grafting_height = grafting::height::<N>();
848    let youngest = chunk_count - 1;
849    let youngest_start = youngest << grafting_height;
850    let youngest_end = (youngest + 1) << grafting_height;
851    let youngest_pos =
852        F::subtree_root_position(Location::<F>::new(youngest_start), grafting_height);
853
854    if F::peak_birth_size(youngest_pos, grafting_height) <= youngest_end {
855        return None;
856    }
857
858    let pair_chunk = youngest & !1;
859    let pair_start = pair_chunk << grafting_height;
860    let pair_pos = F::subtree_root_position(Location::<F>::new(pair_start), grafting_height + 1);
861    Some(F::peak_birth_size(pair_pos, grafting_height + 1))
862}
863
864/// The bitmap's incomplete trailing chunk and the number of bits in it, or `None` if the bitmap
865/// is empty or its bits end exactly on a chunk boundary.
866pub(super) fn partial_chunk<B: bitmap::Readable<N>, const N: usize>(
867    bitmap: &B,
868) -> Option<([u8; N], u64)> {
869    let next_bit = bitmap.len() % bitmap::Prunable::<N>::CHUNK_SIZE_BITS;
870    if next_bit == 0 {
871        return None;
872    }
873    let (last_chunk, _) = bitmap.last_chunk();
874    Some((last_chunk, next_bit))
875}
876
877/// Return complete and graftable chunk counts, enforcing the pending and pruning invariants.
878///
879/// Returns [`Error::DataCorrupted`] if `bitmap` and `ops_leaves` imply more than one
880/// pending chunk, or if pruning has advanced past the graftable chunk boundary.
881fn graftable_chunk_window<F: merkle::Graftable, B: bitmap::Readable<N>, const N: usize>(
882    bitmap: &B,
883    ops_leaves: Location<F>,
884    grafting_height: u32,
885) -> Result<(u64, u64), Error<F>> {
886    let complete = bitmap.complete_chunks() as u64;
887    let graftable = grafting::graftable_chunks::<F>(*ops_leaves, grafting_height).min(complete);
888    let pending = complete - graftable;
889    if pending > 1 {
890        return Err(Error::DataCorrupted("multiple pending bitmap chunks"));
891    }
892
893    let pruned = bitmap.pruned_chunks() as u64;
894    if pruned > graftable {
895        return Err(Error::DataCorrupted(
896            "pruned chunks exceed graftable chunks",
897        ));
898    }
899
900    Ok((complete, graftable))
901}
902
903/// Returns the bytes of the "pending" chunk if the bitmap currently has one, else `None`.
904///
905/// A chunk is pending when its bits are fully written to the bitmap but its h=G ancestor
906/// has not yet been born in the ops tree. At most one chunk is ever in this state (the most
907/// recently completed one); see [`super::grafting::graftable_chunks`] for the structural
908/// argument.
909///
910/// The caller must pass a consistent snapshot of `ops_leaves` (the ops tree's leaf count)
911/// and the bitmap state. Both inputs are used to derive `graftable_chunks`; deriving them from
912/// independent snapshots can violate the pending-window or pruning invariants.
913///
914/// Returns [`Error::DataCorrupted`] when those invariants are violated.
915pub(super) fn pending_chunk<F: merkle::Graftable, B: bitmap::Readable<N>, const N: usize>(
916    bitmap: &B,
917    ops_leaves: Location<F>,
918    grafting_height: u32,
919) -> Result<Option<[u8; N]>, Error<F>> {
920    let (complete, graftable) =
921        graftable_chunk_window::<F, B, N>(bitmap, ops_leaves, grafting_height)?;
922    if complete - graftable != 1 {
923        return Ok(None);
924    }
925    Ok(Some(bitmap.get_chunk(graftable as usize)))
926}
927
928/// Compute the canonical root from the ops root, grafted tree root, and optional pending /
929/// partial chunk digests.
930///
931/// See [Canonical root structure](super::proof#canonical-root-structure) for the full layout.
932/// The pending and partial inputs are independent: either, both, or neither may be set, and
933/// pending precedes partial in hash order when both are present.
934///
935/// # Collision resistance
936///
937/// `pending` contributes `D` bytes when present; `partial` contributes `D + 8` bytes (`D` =
938/// digest size). Different fixed lengths, so the two cannot produce the same input bytes,
939/// even when their digests are identical. Collisions reduce to H.
940pub(super) fn combine_roots<H: Hasher>(
941    ops_root: &H::Digest,
942    grafted_root: &H::Digest,
943    pending: Option<&H::Digest>,
944    partial: Option<(u64, &H::Digest)>,
945) -> H::Digest {
946    let hasher = qmdb::hasher::<H>();
947    match (pending, partial) {
948        (None, None) => hasher.hash(&[ops_root.as_ref(), grafted_root.as_ref()]),
949        (Some(pe), None) => hasher.hash(&[ops_root.as_ref(), grafted_root.as_ref(), pe.as_ref()]),
950        (None, Some((nb, p))) => {
951            let nb_bytes = nb.to_be_bytes();
952            hasher.hash(&[
953                ops_root.as_ref(),
954                grafted_root.as_ref(),
955                nb_bytes.as_slice(),
956                p.as_ref(),
957            ])
958        }
959        (Some(pe), Some((nb, p))) => {
960            let nb_bytes = nb.to_be_bytes();
961            hasher.hash(&[
962                ops_root.as_ref(),
963                grafted_root.as_ref(),
964                pe.as_ref(),
965                nb_bytes.as_slice(),
966                p.as_ref(),
967            ])
968        }
969    }
970}
971
972/// Compute the canonical root digest of a [Db].
973///
974/// See the [Root structure](super) section in the module documentation.
975///
976/// `ops_leaves` must be a single consistent snapshot of the ops tree's leaf count, taken
977/// in the same logical instant as the bitmap state passed via `status`. Both the pending
978/// chunk derivation and `compute_grafted_root` use this value to compute `graftable_chunks`;
979/// deriving them from independent snapshots risks the inconsistent state where a chunk is
980/// counted in one path but not the other.
981#[allow(clippy::too_many_arguments)]
982pub(super) async fn compute_db_root<
983    F: merkle::Graftable,
984    H: Hasher,
985    B: bitmap::Readable<N>,
986    S: MerkleStorage<F, Digest = H::Digest>,
987    const N: usize,
988>(
989    status: &B,
990    storage: &S,
991    ops_leaves: Location<F>,
992    partial_chunk: Option<([u8; N], u64)>,
993    inactivity_floor: Location<F>,
994    ops_root: &H::Digest,
995) -> Result<H::Digest, Error<F>> {
996    let grafted_root =
997        compute_grafted_root::<F, H, B, S, N>(status, storage, ops_leaves, inactivity_floor)
998            .await?;
999    let hasher = qmdb::hasher::<H>();
1000    let pending = pending_chunk::<F, B, N>(status, ops_leaves, grafting::height::<N>())?
1001        .map(|chunk| hasher.digest(&chunk));
1002    let partial = partial_chunk.map(|(chunk, next_bit)| {
1003        let digest = hasher.digest(&chunk);
1004        (next_bit, digest)
1005    });
1006    Ok(combine_roots::<H>(
1007        ops_root,
1008        &grafted_root,
1009        pending.as_ref(),
1010        partial.as_ref().map(|(nb, d)| (*nb, d)),
1011    ))
1012}
1013
1014/// Rebuild the grafted overlay tree and compute the canonical db root from the ops tree and
1015/// bitmap. Returns the rebuilt grafted tree and the db root.
1016pub(super) async fn rebuild_grafted_tree<F, H, S, const N: usize>(
1017    bitmap: &impl bitmap::Readable<N>,
1018    pinned_nodes: &[H::Digest],
1019    ops_tree: &impl MerkleStorage<F, Digest = H::Digest>,
1020    inactivity_floor: Location<F>,
1021    ops_root: H::Digest,
1022    strategy: &S,
1023) -> Result<(Mem<F, H::Digest>, H::Digest), Error<F>>
1024where
1025    F: merkle::Graftable,
1026    H: Hasher,
1027    S: Strategy,
1028{
1029    let ops_leaves = Location::<F>::try_from(ops_tree.size())?;
1030    let grafted_tree =
1031        build_grafted_tree::<F, H, S, N>(bitmap, pinned_nodes, ops_tree, ops_leaves, strategy)
1032            .await?;
1033    let storage =
1034        grafting::Storage::<F, H, _, _>::new(&grafted_tree, grafting::height::<N>(), ops_tree);
1035    let partial_chunk = partial_chunk(bitmap);
1036    let root = compute_db_root::<F, H, _, _, N>(
1037        bitmap,
1038        &storage,
1039        ops_leaves,
1040        partial_chunk,
1041        inactivity_floor,
1042        &ops_root,
1043    )
1044    .await?;
1045    Ok((grafted_tree, root))
1046}
1047
1048/// Compute the root of the grafted structure represented by `storage`.
1049///
1050/// Only **graftable** chunks (those whose h=G ancestor has been born in the ops tree) are
1051/// committed by the grafted tree. The most recently completed but ungraftable chunk, if
1052/// any, is hashed into the canonical root directly by [`combine_roots`] as the pending
1053/// chunk, not by this function.
1054///
1055/// `ops_leaves` must come from the same single snapshot as `status` to preserve the
1056/// `pruned_chunks <= graftable_chunks <= complete_chunks` invariant.
1057pub(super) async fn compute_grafted_root<
1058    F: merkle::Graftable,
1059    H: Hasher,
1060    B: bitmap::Readable<N>,
1061    S: MerkleStorage<F, Digest = H::Digest>,
1062    const N: usize,
1063>(
1064    status: &B,
1065    storage: &S,
1066    ops_leaves: Location<F>,
1067    inactivity_floor: Location<F>,
1068) -> Result<H::Digest, Error<F>> {
1069    let size = storage.size();
1070    let leaves = Location::try_from(size)?;
1071
1072    // Collect peak digests of the grafted structure.
1073    let mut peaks: Vec<H::Digest> = Vec::new();
1074    for (peak_pos, _) in F::peaks(size) {
1075        let digest = storage
1076            .get_node(peak_pos)
1077            .await?
1078            .ok_or_else(|| merkle::Error::<F>::MissingNode(peak_pos))?;
1079        peaks.push(digest);
1080    }
1081
1082    // Validate bitmap invariants (pending <= 1, pruned <= graftable).
1083    let grafting_height = grafting::height::<N>();
1084    let (_complete_chunks, _graftable_chunks) =
1085        graftable_chunk_window::<F, B, N>(status, ops_leaves, grafting_height)?;
1086
1087    let inactive_peaks =
1088        grafting::chunk_aligned_inactive_peaks::<F>(leaves, inactivity_floor, grafting_height)?;
1089    let hasher = qmdb::hasher::<H>();
1090
1091    // Every peak the storage layer surfaces is either a grafted-tree node (graftable chunks already
1092    // incorporate `hash(chunk || h_G_node)`), an ops node above G (hashed normally), or an ops node
1093    // below G (raw, because its chunk is pending and its digest is hashed directly into the
1094    // canonical root rather than through the tree). Bagging is a straight fold; no per-chunk
1095    // transformation is needed.
1096    Ok(hasher.root(leaves, inactive_peaks, peaks.iter())?)
1097}
1098
1099/// Resolve each bitmap chunk's covering ops-tree node, returning
1100/// `(chunk_idx, chunk_ops_digest, chunk)` triples ready for
1101/// [`grafting::graft_chunk_digests`].
1102///
1103/// Callers must pass only **graftable** chunks (those whose h=G ancestor has already been born in
1104/// the ops tree). Each graftable chunk has exactly one covering ops node at height G, looked up via
1105/// [`merkle::Graftable::subtree_root_position`].
1106pub(super) async fn read_graft_inputs<F: merkle::Graftable, D: Digest, const N: usize>(
1107    ops_tree: &impl MerkleStorage<F, Digest = D>,
1108    chunks: impl IntoIterator<Item = (usize, [u8; N])>,
1109) -> Result<Vec<(usize, D, [u8; N])>, Error<F>> {
1110    let grafting_height = grafting::height::<N>();
1111
1112    // Each graftable chunk has a single h=G ancestor at the deterministic
1113    // `subtree_root_position(chunk_idx << G, G)`.
1114    let chunks: Vec<(usize, [u8; N])> = chunks.into_iter().collect();
1115    let positions: Vec<Position<F>> = chunks
1116        .iter()
1117        .map(|&(chunk_idx, _)| {
1118            let leaf_start = Location::<F>::new((chunk_idx as u64) << grafting_height);
1119            F::subtree_root_position(leaf_start, grafting_height)
1120        })
1121        .collect();
1122
1123    // Chunk indices ascend and subtree roots ascend with their leaf ranges, satisfying
1124    // `get_nodes`'s ordering requirement.
1125    let nodes = ops_tree.get_nodes(&positions).await?;
1126    Ok(chunks
1127        .into_iter()
1128        .zip(nodes)
1129        .map(|((chunk_idx, chunk), chunk_ops_digest)| (chunk_idx, chunk_ops_digest, chunk))
1130        .collect())
1131}
1132
1133/// Compute grafted leaf digests for the given bitmap chunks as `(chunk_idx, digest)` pairs.
1134///
1135/// See [`read_graft_inputs`] for the chunk requirements. The grafted leaf digest is `hash(chunk ||
1136/// ops_h_G_node)`; for all-zero chunks the grafted leaf equals the ops digest directly (zero-chunk
1137/// identity).
1138///
1139/// The provided strategy determines if or how to parallelize merkleization.
1140pub(super) async fn compute_grafted_leaves<
1141    F: merkle::Graftable,
1142    H: Hasher,
1143    S: Strategy,
1144    const N: usize,
1145>(
1146    ops_tree: &impl MerkleStorage<F, Digest = H::Digest>,
1147    chunks: impl IntoIterator<Item = (usize, [u8; N])>,
1148    strategy: &S,
1149) -> Result<Vec<(usize, H::Digest)>, Error<F>> {
1150    let inputs = read_graft_inputs::<F, _, N>(ops_tree, chunks).await?;
1151    Ok(grafting::graft_chunk_digests::<H, _, N>(strategy, inputs))
1152}
1153
1154/// Build a grafted [Mem] from scratch using bitmap chunks and the ops tree.
1155///
1156/// For each non-pruned **graftable** chunk (index in `pruned_chunks..graftable_chunks`), reads the
1157/// ops tree node at the grafting height to compute the grafted leaf (see the
1158/// [grafted leaf formula](super) in the module documentation).
1159///
1160/// The most recently completed chunk may not yet be graftable (its h=G ancestor not yet born);
1161/// that chunk is **excluded** from the grafted tree and its digest is hashed directly into
1162/// the canonical root as the pending chunk. The caller must ensure that all ops tree nodes
1163/// for chunks `>= pruned_chunks` are still accessible in the ops tree (i.e., not pruned from
1164/// the journal).
1165///
1166/// `ops_leaves` must be a single consistent snapshot of `ops_tree.size()` taken in the same
1167/// instant as the bitmap state.
1168pub(super) async fn build_grafted_tree<
1169    F: merkle::Graftable,
1170    H: Hasher,
1171    S: Strategy,
1172    const N: usize,
1173>(
1174    bitmap: &impl bitmap::Readable<N>,
1175    pinned_nodes: &[H::Digest],
1176    ops_tree: &impl MerkleStorage<F, Digest = H::Digest>,
1177    ops_leaves: Location<F>,
1178    strategy: &S,
1179) -> Result<Mem<F, H::Digest>, Error<F>> {
1180    let grafting_height = grafting::height::<N>();
1181    let pruned_chunks = bitmap.pruned_chunks();
1182    let complete_chunks = bitmap.complete_chunks();
1183    let graftable_chunks = grafting::graftable_chunks::<F>(*ops_leaves, grafting_height)
1184        .min(complete_chunks as u64) as usize;
1185    assert!(
1186        pruned_chunks <= graftable_chunks && graftable_chunks <= complete_chunks,
1187        "invariant violated: pruned={pruned_chunks} graftable={graftable_chunks} complete={complete_chunks}"
1188    );
1189
1190    // Compute grafted leaves for each unpruned graftable chunk. The pending chunk (if any)
1191    // sits at index `graftable_chunks` and is excluded; its digest is hashed directly into
1192    // the canonical root.
1193    let leaves = compute_grafted_leaves::<F, H, S, N>(
1194        ops_tree,
1195        (pruned_chunks..graftable_chunks).map(|chunk_idx| (chunk_idx, bitmap.get_chunk(chunk_idx))),
1196        strategy,
1197    )
1198    .await?;
1199
1200    // Build the base grafted tree: either from pruned components or empty.
1201    let mut grafted_tree = if pruned_chunks > 0 {
1202        let grafted_pruning_boundary = Location::<F>::new(pruned_chunks as u64);
1203        Mem::from_components(Vec::new(), grafted_pruning_boundary, pinned_nodes.to_vec())
1204            .map_err(|_| Error::<F>::DataCorrupted("grafted tree rebuild failed"))?
1205    } else {
1206        Mem::new()
1207    };
1208
1209    // Add each grafted leaf digest.
1210    if !leaves.is_empty() {
1211        let batch = {
1212            let batch = grafted_tree.new_batch_with_strategy(strategy.clone());
1213            let batch = batch.add_leaf_digests(leaves.iter().map(|&(_, digest)| digest));
1214            let grafted_hasher = grafting::hasher::<F, H>(grafting_height);
1215            batch.merkleize(&grafted_tree, &grafted_hasher)
1216        };
1217        grafted_tree.apply_batch(&batch)?;
1218    }
1219
1220    Ok(grafted_tree)
1221}
1222
1223/// Load the metadata and recover the pruning state persisted by previous runs.
1224///
1225/// The metadata store holds two kinds of entries (keyed by prefix):
1226/// - **Pruned chunks count** ([PRUNED_CHUNKS_PREFIX]): the number of bitmap chunks that have been
1227///   pruned. This tells us where the active portion of the bitmap begins.
1228/// - **Pinned node digests** ([NODE_PREFIX]): grafted tree digests at peak positions whose
1229///   underlying data has been pruned. These are needed to recompute the grafted tree root without
1230///   the pruned chunks.
1231///
1232/// Returns `(metadata_handle, pruned_chunks, pinned_node_digests)`.
1233pub(super) async fn init_metadata<F: merkle::Graftable, E: Context, D: Digest>(
1234    context: E,
1235    partition: &str,
1236) -> Result<(Metadata<E, U64, Vec<u8>>, usize, Vec<D>), Error<F>> {
1237    let metadata_cfg = MConfig {
1238        partition: partition.into(),
1239        codec_config: ((0..).into(), ()),
1240    };
1241    let metadata =
1242        Metadata::<_, U64, Vec<u8>>::init(context.child("metadata"), metadata_cfg).await?;
1243
1244    let key = U64::new(PRUNED_CHUNKS_PREFIX, 0);
1245    let pruned_chunks = match metadata.get(&key) {
1246        Some(bytes) => u64::from_be_bytes(bytes.as_slice().try_into().map_err(|_| {
1247            error!("pruned chunks value not a valid u64");
1248            Error::<F>::DataCorrupted("pruned chunks value not a valid u64")
1249        })?),
1250        None => {
1251            warn!("bitmap metadata does not contain pruned chunks, initializing as empty");
1252            0
1253        }
1254    } as usize;
1255
1256    // Load pinned nodes if database was pruned. We use nodes_to_pin on the grafted leaf count
1257    // to determine how many peaks to read. (Multiplying pruned_chunks by chunk_size is a
1258    // left-shift, preserving popcount, so the peak count is the same in grafted or ops space.)
1259    let pinned_nodes = if pruned_chunks > 0 {
1260        let pruned_loc = Location::<F>::new(pruned_chunks as u64);
1261        if !pruned_loc.is_valid() {
1262            return Err(Error::DataCorrupted("pruned chunks exceeds MAX_LEAVES"));
1263        }
1264        let mut pinned = Vec::new();
1265        for (index, _pos) in F::nodes_to_pin(pruned_loc).enumerate() {
1266            let metadata_key = U64::new(NODE_PREFIX, index as u64);
1267            let Some(bytes) = metadata.get(&metadata_key) else {
1268                return Err(Error::DataCorrupted(
1269                    "missing pinned node in grafted tree metadata",
1270                ));
1271            };
1272            let digest = D::decode(bytes.as_ref())
1273                .map_err(|_| Error::<F>::DataCorrupted("invalid pinned node digest"))?;
1274            pinned.push(digest);
1275        }
1276        pinned
1277    } else {
1278        Vec::new()
1279    };
1280
1281    Ok((metadata, pruned_chunks, pinned_nodes))
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use super::*;
1287    use crate::{
1288        merkle::{Bagging::ForwardFold, hasher::Standard as StandardHasher, mmb, mmr},
1289        qmdb::{
1290            any::traits::{DbAny, UnmerkleizedBatch as _},
1291            current::{tests::fixed_config, unordered::fixed},
1292        },
1293        translator::OneCap,
1294    };
1295    use commonware_codec::FixedSize;
1296    use commonware_cryptography::{Sha256, sha256};
1297    use commonware_macros::test_traced;
1298    use commonware_runtime::{Runner as _, Supervisor as _, deterministic};
1299    use commonware_utils::bitmap::Prunable as PrunableBitMap;
1300
1301    const N: usize = sha256::Digest::SIZE;
1302
1303    #[test]
1304    fn partial_chunk_single_bit() {
1305        let mut bm = PrunableBitMap::<N>::new();
1306        bm.push(true);
1307        let result = partial_chunk::<PrunableBitMap<N>, N>(&bm);
1308        assert!(result.is_some());
1309        let (chunk, next_bit) = result.unwrap();
1310        assert_eq!(next_bit, 1);
1311        assert_eq!(chunk[0], 1); // bit 0 set
1312    }
1313
1314    #[test]
1315    fn partial_chunk_aligned() {
1316        let mut bm = PrunableBitMap::<N>::new();
1317        for _ in 0..PrunableBitMap::<N>::CHUNK_SIZE_BITS {
1318            bm.push(true);
1319        }
1320        let result = partial_chunk::<PrunableBitMap<N>, N>(&bm);
1321        assert!(result.is_none());
1322    }
1323
1324    #[test]
1325    fn partial_chunk_partial() {
1326        let mut bm = PrunableBitMap::<N>::new();
1327        for _ in 0..(PrunableBitMap::<N>::CHUNK_SIZE_BITS + 5) {
1328            bm.push(true);
1329        }
1330        let result = partial_chunk::<PrunableBitMap<N>, N>(&bm);
1331        assert!(result.is_some());
1332        let (_chunk, next_bit) = result.unwrap();
1333        assert_eq!(next_bit, 5);
1334    }
1335
1336    #[test]
1337    fn partial_chunk_empty() {
1338        // An empty bitmap has no partial chunk, and must not panic on `last_chunk`.
1339        let bm = PrunableBitMap::<N>::new();
1340        assert!(partial_chunk::<PrunableBitMap<N>, N>(&bm).is_none());
1341    }
1342
1343    #[test]
1344    fn partial_chunk_fully_pruned() {
1345        // A fully-pruned bitmap is chunk-aligned but its backing store is empty; still no partial
1346        // chunk, and must not panic on `last_chunk`.
1347        let bm = PrunableBitMap::<N>::new_with_pruned_chunks(1).unwrap();
1348        assert!(partial_chunk::<PrunableBitMap<N>, N>(&bm).is_none());
1349    }
1350
1351    #[test]
1352    fn combine_roots_deterministic() {
1353        let ops = Sha256::hash(&[b"ops"]);
1354        let grafted = Sha256::hash(&[b"grafted"]);
1355        let r1 = combine_roots::<Sha256>(&ops, &grafted, None, None);
1356        let r2 = combine_roots::<Sha256>(&ops, &grafted, None, None);
1357        assert_eq!(r1, r2);
1358    }
1359
1360    #[test]
1361    fn combine_roots_with_partial_differs() {
1362        let ops = Sha256::hash(&[b"ops"]);
1363        let grafted = Sha256::hash(&[b"grafted"]);
1364        let partial_digest = Sha256::hash(&[b"partial"]);
1365
1366        let without = combine_roots::<Sha256>(&ops, &grafted, None, None);
1367        let with = combine_roots::<Sha256>(&ops, &grafted, None, Some((5, &partial_digest)));
1368        assert_ne!(without, with);
1369    }
1370
1371    #[test]
1372    fn combine_roots_with_pending_differs() {
1373        let ops = Sha256::hash(&[b"ops"]);
1374        let grafted = Sha256::hash(&[b"grafted"]);
1375        let pending_digest = Sha256::hash(&[b"pending"]);
1376
1377        let without = combine_roots::<Sha256>(&ops, &grafted, None, None);
1378        let with = combine_roots::<Sha256>(&ops, &grafted, Some(&pending_digest), None);
1379        assert_ne!(without, with);
1380    }
1381
1382    #[test]
1383    fn combine_roots_pending_and_partial_independent() {
1384        let ops = Sha256::hash(&[b"ops"]);
1385        let grafted = Sha256::hash(&[b"grafted"]);
1386        let pending_digest = Sha256::hash(&[b"pending"]);
1387        let partial_digest = Sha256::hash(&[b"partial"]);
1388
1389        let only_pending = combine_roots::<Sha256>(&ops, &grafted, Some(&pending_digest), None);
1390        let only_partial =
1391            combine_roots::<Sha256>(&ops, &grafted, None, Some((5, &partial_digest)));
1392        let both = combine_roots::<Sha256>(
1393            &ops,
1394            &grafted,
1395            Some(&pending_digest),
1396            Some((5, &partial_digest)),
1397        );
1398        assert_ne!(only_pending, only_partial);
1399        assert_ne!(only_pending, both);
1400        assert_ne!(only_partial, both);
1401    }
1402
1403    #[test]
1404    fn combine_roots_different_ops_root() {
1405        let ops_a = Sha256::hash(&[b"ops_a"]);
1406        let ops_b = Sha256::hash(&[b"ops_b"]);
1407        let grafted = Sha256::hash(&[b"grafted"]);
1408
1409        let r1 = combine_roots::<Sha256>(&ops_a, &grafted, None, None);
1410        let r2 = combine_roots::<Sha256>(&ops_b, &grafted, None, None);
1411        assert_ne!(r1, r2);
1412    }
1413
1414    /// Pin the canonical-root format down to the byte. A change to `combine_roots`'s hash
1415    /// pre-image (e.g., reordering, dropping a length tag, swapping pending/partial order)
1416    /// would silently break wire compatibility; this test catches that.
1417    #[test]
1418    fn combine_roots_format_golden() {
1419        let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1420        let ops = Sha256::hash(&[b"ops"]);
1421        let grafted = Sha256::hash(&[b"grafted"]);
1422        let pending = Sha256::hash(&[b"pending"]);
1423        let partial = Sha256::hash(&[b"partial"]);
1424        let next_bit: u64 = 0x1122_3344_5566_7788;
1425
1426        // Neither pending nor partial.
1427        assert_eq!(
1428            combine_roots::<Sha256>(&ops, &grafted, None, None),
1429            hasher.hash(&[ops.as_ref(), grafted.as_ref()])
1430        );
1431
1432        // Pending only.
1433        assert_eq!(
1434            combine_roots::<Sha256>(&ops, &grafted, Some(&pending), None),
1435            hasher.hash(&[ops.as_ref(), grafted.as_ref(), pending.as_ref()])
1436        );
1437
1438        // Partial only.
1439        assert_eq!(
1440            combine_roots::<Sha256>(&ops, &grafted, None, Some((next_bit, &partial))),
1441            hasher.hash(&[
1442                ops.as_ref(),
1443                grafted.as_ref(),
1444                next_bit.to_be_bytes().as_slice(),
1445                partial.as_ref(),
1446            ])
1447        );
1448
1449        // Both: pending precedes partial.
1450        assert_eq!(
1451            combine_roots::<Sha256>(&ops, &grafted, Some(&pending), Some((next_bit, &partial))),
1452            hasher.hash(&[
1453                ops.as_ref(),
1454                grafted.as_ref(),
1455                pending.as_ref(),
1456                next_bit.to_be_bytes().as_slice(),
1457                partial.as_ref(),
1458            ])
1459        );
1460    }
1461
1462    type MmrDb = fixed::Db<
1463        mmr::Family,
1464        deterministic::Context,
1465        sha256::Digest,
1466        sha256::Digest,
1467        Sha256,
1468        OneCap,
1469        32,
1470        commonware_parallel::Sequential,
1471    >;
1472    type MmbDb = fixed::Db<
1473        mmb::Family,
1474        deterministic::Context,
1475        sha256::Digest,
1476        sha256::Digest,
1477        Sha256,
1478        OneCap,
1479        32,
1480        commonware_parallel::Sequential,
1481    >;
1482
1483    #[boxed]
1484    async fn populate_fixed_db<F, DB>(db: DB, start: u64, count: u64) -> DB
1485    where
1486        F: merkle::Graftable,
1487        DB: DbAny<F, Key = sha256::Digest, Value = sha256::Digest>,
1488    {
1489        let mut batch = db.new_batch();
1490        for idx in start..start + count {
1491            let key = Sha256::hash(&[&idx.to_be_bytes()]);
1492            let value = Sha256::hash(&[&(idx + count).to_be_bytes()]);
1493            batch = batch.write(key, Some(value));
1494        }
1495        let merkleized = batch.merkleize(&db, None).await.unwrap();
1496        let (db, _) = db.apply_batch(merkleized).await.unwrap();
1497        db.commit().await.unwrap()
1498    }
1499
1500    /// `operations()` on a current batch must cover exactly the batch's own applied range
1501    /// in the ops log.
1502    #[test_traced]
1503    fn test_operations_match_applied_range() {
1504        let executor = deterministic::Runner::default();
1505        executor.start(|ctx| async move {
1506            let db = MmrDb::init(
1507                ctx.child("db"),
1508                fixed_config::<OneCap>("operations-match-applied-range", &ctx),
1509            )
1510            .await
1511            .unwrap();
1512            let db = populate_fixed_db::<mmr::Family, _>(db, 0, 8).await;
1513
1514            let mut batch = db.new_batch();
1515            for idx in 0..4u64 {
1516                let key = Sha256::hash(&[&idx.to_be_bytes()]);
1517                let value = Sha256::hash(&[&(idx + 100).to_be_bytes()]);
1518                batch = batch.write(key, Some(value));
1519            }
1520            let merkleized = batch.merkleize(&db, None).await.unwrap();
1521            let (start, ops) = merkleized.operations();
1522            let (db, range) = db.apply_batch(merkleized).await.unwrap();
1523            assert_eq!(start, range.start);
1524            assert_eq!(*start + ops.len() as u64, *range.end);
1525            db.destroy().await.unwrap();
1526        });
1527    }
1528
1529    /// State committed via an awaited start_sync handle is recovered on reopen, including the
1530    /// grafted bitmap contribution to the root.
1531    #[test_traced]
1532    fn test_start_sync_recovery() {
1533        let executor = deterministic::Runner::default();
1534        executor.start(|ctx| async move {
1535            let db = MmrDb::init(
1536                ctx.child("first"),
1537                fixed_config::<OneCap>("start-sync-recovery", &ctx),
1538            )
1539            .await
1540            .unwrap();
1541            let key = Sha256::hash(&[&0u64.to_be_bytes()]);
1542            let value = Sha256::hash(&[&1u64.to_be_bytes()]);
1543            let merkleized = db
1544                .new_batch()
1545                .write(key, Some(value))
1546                .merkleize(&db, None)
1547                .await
1548                .unwrap();
1549            let (db, _) = db.apply_batch(merkleized).await.unwrap();
1550            let (db, handle) = db.start_sync().await.unwrap();
1551            handle.await.unwrap();
1552            let root = db.root();
1553            drop(db);
1554
1555            let db = MmrDb::init(
1556                ctx.child("second"),
1557                fixed_config::<OneCap>("start-sync-recovery", &ctx),
1558            )
1559            .await
1560            .unwrap();
1561            assert_eq!(db.root(), root);
1562            assert_eq!(db.get(&key).await.unwrap(), Some(value));
1563            db.destroy().await.unwrap();
1564        });
1565    }
1566
1567    /// A prune dropped between the pruning-metadata sync and the log prune must remain
1568    /// recoverable: the metadata durably records a bitmap boundary derived from a floor that
1569    /// may exist only in buffered operations, and reopening panics if the recovered floor
1570    /// lies below that boundary.
1571    #[test_traced]
1572    fn test_current_prune_dropped_before_log_prune() {
1573        let executor = deterministic::Runner::default();
1574        executor.start(|ctx| async move {
1575            let db = MmrDb::init(
1576                ctx.child("storage"),
1577                fixed_config::<OneCap>("prune-park", &ctx),
1578            )
1579            .await
1580            .unwrap();
1581
1582            // Establish a durable state, then apply (but do not commit) a batch that rewrites
1583            // every key, advancing the in-memory floor well past the durable commit's floor.
1584            let db = populate_fixed_db::<mmr::Family, _>(db, 0, 512).await;
1585            let durable_floor = db.inactivity_floor_loc();
1586            let mut batch = db.new_batch();
1587            for idx in 0..512u64 {
1588                let key = Sha256::hash(&[&idx.to_be_bytes()]);
1589                let value = Sha256::hash(&[&(idx + 1024).to_be_bytes()]);
1590                batch = batch.write(key, Some(value));
1591            }
1592            let merkleized = batch.merkleize(&db, None).await.unwrap();
1593            let (mut db, _) = db.apply_batch(merkleized).await.unwrap();
1594            assert!(db.sync_boundary() > durable_floor);
1595            let bounds = db.bounds();
1596            let floor = db.inactivity_floor_loc();
1597            let root = db.root();
1598
1599            // Drop the production prune future while it is parked after the metadata sync,
1600            // before the log prune: a genuine cancellation at that await.
1601            db.halt_before_prune_log = true;
1602            let boundary = db.sync_boundary();
1603            {
1604                let fut = db.prune(boundary);
1605                futures::pin_mut!(fut);
1606                assert!(
1607                    futures::poll!(fut.as_mut()).is_pending(),
1608                    "prune must park before the log prune"
1609                );
1610            }
1611
1612            // Reopening must succeed and recover the post-batch state: prune committed the
1613            // buffered operations before durably recording the pruning metadata that depends
1614            // on them. Asserting the advanced floor, root, and persisted pruned boundary
1615            // proves the drop happened after both the commit and the metadata sync.
1616            let db = MmrDb::init(
1617                ctx.child("reopen"),
1618                fixed_config::<OneCap>("prune-park", &ctx),
1619            )
1620            .await
1621            .expect("prune crash must leave the db recoverable");
1622            assert_eq!(db.bounds(), bounds);
1623            assert_eq!(db.inactivity_floor_loc(), floor);
1624            assert_eq!(db.root(), root);
1625            assert!(db.any.bitmap.pruned_bits() > *durable_floor);
1626            db.destroy().await.unwrap();
1627        });
1628    }
1629
1630    #[test_traced]
1631    fn test_ops_root_witness_verifies_without_partial_chunk() {
1632        let executor = deterministic::Runner::default();
1633        executor.start(|ctx| async move {
1634            let mut db = MmrDb::init(
1635                ctx.child("storage"),
1636                fixed_config::<OneCap>("ops-root-witness-full", &ctx),
1637            )
1638            .await
1639            .unwrap();
1640            let mut next_idx = 0;
1641            db = populate_fixed_db::<mmr::Family, _>(db, next_idx, 256).await;
1642            next_idx += 256;
1643            while partial_chunk::<_, 32>(db.any.bitmap.as_ref()).is_some() {
1644                db = populate_fixed_db::<mmr::Family, _>(db, next_idx, 1).await;
1645                next_idx += 1;
1646            }
1647            let witness = db.ops_root_witness().await.unwrap();
1648            let ops_root = db.ops_root();
1649            let canonical_root = db.root();
1650
1651            assert!(witness.partial_chunk.is_none());
1652            assert!(witness.verify::<Sha256>(&ops_root, &canonical_root));
1653
1654            let wrong_ops_root = Sha256::hash(&[b"wrong ops root"]);
1655            assert!(!witness.verify::<Sha256>(&wrong_ops_root, &canonical_root));
1656
1657            let wrong_canonical_root = Sha256::hash(&[b"wrong canonical root"]);
1658            assert!(!witness.verify::<Sha256>(&ops_root, &wrong_canonical_root));
1659
1660            let mut tampered = witness;
1661            tampered.grafted_root = Sha256::hash(&[b"wrong grafted root"]);
1662            assert!(!tampered.verify::<Sha256>(&ops_root, &canonical_root));
1663        });
1664    }
1665
1666    #[test_traced]
1667    fn test_ops_root_witness_verifies_with_partial_chunk() {
1668        let executor = deterministic::Runner::default();
1669        executor.start(|ctx| async move {
1670            let db = MmbDb::init(
1671                ctx.child("storage"),
1672                fixed_config::<OneCap>("ops-root-witness-partial", &ctx),
1673            )
1674            .await
1675            .unwrap();
1676            let db = populate_fixed_db::<mmb::Family, _>(db, 0, 260).await;
1677            let witness = db.ops_root_witness().await.unwrap();
1678            let ops_root = db.ops_root();
1679            let canonical_root = db.root();
1680
1681            assert!(witness.partial_chunk.is_some());
1682            assert!(witness.verify::<Sha256>(&ops_root, &canonical_root));
1683
1684            let wrong_ops_root = Sha256::hash(&[b"wrong ops root"]);
1685            assert!(!witness.verify::<Sha256>(&wrong_ops_root, &canonical_root));
1686
1687            let wrong_canonical_root = Sha256::hash(&[b"wrong canonical root"]);
1688            assert!(!witness.verify::<Sha256>(&ops_root, &wrong_canonical_root));
1689
1690            let mut tampered = witness.clone();
1691            tampered.grafted_root = Sha256::hash(&[b"wrong grafted root"]);
1692            assert!(!tampered.verify::<Sha256>(&ops_root, &canonical_root));
1693
1694            let mut tampered = witness.clone();
1695            tampered.partial_chunk.as_mut().unwrap().0 += 1;
1696            assert!(!tampered.verify::<Sha256>(&ops_root, &canonical_root));
1697
1698            let mut tampered = witness;
1699            tampered.partial_chunk.as_mut().unwrap().1 = Sha256::hash(&[b"wrong partial chunk"]);
1700            assert!(!tampered.verify::<Sha256>(&ops_root, &canonical_root));
1701        });
1702    }
1703
1704    #[test_traced]
1705    fn test_ops_root_witness_verifies_with_pruned_db() {
1706        let executor = deterministic::Runner::default();
1707        executor.start(|ctx| async move {
1708            let mut db = MmrDb::init(
1709                ctx.child("storage"),
1710                fixed_config::<OneCap>("ops-root-witness-pruned", &ctx),
1711            )
1712            .await
1713            .unwrap();
1714
1715            // Churn the same keys repeatedly to drive the inactivity floor past chunk boundaries.
1716            for _ in 0..5 {
1717                db = populate_fixed_db::<mmr::Family, _>(db, 0, 512).await;
1718            }
1719            let boundary = db.sync_boundary();
1720            let db = db.prune(boundary).await.unwrap();
1721            assert!(
1722                db.any.bitmap.pruned_chunks() > 0,
1723                "test requires at least one pruned chunk to exercise the zero-chunk path"
1724            );
1725            let witness = db.ops_root_witness().await.unwrap();
1726            let ops_root = db.ops_root();
1727            let canonical_root = db.root();
1728
1729            assert!(witness.verify::<Sha256>(&ops_root, &canonical_root));
1730
1731            let wrong_canonical_root = Sha256::hash(&[b"wrong canonical root"]);
1732            assert!(!witness.verify::<Sha256>(&ops_root, &wrong_canonical_root));
1733
1734            let mut tampered = witness;
1735            tampered.grafted_root = Sha256::hash(&[b"wrong grafted root"]);
1736            assert!(!tampered.verify::<Sha256>(&ops_root, &canonical_root));
1737        });
1738    }
1739
1740    #[test_traced]
1741    fn test_ops_root_witness_verifies_on_fresh_db() {
1742        let executor = deterministic::Runner::default();
1743        executor.start(|ctx| async move {
1744            let db = MmrDb::init(
1745                ctx.child("storage"),
1746                fixed_config::<OneCap>("ops-root-witness-fresh", &ctx),
1747            )
1748            .await
1749            .unwrap();
1750            let witness = db.ops_root_witness().await.unwrap();
1751            let ops_root = db.ops_root();
1752            let canonical_root = db.root();
1753
1754            assert!(witness.verify::<Sha256>(&ops_root, &canonical_root));
1755        });
1756    }
1757}