Skip to main content

commonware_storage/qmdb/immutable/
compact.rs

1//! An immutable authenticated db that discards historical operations, retaining only a witness
2//! for each applied batch.
3//!
4//! Mirrors the API of [`crate::qmdb::immutable::Immutable`] (`new_batch -> merkleize ->
5//! apply_batch -> commit / sync / start_sync`, pipelined batch chains, `StaleBatch` validation)
6//! but is backed by the peak-only [`crate::merkle::compact`]. Because history is discarded,
7//! the db has no `get` / `proof` / `bounds` methods. A merkleized batch can prove only its own
8//! operations, and only until it is applied. Use the full variant for historical proofs.
9//!
10//! # Witness journal
11//!
12//! The witness journal holds a complete snapshot of every applied batch. [`Db::init`]
13//! restores a retained applied state within its operation cap. [`Db::prune`] bounds the retained
14//! history. Initialization restores the db's in-memory state from an entry. The Merkle is rebuilt
15//! from the stored pinned nodes and operation, and the commit fields are decoded from the
16//! operation. An entry that cannot rebuild surfaces as [`Error::DataCorrupted`]. Each retained
17//! witness also serves compact sync at its size without retaining historical operations.
18//!
19//! # Inactivity floor
20//!
21//! Commits carry an inactivity floor for wire-format compatibility with
22//! [`crate::qmdb::immutable::Immutable`]: the root is computed over the encoded operation
23//! sequence, and that sequence must include the same floor to produce the same root as the
24//! full variant. The floor has no effect on pruning or snapshot rebuilding here; all
25//! historical in-memory state is discarded whenever a batch is applied.
26
27use super::operation::Operation;
28pub use crate::qmdb::compact::Config;
29use crate::{
30    Context,
31    merkle::{Family, Location, Proof, batch, compact as compact_merkle},
32    qmdb::{
33        self, Error,
34        any::value::ValueEncoding,
35        chain::{self, Bounds, Commitment},
36        compact::{batch as compact_batch, witness},
37        operation::Key,
38        sync::{CompactTarget, Request, Source, source},
39    },
40};
41use commonware_codec::{Encode, EncodeShared, Read};
42use commonware_cryptography::{Digest, Hasher};
43use commonware_macros::boxed;
44use commonware_parallel::Strategy;
45use commonware_runtime::Handle;
46use core::marker::PhantomData;
47use std::{
48    collections::BTreeMap,
49    sync::{Arc, Weak},
50};
51
52/// An immutable authenticated db that discards historical operations, retaining only a witness
53/// for each applied batch.
54pub struct Db<F, E, K, V, H, S: Strategy>
55where
56    F: Family,
57    E: Context,
58    K: Key,
59    V: ValueEncoding,
60    H: Hasher,
61    Operation<F, K, V>: EncodeShared,
62    Operation<F, K, V>: Read,
63{
64    merkle: compact_merkle::Merkle<F, H::Digest, S>,
65    last_commit_metadata: Option<V::Value>,
66    inactivity_floor_loc: Location<F>,
67    commit_codec_config: <Operation<F, K, V> as Read>::Cfg,
68    witness: witness::Store<E, F, H::Digest>,
69    _key: PhantomData<K>,
70}
71
72impl<F, E, K, V, H, S: Strategy> std::fmt::Debug for Db<F, E, K, V, H, S>
73where
74    F: Family,
75    E: Context,
76    K: Key,
77    V: ValueEncoding,
78    H: Hasher,
79    Operation<F, K, V>: EncodeShared,
80    Operation<F, K, V>: Read,
81{
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("Db")
84            .field("size", &self.size())
85            .field("inactivity_floor_loc", &self.inactivity_floor_loc())
86            .finish_non_exhaustive()
87    }
88}
89
90/// Result of merkleizing a batch.
91type MerkleizeResult<F, D, K, V, S> = Result<Arc<MerkleizedBatch<F, D, K, V, S>>, Error<F>>;
92
93/// A speculative batch for a compact immutable db.
94#[allow(clippy::type_complexity)]
95pub struct UnmerkleizedBatch<F, H, K, V, S: Strategy>
96where
97    F: Family,
98    K: Key,
99    V: ValueEncoding,
100    H: Hasher,
101    Operation<F, K, V>: EncodeShared,
102{
103    merkle_batch: compact_merkle::UnmerkleizedBatch<F, H::Digest, S>,
104    mutations: BTreeMap<K, V::Value>,
105    parent: Option<Arc<MerkleizedBatch<F, H::Digest, K, V, S>>>,
106    base: chain::Commitment<F, H::Digest>,
107}
108
109/// A speculative batch whose root digest has been computed.
110#[derive(Clone)]
111pub struct MerkleizedBatch<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy>
112where
113    Operation<F, K, V>: EncodeShared,
114{
115    pub(super) merkle_batch: Arc<batch::MerkleizedBatch<F, D, S>>,
116    operations: Arc<Vec<Operation<F, K, V>>>,
117    pub(super) commit_metadata: Option<V::Value>,
118    pub(super) parent: Option<Weak<Self>>,
119    pub(super) bounds: chain::Bounds<F, D>,
120}
121
122impl<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy> MerkleizedBatch<F, D, K, V, S>
123where
124    Operation<F, K, V>: EncodeShared,
125{
126    pub(super) fn ancestors(&self) -> impl Iterator<Item = Arc<Self>> + use<F, D, K, V, S> {
127        chain::ancestors(self.parent.clone(), |batch| batch.parent.as_ref())
128    }
129
130    /// The [`Commitment`] this batch commits to.
131    pub(super) const fn commitment(&self) -> Commitment<F, D> {
132        self.bounds.tip
133    }
134
135    /// Return the root digest after this batch is applied.
136    pub const fn root(&self) -> D {
137        self.bounds.tip.root
138    }
139
140    /// Return the [`Bounds`] of the batch.
141    pub const fn bounds(&self) -> &Bounds<F, D> {
142        &self.bounds
143    }
144
145    /// Return the operations this batch appends to the log and the location of the first.
146    #[allow(clippy::type_complexity)]
147    pub fn operations(&self) -> (Location<F>, Arc<Vec<Operation<F, K, V>>>) {
148        (self.bounds.base.size, Arc::clone(&self.operations))
149    }
150
151    /// Inclusion proof for the operations returned by [`Self::operations`], anchored at
152    /// this batch's tip. The pair verifies against [`Self::root`] via
153    /// [`crate::qmdb::verify_proof`]. Together with [`Self::pinned_nodes`] they verify via
154    /// [`crate::qmdb::verify_proof_and_pinned_nodes`].
155    ///
156    /// Nodes of unapplied ancestors are read through the chain, so those ancestors must still be
157    /// held. Nodes below the chain are read from `db`'s [Merkle store][crate::merkle::mem::Mem],
158    /// which retains them at least until this batch's changes are applied (applying it or a
159    /// descendant prunes the store to its frontier).
160    ///
161    /// # Errors
162    ///
163    /// Returns [`Error::StaleRead`] if `db` is not at a state of this batch's chain,
164    /// [`crate::merkle::Error::ElementPruned`] if a required node has been pruned or belongs to a
165    /// dropped unapplied ancestor, and [`crate::merkle::Error::Empty`] if the batch has no
166    /// operations (such as a batch from [`Db::to_batch`]).
167    pub fn proof<E, H>(&self, db: &Db<F, E, K, V, H, S>) -> Result<Proof<F, D>, Error<F>>
168    where
169        E: Context,
170        H: Hasher<Digest = D>,
171        Operation<F, K, V>: Read,
172    {
173        let db = self.bounds.compatible(db, db.commitment())?;
174        let inactive_peaks = F::inactive_peaks(self.bounds.tip.size, self.bounds.inactivity_floor);
175        let hasher = qmdb::hasher::<H>();
176        self.merkle_batch
177            .range_proof(
178                db.merkle.mem(),
179                &hasher,
180                self.bounds.base.size..self.bounds.tip.size,
181                inactive_peaks,
182            )
183            .map_err(Into::into)
184    }
185
186    /// The Merkle frontier at the first operation returned by [`Self::operations`]
187    /// ([`Family::nodes_to_pin`]), which lets a consumer holding only this batch's base rebuild
188    /// compact state and replay the operations. The operations, [`Self::proof`], and pinned
189    /// nodes verify against [`Self::root`] via [`crate::qmdb::verify_proof_and_pinned_nodes`].
190    ///
191    /// Nodes of unapplied ancestors are read through the chain, so those ancestors must still be
192    /// held. Nodes below the chain are read from `db`'s [Merkle store][crate::merkle::mem::Mem],
193    /// which retains them at least until this batch's changes are applied (applying it or a
194    /// descendant prunes the store to its frontier).
195    ///
196    /// # Errors
197    ///
198    /// Returns [`Error::StaleRead`] if `db` is not at a state of this batch's chain, and
199    /// [`crate::merkle::Error::ElementPruned`] if a required node has been pruned or belongs to a
200    /// dropped unapplied ancestor.
201    pub fn pinned_nodes<E, H>(&self, db: &Db<F, E, K, V, H, S>) -> Result<Vec<D>, Error<F>>
202    where
203        E: Context,
204        H: Hasher<Digest = D>,
205        Operation<F, K, V>: Read,
206    {
207        let db = self.bounds.compatible(db, db.commitment())?;
208        let base = db.merkle.mem();
209        F::nodes_to_pin(self.bounds.base.size)
210            .map(|pos| {
211                self.merkle_batch
212                    .get_node(pos)
213                    .or_else(|| base.get_node(pos))
214                    .ok_or(crate::merkle::Error::ElementPruned(pos))
215            })
216            .collect::<Result<Vec<_>, _>>()
217            .map_err(Into::into)
218    }
219
220    /// Create a new speculative batch with this one as its parent.
221    ///
222    /// Every unapplied ancestor in the chain must be held until the child (or any descendant) is
223    /// merkleized. Otherwise, `merkleize` returns [`Error::StaleBatch`].
224    pub fn new_batch<H>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, K, V, S>
225    where
226        H: Hasher<Digest = D>,
227    {
228        UnmerkleizedBatch {
229            merkle_batch: compact_merkle::UnmerkleizedBatch::wrap(self.merkle_batch.new_batch()),
230            mutations: BTreeMap::new(),
231            parent: Some(Arc::clone(self)),
232            base: self.commitment(),
233        }
234    }
235}
236
237impl<F, H, K, V, S> UnmerkleizedBatch<F, H, K, V, S>
238where
239    F: Family,
240    K: Key,
241    V: ValueEncoding,
242    H: Hasher,
243    S: Strategy,
244    Operation<F, K, V>: EncodeShared,
245{
246    pub(super) fn new<E>(db: &Db<F, E, K, V, H, S>, base: chain::Commitment<F, H::Digest>) -> Self
247    where
248        E: Context,
249        Operation<F, K, V>: Read,
250    {
251        Self {
252            merkle_batch: db.merkle.new_batch(),
253            mutations: BTreeMap::new(),
254            parent: None,
255            base,
256        }
257    }
258
259    /// The database boundary for this batch chain.
260    ///
261    /// A batch created from the database uses its base. A child inherits its parent's `db`.
262    fn db(&self) -> Commitment<F, H::Digest> {
263        self.parent
264            .as_ref()
265            .map_or(self.base, |parent| parent.bounds.db)
266    }
267
268    pub fn set(mut self, key: K, value: V::Value) -> Self {
269        self.mutations.insert(key, value);
270        self
271    }
272
273    /// Resolve mutations into operations, merkleize, and return the batch.
274    ///
275    /// `inactivity_floor` is threaded through the commit operation for wire-format parity with
276    /// [`crate::qmdb::immutable::Immutable`]. It must be at least the floor this batch builds on
277    /// (its parent's, or the database's) and at most the batch's commit location
278    /// (`total_size - 1`). These bounds are validated, but the floor does not drive any local
279    /// pruning or retention in this variant.
280    ///
281    /// # Errors
282    ///
283    /// - Returns [`Error::StaleBatch`] if `db` is not at a state of this batch's chain or an
284    ///   unapplied ancestor was dropped.
285    /// - Returns [`Error::FloorRegressed`] if `inactivity_floor` is below the floor this batch
286    ///   builds on, and [`Error::FloorBeyondSize`] if it is past the commit location.
287    #[tracing::instrument(
288        name = "qmdb.immutable.compact.batch.merkleize",
289        level = "info",
290        skip_all
291    )]
292    pub async fn merkleize<E>(
293        self,
294        db: &Db<F, E, K, V, H, S>,
295        metadata: Option<V::Value>,
296        inactivity_floor: Location<F>,
297    ) -> MerkleizeResult<F, H::Digest, K, V, S>
298    where
299        E: Context,
300        Operation<F, K, V>: Read,
301    {
302        let live_ancestors: Vec<_> =
303            chain::parent_and_ancestors(self.parent.as_ref(), |parent| parent.ancestors())
304                .collect();
305        let boundary = chain::effective_boundary(
306            self.db(),
307            live_ancestors.last().map(|oldest| oldest.bounds.base),
308        );
309
310        let ancestors: Vec<_> = live_ancestors
311            .iter()
312            .map(|batch| batch.commitment())
313            .collect();
314        let db = chain::merkleizable(db, db.commitment(), boundary, ancestors.iter().copied())?;
315
316        let mut ops: Vec<Operation<F, K, V>> = Vec::with_capacity(self.mutations.len() + 1);
317        for (key, value) in self.mutations {
318            ops.push(Operation::Set(key, value));
319        }
320        ops.push(Operation::Commit(metadata.clone(), inactivity_floor));
321
322        let operations = Arc::new(ops);
323        let total_size = self.base.size + operations.len() as u64;
324        chain::validate_merkleize_floor::<F>(
325            self.parent.as_ref().map_or_else(
326                || db.inactivity_floor_loc(),
327                |parent| parent.bounds.inactivity_floor,
328            ),
329            inactivity_floor,
330            total_size - 1,
331        )?;
332        let inactive_peaks = F::inactive_peaks(total_size, inactivity_floor);
333        let (merkle, root) = compact_batch::merkleize_ops::<F, H, S, _>(
334            &db.merkle,
335            self.merkle_batch,
336            Arc::clone(&operations),
337            inactive_peaks,
338        )
339        .await?;
340
341        // Hold ancestor batches until their operations and nodes have been captured.
342        drop(live_ancestors);
343
344        Ok(Arc::new(MerkleizedBatch {
345            merkle_batch: merkle,
346            operations,
347            commit_metadata: metadata,
348            parent: self.parent.as_ref().map(Arc::downgrade),
349            bounds: chain::Bounds {
350                base: self.base,
351                db: boundary,
352                tip: Commitment::new(total_size, root),
353                ancestors,
354                inactivity_floor,
355            },
356        }))
357    }
358}
359
360impl<F, E, K, V, H, S> Db<F, E, K, V, H, S>
361where
362    F: Family,
363    E: Context,
364    K: Key,
365    V: ValueEncoding,
366    H: Hasher,
367    S: Strategy,
368    Operation<F, K, V>: EncodeShared,
369    Operation<F, K, V>: Read,
370{
371    /// Initialize from a retained witness.
372    /// `Some(max_size)` selects the latest retained witness for at most `max_size` operations.
373    /// `None` selects the latest retained state.
374    ///
375    /// Fresh storage receives a durable bootstrap commit and witness.
376    #[boxed]
377    pub async fn init(
378        context: E,
379        cfg: Config<<Operation<F, K, V> as Read>::Cfg, S>,
380        max_size: Option<Location<F>>,
381    ) -> Result<Self, Error<F>> {
382        let Config {
383            strategy,
384            witness: witness_config,
385            commit_codec_config,
386        } = cfg;
387
388        // Reconstruct and verify the selected witness before exposing any state derived from it.
389        let mut merkle = compact_merkle::Merkle::new(strategy);
390        let (witness, last_commit_op) = witness::init::<E, F, H, S, Operation<F, K, V>>(
391            context.child("witness"),
392            witness_config,
393            max_size,
394            &mut merkle,
395            &commit_codec_config,
396            Operation::<F, K, V>::Commit(None, Location::new(0))
397                .encode()
398                .to_vec(),
399        )
400        .await?;
401
402        let Operation::Commit(last_commit_metadata, inactivity_floor_loc) = last_commit_op else {
403            return Err(Error::DataCorrupted("last operation was not a commit"));
404        };
405
406        Ok(Self {
407            merkle,
408            last_commit_metadata,
409            inactivity_floor_loc,
410            commit_codec_config,
411            witness,
412            _key: PhantomData,
413        })
414    }
415
416    fn encode_commit_op(metadata: Option<V::Value>, inactivity_floor_loc: Location<F>) -> Vec<u8> {
417        Operation::<F, K, V>::Commit(metadata, inactivity_floor_loc)
418            .encode()
419            .to_vec()
420    }
421
422    /// Build a compact db from state fetched by the sync engine.
423    ///
424    /// The imported witness lives only in memory until the first [`Self::apply_batch`],
425    /// [`Self::commit`], [`Self::sync`], or [`Self::start_sync`]. Applying a batch replaces it with
426    /// the newly applied journal checkpoint. A durability method journals it directly. Until one of
427    /// those operations succeeds, prune is rejected.
428    pub(crate) fn init_from_sync(
429        strategy: S,
430        journal: witness::Journal<E, F, H::Digest>,
431        commit_codec_config: <Operation<F, K, V> as Read>::Cfg,
432        last_commit_loc: Location<F>,
433        pinned_nodes: Vec<H::Digest>,
434        last_commit_op: Operation<F, K, V>,
435    ) -> Result<Self, Error<F>> {
436        let Operation::Commit(last_commit_metadata, inactivity_floor_loc) = last_commit_op else {
437            return Err(Error::UnexpectedData(last_commit_loc));
438        };
439        witness::validate_inactivity_floor(inactivity_floor_loc, last_commit_loc)?;
440
441        let op_bytes = Self::encode_commit_op(last_commit_metadata.clone(), inactivity_floor_loc);
442        let mut merkle =
443            compact_merkle::Merkle::from_compact_state(strategy, last_commit_loc, pinned_nodes)?;
444        let hasher = qmdb::hasher::<H>();
445        merkle.append_leaf(&hasher, &op_bytes)?;
446        let imported = witness::build_witness::<F, H, S>(&merkle, inactivity_floor_loc, op_bytes)?;
447        merkle.prune_to_frontier();
448
449        let witness = witness::Store::from_import(journal, imported);
450        Ok(Self {
451            merkle,
452            last_commit_metadata,
453            inactivity_floor_loc,
454            commit_codec_config,
455            witness,
456            _key: PhantomData,
457        })
458    }
459
460    /// Return the root of the db.
461    pub const fn root(&self) -> H::Digest {
462        self.witness.tip().root
463    }
464
465    /// Return the inactivity floor declared by the last committed batch.
466    pub const fn inactivity_floor_loc(&self) -> Location<F> {
467        self.inactivity_floor_loc
468    }
469
470    /// Return the location of the next operation appended to this db.
471    pub const fn size(&self) -> Location<F> {
472        self.witness.tip().size()
473    }
474
475    /// Get the metadata associated with the last commit.
476    pub fn get_metadata(&self) -> Option<V::Value> {
477        self.last_commit_metadata.clone()
478    }
479
480    /// Return the compact-sync target described by the current witness.
481    ///
482    /// This reflects the most recently applied batch. The target remains non-durable until a
483    /// covering [`Self::commit`], [`Self::sync`], or [`Self::start_sync`] completes.
484    pub const fn target(&self) -> CompactTarget<F, H::Digest> {
485        self.witness.tip().target()
486    }
487
488    /// The [`Commitment`] for the database's current state.
489    pub(crate) const fn commitment(&self) -> chain::Commitment<F, H::Digest> {
490        chain::Commitment::new(self.size(), self.root())
491    }
492
493    /// Create a new speculative batch of operations with this database as its parent.
494    pub fn new_batch(&self) -> UnmerkleizedBatch<F, H, K, V, S> {
495        UnmerkleizedBatch::new(self, self.commitment())
496    }
497
498    /// Create an owned merkleized batch representing the current applied state.
499    pub fn to_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, K, V, S>> {
500        Arc::new(MerkleizedBatch {
501            merkle_batch: self.merkle.to_batch(),
502            operations: Arc::new(Vec::new()),
503            commit_metadata: self.last_commit_metadata.clone(),
504            parent: None,
505            bounds: chain::Bounds::from_db(self.commitment(), self.inactivity_floor_loc),
506        })
507    }
508
509    /// Check that `batch` can be applied to the database in its current state, without
510    /// applying it.
511    ///
512    /// [`Self::apply_batch`] runs the same validation but consumes the database when it
513    /// fails; callers that want to reject a bad batch and keep the handle can check first.
514    pub fn validate_batch(
515        &self,
516        batch: &MerkleizedBatch<F, H::Digest, K, V, S>,
517    ) -> Result<(), Error<F>> {
518        batch.bounds.validate_apply_to(self.commitment())
519    }
520
521    /// Apply a merkleized batch to the database.
522    ///
523    /// The state is updated in memory and appended to the witness journal. Call [`Self::commit`]
524    /// or [`Self::sync`], or await the handle returned by [`Self::start_sync`], to make the
525    /// applied state durable.
526    ///
527    /// # Errors
528    ///
529    /// Returns [`Error::StaleBatch`] if the database is at neither the batch's recorded database
530    /// boundary nor an ancestor's tip.
531    #[tracing::instrument(
532        name = "qmdb.immutable.compact.db.apply_batch",
533        level = "info",
534        skip_all
535    )]
536    pub async fn apply_batch(
537        mut self,
538        batch: Arc<MerkleizedBatch<F, H::Digest, K, V, S>>,
539    ) -> Result<Self, Error<F>> {
540        self.validate_batch(&batch)?;
541        self.merkle.apply_batch(&batch.merkle_batch)?;
542        self.last_commit_metadata = batch.commit_metadata.clone();
543        self.inactivity_floor_loc = batch.bounds.inactivity_floor;
544        let last_commit_metadata = self.last_commit_metadata.clone();
545        let inactivity_floor_loc = self.inactivity_floor_loc;
546        self.witness = self
547            .witness
548            .apply::<H, S>(&mut self.merkle, inactivity_floor_loc, || {
549                Self::encode_commit_op(last_commit_metadata, inactivity_floor_loc)
550            })
551            .await?;
552        assert_eq!(self.commitment(), batch.bounds.tip);
553        Ok(self)
554    }
555
556    /// Begin durably persisting the current db state to disk.
557    ///
558    /// Awaiting the returned [Handle] provides the same durability guarantee as [Self::commit],
559    /// plus a best-effort attempt to bound the recovery needed on reopen. Use [Self::sync] to
560    /// guarantee none is needed. A new sync waits for the prior sync before starting. Failures
561    /// of the deferred durability work surface on the returned handle and the next durability
562    /// operation.
563    #[tracing::instrument(
564        name = "qmdb.immutable.compact.db.start_sync",
565        level = "info",
566        skip_all
567    )]
568    pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error<F>> {
569        let last_commit_metadata = self.last_commit_metadata.clone();
570        let inactivity_floor_loc = self.inactivity_floor_loc;
571        let handle;
572        (self.witness, handle) = self
573            .witness
574            .start_sync::<H, S>(&mut self.merkle, inactivity_floor_loc, || {
575                Self::encode_commit_op(last_commit_metadata, inactivity_floor_loc)
576            })
577            .await?;
578        Ok((self, handle))
579    }
580
581    /// Durably persist the current db state to disk. This is faster than [`Self::sync`] but
582    /// reopen may need to replay the witness journal's tail to recover.
583    #[tracing::instrument(name = "qmdb.immutable.compact.db.commit", level = "info", skip_all)]
584    pub async fn commit(mut self) -> Result<Self, Error<F>> {
585        let last_commit_metadata = self.last_commit_metadata.clone();
586        let inactivity_floor_loc = self.inactivity_floor_loc;
587        self.witness = self
588            .witness
589            .commit::<H, S>(&mut self.merkle, inactivity_floor_loc, || {
590                Self::encode_commit_op(last_commit_metadata, inactivity_floor_loc)
591            })
592            .await?;
593        Ok(self)
594    }
595
596    /// Durably persist the current db state to disk, also persisting journal metadata to
597    /// minimize recovery work on reopen.
598    #[tracing::instrument(name = "qmdb.immutable.compact.db.sync", level = "info", skip_all)]
599    pub async fn sync(mut self) -> Result<Self, Error<F>> {
600        let last_commit_metadata = self.last_commit_metadata.clone();
601        let inactivity_floor_loc = self.inactivity_floor_loc;
602        self.witness = self
603            .witness
604            .sync::<H, S>(&mut self.merkle, inactivity_floor_loc, || {
605                Self::encode_commit_op(last_commit_metadata, inactivity_floor_loc)
606            })
607            .await?;
608        Ok(self)
609    }
610
611    /// Drop witnesses for commits with fewer than `pruning_boundary` operations. Some witness below
612    /// the boundary may survive.
613    ///
614    /// Bounded initialization and compact sync can only use witnesses that survive. The current
615    /// commit's witness always survives. The prune is made durable before this method returns.
616    ///
617    /// # Errors
618    ///
619    /// Fails if a compact-sync import has not yet been applied to the witness journal.
620    #[tracing::instrument(name = "qmdb.immutable.compact.db.prune", level = "info", skip_all)]
621    pub async fn prune(mut self, pruning_boundary: Location<F>) -> Result<Self, Error<F>> {
622        self.witness = self.witness.prune(pruning_boundary).await?;
623        Ok(self)
624    }
625
626    /// Destroy all persisted state associated with this database.
627    #[boxed]
628    pub async fn destroy(self) -> Result<(), Error<F>> {
629        self.witness.destroy().await?;
630        Ok(())
631    }
632}
633
634impl<F, E, K, V, H, S> Source for Db<F, E, K, V, H, S>
635where
636    F: Family,
637    E: Context,
638    K: Key,
639    V: ValueEncoding,
640    H: Hasher,
641    Operation<F, K, V>: EncodeShared + Read,
642    S: Strategy,
643{
644    type Family = F;
645    type Digest = H::Digest;
646    type Op = Operation<F, K, V>;
647    type Error = qmdb::Error<F>;
648
649    async fn serve(&self, request: Request<F>) -> source::Result<Self> {
650        let response = self
651            .witness
652            .compact_state::<H, S, Self::Op>(
653                self.merkle.strategy(),
654                &self.commit_codec_config,
655                request,
656            )
657            .await?;
658        Ok((response, None))
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use crate::{
666        journal::contiguous::{Contiguous as _, fixed, variable::Config as JournalConfig},
667        merkle::{mmb, mmr},
668        metadata::{Config as MetadataConfig, Metadata},
669        qmdb::{
670            any::value::FixedEncoding, compact::witness, verify_proof,
671            verify_proof_and_pinned_nodes,
672        },
673    };
674    use commonware_cryptography::{Sha256, sha256::Digest};
675    use commonware_macros::test_traced;
676    use commonware_parallel::Sequential;
677    use commonware_runtime::{
678        Blob as _, Runner as _, Storage as _, Supervisor as _,
679        buffer::paged::CacheRef,
680        deterministic,
681        mocks::{DelayedSyncContext, PendingSyncs, drive_pending_syncs},
682    };
683    use commonware_utils::{NZU16, NZU64, NZUsize, sequence::VecU64};
684    use core::future::Future;
685    use std::num::{NonZeroU16, NonZeroUsize};
686
687    type TestDb<F> =
688        Db<F, deterministic::Context, Digest, FixedEncoding<Digest>, Sha256, Sequential>;
689
690    const WITNESS_PAGE_SIZE: NonZeroU16 = NZU16!(77);
691    const WITNESS_PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(9);
692
693    fn witness_config(partition: &str) -> JournalConfig<()> {
694        JournalConfig {
695            partition: format!("{partition}-witness"),
696            items_per_section: NZU64!(64),
697            compression: None,
698            codec_config: (),
699            page_cache: CacheRef::new(WITNESS_PAGE_SIZE, WITNESS_PAGE_CACHE_SIZE),
700            write_buffer: NZUsize!(1024),
701            replay_buffer: NZUsize!(1024),
702        }
703    }
704
705    async fn open_db<F: Family>(context: deterministic::Context, partition: &str) -> TestDb<F> {
706        let witness_cfg = witness_config(partition);
707        let cfg = Config {
708            strategy: Sequential,
709            witness: witness_cfg,
710            commit_codec_config: (),
711        };
712        Db::init(context, cfg, None).await.unwrap()
713    }
714
715    async fn compact_merkleize_foreign_db_inner<F: Family>(context: deterministic::Context) {
716        let db = open_db::<F>(context.child("db"), "immutable-merkleize-foreign-db").await;
717        let foreign = open_db::<F>(
718            context.child("foreign"),
719            "immutable-merkleize-foreign-db-foreign",
720        )
721        .await;
722
723        let batch = db
724            .new_batch()
725            .set(Sha256::fill(1), Sha256::fill(11))
726            .merkleize(&db, None, Location::new(0))
727            .await
728            .unwrap();
729        let db = db.apply_batch(batch).await.unwrap();
730        let batch = foreign
731            .new_batch()
732            .set(Sha256::fill(2), Sha256::fill(99))
733            .merkleize(&foreign, None, Location::new(0))
734            .await
735            .unwrap();
736        let foreign = foreign.apply_batch(batch).await.unwrap();
737        assert_eq!(db.size(), foreign.size());
738        assert_ne!(db.root(), foreign.root());
739
740        let batch = db.new_batch().set(Sha256::fill(3), Sha256::fill(22));
741        assert!(matches!(
742            batch.merkleize(&foreign, None, Location::new(0)).await,
743            Err(Error::StaleBatch)
744        ));
745
746        let parent = db
747            .new_batch()
748            .set(Sha256::fill(4), Sha256::fill(33))
749            .merkleize(&db, None, Location::new(0))
750            .await
751            .unwrap();
752        let child = parent
753            .new_batch::<Sha256>()
754            .set(Sha256::fill(5), Sha256::fill(44));
755        assert!(matches!(
756            child.merkleize(&foreign, None, Location::new(0)).await,
757            Err(Error::StaleBatch)
758        ));
759        db.destroy().await.unwrap();
760        foreign.destroy().await.unwrap();
761    }
762
763    async fn compact_merkleize_stale_sibling_inner<F: Family>(context: deterministic::Context) {
764        let db = open_db::<F>(context.child("db"), "immutable-merkleize-stale-sibling").await;
765
766        let direct = db.new_batch().set(Sha256::fill(1), Sha256::fill(11));
767        let sibling = db
768            .new_batch()
769            .set(Sha256::fill(2), Sha256::fill(22))
770            .merkleize(&db, None, Location::new(0))
771            .await
772            .unwrap();
773        let db = db.apply_batch(sibling).await.unwrap();
774        assert!(matches!(
775            direct.merkleize(&db, None, Location::new(0)).await,
776            Err(Error::StaleBatch)
777        ));
778
779        let parent = db
780            .new_batch()
781            .set(Sha256::fill(3), Sha256::fill(33))
782            .merkleize(&db, None, Location::new(0))
783            .await
784            .unwrap();
785        let child = parent
786            .new_batch::<Sha256>()
787            .set(Sha256::fill(4), Sha256::fill(44));
788        let sibling = db
789            .new_batch()
790            .set(Sha256::fill(5), Sha256::fill(55))
791            .merkleize(&db, None, Location::new(0))
792            .await
793            .unwrap();
794        let db = db.apply_batch(sibling).await.unwrap();
795        assert!(matches!(
796            child.merkleize(&db, None, Location::new(0)).await,
797            Err(Error::StaleBatch)
798        ));
799
800        db.destroy().await.unwrap();
801    }
802
803    async fn compact_merkleize_ancestor_states_inner<F: Family>(context: deterministic::Context) {
804        let db = open_db::<F>(context.child("db"), "immutable-merkleize-ancestor-states").await;
805
806        let grandparent = db
807            .new_batch()
808            .set(Sha256::fill(1), Sha256::fill(11))
809            .merkleize(&db, None, Location::new(0))
810            .await
811            .unwrap();
812        let parent = grandparent
813            .new_batch::<Sha256>()
814            .set(Sha256::fill(2), Sha256::fill(22))
815            .merkleize(&db, None, Location::new(0))
816            .await
817            .unwrap();
818        let pending = parent
819            .new_batch::<Sha256>()
820            .set(Sha256::fill(3), Sha256::fill(33))
821            .merkleize(&db, None, Location::new(0))
822            .await
823            .unwrap();
824
825        let db = db.apply_batch(Arc::clone(&grandparent)).await.unwrap();
826        let applied = parent
827            .new_batch::<Sha256>()
828            .set(Sha256::fill(3), Sha256::fill(33))
829            .merkleize(&db, None, Location::new(0))
830            .await
831            .unwrap();
832        assert_eq!(pending.root(), applied.root());
833
834        drop(grandparent);
835        let retired = parent
836            .new_batch::<Sha256>()
837            .set(Sha256::fill(3), Sha256::fill(33))
838            .merkleize(&db, None, Location::new(0))
839            .await
840            .unwrap();
841        assert_eq!(retired.bounds().db, db.commitment());
842        assert_eq!(pending.root(), retired.root());
843
844        let child = parent
845            .new_batch::<Sha256>()
846            .set(Sha256::fill(3), Sha256::fill(33));
847        let db = db.apply_batch(parent).await.unwrap();
848        let child = child.merkleize(&db, None, Location::new(0)).await.unwrap();
849        assert_eq!(pending.root(), child.root());
850        let expected_root = child.root();
851        let db = db.apply_batch(child).await.unwrap();
852        assert_eq!(db.root(), expected_root);
853        db.destroy().await.unwrap();
854    }
855
856    #[test_traced]
857    fn test_compact_merkleize_foreign_db_mmr() {
858        deterministic::Runner::default().start(compact_merkleize_foreign_db_inner::<mmr::Family>);
859    }
860
861    #[test_traced]
862    fn test_compact_merkleize_foreign_db_mmb() {
863        deterministic::Runner::default().start(compact_merkleize_foreign_db_inner::<mmb::Family>);
864    }
865
866    #[test_traced]
867    fn test_compact_merkleize_stale_sibling_mmr() {
868        deterministic::Runner::default()
869            .start(compact_merkleize_stale_sibling_inner::<mmr::Family>);
870    }
871
872    #[test_traced]
873    fn test_compact_merkleize_stale_sibling_mmb() {
874        deterministic::Runner::default()
875            .start(compact_merkleize_stale_sibling_inner::<mmb::Family>);
876    }
877
878    #[test_traced]
879    fn test_compact_merkleize_ancestor_states_mmr() {
880        deterministic::Runner::default()
881            .start(compact_merkleize_ancestor_states_inner::<mmr::Family>);
882    }
883
884    #[test_traced]
885    fn test_compact_merkleize_ancestor_states_mmb() {
886        deterministic::Runner::default()
887            .start(compact_merkleize_ancestor_states_inner::<mmb::Family>);
888    }
889
890    async fn open_bounded<F: Family>(
891        context: deterministic::Context,
892        witness_cfg: JournalConfig<()>,
893        cap: Location<F>,
894    ) -> Result<TestDb<F>, Error<F>> {
895        let cfg = Config {
896            strategy: Sequential,
897            witness: witness_cfg,
898            commit_codec_config: (),
899        };
900        Db::init(context, cfg, Some(cap)).await
901    }
902
903    /// A batch's proof and pinned nodes return `StaleRead` after a bounded initialization and
904    /// another batch bring the database to the batch's base size with a different root.
905    async fn proof_refused_after_off_chain_reopen<F: Family>(context: deterministic::Context) {
906        let db = open_db::<F>(context.child("db"), "off-chain-proof").await;
907        let mut seed = db.new_batch();
908        for value in 1..=6 {
909            seed = seed.set(Sha256::fill(value), Sha256::fill(value));
910        }
911        let seed = seed.merkleize(&db, None, Location::new(0)).await.unwrap();
912        let db = db.apply_batch(seed).await.unwrap();
913        let db = db.sync().await.unwrap();
914        let batch = db
915            .new_batch()
916            .set(Sha256::fill(100), Sha256::fill(100))
917            .merkleize(&db, None, Location::new(0))
918            .await
919            .unwrap();
920        let (start, ops) = batch.operations();
921        let root = batch.root();
922        let original_proof = batch.proof(&db).unwrap();
923        let original_pins = batch.pinned_nodes(&db).unwrap();
924        assert!(verify_proof_and_pinned_nodes::<Sha256, _, _>(
925            &original_proof,
926            start,
927            &ops,
928            &original_pins,
929            &root
930        ));
931        drop(db);
932
933        let db = open_bounded::<F>(
934            context.child("reopen"),
935            witness_config("off-chain-proof"),
936            Location::new(1),
937        )
938        .await
939        .unwrap();
940        let mut other = db.new_batch();
941        for value in 11..=16 {
942            other = other.set(Sha256::fill(value), Sha256::fill(value));
943        }
944        let other = other.merkleize(&db, None, Location::new(0)).await.unwrap();
945        let db = db.apply_batch(other).await.unwrap();
946        assert_eq!(db.size(), start);
947        assert!(matches!(
948            batch
949                .new_batch::<Sha256>()
950                .merkleize(&db, None, Location::new(0))
951                .await,
952            Err(Error::StaleBatch)
953        ));
954
955        let proof = batch.proof(&db);
956        let pins = batch.pinned_nodes(&db);
957        assert!(
958            matches!(proof, Err(Error::StaleRead)),
959            "proof must return StaleRead at a state outside the batch's chain"
960        );
961        assert!(
962            matches!(pins, Err(Error::StaleRead)),
963            "pinned nodes must return StaleRead at a state outside the batch's chain"
964        );
965    }
966
967    #[test]
968    fn compact_mmr_proof_refused_after_off_chain_reopen() {
969        deterministic::Runner::default().start(proof_refused_after_off_chain_reopen::<mmr::Family>);
970    }
971
972    #[test]
973    fn compact_mmb_proof_refused_after_off_chain_reopen() {
974        deterministic::Runner::default().start(proof_refused_after_off_chain_reopen::<mmb::Family>);
975    }
976
977    /// Batch artifacts (operations, range proof, pinned frontier) verify against the batch root,
978    /// survive applying and dropping ancestors, and return `ElementPruned` once the batch itself is
979    /// applied and the compact store is pruned past them.
980    async fn compact_operations_and_proof_inner<F: Family>(context: deterministic::Context) {
981        let db = open_db::<F>(context.child("db"), "immutable-operations-and-proof").await;
982        let value = |key: u8| Sha256::fill(key.wrapping_add(100));
983
984        // Seed committed state so the chain below starts above a pruned frontier.
985        let mut seed = db.new_batch();
986        for key in 1u8..=6 {
987            seed = seed.set(Sha256::fill(key), value(key));
988        }
989        let seed = seed
990            .merkleize(&db, Some(Sha256::fill(7)), Location::new(0))
991            .await
992            .unwrap();
993        let db = db.apply_batch(seed).await.unwrap().sync().await.unwrap();
994        let floor = db.size();
995        assert_eq!(floor, Location::new(8));
996
997        // A batch from `to_batch` has no operations to prove.
998        assert!(matches!(
999            db.to_batch().proof(&db),
1000            Err(Error::Merkle(crate::merkle::Error::Empty))
1001        ));
1002
1003        // A two-deep unapplied chain. The child's artifacts read the parent's nodes through the
1004        // chain.
1005        let mut parent = db.new_batch();
1006        for key in 8u8..=12 {
1007            parent = parent.set(Sha256::fill(key), value(key));
1008        }
1009        let parent = parent
1010            .merkleize(&db, Some(Sha256::fill(13)), floor)
1011            .await
1012            .unwrap();
1013        let child = parent
1014            .new_batch::<Sha256>()
1015            .set(Sha256::fill(14), value(14))
1016            .merkleize(&db, Some(Sha256::fill(15)), floor)
1017            .await
1018            .unwrap();
1019
1020        // The operation suffix is the batch's own sets plus its commit, handed out zero-copy.
1021        let (child_start, child_ops) = child.operations();
1022        let (_, child_ops_again) = child.operations();
1023        let child_end = child.bounds().tip.size;
1024        assert!(Arc::ptr_eq(&child_ops, &child_ops_again));
1025        assert_eq!(child_start, parent.bounds().tip.size);
1026        assert_eq!(*child_start + child_ops.len() as u64, *child_end);
1027        assert_eq!(child_end, Location::new(16));
1028        assert!(matches!(
1029            child_ops.as_slice(),
1030            [Operation::Set(key, set_value), Operation::Commit(Some(metadata), operation_floor)]
1031                if key == &Sha256::fill(14)
1032                    && set_value == &value(14)
1033                    && metadata == &Sha256::fill(15)
1034                    && operation_floor == &floor
1035        ));
1036
1037        // The proof is anchored at the batch tip and verifies with or without the pins.
1038        let child_root = child.root();
1039        let child_proof = child.proof(&db).unwrap();
1040        let child_pins = child.pinned_nodes(&db).unwrap();
1041        assert_eq!(child_proof.leaves, child_end);
1042        assert_eq!(
1043            child_proof.inactive_peaks,
1044            F::inactive_peaks(child_end, floor),
1045        );
1046        assert!(verify_proof::<Sha256, _, _>(
1047            &child_proof,
1048            child_start,
1049            &child_ops,
1050            &child_root
1051        ));
1052        assert!(verify_proof_and_pinned_nodes::<Sha256, _, _>(
1053            &child_proof,
1054            child_start,
1055            &child_ops,
1056            &child_pins,
1057            &child_root
1058        ));
1059
1060        // The pins are order-sensitive.
1061        assert!(child_pins.len() > 1);
1062        let mut reordered_child_pins = child_pins.clone();
1063        reordered_child_pins.swap(0, 1);
1064        assert!(!verify_proof_and_pinned_nodes::<Sha256, _, _>(
1065            &child_proof,
1066            child_start,
1067            &child_ops,
1068            &reordered_child_pins,
1069            &child_root
1070        ));
1071
1072        // Pipelined consumer: applying the parent prunes the store to the parent's tip, which is
1073        // exactly the frontier the child's base pins, so the artifacts survive dropping the
1074        // parent.
1075        let db = db.apply_batch(parent).await.unwrap();
1076        let child_proof_after = child.proof(&db).unwrap();
1077        assert_eq!(child.pinned_nodes(&db).unwrap(), child_pins);
1078        assert!(verify_proof_and_pinned_nodes::<Sha256, _, _>(
1079            &child_proof_after,
1080            child_start,
1081            &child_ops,
1082            &child_pins,
1083            &child_root
1084        ));
1085        let applied_start = db.size();
1086        let db = db.apply_batch(child).await.unwrap();
1087        let child_range = applied_start..db.size();
1088        assert_eq!(child_range, child_start..child_end);
1089
1090        // A commit-only batch proves exactly its commit operation.
1091        let commit_floor = db.size();
1092        let commit_only = db
1093            .new_batch()
1094            .merkleize(&db, Some(Sha256::fill(16)), commit_floor)
1095            .await
1096            .unwrap();
1097        let (commit_start, commit_ops) = commit_only.operations();
1098        let commit_end = commit_only.bounds().tip.size;
1099        let commit_root = commit_only.root();
1100        let commit_proof = commit_only.proof(&db).unwrap();
1101        let commit_pins = commit_only.pinned_nodes(&db).unwrap();
1102        assert_eq!(commit_start, commit_floor);
1103        assert!(matches!(
1104            commit_ops.as_slice(),
1105            [Operation::Commit(Some(metadata), operation_floor)]
1106                if metadata == &Sha256::fill(16) && operation_floor == &commit_floor
1107        ));
1108        assert_eq!(*commit_start + commit_ops.len() as u64, *commit_end);
1109        assert_eq!(commit_proof.leaves, commit_end);
1110        assert_eq!(
1111            commit_proof.inactive_peaks,
1112            F::inactive_peaks(commit_end, commit_floor)
1113        );
1114        assert!(verify_proof_and_pinned_nodes::<Sha256, _, _>(
1115            &commit_proof,
1116            commit_start,
1117            &commit_ops,
1118            &commit_pins,
1119            &commit_root
1120        ));
1121        let applied_start = db.size();
1122        let db = db.apply_batch(commit_only).await.unwrap();
1123        let commit_range = applied_start..db.size();
1124        assert_eq!(commit_range, commit_start..commit_end);
1125        let db = db.sync().await.unwrap();
1126
1127        // Applying a batch built mid-mountain prunes its own artifacts. The accessors return
1128        // `ElementPruned` rather than a proof that fails to verify, while a child merkleized
1129        // before the apply still finds its base frontier in the store.
1130        let late_parent = db
1131            .new_batch()
1132            .set(Sha256::fill(17), value(17))
1133            .merkleize(&db, Some(Sha256::fill(18)), db.size())
1134            .await
1135            .unwrap();
1136        let late = late_parent
1137            .new_batch::<Sha256>()
1138            .set(Sha256::fill(19), value(19))
1139            .merkleize(&db, Some(Sha256::fill(20)), db.size())
1140            .await
1141            .unwrap();
1142        let db = db.apply_batch(Arc::clone(&late_parent)).await.unwrap();
1143        assert!(matches!(
1144            late_parent.proof(&db),
1145            Err(Error::Merkle(crate::merkle::Error::ElementPruned(_)))
1146        ));
1147        assert!(matches!(
1148            late_parent.pinned_nodes(&db),
1149            Err(Error::Merkle(crate::merkle::Error::ElementPruned(_)))
1150        ));
1151        drop(late_parent);
1152
1153        let (late_start, late_ops) = late.operations();
1154        let late_root = late.root();
1155        let late_proof = late.proof(&db).unwrap();
1156        let late_pins = late.pinned_nodes(&db).unwrap();
1157        assert!(verify_proof_and_pinned_nodes::<Sha256, _, _>(
1158            &late_proof,
1159            late_start,
1160            &late_ops,
1161            &late_pins,
1162            &late_root
1163        ));
1164        let db = db.apply_batch(late).await.unwrap();
1165
1166        db.destroy().await.unwrap();
1167    }
1168
1169    #[test_traced]
1170    fn test_compact_operations_and_proof_mmr() {
1171        deterministic::Runner::default().start(compact_operations_and_proof_inner::<mmr::Family>);
1172    }
1173
1174    #[test_traced]
1175    fn test_compact_operations_and_proof_mmb() {
1176        deterministic::Runner::default().start(compact_operations_and_proof_inner::<mmb::Family>);
1177    }
1178
1179    /// Open the persisted witness journal directly so tests can corrupt the tip entry.
1180    async fn open_witness_journal(
1181        context: deterministic::Context,
1182        partition: &str,
1183    ) -> witness::Journal<deterministic::Context, mmr::Family, Digest> {
1184        let cfg = witness_config(partition);
1185        witness::Journal::init(context, cfg).await.unwrap()
1186    }
1187
1188    /// A compact db over a delayed-sync storage backend.
1189    type DelayedDb = Db<
1190        mmr::Family,
1191        DelayedSyncContext<deterministic::Context>,
1192        Digest,
1193        FixedEncoding<Digest>,
1194        Sha256,
1195        Sequential,
1196    >;
1197
1198    /// Open a [DelayedDb] whose blob syncs park on `pending`.
1199    ///
1200    /// Init durably persists the bootstrap witness, so while syncs park the returned future
1201    /// must be driven with `drive_pending_syncs` (or the mock unblocked first).
1202    fn open_delayed_db(
1203        context: deterministic::Context,
1204        partition: &str,
1205        pending: &PendingSyncs,
1206    ) -> impl Future<Output = Result<DelayedDb, Error<mmr::Family>>> {
1207        let witness_cfg = witness_config(partition);
1208        let cfg = Config {
1209            strategy: Sequential,
1210            witness: witness_cfg,
1211            commit_codec_config: (),
1212        };
1213        let context = DelayedSyncContext {
1214            inner: context,
1215            pending: pending.clone(),
1216        };
1217        DelayedDb::init(context, cfg, None)
1218    }
1219
1220    /// Apply a single-key batch writing `key -> value` with `metadata`.
1221    async fn apply_set(db: DelayedDb, key: Digest, value: Digest, metadata: Digest) -> DelayedDb {
1222        let floor = db.inactivity_floor_loc();
1223        let batch = db
1224            .new_batch()
1225            .set(key, value)
1226            .merkleize(&db, Some(metadata), floor)
1227            .await
1228            .unwrap();
1229        db.apply_batch(batch).await.unwrap()
1230    }
1231
1232    /// State persisted via an awaited start_sync handle is recovered on reopen.
1233    #[test_traced]
1234    fn test_compact_start_sync_recovery() {
1235        deterministic::Runner::default().start(|ctx| async move {
1236            let partition = "immutable-start-sync-recovery";
1237            let pending = PendingSyncs::default();
1238            pending.unblock();
1239            let mut db = open_delayed_db(ctx.child("delayed"), partition, &pending)
1240                .await
1241                .unwrap();
1242            let metadata = Sha256::fill(9u8);
1243            db = apply_set(db, Sha256::fill(1u8), Sha256::fill(2u8), metadata).await;
1244
1245            let handle;
1246            (db, handle) = db.start_sync().await.unwrap();
1247            handle.await.unwrap();
1248            let root = db.root();
1249            drop(db);
1250
1251            let db = open_delayed_db(ctx.child("reopen"), partition, &pending)
1252                .await
1253                .unwrap();
1254            assert_eq!(db.root(), root);
1255            assert_eq!(db.get_metadata(), Some(metadata));
1256            db.destroy().await.unwrap();
1257        });
1258    }
1259
1260    /// A sync begun by `start_sync` that fails in flight surfaces the error through both the
1261    /// returned handle and the next durability operation, even when that operation has nothing
1262    /// new to persist.
1263    #[test_traced]
1264    fn test_compact_start_sync_failure_propagates() {
1265        deterministic::Runner::default().start(|ctx| async move {
1266            let pending = PendingSyncs::default();
1267            pending.unblock();
1268            let mut db =
1269                open_delayed_db(ctx.child("delayed"), "immutable-start-sync-fail", &pending)
1270                    .await
1271                    .unwrap();
1272            db = apply_set(db, Sha256::fill(1u8), Sha256::fill(2u8), Sha256::fill(9u8)).await;
1273
1274            // Arm all future syncs to resolve to an injected error.
1275            pending.arm_fail();
1276
1277            let handle;
1278            (db, handle) = db.start_sync().await.unwrap();
1279            assert!(
1280                handle.await.is_err(),
1281                "the sync handle surfaces the failure"
1282            );
1283            let starts_before = pending.starts();
1284
1285            // The witness entry was already appended, so this commit has nothing to stage.
1286            // It must still observe the retained failure rather than no-op.
1287            assert!(
1288                db.commit().await.is_err(),
1289                "the next durability op surfaces the failed in-flight sync"
1290            );
1291            assert_eq!(
1292                pending.starts(),
1293                starts_before,
1294                "the surfaced error is the retained failure, not a fresh sync's"
1295            );
1296        });
1297    }
1298
1299    #[test_traced("INFO")]
1300    fn test_compact_stale_batch_rejected() {
1301        deterministic::Runner::default().start(|context| async move {
1302            let db = open_db::<mmr::Family>(context.child("db"), "immutable-stale").await;
1303
1304            let key1 = Sha256::hash(&[&[1]]);
1305            let key2 = Sha256::hash(&[&[2]]);
1306            let value1 = Sha256::fill(10u8);
1307            let value2 = Sha256::fill(20u8);
1308
1309            let batch_a = db
1310                .new_batch()
1311                .set(key1, value1)
1312                .merkleize(&db, None, Location::new(0))
1313                .await
1314                .unwrap();
1315            let batch_b = db
1316                .new_batch()
1317                .set(key2, value2)
1318                .merkleize(&db, None, Location::new(0))
1319                .await
1320                .unwrap();
1321
1322            let stale = db.new_batch().set(key2, value2);
1323            let expected_root = batch_a.root();
1324            let db = db.apply_batch(batch_a).await.unwrap();
1325            assert_eq!(db.root(), expected_root);
1326
1327            // After the apply, merkleizing a batch built on the pre-apply state and applying the
1328            // merkleized sibling both return `StaleBatch`.
1329            assert!(matches!(
1330                stale.merkleize(&db, None, Location::new(0)).await,
1331                Err(Error::StaleBatch)
1332            ));
1333            assert!(matches!(
1334                db.apply_batch(batch_b).await,
1335                Err(Error::StaleBatch)
1336            ));
1337        });
1338    }
1339
1340    #[test_traced("INFO")]
1341    fn test_compact_delayed_merkleize_after_ancestor_apply() {
1342        deterministic::Runner::default().start(|context| async move {
1343            let db = open_db::<mmr::Family>(context.child("db"), "immutable-delayed-child").await;
1344            let key1 = Sha256::hash(&[&[1]]);
1345            let key2 = Sha256::hash(&[&[2]]);
1346            let key3 = Sha256::hash(&[&[3]]);
1347            let value1 = Sha256::fill(10u8);
1348            let value2 = Sha256::fill(20u8);
1349            let value3 = Sha256::fill(30u8);
1350
1351            let a = db
1352                .new_batch()
1353                .set(key1, value1)
1354                .merkleize(&db, None, Location::new(0))
1355                .await
1356                .unwrap();
1357            let b = a
1358                .new_batch::<Sha256>()
1359                .set(key2, value2)
1360                .merkleize(&db, None, Location::new(0))
1361                .await
1362                .unwrap();
1363            let c = b.new_batch::<Sha256>().set(key3, value3);
1364
1365            let db = db.apply_batch(a).await.unwrap();
1366            let c = c.merkleize(&db, None, Location::new(0)).await.unwrap();
1367            let expected_root = c.root();
1368            let db = db.apply_batch(c).await.unwrap();
1369
1370            assert_eq!(db.root(), expected_root);
1371        });
1372    }
1373
1374    /// `to_batch()` reflects the current applied state before it becomes durable.
1375    #[test_traced("INFO")]
1376    fn test_compact_to_batch_reflects_live_state() {
1377        deterministic::Runner::default().start(|context| async move {
1378            let db = open_db::<mmr::Family>(context.child("db"), "immutable-to-batch-live").await;
1379
1380            let pre_apply_root = db.root();
1381            let pre_snapshot = db.to_batch();
1382            assert_eq!(
1383                pre_snapshot.root(),
1384                pre_apply_root,
1385                "snapshot before any mutation should match the live root"
1386            );
1387
1388            let key = Sha256::hash(&[&[1]]);
1389            let value = Sha256::fill(10u8);
1390            let batch = db
1391                .new_batch()
1392                .set(key, value)
1393                .merkleize(&db, None, Location::new(0))
1394                .await
1395                .unwrap();
1396            let db = db.apply_batch(batch).await.unwrap();
1397
1398            // Observe the applied state before making it durable.
1399            let live_root = db.root();
1400            assert_ne!(
1401                live_root, pre_apply_root,
1402                "applying a non-empty batch must change the live root"
1403            );
1404
1405            let snapshot = db.to_batch();
1406            assert_eq!(
1407                snapshot.root(),
1408                live_root,
1409                "to_batch().root() must match the live db.root() even before sync"
1410            );
1411
1412            db.destroy().await.unwrap();
1413        });
1414    }
1415
1416    #[test_traced("INFO")]
1417    fn test_compact_stale_batch_chained() {
1418        deterministic::Runner::default().start(|context| async move {
1419            let db = open_db::<mmr::Family>(context.child("db"), "immutable-chained-stale").await;
1420
1421            let common_parent = db
1422                .new_batch()
1423                .set(Sha256::hash(&[&[10]]), Sha256::fill(10u8))
1424                .merkleize(&db, None, Location::new(0))
1425                .await
1426                .unwrap();
1427            let sibling_a = common_parent
1428                .new_batch::<Sha256>()
1429                .set(Sha256::hash(&[&[11]]), Sha256::fill(11u8))
1430                .merkleize(&db, None, Location::new(0))
1431                .await
1432                .unwrap();
1433            let sibling_b = common_parent
1434                .new_batch::<Sha256>()
1435                .set(Sha256::hash(&[&[12]]), Sha256::fill(12u8))
1436                .merkleize(&db, None, Location::new(0))
1437                .await
1438                .unwrap();
1439            let db = db.apply_batch(sibling_a).await.unwrap();
1440            assert!(matches!(
1441                db.validate_batch(&sibling_b),
1442                Err(Error::StaleBatch)
1443            ));
1444
1445            let parent_a = db
1446                .new_batch()
1447                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
1448                .merkleize(&db, None, Location::new(0))
1449                .await
1450                .unwrap();
1451            let parent_b = db
1452                .new_batch()
1453                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1454                .merkleize(&db, None, Location::new(0))
1455                .await
1456                .unwrap();
1457            let child_b = parent_b
1458                .new_batch::<Sha256>()
1459                .set(Sha256::hash(&[&[3]]), Sha256::fill(3u8))
1460                .merkleize(&db, None, Location::new(0))
1461                .await
1462                .unwrap();
1463
1464            let db = db.apply_batch(parent_a).await.unwrap();
1465            assert!(matches!(
1466                db.validate_batch(&child_b),
1467                Err(Error::StaleBatch)
1468            ));
1469            db.destroy().await.unwrap();
1470        });
1471    }
1472
1473    #[test_traced("INFO")]
1474    fn test_compact_stale_parent_after_child_applied() {
1475        deterministic::Runner::default().start(|context| async move {
1476            let db =
1477                open_db::<mmr::Family>(context.child("db"), "immutable-child-before-parent").await;
1478
1479            let parent = db
1480                .new_batch()
1481                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
1482                .merkleize(&db, None, Location::new(0))
1483                .await
1484                .unwrap();
1485            let child = parent
1486                .new_batch::<Sha256>()
1487                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1488                .merkleize(&db, None, Location::new(0))
1489                .await
1490                .unwrap();
1491
1492            let db = db.apply_batch(child).await.unwrap();
1493            assert!(matches!(
1494                db.apply_batch(parent).await,
1495                Err(Error::StaleBatch)
1496            ));
1497        });
1498    }
1499
1500    #[test_traced("INFO")]
1501    fn test_compact_sequential_commit_parent_then_child() {
1502        deterministic::Runner::default().start(|context| async move {
1503            let db = open_db::<mmr::Family>(context.child("db"), "immutable-parent-child").await;
1504
1505            let parent = db
1506                .new_batch()
1507                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
1508                .merkleize(&db, None, Location::new(0))
1509                .await
1510                .unwrap();
1511            let child = parent
1512                .new_batch::<Sha256>()
1513                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1514                .merkleize(&db, None, Location::new(0))
1515                .await
1516                .unwrap();
1517            let expected_root = child.root();
1518
1519            let db = db.apply_batch(parent).await.unwrap();
1520            let db = db.apply_batch(child).await.unwrap().sync().await.unwrap();
1521
1522            assert_eq!(db.root(), expected_root);
1523
1524            db.destroy().await.unwrap();
1525        });
1526    }
1527
1528    #[test_traced("INFO")]
1529    fn test_compact_floor_regressed() {
1530        deterministic::Runner::default().start(|context| async move {
1531            let db = open_db::<mmr::Family>(context.child("db"), "immutable-floor-regressed").await;
1532
1533            let advance_floor = db.new_batch().set(Sha256::hash(&[&[1]]), Sha256::fill(1u8));
1534            let advance_floor = advance_floor
1535                .merkleize(&db, None, Location::new(1))
1536                .await
1537                .unwrap();
1538            let db = db.apply_batch(advance_floor).await.unwrap();
1539            let db = db.sync().await.unwrap();
1540
1541            let regressed = db
1542                .new_batch()
1543                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1544                .merkleize(&db, None, Location::new(0))
1545                .await;
1546
1547            assert!(matches!(
1548                regressed,
1549                Err(Error::FloorRegressed(new, current))
1550                    if new == Location::new(0) && current == Location::new(1)
1551            ));
1552        });
1553    }
1554
1555    // A chained batch whose tip floor is below its parent's floor must be rejected:
1556    // the parent's Commit participates in the per-commit monotonicity invariant even
1557    // before it is applied.
1558    #[test_traced("INFO")]
1559    fn test_compact_chained_floor_regression() {
1560        deterministic::Runner::default().start(|context| async move {
1561            let db =
1562                open_db::<mmr::Family>(context.child("db"), "immutable-regressed-ancestor-floor")
1563                    .await;
1564
1565            let parent = db
1566                .new_batch()
1567                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
1568                .merkleize(&db, None, Location::new(1))
1569                .await
1570                .unwrap();
1571            let child = parent
1572                .new_batch::<Sha256>()
1573                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1574                .merkleize(&db, None, Location::new(0))
1575                .await;
1576
1577            assert!(matches!(
1578                child,
1579                Err(Error::FloorRegressed(new, prev))
1580                    if new == Location::new(0) && prev == Location::new(1)
1581            ));
1582        });
1583    }
1584
1585    #[test_traced("INFO")]
1586    fn test_compact_bounded_initialization_restores_commit_metadata_and_floor() {
1587        deterministic::Runner::default().start(|context| async move {
1588            let db = open_db::<mmr::Family>(context.child("db"), "immutable-rewind-meta").await;
1589
1590            let k1 = Sha256::hash(&[&[1]]);
1591            let v1 = Sha256::fill(11u8);
1592            let meta1 = Sha256::fill(0xaa);
1593            let floor1 = Location::new(0);
1594            let batch = db
1595                .new_batch()
1596                .set(k1, v1)
1597                .merkleize(&db, Some(meta1), floor1)
1598                .await
1599                .unwrap();
1600            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1601            let root_after_first = db.root();
1602            let size_after_first = db.size();
1603
1604            let k2 = Sha256::hash(&[&[2]]);
1605            let v2 = Sha256::fill(22u8);
1606            let meta2 = Sha256::fill(0xbb);
1607            // Advance the floor to the commit of the first batch (loc 1).
1608            let floor2 = Location::new(1);
1609            let batch = db
1610                .new_batch()
1611                .set(k2, v2)
1612                .merkleize(&db, Some(meta2), floor2)
1613                .await
1614                .unwrap();
1615            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1616            assert_eq!(db.get_metadata(), Some(meta2));
1617            assert_eq!(db.inactivity_floor_loc(), floor2);
1618
1619            let db = {
1620                _ = db.sync().await.unwrap();
1621                open_bounded::<mmr::Family>(
1622                    context.child("cap"),
1623                    witness_config("immutable-rewind-meta"),
1624                    size_after_first,
1625                )
1626                .await
1627            }
1628            .unwrap();
1629            assert_eq!(db.root(), root_after_first);
1630            assert_eq!(db.get_metadata(), Some(meta1));
1631            assert_eq!(db.inactivity_floor_loc(), floor1);
1632
1633            db.destroy().await.unwrap();
1634        });
1635    }
1636
1637    #[test_traced("INFO")]
1638    fn test_compact_bounded_initialization_persists_across_reopen() {
1639        deterministic::Runner::default().start(|context| async move {
1640            let partition = "immutable-rewind-reopen";
1641            let meta1 = Sha256::fill(0xaa);
1642            let floor1 = Location::new(0);
1643            let meta2 = Sha256::fill(0xbb);
1644            let floor2 = Location::new(1);
1645
1646            let root_after_first = {
1647                let db = open_db::<mmr::Family>(context.child("first"), partition).await;
1648                let batch = db
1649                    .new_batch()
1650                    .set(Sha256::hash(&[&[1]]), Sha256::fill(11u8))
1651                    .merkleize(&db, Some(meta1), floor1)
1652                    .await
1653                    .unwrap();
1654                let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1655                let root = db.root();
1656                let size_after_first = db.size();
1657
1658                let batch = db
1659                    .new_batch()
1660                    .set(Sha256::hash(&[&[2]]), Sha256::fill(22u8))
1661                    .merkleize(&db, Some(meta2), floor2)
1662                    .await
1663                    .unwrap();
1664                let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1665
1666                let _db = {
1667                    _ = db.sync().await.unwrap();
1668                    open_bounded::<mmr::Family>(
1669                        context.child("cap"),
1670                        witness_config(partition),
1671                        size_after_first,
1672                    )
1673                    .await
1674                }
1675                .unwrap();
1676                root
1677            };
1678
1679            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
1680            assert_eq!(db.root(), root_after_first);
1681            assert_eq!(db.get_metadata(), Some(meta1));
1682            assert_eq!(db.inactivity_floor_loc(), floor1);
1683
1684            db.destroy().await.unwrap();
1685        });
1686    }
1687
1688    #[test_traced("INFO")]
1689    fn test_compact_commit_persists_across_reopen() {
1690        deterministic::Runner::default().start(|context| async move {
1691            let partition = "immutable-commit-reopen";
1692            let meta1 = Sha256::fill(0xaa);
1693            let meta2 = Sha256::fill(0xbb);
1694
1695            let root_after_second = {
1696                let db = open_db::<mmr::Family>(context.child("first"), partition).await;
1697                let batch = db
1698                    .new_batch()
1699                    .set(Sha256::hash(&[&[1]]), Sha256::fill(11u8))
1700                    .merkleize(&db, Some(meta1), Location::new(0))
1701                    .await
1702                    .unwrap();
1703                let db = db.apply_batch(batch).await.unwrap().commit().await.unwrap();
1704
1705                let batch = db
1706                    .new_batch()
1707                    .set(Sha256::hash(&[&[2]]), Sha256::fill(22u8))
1708                    .merkleize(&db, Some(meta2), Location::new(1))
1709                    .await
1710                    .unwrap();
1711                let db = db.apply_batch(batch).await.unwrap().commit().await.unwrap();
1712                db.root()
1713            };
1714
1715            // Reopen recovers the committed tip even though the journal was never synced.
1716            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
1717            assert_eq!(db.root(), root_after_second);
1718            assert_eq!(db.get_metadata(), Some(meta2));
1719            assert_eq!(db.inactivity_floor_loc(), Location::new(1));
1720            db.destroy().await.unwrap();
1721        });
1722    }
1723
1724    #[test_traced("INFO")]
1725    fn test_compact_bounded_initialization_to_committed_entry_after_reopen() {
1726        deterministic::Runner::default().start(|context| async move {
1727            let partition = "immutable-commit-rewind-reopen";
1728            let meta1 = Sha256::fill(0xaa);
1729            let meta2 = Sha256::fill(0xbb);
1730
1731            let (root_a, size_a) = {
1732                let db = open_db::<mmr::Family>(context.child("first"), partition).await;
1733                let batch = db
1734                    .new_batch()
1735                    .set(Sha256::hash(&[&[1]]), Sha256::fill(11u8))
1736                    .merkleize(&db, Some(meta1), Location::new(0))
1737                    .await
1738                    .unwrap();
1739                let db = db.apply_batch(batch).await.unwrap().commit().await.unwrap();
1740                let root_a = db.root();
1741                let size_a = db.size();
1742
1743                let batch = db
1744                    .new_batch()
1745                    .set(Sha256::hash(&[&[2]]), Sha256::fill(22u8))
1746                    .merkleize(&db, Some(meta2), Location::new(1))
1747                    .await
1748                    .unwrap();
1749                let _db = db.apply_batch(batch).await.unwrap().commit().await.unwrap();
1750                (root_a, size_a)
1751            };
1752
1753            // Both committed witnesses survive the crash. Reopen recovers the tip, and the earlier
1754            // commit remains a valid initialization target.
1755            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
1756            let db = {
1757                _ = db.sync().await.unwrap();
1758                open_bounded::<mmr::Family>(context.child("cap"), witness_config(partition), size_a)
1759                    .await
1760            }
1761            .unwrap();
1762            assert_eq!(db.root(), root_a);
1763            assert_eq!(db.get_metadata(), Some(meta1));
1764            db.destroy().await.unwrap();
1765        });
1766    }
1767
1768    #[test_traced("INFO")]
1769    fn test_compact_sync_after_commit() {
1770        deterministic::Runner::default().start(|context| async move {
1771            let partition = "immutable-sync-after-commit";
1772            let meta = Sha256::fill(0xaa);
1773
1774            let root = {
1775                let db = open_db::<mmr::Family>(context.child("first"), partition).await;
1776                let batch = db
1777                    .new_batch()
1778                    .set(Sha256::hash(&[&[1]]), Sha256::fill(11u8))
1779                    .merkleize(&db, Some(meta), Location::new(0))
1780                    .await
1781                    .unwrap();
1782                let db = db.apply_batch(batch).await.unwrap();
1783
1784                // Commit persists witness data. Sync must also persist recovery metadata.
1785                let db = db.commit().await.unwrap();
1786                let db = db.sync().await.unwrap();
1787                db.root()
1788            };
1789
1790            // Check the watermark before reopening can rebuild the offsets journal.
1791            let metadata = Metadata::<_, u64, VecU64>::init(
1792                context.child("checkpoint"),
1793                MetadataConfig {
1794                    partition: format!("{partition}-witness_offsets-metadata"),
1795                    codec_config: (),
1796                },
1797            )
1798            .await
1799            .unwrap();
1800
1801            // Key 3 records the durable prefix: the bootstrap witness and the applied batch.
1802            assert_eq!(metadata.get(&3).copied().map(u64::from), Some(2));
1803            drop(metadata);
1804
1805            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
1806            assert_eq!(db.root(), root);
1807            assert_eq!(db.get_metadata(), Some(meta));
1808            db.destroy().await.unwrap();
1809        });
1810    }
1811
1812    #[test_traced("INFO")]
1813    fn test_compact_reopen_rejects_tampered_witness() {
1814        deterministic::Runner::default().start(|context| async move {
1815            let partition = "immutable-witness-tamper";
1816            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
1817            let batch = db
1818                .new_batch()
1819                .set(Sha256::hash(&[&[7]]), Sha256::fill(7u8))
1820                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(1))
1821                .await
1822                .unwrap();
1823            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1824            drop(db);
1825
1826            // Corrupt the entry structurally. An extra pinned node cannot rebuild the Merkle.
1827            let journal = open_witness_journal(context.child("tamper"), partition).await;
1828            let (op_bytes, size, mut pinned_nodes) = witness::tests::tip(&journal).await;
1829            pinned_nodes.push(Sha256::fill(0xff));
1830            witness::tests::overwrite_tip(journal, op_bytes, size, pinned_nodes).await;
1831
1832            let cfg = Config {
1833                strategy: Sequential,
1834                witness: witness_config(partition),
1835                commit_codec_config: (),
1836            };
1837            let reopened =
1838                TestDb::<mmr::Family>::init(context.child("reopen_witness"), cfg, None).await;
1839            assert!(matches!(reopened, Err(Error::DataCorrupted(_))));
1840        });
1841    }
1842
1843    #[test_traced("INFO")]
1844    fn test_compact_bounded_initialization_rejects_corrupt_target_entry() {
1845        deterministic::Runner::default().start(|context| async move {
1846            let partition = "immutable-corrupt-rewind-target";
1847            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
1848            let batch = db
1849                .new_batch()
1850                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
1851                .merkleize(&db, None, Location::new(1))
1852                .await
1853                .unwrap();
1854            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1855            let initialization_bound = db.target().size;
1856            let batch = db
1857                .new_batch()
1858                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
1859                .merkleize(&db, None, Location::new(1))
1860                .await
1861                .unwrap();
1862            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1863            let tip_target = db.target();
1864            drop(db);
1865
1866            // Corrupt the initialization target's entry (the journal holds bootstrap, target, tip).
1867            let mut journal = open_witness_journal(context.child("corrupt"), partition).await;
1868            journal = witness::tests::corrupt_entry(journal, 1, |entry| {
1869                entry.pinned_nodes.push(Sha256::fill(0xff));
1870            })
1871            .await;
1872            drop(journal);
1873
1874            // The tip entry is intact, so reopen succeeds.
1875            let cfg = Config {
1876                strategy: Sequential,
1877                witness: witness_config(partition),
1878                commit_codec_config: (),
1879            };
1880            let reopened = TestDb::<mmr::Family>::init(context.child("reopen"), cfg, None)
1881                .await
1882                .unwrap();
1883            assert_eq!(reopened.target(), tip_target);
1884
1885            // The corrupt entry fails the recovery before any truncation.
1886            assert!(matches!(
1887                {
1888                    drop(reopened);
1889                    open_bounded::<mmr::Family>(
1890                        context.child("cap"),
1891                        witness_config(partition),
1892                        initialization_bound,
1893                    )
1894                    .await
1895                },
1896                Err(Error::DataCorrupted(_))
1897            ));
1898
1899            // The newer history survives: reopen still lands on the original tip.
1900            let cfg = Config {
1901                strategy: Sequential,
1902                witness: witness_config(partition),
1903                commit_codec_config: (),
1904            };
1905            let reopened = TestDb::<mmr::Family>::init(context.child("reopen2"), cfg, None)
1906                .await
1907                .unwrap();
1908            assert_eq!(reopened.target(), tip_target);
1909            reopened.destroy().await.unwrap();
1910        });
1911    }
1912
1913    #[test_traced("INFO")]
1914    fn test_compact_reopen_rejects_interrupted_import() {
1915        deterministic::Runner::default().start(|context| async move {
1916            let partition = "immutable-interrupted-import";
1917            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
1918            let batch = db
1919                .new_batch()
1920                .set(Sha256::hash(&[&[7]]), Sha256::fill(7u8))
1921                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(1))
1922                .await
1923                .unwrap();
1924            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1925            drop(db);
1926
1927            // Simulate a crash between an import's journal clear and its entry append: the
1928            // journal is empty but its size is nonzero.
1929            let journal = open_witness_journal(context.child("clear"), partition).await;
1930            let size = journal.size();
1931            let journal = journal.clear_to_size(size.max(1)).await.unwrap();
1932            drop(journal);
1933
1934            // Reopen must fail rather than bootstrap a fresh db.
1935            let cfg = Config {
1936                strategy: Sequential,
1937                witness: witness_config(partition),
1938                commit_codec_config: (),
1939            };
1940            let reopened =
1941                TestDb::<mmr::Family>::init(context.child("reopen_witness"), cfg, None).await;
1942            assert!(matches!(
1943                reopened,
1944                Err(Error::DataCorrupted("witness journal has no tip"))
1945            ));
1946        });
1947    }
1948
1949    #[test_traced("INFO")]
1950    fn test_compact_reopen_rejects_commit_floor_beyond_tip() {
1951        deterministic::Runner::default().start(|context| async move {
1952            let partition = "immutable-invalid-persisted-floor";
1953            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
1954            let batch = db
1955                .new_batch()
1956                .set(Sha256::hash(&[&[7]]), Sha256::fill(7u8))
1957                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(1))
1958                .await
1959                .unwrap();
1960            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
1961            drop(db);
1962            let oversized_floor = Location::new(10);
1963
1964            // Overwrite the persisted commit op with a floor beyond its own commit location.
1965            let journal = open_witness_journal(context.child("tamper"), partition).await;
1966            let (_, size, pinned_nodes) = witness::tests::tip(&journal).await;
1967            let bad_op = Operation::<mmr::Family, Digest, FixedEncoding<Digest>>::Commit(
1968                Some(Sha256::fill(0xaa)),
1969                oversized_floor,
1970            )
1971            .encode()
1972            .to_vec();
1973            witness::tests::overwrite_tip(journal, bad_op, size, pinned_nodes).await;
1974
1975            let cfg = Config {
1976                strategy: Sequential,
1977                witness: witness_config(partition),
1978                commit_codec_config: (),
1979            };
1980            let reopened =
1981                TestDb::<mmr::Family>::init(context.child("reopen_witness"), cfg, None).await;
1982            assert!(matches!(
1983                reopened,
1984                Err(Error::DataCorrupted("invalid compact witness"))
1985            ));
1986        });
1987    }
1988
1989    #[test_traced("INFO")]
1990    fn test_compact_reopen_rejects_tampered_pinned_nodes() {
1991        deterministic::Runner::default().start(|context| async move {
1992            let partition = "immutable-pinned-nodes-tamper";
1993            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
1994            let batch = db
1995                .new_batch()
1996                .set(Sha256::hash(&[&[7]]), Sha256::fill(7u8))
1997                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(1))
1998                .await
1999                .unwrap();
2000            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2001            let tampered_target = db.target();
2002            drop(db);
2003
2004            // Flip one pinned-node digest. There is no stored proof to cross-check against, so the
2005            // rebuild succeeds and yields a different root, the same way a bit-flipped replay
2006            // journal reopens with a different root.
2007            let journal = open_witness_journal(context.child("tamper"), partition).await;
2008            let (op_bytes, size, mut pinned_nodes) = witness::tests::tip(&journal).await;
2009            pinned_nodes[0] = Sha256::fill(0xff);
2010            witness::tests::overwrite_tip(journal, op_bytes, size, pinned_nodes).await;
2011
2012            let cfg = Config {
2013                strategy: Sequential,
2014                witness: witness_config(partition),
2015                commit_codec_config: (),
2016            };
2017            let reopened = TestDb::<mmr::Family>::init(context.child("reopen_witness"), cfg, None)
2018                .await
2019                .unwrap();
2020            assert_ne!(reopened.target(), tampered_target);
2021            reopened.destroy().await.unwrap();
2022        });
2023    }
2024
2025    /// A witness entry appended but not synced (a commit interrupted before its journal sync)
2026    /// must be dropped on reopen, recovering the last synced commit.
2027    #[test_traced("INFO")]
2028    fn test_compact_reopen_drops_unsynced_witness() {
2029        deterministic::Runner::default().start(|context| async move {
2030            let partition = "immutable-witness-unsynced";
2031            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
2032
2033            // Commit state A.
2034            let batch = db
2035                .new_batch()
2036                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2037                .merkleize(&db, Some(Sha256::fill(0xa1)), Location::new(1))
2038                .await
2039                .unwrap();
2040            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2041            let target_a = db.target();
2042            drop(db);
2043
2044            // Simulate the crash window: append an entry ahead of the tip without syncing it,
2045            // then drop the journal. The unsynced tail must not survive reopen.
2046            let journal = open_witness_journal(context.child("crash"), partition).await;
2047            let (op_bytes, mut size, pinned_nodes) = witness::tests::tip(&journal).await;
2048            size += 2;
2049            witness::tests::append_unsynced(journal, op_bytes, size, pinned_nodes).await;
2050
2051            // Reopen must drop the unsynced entry and recover state A.
2052            let reopened = open_db::<mmr::Family>(context.child("reopen"), partition).await;
2053            assert_eq!(reopened.target(), target_a);
2054            reopened.destroy().await.unwrap();
2055        });
2056    }
2057
2058    #[test_traced("INFO")]
2059    fn test_compact_initialization_zero_and_above_end() {
2060        deterministic::Runner::default().start(|context| async move {
2061            let cfg = witness_config("immutable-caps");
2062            assert!(matches!(
2063                open_bounded::<mmr::Family>(context.child("zero"), cfg.clone(), Location::new(0))
2064                    .await,
2065                Err(Error::InvalidInitializationBound)
2066            ));
2067            let db = open_bounded::<mmr::Family>(
2068                context.child("fresh"),
2069                cfg.clone(),
2070                Location::new(100),
2071            )
2072            .await
2073            .unwrap();
2074            let root = db.root();
2075            assert_eq!(db.size(), Location::new(1));
2076            drop(db);
2077            let db = open_bounded::<mmr::Family>(context.child("above"), cfg, Location::new(100))
2078                .await
2079                .unwrap();
2080            assert_eq!(db.root(), root);
2081            assert_eq!(db.size(), Location::new(1));
2082        });
2083    }
2084
2085    #[test_traced("INFO")]
2086    fn test_compact_bounded_initialization_between_commits() {
2087        deterministic::Runner::default().start(|context| async move {
2088            let db = open_db::<mmr::Family>(context.child("db"), "immutable-rewind-between").await;
2089            let initial_root = db.root();
2090
2091            // A multi-op commit jumps the committed size from 1 (bootstrap) to 4.
2092            let batch = db
2093                .new_batch()
2094                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2095                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
2096                .merkleize(&db, Some(Sha256::fill(0xa1)), Location::new(0))
2097                .await
2098                .unwrap();
2099            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2100            let root_a = db.root();
2101            let size_a = db.size();
2102            assert_eq!(size_a, Location::new(4));
2103
2104            // A second commit moves the size to 6.
2105            let batch = db
2106                .new_batch()
2107                .set(Sha256::hash(&[&[3]]), Sha256::fill(3u8))
2108                .merkleize(&db, Some(Sha256::fill(0xb1)), Location::new(0))
2109                .await
2110                .unwrap();
2111            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2112            let cfg = witness_config("immutable-rewind-between");
2113            drop(db);
2114            for (target, size, root) in [(5, 4, root_a), (3, 1, initial_root), (2, 1, initial_root)]
2115            {
2116                let db = open_bounded::<mmr::Family>(
2117                    context.child("cap"),
2118                    cfg.clone(),
2119                    Location::new(target),
2120                )
2121                .await
2122                .unwrap();
2123                assert_eq!(db.size(), Location::new(size));
2124                assert_eq!(db.root(), root);
2125                drop(db);
2126                let db =
2127                    open_db::<mmr::Family>(context.child("restart"), "immutable-rewind-between")
2128                        .await;
2129                assert_eq!(db.root(), root);
2130            }
2131        });
2132    }
2133
2134    #[test_traced("INFO")]
2135    fn test_compact_bounded_initialization_multiple_commits() {
2136        deterministic::Runner::default().start(|context| async move {
2137            let partition = "immutable-rewind-multi";
2138            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
2139
2140            // Commit A, B, C, recording the state after A.
2141            let batch = db
2142                .new_batch()
2143                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2144                .merkleize(&db, Some(Sha256::fill(0xa1)), Location::new(0))
2145                .await
2146                .unwrap();
2147            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2148            let root_a = db.root();
2149            let size_a = db.size();
2150            let target_a = db.target();
2151
2152            let mut db = db;
2153            for i in [2u8, 3] {
2154                let batch = db
2155                    .new_batch()
2156                    .set(Sha256::hash(&[&[i]]), Sha256::fill(i))
2157                    .merkleize(&db, Some(Sha256::fill(i)), Location::new(0))
2158                    .await
2159                    .unwrap();
2160                db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2161            }
2162            assert_ne!(db.root(), root_a);
2163
2164            // Reopen two commits earlier.
2165            let db = {
2166                _ = db.sync().await.unwrap();
2167                open_bounded::<mmr::Family>(context.child("cap"), witness_config(partition), size_a)
2168                    .await
2169            }
2170            .unwrap();
2171            assert_eq!(db.root(), root_a);
2172            assert_eq!(db.size(), size_a);
2173            assert_eq!(db.get_metadata(), Some(Sha256::fill(0xa1)));
2174            assert_eq!(db.target(), target_a);
2175            drop(db);
2176
2177            // The recovery is durable: reopen recovers state A.
2178            let db = open_db::<mmr::Family>(context.child("reopen"), partition).await;
2179            assert_eq!(db.root(), root_a);
2180            assert_eq!(db.target(), target_a);
2181            db.destroy().await.unwrap();
2182        });
2183    }
2184
2185    #[test_traced("INFO")]
2186    fn test_compact_bounded_initialization_preserves_current_state() {
2187        deterministic::Runner::default().start(|context| async move {
2188            let db = open_db::<mmr::Family>(context.child("db"), "immutable-rewind-noop").await;
2189            let batch = db
2190                .new_batch()
2191                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2192                .merkleize(&db, Some(Sha256::fill(0xa1)), Location::new(0))
2193                .await
2194                .unwrap();
2195            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2196            let root = db.root();
2197            let size = db.size();
2198
2199            let db = {
2200                _ = db.sync().await.unwrap();
2201                open_bounded::<mmr::Family>(
2202                    context.child("cap"),
2203                    witness_config("immutable-rewind-noop"),
2204                    size,
2205                )
2206                .await
2207            }
2208            .unwrap();
2209            assert_eq!(db.root(), root);
2210            assert_eq!(db.size(), size);
2211            db.destroy().await.unwrap();
2212        });
2213    }
2214
2215    #[test_traced("INFO")]
2216    fn test_compact_prune_then_bounded_initialization() {
2217        deterministic::Runner::default().start(|context| async move {
2218            // One entry per section so pruning takes effect at entry granularity (pruning is
2219            // section-aligned and never drops a partial section).
2220            let mut witness_cfg = witness_config("immutable-prune-rewind");
2221            witness_cfg.items_per_section = NZU64!(1);
2222            let cfg = Config {
2223                strategy: Sequential,
2224                witness: witness_cfg.clone(),
2225                commit_codec_config: (),
2226            };
2227            let mut db: TestDb<mmr::Family> =
2228                Db::init(context.child("db"), cfg, None).await.unwrap();
2229
2230            // Commit A, B, C.
2231            let mut sizes = Vec::new();
2232            for i in [1u8, 2, 3] {
2233                let batch = db
2234                    .new_batch()
2235                    .set(Sha256::hash(&[&[i]]), Sha256::fill(i))
2236                    .merkleize(&db, Some(Sha256::fill(i)), Location::new(0))
2237                    .await
2238                    .unwrap();
2239                db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2240                sizes.push(db.size());
2241            }
2242
2243            // Reopen retained state B before the terminal check that pruned state A is unavailable.
2244            let db = db.prune(sizes[1]).await.unwrap().sync().await.unwrap();
2245            drop(db);
2246            let db = open_bounded::<mmr::Family>(
2247                context.child("retained"),
2248                witness_cfg.clone(),
2249                sizes[1],
2250            )
2251            .await
2252            .unwrap();
2253            assert_eq!(db.size(), sizes[1]);
2254            assert_eq!(db.get_metadata(), Some(Sha256::fill(2)));
2255            drop(db);
2256
2257            assert!(matches!(
2258                open_bounded::<mmr::Family>(context.child("pruned"), witness_cfg, sizes[0]).await,
2259                Err(Error::HistoricalFloorPruned(_))
2260            ));
2261        });
2262    }
2263
2264    /// Witness config holding one witness per section, so every commit occupies its own blobs.
2265    fn sectioned_witness_config(partition: &str) -> JournalConfig<()> {
2266        let mut cfg = witness_config(partition);
2267        cfg.items_per_section = NZU64!(1);
2268        cfg
2269    }
2270
2271    /// Seed a db with the bootstrap witness plus `commits` synced metadata-only commits,
2272    /// returning the size and root after each commit. Every commit appends exactly its commit
2273    /// operation, so the witness at position `p` has size `p + 1`.
2274    async fn seed_witness_sections(
2275        context: deterministic::Context,
2276        witness_cfg: JournalConfig<()>,
2277        commits: u8,
2278    ) -> Vec<(Location<mmr::Family>, Digest)> {
2279        let cfg = Config {
2280            strategy: Sequential,
2281            witness: witness_cfg,
2282            commit_codec_config: (),
2283        };
2284        let mut db: TestDb<mmr::Family> = Db::init(context, cfg, None).await.unwrap();
2285        let mut states = Vec::new();
2286        for i in 1..=commits {
2287            let batch = db
2288                .new_batch()
2289                .merkleize(&db, Some(Sha256::fill(i)), db.inactivity_floor_loc())
2290                .await
2291                .unwrap();
2292            db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2293            states.push((db.size(), db.root()));
2294        }
2295        states
2296    }
2297
2298    /// Leave witness data blob `blob` with a partial trailing page, as a crash mid-write would.
2299    /// Paged tail recovery repairs (resizes and syncs) such a tail when the blob is opened.
2300    async fn tear_witness_data(
2301        context: &deterministic::Context,
2302        witness_cfg: &JournalConfig<()>,
2303        blob: u64,
2304    ) {
2305        // The variable journal keeps data blobs in `{partition}_data`, named by the big-endian
2306        // section index.
2307        let partition = format!("{}_data", witness_cfg.partition);
2308        let (handle, len) = context.open(&partition, &blob.to_be_bytes()).await.unwrap();
2309        handle.resize(len - 1).await.unwrap();
2310        handle.sync().await.unwrap();
2311    }
2312
2313    /// Bounded init through a delayed-sync backend, returning the db and the durability calls
2314    /// initialization made.
2315    async fn open_bounded_counting(
2316        context: deterministic::Context,
2317        witness_cfg: JournalConfig<()>,
2318        cap: Location<mmr::Family>,
2319    ) -> (DelayedDb, usize) {
2320        // Arming counts every durability call from here on. The gate blocks the first call and
2321        // every later started sync parks. `drive_pending_syncs` releases them whenever
2322        // initialization stalls.
2323        let pending = PendingSyncs::default();
2324        pending.arm();
2325        let delayed = DelayedSyncContext {
2326            inner: context,
2327            pending: pending.clone(),
2328        };
2329        let cfg = Config {
2330            strategy: Sequential,
2331            witness: witness_cfg,
2332            commit_codec_config: (),
2333        };
2334        let db = drive_pending_syncs(&pending, DelayedDb::init(delayed, cfg, Some(cap)))
2335            .await
2336            .unwrap();
2337        (db, pending.calls())
2338    }
2339
2340    /// Bounded initialization opens no witness section the bound discards, so a torn tail there
2341    /// costs no repair.
2342    #[test_traced]
2343    fn test_compact_bounded_initialization_ignores_discarded_witness_sections() {
2344        deterministic::Runner::default().start(|context| async move {
2345            // Build twin journals with one witness per section, so position `p` lives in data
2346            // blob `p`. Each holds the bootstrap witness (position 0, size 1), a synced commit
2347            // (position 1, size 2), and a commit without sync (position 2, size 3). The control
2348            // twin stays intact and sets the baseline durability count.
2349            let control_cfg = sectioned_witness_config("immutable-skip-discarded-control");
2350            let torn_cfg = sectioned_witness_config("immutable-skip-discarded-torn");
2351            let mut states = Vec::new();
2352            for (label, witness_cfg) in [("control", &control_cfg), ("torn", &torn_cfg)] {
2353                let context = context.child(label);
2354                states.push(
2355                    seed_witness_sections(context.child("seed"), witness_cfg.clone(), 1).await[0],
2356                );
2357
2358                // Commit without syncing so the witness at position 2 lies above the recovery
2359                // watermark, where a torn tail is a crash shape rather than corruption.
2360                let cfg = Config {
2361                    strategy: Sequential,
2362                    witness: witness_cfg.clone(),
2363                    commit_codec_config: (),
2364                };
2365                let db: TestDb<mmr::Family> =
2366                    Db::init(context.child("extend"), cfg, None).await.unwrap();
2367                let batch = db
2368                    .new_batch()
2369                    .merkleize(&db, Some(Sha256::fill(2)), db.inactivity_floor_loc())
2370                    .await
2371                    .unwrap();
2372                let db = db.apply_batch(batch).await.unwrap();
2373                drop(db.commit().await.unwrap());
2374            }
2375
2376            // The twins share one history, so the synced commit's size and root are the expected
2377            // recovery for both.
2378            assert_eq!(states[0], states[1]);
2379            let (size, root) = states[0];
2380
2381            // The witness at position 1 has size `size`, so the bound discards position 2 and
2382            // must not open it. Tear its data blob: repairing the tail would sync it.
2383            tear_witness_data(&context, &torn_cfg, 2).await;
2384
2385            // Both twins recover the synced commit under the bound. They differ only in data
2386            // blob 2, so equal durability counts show the torn blob was not repaired.
2387            let (control, control_calls) =
2388                open_bounded_counting(context.child("control"), control_cfg, size).await;
2389            let (torn, torn_calls) =
2390                open_bounded_counting(context.child("torn"), torn_cfg, size).await;
2391            assert_eq!(control.size(), size);
2392            assert_eq!(control.root(), root);
2393            assert_eq!(torn.size(), size);
2394            assert_eq!(torn.root(), root);
2395            assert_eq!(
2396                torn_calls, control_calls,
2397                "the discarded torn section must not be repaired"
2398            );
2399            control.destroy().await.unwrap();
2400            torn.destroy().await.unwrap();
2401        });
2402    }
2403
2404    /// A torn tail in a retained witness section is still repaired under a bound. The witness it
2405    /// held is lost and initialization falls back to the previous one.
2406    #[test_traced]
2407    fn test_compact_bounded_initialization_repairs_retained_witness_section() {
2408        deterministic::Runner::default().start(|context| async move {
2409            // Both witnesses share a section, so the bootstrap witness survives the torn tail
2410            // page and ends mid-page, where recovery must rewrite rather than only shrink.
2411            let clean_cfg = witness_config("immutable-repair-retained-clean");
2412            let torn_cfg = witness_config("immutable-repair-retained-torn");
2413            let mut states = Vec::new();
2414            for (label, witness_cfg) in [("clean", &clean_cfg), ("torn", &torn_cfg)] {
2415                // Fresh storage bootstraps the witness at position 0 (size 1), the state the torn
2416                // twin falls back to.
2417                let cfg = Config {
2418                    strategy: Sequential,
2419                    witness: witness_cfg.clone(),
2420                    commit_codec_config: (),
2421                };
2422                let db: TestDb<mmr::Family> =
2423                    Db::init(context.child(label), cfg, None).await.unwrap();
2424                let genesis = db.root();
2425
2426                // Commit without syncing so the witness at position 1 lies above the recovery
2427                // watermark, where a torn tail is a crash shape rather than corruption.
2428                let batch = db
2429                    .new_batch()
2430                    .merkleize(&db, Some(Sha256::fill(1)), db.inactivity_floor_loc())
2431                    .await
2432                    .unwrap();
2433                let db = db.apply_batch(batch).await.unwrap().commit().await.unwrap();
2434                states.push((genesis, db.size(), db.root()));
2435            }
2436            assert_eq!(states[0], states[1]);
2437            let (genesis, size, root) = states[0];
2438
2439            // The torn twin's last page in section 0 holds the end of the witness at position 1,
2440            // so tearing it leaves that witness incomplete.
2441            tear_witness_data(&context, &torn_cfg, 0).await;
2442
2443            // Section 0 lies below the bound, so both twins open it. The torn twin trims its
2444            // tail and republishes the journal without the lost witness.
2445            let (clean, clean_calls) =
2446                open_bounded_counting(context.child("clean"), clean_cfg, size).await;
2447            let (torn, torn_calls) =
2448                open_bounded_counting(context.child("torn"), torn_cfg, size).await;
2449
2450            // The clean twin recovers the witness at position 1. The torn twin recovers the
2451            // bootstrap witness and spends extra durability calls on the repair.
2452            assert_eq!(clean.size(), size);
2453            assert_eq!(clean.root(), root);
2454            assert_eq!(torn.size(), Location::new(1));
2455            assert_eq!(torn.root(), genesis);
2456            assert!(
2457                torn_calls > clean_calls,
2458                "the retained torn section is repaired"
2459            );
2460            clean.destroy().await.unwrap();
2461            torn.destroy().await.unwrap();
2462        });
2463    }
2464
2465    /// A compact-sync import lands at the journal end whatever its size, so afterwards witness
2466    /// positions can exceed sizes. Bounded initialization still selects by size, widening its
2467    /// view when the positions below the bound cannot settle the selection.
2468    #[test_traced("INFO")]
2469    fn test_compact_bounded_initialization_after_import_below_positions() {
2470        deterministic::Runner::default().start(|context| async move {
2471            // A source state of size 2, captured from its witness journal. Its tip witness holds
2472            // the pinned nodes one operation below the commit, which a compact-sync boundary
2473            // response carries.
2474            let src_cfg = sectioned_witness_config("immutable-import-below-src");
2475            let (src_size, src_root) =
2476                seed_witness_sections(context.child("src"), src_cfg.clone(), 1).await[0];
2477            let journal =
2478                witness::Journal::<_, mmr::Family, Digest>::init(context.child("tip"), src_cfg)
2479                    .await
2480                    .unwrap();
2481            let (_, _, src_pinned) = witness::tests::tip(&journal).await;
2482            drop(journal);
2483
2484            // The destination has used positions 0 through 3, so the import lands at position 4.
2485            let dst_cfg = sectioned_witness_config("immutable-import-below-dst");
2486            seed_witness_sections(context.child("dst"), dst_cfg.clone(), 3).await;
2487
2488            // Import the source state from its last commit location, pinned nodes, and commit
2489            // operation. `seed_witness_sections` committed it with metadata `fill(1)` at floor 0,
2490            // so the rebuilt root matches the source root.
2491            let journal = witness::Journal::init(context.child("import"), dst_cfg.clone())
2492                .await
2493                .unwrap();
2494            let imported = TestDb::<mmr::Family>::init_from_sync(
2495                Sequential,
2496                journal,
2497                (),
2498                src_size - 1,
2499                src_pinned,
2500                Operation::Commit(Some(Sha256::fill(1)), Location::new(0)),
2501            )
2502            .unwrap();
2503            assert_eq!(imported.root(), src_root);
2504
2505            // Committing applies the pending import. It clears the destination journal at its end
2506            // and appends the imported witness there, so its only entry has size 2 at position 4.
2507            drop(imported.commit().await.unwrap());
2508            let journal = witness::Journal::<_, mmr::Family, Digest>::init(
2509                context.child("placed"),
2510                dst_cfg.clone(),
2511            )
2512            .await
2513            .unwrap();
2514            assert_eq!(journal.bounds(), 4..5);
2515            drop(journal);
2516
2517            // Reopening recovers the imported state. Three more commits occupy positions 5 through
2518            // 7 with sizes 3 through 5. `states` holds the size and root at positions 4 through 7.
2519            let cfg = Config {
2520                strategy: Sequential,
2521                witness: dst_cfg.clone(),
2522                commit_codec_config: (),
2523            };
2524            let mut db: TestDb<mmr::Family> =
2525                Db::init(context.child("reopen"), cfg, None).await.unwrap();
2526            assert_eq!(db.size(), src_size);
2527            let mut states = vec![(src_size, src_root)];
2528            for i in 2..=4u8 {
2529                let batch = db
2530                    .new_batch()
2531                    .merkleize(&db, Some(Sha256::fill(i)), db.inactivity_floor_loc())
2532                    .await
2533                    .unwrap();
2534                db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2535                states.push((db.size(), db.root()));
2536            }
2537            drop(db);
2538
2539            // Open at each recorded size from the tip down. The cap-5 bounded view holds only
2540            // position 4 (size 2). It ends at the bound with a tip size below the bound, so
2541            // recovery widens and selects position 7 by size. Caps 4, 3, and 2 lie at or below the
2542            // retained start (position 4), so recovery opens unbounded and selects positions 6, 5,
2543            // and 4. Each open discards the witnesses above its selection, so the caps must
2544            // descend.
2545            for (size, root) in states.into_iter().rev() {
2546                let db = open_bounded::<mmr::Family>(
2547                    context.child("bounded").with_attribute("cap", *size),
2548                    dst_cfg.clone(),
2549                    size,
2550                )
2551                .await
2552                .unwrap();
2553                assert_eq!(db.size(), size);
2554                assert_eq!(db.root(), root);
2555            }
2556
2557            // Only the imported witness remains, and its size 2 exceeds cap 1.
2558            assert!(matches!(
2559                open_bounded::<mmr::Family>(context.child("pruned"), dst_cfg, Location::new(1))
2560                    .await,
2561                Err(Error::HistoricalFloorPruned(pruned)) if pruned == Location::new(1)
2562            ));
2563        });
2564    }
2565
2566    /// A bounded initialization whose selection fails leaves the witness offsets watermark
2567    /// acknowledging every synced witness.
2568    #[test_traced]
2569    fn test_compact_failed_bounded_selection_preserves_acknowledged_offsets() {
2570        deterministic::Runner::default().start(|context| async move {
2571            let partition = "immutable-failed-bounded-offsets";
2572            let cap = Location::new(5);
2573            let witness_cfg = sectioned_witness_config(partition);
2574
2575            // The witness journal lives in `{partition}-witness`. Its offsets journal, whose
2576            // checkpoint persists the recovery watermark, lives in `{partition}-witness_offsets`.
2577            let offsets_partition = format!("{partition}-witness_offsets");
2578            let cfg = Config {
2579                strategy: Sequential,
2580                witness: witness_cfg.clone(),
2581                commit_codec_config: (),
2582            };
2583            let mut db: TestDb<mmr::Family> =
2584                Db::init(context.child("seed"), cfg, None).await.unwrap();
2585
2586            // The first batch appends six sets and a commit after the bootstrap commit, so the
2587            // witness at position 1 has size 8, above the cap used below.
2588            let mut batch = db.new_batch();
2589            for key in 1..=6u8 {
2590                batch = batch.set(Sha256::fill(key), Sha256::fill(key));
2591            }
2592            let batch = batch.merkleize(&db, None, Location::new(0)).await.unwrap();
2593            db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2594            let first_retained_size = db.size();
2595            assert!(first_retained_size > cap);
2596
2597            // Eight synced empty commits occupy positions 2 through 9 with sizes 9 through 16.
2598            // One witness per section puts positions 1 through 4 in the sections the cap-5 view
2599            // opens, and positions 5 through 9 in sections it discards.
2600            for _ in 0..8 {
2601                let batch = db
2602                    .new_batch()
2603                    .merkleize(&db, None, db.inactivity_floor_loc())
2604                    .await
2605                    .unwrap();
2606                db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2607            }
2608            let tip = db.size();
2609
2610            // Pruning at the first commit removes the bootstrap section. The retained start
2611            // (position 1) lies below the cap while its size lies above it. A retained start at
2612            // or above the cap would open unbounded instead.
2613            let db = db
2614                .prune(first_retained_size)
2615                .await
2616                .unwrap()
2617                .sync()
2618                .await
2619                .unwrap();
2620            drop(db);
2621
2622            // Sync acknowledged all ten witnesses (positions 0 through 9). The watermark lies above
2623            // the cap, so any lowering to the cap is visible below.
2624            let watermark = fixed::Journal::<_, u64>::persisted_watermark(
2625                context.child("before"),
2626                &offsets_partition,
2627            )
2628            .await
2629            .unwrap();
2630            assert_eq!(watermark, Some(10));
2631
2632            // No compact-sync import put a smaller size at the journal end, so the bounded view
2633            // ends at the cap with a tip size above it and recovery stays bounded. The offsets open
2634            // clamps the watermark to the cap, so inspection anchors at the ceiling and leaves the
2635            // acknowledged offsets untouched. No retained witness fits under the cap, so selection
2636            // fails.
2637            assert!(matches!(
2638                open_bounded::<mmr::Family>(context.child("failed"), witness_cfg.clone(), cap)
2639                    .await,
2640                Err(Error::HistoricalFloorPruned(found)) if found == cap
2641            ));
2642
2643            // Selection fails before publication, and inspection anchored at the ceiling skips the
2644            // offsets truncate, so the failed attempt leaves the durable watermark at 10.
2645            let watermark = fixed::Journal::<_, u64>::persisted_watermark(
2646                context.child("after"),
2647                &offsets_partition,
2648            )
2649            .await
2650            .unwrap();
2651            assert_eq!(watermark, Some(10));
2652
2653            // Reinitializing at `tip` recovers every retained witness.
2654            let reopened = open_bounded::<mmr::Family>(context.child("retry"), witness_cfg, tip)
2655                .await
2656                .unwrap();
2657            assert_eq!(reopened.size(), tip);
2658            reopened.destroy().await.unwrap();
2659        });
2660    }
2661
2662    #[test_traced("INFO")]
2663    fn test_compact_prune_past_tip_keeps_tip() {
2664        deterministic::Runner::default().start(|context| async move {
2665            let partition = "immutable-prune-past-tip";
2666            let db = open_db::<mmr::Family>(context.child("db"), partition).await;
2667            let batch = db
2668                .new_batch()
2669                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2670                .merkleize(&db, Some(Sha256::fill(0xa1)), Location::new(0))
2671                .await
2672                .unwrap();
2673            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2674            let target = db.target();
2675
2676            // Prune with a boundary beyond the tip: the tip entry must survive.
2677            let boundary = db.size() + 100;
2678            let db = db.prune(boundary).await.unwrap();
2679            assert_eq!(db.target(), target);
2680            drop(db);
2681
2682            let reopened = open_db::<mmr::Family>(context.child("reopen"), partition).await;
2683            assert_eq!(reopened.target(), target);
2684            reopened.destroy().await.unwrap();
2685        });
2686    }
2687
2688    #[test_traced("INFO")]
2689    fn test_compact_bounded_initialization_preserves_pre_advance_batch() {
2690        deterministic::Runner::default().start(|context| async move {
2691            let db = open_db::<mmr::Family>(
2692                context.child("db"),
2693                "immutable-rewind-preserves-pre-advance",
2694            )
2695            .await;
2696
2697            let batch = db
2698                .new_batch()
2699                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2700                .merkleize(&db, None, Location::new(0))
2701                .await
2702                .unwrap();
2703            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2704            let size_after_first = db.size();
2705
2706            // Merkleize a batch against the post-commit-A state.
2707            let held = db
2708                .new_batch()
2709                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
2710                .merkleize(&db, None, Location::new(0))
2711                .await
2712                .unwrap();
2713
2714            // Advance past that state and commit, then reopen at that state.
2715            let batch = db
2716                .new_batch()
2717                .set(Sha256::hash(&[&[3]]), Sha256::fill(3u8))
2718                .merkleize(&db, None, Location::new(0))
2719                .await
2720                .unwrap();
2721            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2722            let db = {
2723                _ = db.sync().await.unwrap();
2724                open_bounded::<mmr::Family>(
2725                    context.child("cap"),
2726                    witness_config("immutable-rewind-preserves-pre-advance"),
2727                    size_after_first,
2728                )
2729                .await
2730            }
2731            .unwrap();
2732
2733            // The recovery restored the state that `held` was merkleized against, so it still
2734            // matches the Merkle size and applies cleanly.
2735            let db = db.apply_batch(held).await.unwrap();
2736
2737            db.destroy().await.unwrap();
2738        });
2739    }
2740
2741    #[test_traced("INFO")]
2742    fn test_compact_noop_commit_after_commit() {
2743        deterministic::Runner::default().start(|context| async move {
2744            let db =
2745                open_db::<mmr::Family>(context.child("db"), "immutable-noop-after-commit").await;
2746
2747            let k1 = Sha256::hash(&[&[1]]);
2748            let v1 = Sha256::fill(11u8);
2749            let k2 = Sha256::hash(&[&[2]]);
2750            let v2 = Sha256::fill(22u8);
2751            let batch = db
2752                .new_batch()
2753                .set(k1, v1)
2754                .set(k2, v2)
2755                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(0))
2756                .await
2757                .unwrap();
2758            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2759            let root_after_first = db.root();
2760            let size_after_first = db.size();
2761
2762            let db = db.sync().await.unwrap();
2763            assert_eq!(db.size(), size_after_first);
2764            assert_eq!(db.root(), root_after_first);
2765
2766            db.destroy().await.unwrap();
2767        });
2768    }
2769
2770    #[test_traced("INFO")]
2771    fn test_compact_noop_commit_after_reopen() {
2772        deterministic::Runner::default().start(|context| async move {
2773            let partition = "immutable-noop-after-reopen";
2774
2775            let (root_before_drop, size_before_drop) = {
2776                let db = open_db::<mmr::Family>(context.child("first"), partition).await;
2777                let k1 = Sha256::hash(&[&[1]]);
2778                let v1 = Sha256::fill(11u8);
2779                let k2 = Sha256::hash(&[&[2]]);
2780                let v2 = Sha256::fill(22u8);
2781                let batch = db
2782                    .new_batch()
2783                    .set(k1, v1)
2784                    .set(k2, v2)
2785                    .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(0))
2786                    .await
2787                    .unwrap();
2788                let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2789                (db.root(), db.size())
2790            };
2791
2792            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
2793            assert_eq!(db.root(), root_before_drop);
2794            assert_eq!(db.size(), size_before_drop);
2795
2796            let db = db.sync().await.unwrap();
2797            assert_eq!(db.size(), size_before_drop);
2798            assert_eq!(db.root(), root_before_drop);
2799
2800            db.destroy().await.unwrap();
2801        });
2802    }
2803
2804    #[test_traced("INFO")]
2805    fn test_compact_noop_commit_after_bounded_initialization() {
2806        deterministic::Runner::default().start(|context| async move {
2807            let db =
2808                open_db::<mmr::Family>(context.child("db"), "immutable-noop-after-rewind").await;
2809
2810            let k1 = Sha256::hash(&[&[1]]);
2811            let v1 = Sha256::fill(11u8);
2812            let k2 = Sha256::hash(&[&[2]]);
2813            let v2 = Sha256::fill(22u8);
2814            let batch = db
2815                .new_batch()
2816                .set(k1, v1)
2817                .set(k2, v2)
2818                .merkleize(&db, Some(Sha256::fill(0xaa)), Location::new(0))
2819                .await
2820                .unwrap();
2821            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2822            let root_after_first = db.root();
2823            let size_after_first = db.size();
2824
2825            let k3 = Sha256::hash(&[&[3]]);
2826            let v3 = Sha256::fill(33u8);
2827            let batch = db
2828                .new_batch()
2829                .set(k3, v3)
2830                .merkleize(&db, Some(Sha256::fill(0xbb)), Location::new(1))
2831                .await
2832                .unwrap();
2833            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2834
2835            let db = {
2836                _ = db.sync().await.unwrap();
2837                open_bounded::<mmr::Family>(
2838                    context.child("cap"),
2839                    witness_config("immutable-noop-after-rewind"),
2840                    size_after_first,
2841                )
2842                .await
2843            }
2844            .unwrap();
2845            assert_eq!(db.size(), size_after_first);
2846            assert_eq!(db.root(), root_after_first);
2847
2848            let db = db.sync().await.unwrap();
2849            assert_eq!(db.size(), size_after_first);
2850            assert_eq!(db.root(), root_after_first);
2851
2852            db.destroy().await.unwrap();
2853        });
2854    }
2855
2856    #[test_traced("INFO")]
2857    fn test_compact_bounded_initialization_makes_post_advance_batch_stale() {
2858        deterministic::Runner::default().start(|context| async move {
2859            let db =
2860                open_db::<mmr::Family>(context.child("db"), "immutable-rewind-makes-stale").await;
2861
2862            let batch = db
2863                .new_batch()
2864                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2865                .merkleize(&db, None, Location::new(0))
2866                .await
2867                .unwrap();
2868            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2869            let size_after_first = db.size();
2870
2871            let batch = db
2872                .new_batch()
2873                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
2874                .merkleize(&db, None, Location::new(0))
2875                .await
2876                .unwrap();
2877            let db = db.apply_batch(batch).await.unwrap().sync().await.unwrap();
2878
2879            // Merkleize a batch against the post-commit-B state, which the recovery will discard.
2880            let held = db
2881                .new_batch()
2882                .set(Sha256::hash(&[&[3]]), Sha256::fill(3u8))
2883                .merkleize(&db, None, Location::new(0))
2884                .await
2885                .unwrap();
2886
2887            let db = {
2888                _ = db.sync().await.unwrap();
2889                open_bounded::<mmr::Family>(
2890                    context.child("cap"),
2891                    witness_config("immutable-rewind-makes-stale"),
2892                    size_after_first,
2893                )
2894                .await
2895            }
2896            .unwrap();
2897
2898            // After recovery, mem.size reflects post-commit-A, but the held batch starts after
2899            // post-commit-B. Apply must be rejected with StaleBatch.
2900            assert!(matches!(db.apply_batch(held).await, Err(Error::StaleBatch)));
2901        });
2902    }
2903
2904    #[test_traced("INFO")]
2905    fn test_compact_floor_beyond_size() {
2906        deterministic::Runner::default().start(|context| async move {
2907            let db = open_db::<mmr::Family>(context.child("db"), "immutable-floor-beyond").await;
2908
2909            let batch = db.new_batch().merkleize(&db, None, Location::new(2)).await;
2910
2911            assert!(matches!(
2912                batch,
2913                Err(Error::FloorBeyondSize(floor, tip))
2914                    if floor == Location::new(2) && tip == Location::new(1)
2915            ));
2916        });
2917    }
2918
2919    // Merkleize returns `FloorBeyondSize` for a chained batch whose floor exceeds its own commit
2920    // location. The commit location includes the parent's operations.
2921    #[test_traced("INFO")]
2922    fn test_compact_chained_floor_beyond_commit() {
2923        deterministic::Runner::default().start(|context| async move {
2924            let db = open_db::<mmr::Family>(context.child("db"), "immutable-ancestor-floor-beyond")
2925                .await;
2926
2927            // parent: set + commit at loc 2, floor=2.
2928            let parent = db
2929                .new_batch()
2930                .set(Sha256::hash(&[&[1]]), Sha256::fill(1u8))
2931                .merkleize(&db, None, Location::new(2))
2932                .await
2933                .unwrap();
2934
2935            // child: set + commit at loc 4, floor=5 (one past its commit).
2936            let child = parent
2937                .new_batch::<Sha256>()
2938                .set(Sha256::hash(&[&[2]]), Sha256::fill(2u8))
2939                .merkleize(&db, None, Location::new(5))
2940                .await;
2941
2942            assert!(matches!(
2943                child,
2944                Err(Error::FloorBeyondSize(floor, commit))
2945                    if floor == Location::new(5) && commit == Location::new(4)
2946            ));
2947        });
2948    }
2949}