Skip to main content

commonware_storage/qmdb/keyless/
compact.rs

1//! A keyless authenticated db that discards historical operations, retaining only a witness
2//! for each synced commit.
3//!
4//! Mirrors the API of [`crate::qmdb::keyless::Keyless`] (`new_batch -> merkleize ->
5//! apply_batch -> sync`, pipelined batch chains, `StaleBatch` validation) but is backed by
6//! the peak-only [`crate::merkle::compact`]. Because history is discarded, there are no
7//! `get` / `proof` / `bounds` methods; use the full variant if you need them.
8//!
9//! # Witness journal
10//!
11//! The witness journal holds a complete snapshot of every synced commit, so [`Db::rewind`] can
12//! restore any commit still retained there (history is bounded only by [`Db::prune`]). Reopen
13//! and rewind re-verify the persisted snapshot; corruption surfaces as [`Error::DataCorrupted`].
14//! The witness (the last-commit operation plus its inclusion proof) is also what lets compact
15//! nodes serve compact sync without retaining historical operations.
16//!
17//! # Inactivity floor
18//!
19//! Commits carry an inactivity floor for wire-format compatibility with
20//! [`crate::qmdb::keyless::Keyless`]: the root is computed over the encoded operation
21//! sequence, and that sequence must include the same floor to produce the same root as the
22//! full variant. The floor has no effect on pruning or snapshot rebuilding here; all
23//! historical in-memory state is discarded on every sync.
24
25use super::operation::Operation;
26use crate::{
27    journal::contiguous::variable::{self, Config as JournalConfig},
28    merkle::{batch, compact as compact_merkle, Family, Location},
29    qmdb::{
30        self,
31        any::value::ValueEncoding,
32        batch_chain::{self, Bounds},
33        compact::{
34            batch as compact_batch,
35            witness::{self, VerifiedWitness, Witness},
36        },
37        sync::compact as compact_sync,
38        Error,
39    },
40    Context,
41};
42use commonware_codec::{Decode as _, Encode, EncodeShared, Read};
43use commonware_cryptography::{Digest, Hasher};
44use commonware_macros::boxed;
45use commonware_parallel::Strategy;
46use std::sync::{Arc, Weak};
47
48/// Configuration for a compact keyless authenticated db.
49#[derive(Clone)]
50pub struct Config<C, S: Strategy> {
51    /// Strategy used to parallelize merkleization.
52    pub strategy: S,
53
54    /// Configuration for the journal that persists the witness.
55    pub witness: JournalConfig<()>,
56
57    /// Codec config used to decode the persisted last commit operation on reopen.
58    pub commit_codec_config: C,
59}
60
61/// A keyless authenticated db that discards historical operations, retaining only a witness
62/// for each synced commit.
63pub struct Db<F, E, V, H, C, S: Strategy>
64where
65    F: Family,
66    E: Context,
67    V: ValueEncoding,
68    H: Hasher,
69    Operation<F, V>: EncodeShared,
70    Operation<F, V>: Read<Cfg = C>,
71    C: Clone + Send + Sync + 'static,
72{
73    merkle: compact_merkle::Merkle<F, H::Digest, S>,
74    last_commit_loc: Location<F>,
75    last_commit_metadata: Option<V::Value>,
76    inactivity_floor_loc: Location<F>,
77    commit_codec_config: C,
78    witness: witness::Store<E, F, H::Digest>,
79}
80
81type CompactStateResult<F, V, D> =
82    Result<compact_sync::State<F, Operation<F, V>, D>, compact_sync::ServeError<F, D>>;
83
84/// A speculative batch for a compact keyless db.
85#[allow(clippy::type_complexity)]
86pub struct UnmerkleizedBatch<F, H, V, S: Strategy>
87where
88    F: Family,
89    V: ValueEncoding,
90    H: Hasher,
91    Operation<F, V>: EncodeShared,
92{
93    merkle_batch: compact_merkle::UnmerkleizedBatch<F, H::Digest, S>,
94    appends: Vec<V::Value>,
95    parent: Option<Arc<MerkleizedBatch<F, H::Digest, V, S>>>,
96    base_size: u64,
97    db_size: u64,
98}
99
100/// A speculative batch whose root digest has been computed.
101#[derive(Clone)]
102pub struct MerkleizedBatch<F: Family, D: Digest, V: ValueEncoding, S: Strategy>
103where
104    Operation<F, V>: EncodeShared,
105{
106    pub(super) merkle_batch: Arc<batch::MerkleizedBatch<F, D, S>>,
107    pub(super) root: D,
108    pub(super) commit_metadata: Option<V::Value>,
109    pub(super) parent: Option<Weak<Self>>,
110    pub(super) bounds: batch_chain::Bounds<F>,
111}
112
113impl<F: Family, D: Digest, V: ValueEncoding, S: Strategy> MerkleizedBatch<F, D, V, S>
114where
115    Operation<F, V>: EncodeShared,
116{
117    pub(super) fn ancestors(&self) -> impl Iterator<Item = Arc<Self>> {
118        batch_chain::ancestors(self.parent.clone(), |batch| batch.parent.as_ref())
119    }
120
121    /// Return the root digest after this batch is applied.
122    pub const fn root(&self) -> D {
123        self.root
124    }
125
126    /// Return the [`Bounds`] of the batch.
127    pub const fn bounds(&self) -> &Bounds<F> {
128        &self.bounds
129    }
130
131    /// Create a new speculative batch with this one as its parent.
132    pub fn new_batch<H>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, V, S>
133    where
134        H: Hasher<Digest = D>,
135    {
136        UnmerkleizedBatch {
137            merkle_batch: compact_merkle::UnmerkleizedBatch::wrap(self.merkle_batch.new_batch()),
138            appends: Vec::new(),
139            parent: Some(Arc::clone(self)),
140            base_size: self.bounds.total_size,
141            db_size: self.bounds.db_size,
142        }
143    }
144}
145
146impl<F, H, V, S> UnmerkleizedBatch<F, H, V, S>
147where
148    F: Family,
149    V: ValueEncoding,
150    H: Hasher,
151    S: Strategy,
152    Operation<F, V>: EncodeShared,
153{
154    pub(super) fn new<E, C>(db: &Db<F, E, V, H, C, S>, committed_size: u64) -> Self
155    where
156        E: Context,
157        C: Clone + Send + Sync + 'static,
158        Operation<F, V>: Read<Cfg = C>,
159    {
160        Self {
161            merkle_batch: db.merkle.new_batch(),
162            appends: Vec::new(),
163            parent: None,
164            base_size: committed_size,
165            db_size: committed_size,
166        }
167    }
168
169    pub fn append(mut self, value: V::Value) -> Self {
170        self.appends.push(value);
171        self
172    }
173
174    /// Resolve appends into operations, merkleize, and return an `Arc<MerkleizedBatch>`.
175    ///
176    /// `inactivity_floor` is threaded through the commit operation for wire-format parity with
177    /// [`crate::qmdb::keyless::Keyless`]. It must be >= the database's current floor
178    /// (monotonically non-decreasing) and at most the batch's commit location
179    /// (`total_size - 1`); these bounds are validated, but the floor does not drive any local
180    /// pruning or retention in this variant.
181    #[tracing::instrument(
182        name = "qmdb.keyless.compact.batch.merkleize",
183        level = "info",
184        skip_all
185    )]
186    pub async fn merkleize<E, C>(
187        self,
188        db: &Db<F, E, V, H, C, S>,
189        metadata: Option<V::Value>,
190        inactivity_floor: Location<F>,
191    ) -> Arc<MerkleizedBatch<F, H::Digest, V, S>>
192    where
193        F: Family,
194        E: Context,
195        C: Clone + Send + Sync + 'static,
196        Operation<F, V>: Read<Cfg = C>,
197    {
198        let mut ops: Vec<Operation<F, V>> = Vec::with_capacity(self.appends.len() + 1);
199        for value in self.appends {
200            ops.push(Operation::Append(value));
201        }
202        ops.push(Operation::Commit(metadata.clone(), inactivity_floor));
203
204        let total_size = self.base_size + ops.len() as u64;
205        let inactive_peaks = F::inactive_peaks(
206            F::location_to_position(Location::new(total_size)),
207            inactivity_floor,
208        );
209        let (merkle, root) = compact_batch::merkleize_ops::<F, H, S, _>(
210            &db.merkle,
211            self.merkle_batch,
212            ops,
213            inactive_peaks,
214        )
215        .await
216        .expect("inactive_peaks computed from batch size");
217
218        let ancestors =
219            batch_chain::parent_and_ancestors(self.parent.as_ref(), |parent| parent.ancestors());
220        let ancestors = batch_chain::collect_ancestor_bounds(
221            ancestors,
222            |batch| batch.bounds.inactivity_floor,
223            |batch| batch.bounds.total_size,
224        );
225
226        Arc::new(MerkleizedBatch {
227            merkle_batch: merkle,
228            root,
229            commit_metadata: metadata,
230            parent: self.parent.as_ref().map(Arc::downgrade),
231            bounds: batch_chain::Bounds {
232                base_size: self.base_size,
233                db_size: self.db_size,
234                total_size,
235                ancestors,
236                inactivity_floor,
237            },
238        })
239    }
240}
241
242impl<F, E, V, H, C, S> Db<F, E, V, H, C, S>
243where
244    F: Family,
245    E: Context,
246    V: ValueEncoding,
247    H: Hasher,
248    S: Strategy,
249    Operation<F, V>: EncodeShared,
250    Operation<F, V>: Read<Cfg = C>,
251    C: Clone + Send + Sync + 'static,
252{
253    fn encode_commit_op(metadata: Option<V::Value>, inactivity_floor_loc: Location<F>) -> Vec<u8> {
254        Operation::<F, V>::Commit(metadata, inactivity_floor_loc)
255            .encode()
256            .to_vec()
257    }
258
259    /// Build a compact db handle from already-validated compact state.
260    ///
261    /// The caller has reconstructed the compact Merkle in memory and already authenticated the
262    /// supplied witness/root pair. The import lives only in memory until the first [`Self::commit`]
263    /// or [`Self::sync`], which replaces the journal's contents with it. Until then, dropping the
264    /// handle leaves the previous on-disk state untouched, and rewind/prune are rejected.
265    pub(crate) fn init_from_validated_state(
266        strategy: S,
267        journal: witness::Journal<E, F, H::Digest>,
268        commit_codec_config: C,
269        validated: compact_sync::ValidatedState<F, Operation<F, V>, H::Digest>,
270    ) -> Result<Self, Error<F>> {
271        let compact_sync::ValidatedState {
272            state:
273                compact_sync::State {
274                    leaf_count,
275                    pinned_nodes,
276                    last_commit_op,
277                    last_commit_proof,
278                },
279            root,
280        } = validated;
281        let last_commit_loc = Location::new(*leaf_count - 1);
282        let Operation::Commit(last_commit_metadata, inactivity_floor_loc) = last_commit_op else {
283            return Err(Error::UnexpectedData(last_commit_loc));
284        };
285
286        let merkle =
287            compact_merkle::Merkle::from_compact_state(strategy, leaf_count, pinned_nodes.clone())?;
288        let imported = VerifiedWitness {
289            witness: Witness {
290                op_bytes: Self::encode_commit_op(
291                    last_commit_metadata.clone(),
292                    inactivity_floor_loc,
293                ),
294                proof: last_commit_proof,
295                pinned_nodes,
296            },
297            root,
298        };
299
300        let witness = witness::Store::from_import(journal, imported);
301        Ok(Self {
302            merkle,
303            last_commit_loc,
304            last_commit_metadata,
305            inactivity_floor_loc,
306            commit_codec_config,
307            witness,
308        })
309    }
310
311    /// Open a compact db from persisted compact state and rebuild its witness store.
312    ///
313    /// On first open, this bootstraps the initial commit and its witness so every later reopen and
314    /// rewind can assume the journal tip is a complete compact witness.
315    #[boxed]
316    pub(crate) async fn init_from_merkle(
317        mut merkle: compact_merkle::Merkle<F, H::Digest, S>,
318        witness_context: E,
319        witness_config: JournalConfig<()>,
320        commit_codec_config: C,
321    ) -> Result<Self, Error<F>>
322    where
323        F: Family,
324        Operation<F, V>: Read<Cfg = C>,
325    {
326        // Bootstrap: append an initial Commit(None, 0) on first open.
327        let journal: witness::Journal<E, F, H::Digest> =
328            variable::Journal::init(witness_context, witness_config).await?;
329        let (witness, last_commit_op) = witness::init::<E, F, H, S, Operation<F, V>>(
330            journal,
331            &mut merkle,
332            &commit_codec_config,
333            Operation::<F, V>::Commit(None, Location::new(0))
334                .encode()
335                .to_vec(),
336            Operation::has_floor,
337        )
338        .await?;
339        let Operation::Commit(last_commit_metadata, inactivity_floor_loc) = last_commit_op else {
340            return Err(Error::DataCorrupted("last operation was not a commit"));
341        };
342        let last_commit_loc = Location::new(*witness.with(|w| w.leaf_count()) - 1);
343
344        Ok(Self {
345            merkle,
346            last_commit_loc,
347            last_commit_metadata,
348            inactivity_floor_loc,
349            commit_codec_config,
350            witness,
351        })
352    }
353
354    /// Return the root of the db.
355    pub fn root(&self) -> H::Digest
356    where
357        F: Family,
358    {
359        let hasher = qmdb::hasher::<H>();
360        let inactive_peaks = F::inactive_peaks(
361            F::location_to_position(Location::new(*self.last_commit_loc + 1)),
362            self.inactivity_floor_loc,
363        );
364        self.merkle
365            .root(&hasher, inactive_peaks)
366            .expect("compact Merkle root should not fail")
367    }
368
369    /// Return a reference to the merkleization strategy.
370    pub const fn strategy(&self) -> &S {
371        self.merkle.strategy()
372    }
373
374    /// Return the location of the last commit.
375    pub const fn last_commit_loc(&self) -> Location<F> {
376        self.last_commit_loc
377    }
378
379    /// Return the inactivity floor declared by the last committed batch.
380    pub const fn inactivity_floor_loc(&self) -> Location<F> {
381        self.inactivity_floor_loc
382    }
383
384    /// Return the location of the next operation appended to this db.
385    pub fn size(&self) -> Location<F> {
386        Location::new(*self.last_commit_loc + 1)
387    }
388
389    /// Get the metadata associated with the last commit.
390    pub fn get_metadata(&self) -> Option<V::Value> {
391        self.last_commit_metadata.clone()
392    }
393
394    /// Return the compact-sync target described by the current witness.
395    ///
396    /// This reflects the last durably persisted commit, which may lag behind live in-memory
397    /// mutations until [`Self::commit`] or [`Self::sync`] is called.
398    pub fn target(&self) -> compact_sync::Target<F, H::Digest> {
399        self.witness.with(VerifiedWitness::target)
400    }
401
402    /// Return the compact-sync state for `target`, or a stale-target error if the source's
403    /// current witness no longer matches.
404    pub(crate) fn compact_state(
405        &self,
406        target: compact_sync::Target<F, H::Digest>,
407    ) -> CompactStateResult<F, V, H::Digest>
408    where
409        Operation<F, V>: Read<Cfg = C>,
410    {
411        // Hold the witness lock only long enough to verify the requested target and snapshot the
412        // entry; decode outside it so concurrent readers do not contend.
413        let (entry, leaf_count) = self.witness.with(|w| {
414            if target.root != w.root || target.leaf_count != w.leaf_count() {
415                return Err(compact_sync::ServeError::StaleTarget {
416                    requested: target.clone(),
417                    current: w.target(),
418                });
419            }
420            Ok((w.witness.clone(), w.leaf_count()))
421        })?;
422        let Witness {
423            op_bytes,
424            proof: last_commit_proof,
425            pinned_nodes,
426        } = entry;
427        let op = Operation::<F, V>::decode_cfg(op_bytes.as_ref(), &self.commit_codec_config)
428            .map_err(|_| {
429                compact_sync::ServeError::Database(Error::DataCorrupted("invalid commit operation"))
430            })?;
431        Ok(compact_sync::State {
432            leaf_count,
433            pinned_nodes,
434            last_commit_op: op,
435            last_commit_proof,
436        })
437    }
438
439    /// Create a new speculative batch of operations with this database as its parent.
440    pub fn new_batch(&self) -> UnmerkleizedBatch<F, H, V, S> {
441        let committed_size = *self.last_commit_loc + 1;
442        UnmerkleizedBatch::new(self, committed_size)
443    }
444
445    /// Create an owned merkleized batch representing the current applied state.
446    pub fn to_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, V, S>>
447    where
448        F: Family,
449    {
450        let committed_size = *self.last_commit_loc + 1;
451        Arc::new(MerkleizedBatch {
452            merkle_batch: self.merkle.to_batch(),
453            root: self.root(),
454            commit_metadata: self.last_commit_metadata.clone(),
455            parent: None,
456            bounds: batch_chain::Bounds {
457                base_size: committed_size,
458                db_size: committed_size,
459                total_size: committed_size,
460                ancestors: Vec::new(),
461                inactivity_floor: self.inactivity_floor_loc,
462            },
463        })
464    }
465
466    /// Apply a merkleized batch to the database.
467    ///
468    /// Returns the range of locations written. The state is updated in memory only; call
469    /// [`Self::commit`] or [`Self::sync`] to persist.
470    ///
471    /// # Errors
472    ///
473    /// - [`Error::StaleBatch`] if the batch is detected as stale (see
474    ///   [`crate::qmdb::batch_chain`] for more details).
475    /// - [`Error::FloorRegressed`] if any commit in the chain declares a floor below the
476    ///   previous commit's floor.
477    /// - [`Error::FloorBeyondSize`] if any commit in the chain declares a floor beyond its own
478    ///   commit location.
479    #[tracing::instrument(name = "qmdb.keyless.compact.db.apply_batch", level = "info", skip_all)]
480    pub fn apply_batch(
481        &mut self,
482        batch: Arc<MerkleizedBatch<F, H::Digest, V, S>>,
483    ) -> Result<core::ops::Range<Location<F>>, Error<F>> {
484        let db_size = *self.last_commit_loc + 1;
485        batch
486            .bounds
487            .validate_apply_to(db_size, self.inactivity_floor_loc)?;
488
489        let start_loc = self.last_commit_loc + 1;
490        self.merkle.apply_batch(&batch.merkle_batch)?;
491        self.last_commit_loc = Location::new(batch.bounds.total_size - 1);
492        self.last_commit_metadata = batch.commit_metadata.clone();
493        self.inactivity_floor_loc = batch.bounds.inactivity_floor;
494        Ok(start_loc..Location::new(batch.bounds.total_size))
495    }
496
497    /// Durably persist the current db state to disk. This is faster than [`Self::sync`] but
498    /// reopen may need to replay the witness journal's tail to recover.
499    #[tracing::instrument(name = "qmdb.keyless.compact.db.commit", level = "info", skip_all)]
500    pub async fn commit(&mut self) -> Result<(), Error<F>> {
501        self.witness
502            .commit::<H, S>(&self.merkle, self.inactivity_floor_loc, || {
503                Self::encode_commit_op(self.last_commit_metadata.clone(), self.inactivity_floor_loc)
504            })
505            .await
506    }
507
508    /// Durably persist the current db state to disk, also persisting journal metadata to
509    /// minimize recovery work on reopen.
510    #[tracing::instrument(name = "qmdb.keyless.compact.db.sync", level = "info", skip_all)]
511    pub async fn sync(&mut self) -> Result<(), Error<F>> {
512        self.witness
513            .sync::<H, S>(&self.merkle, self.inactivity_floor_loc, || {
514                Self::encode_commit_op(self.last_commit_metadata.clone(), self.inactivity_floor_loc)
515            })
516            .await
517    }
518
519    /// Rewind the db to the synced commit with exactly `target` operations, discarding any
520    /// uncommitted batches and any later commits. The rewind is made durable before this
521    /// method returns.
522    ///
523    /// # Errors
524    ///
525    /// Returns [`crate::merkle::Error::RewindBeyondHistory`] (wrapped as [`Error::Merkle`]) if
526    /// no retained commit has exactly `target` operations (never synced, or pruned).
527    #[tracing::instrument(name = "qmdb.keyless.compact.db.rewind", level = "info", skip_all)]
528    pub async fn rewind(&mut self, target: Location<F>) -> Result<(), Error<F>>
529    where
530        F: Family,
531    {
532        // Fast path: already durably at `target` with no uncommitted state.
533        if self.size() == target
534            && self.witness.with(|w| w.leaf_count()) == target
535            && !self.witness.import_pending()
536        {
537            return Ok(());
538        }
539
540        let last_commit_op = self
541            .witness
542            .rewind::<H, S, Operation<F, V>>(
543                &self.merkle,
544                target,
545                &self.commit_codec_config,
546                Operation::has_floor,
547            )
548            .await?;
549        let Operation::Commit(last_commit_metadata, inactivity_floor_loc) = last_commit_op else {
550            return Err(Error::DataCorrupted("last operation was not a commit"));
551        };
552        self.last_commit_metadata = last_commit_metadata;
553        self.inactivity_floor_loc = inactivity_floor_loc;
554        self.last_commit_loc = Location::new(*target - 1);
555        Ok(())
556    }
557
558    /// Drop witnesses for commits with fewer than `pruning_boundary` operations. Some witness
559    /// below the boundary may survive.
560    ///
561    /// Pruning bounds how far back [`Self::rewind`] can reach; the current commit's witness
562    /// always survives. The prune is made durable before this method returns.
563    ///
564    /// # Errors
565    ///
566    /// Fails if a compact-sync import has not yet been persisted by [`Self::commit`] or
567    /// [`Self::sync`].
568    pub async fn prune(&mut self, pruning_boundary: Location<F>) -> Result<(), Error<F>> {
569        self.witness.prune(pruning_boundary).await
570    }
571
572    /// Destroy all persisted state associated with this database.
573    #[boxed]
574    pub async fn destroy(self) -> Result<(), Error<F>> {
575        self.witness.destroy().await?;
576        Ok(())
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use crate::{
584        merkle::mmr,
585        qmdb::{any::value::FixedEncoding, compact::witness},
586    };
587    use commonware_cryptography::{sha256::Digest, Sha256};
588    use commonware_macros::test_traced;
589    use commonware_parallel::Sequential;
590    use commonware_runtime::{
591        buffer::paged::CacheRef, deterministic, BufferPooler, Runner as _, Supervisor as _,
592    };
593    use commonware_utils::{sequence::U64, NZUsize, NZU16, NZU64};
594    use std::num::{NonZeroU16, NonZeroUsize};
595
596    type TestDb<F> = Db<F, deterministic::Context, FixedEncoding<U64>, Sha256, (), Sequential>;
597
598    const WITNESS_PAGE_SIZE: NonZeroU16 = NZU16!(77);
599    const WITNESS_PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(9);
600
601    fn witness_config(partition: &str, pooler: &impl BufferPooler) -> variable::Config<()> {
602        variable::Config {
603            partition: format!("{partition}-witness"),
604            items_per_section: NZU64!(64),
605            compression: None,
606            codec_config: (),
607            page_cache: CacheRef::from_pooler(pooler, WITNESS_PAGE_SIZE, WITNESS_PAGE_CACHE_SIZE),
608            write_buffer: NZUsize!(1024),
609        }
610    }
611
612    async fn open_db<F: Family>(context: deterministic::Context, partition: &str) -> TestDb<F> {
613        let witness_cfg = witness_config(partition, &context);
614        let merkle = crate::merkle::compact::Merkle::new(Sequential);
615        Db::init_from_merkle(merkle, context.child("witness"), witness_cfg, ())
616            .await
617            .unwrap()
618    }
619
620    /// Open the persisted witness journal directly so tests can corrupt the tip entry.
621    async fn open_witness_journal(
622        context: deterministic::Context,
623        partition: &str,
624    ) -> witness::Journal<deterministic::Context, mmr::Family, Digest> {
625        let cfg = witness_config(partition, &context);
626        variable::Journal::init(context, cfg).await.unwrap()
627    }
628
629    #[test_traced("INFO")]
630    fn test_compact_stale_batch_rejected() {
631        deterministic::Runner::default().start(|context| async move {
632            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-stale").await;
633            let floor = db.inactivity_floor_loc();
634
635            let batch_a = db
636                .new_batch()
637                .append(U64::new(1))
638                .merkleize(&db, Some(U64::new(11)), floor)
639                .await;
640            let batch_b = db
641                .new_batch()
642                .append(U64::new(2))
643                .merkleize(&db, Some(U64::new(22)), floor)
644                .await;
645
646            let expected_root = batch_a.root();
647            db.apply_batch(batch_a).unwrap();
648            assert_eq!(db.root(), expected_root);
649            assert!(matches!(
650                db.apply_batch(batch_b),
651                Err(Error::StaleBatch { .. })
652            ));
653
654            db.destroy().await.unwrap();
655        });
656    }
657
658    /// Regression: `to_batch()` must snapshot the live in-memory state, not the lagging witness
659    /// cache.
660    #[test_traced("INFO")]
661    fn test_compact_to_batch_reflects_live_state() {
662        deterministic::Runner::default().start(|context| async move {
663            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-to-batch-live").await;
664            let floor = db.inactivity_floor_loc();
665
666            let pre_apply_root = db.root();
667            let pre_snapshot = db.to_batch();
668            assert_eq!(
669                pre_snapshot.root(),
670                pre_apply_root,
671                "snapshot before any mutation should match the live root"
672            );
673
674            db.apply_batch(
675                db.new_batch()
676                    .append(U64::new(1))
677                    .merkleize(&db, Some(U64::new(11)), floor)
678                    .await,
679            )
680            .unwrap();
681
682            // Leave the witness cache behind the live Merkle state.
683            let live_root = db.root();
684            assert_ne!(
685                live_root, pre_apply_root,
686                "applying a non-empty batch must change the live root"
687            );
688
689            let snapshot = db.to_batch();
690            assert_eq!(
691                snapshot.root(),
692                live_root,
693                "to_batch().root() must match the live db.root() even before sync"
694            );
695
696            db.destroy().await.unwrap();
697        });
698    }
699
700    #[test_traced("INFO")]
701    fn test_compact_stale_batch_chained() {
702        deterministic::Runner::default().start(|context| async move {
703            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-chained-stale").await;
704            let floor = db.inactivity_floor_loc();
705
706            let parent = db
707                .new_batch()
708                .append(U64::new(1))
709                .merkleize(&db, Some(U64::new(11)), floor)
710                .await;
711            let child_a = parent
712                .new_batch::<Sha256>()
713                .append(U64::new(2))
714                .merkleize(&db, Some(U64::new(22)), floor)
715                .await;
716            let child_b = parent
717                .new_batch::<Sha256>()
718                .append(U64::new(3))
719                .merkleize(&db, Some(U64::new(33)), floor)
720                .await;
721
722            db.apply_batch(child_a).unwrap();
723            assert!(matches!(
724                db.apply_batch(child_b),
725                Err(Error::StaleBatch { .. })
726            ));
727
728            db.destroy().await.unwrap();
729        });
730    }
731
732    #[test_traced("INFO")]
733    fn test_compact_stale_parent_after_child_applied() {
734        deterministic::Runner::default().start(|context| async move {
735            let mut db =
736                open_db::<mmr::Family>(context.child("db"), "keyless-child-before-parent").await;
737            let floor = db.inactivity_floor_loc();
738
739            let parent = db
740                .new_batch()
741                .append(U64::new(1))
742                .merkleize(&db, Some(U64::new(11)), floor)
743                .await;
744            let child = parent
745                .new_batch::<Sha256>()
746                .append(U64::new(2))
747                .merkleize(&db, Some(U64::new(22)), floor)
748                .await;
749
750            db.apply_batch(child).unwrap();
751            assert!(matches!(
752                db.apply_batch(parent),
753                Err(Error::StaleBatch { .. })
754            ));
755
756            db.destroy().await.unwrap();
757        });
758    }
759
760    #[test_traced("INFO")]
761    fn test_compact_sequential_commit_parent_then_child() {
762        deterministic::Runner::default().start(|context| async move {
763            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-parent-child").await;
764            let floor = db.inactivity_floor_loc();
765
766            let parent = db
767                .new_batch()
768                .append(U64::new(1))
769                .merkleize(&db, Some(U64::new(11)), floor)
770                .await;
771            let child = parent
772                .new_batch::<Sha256>()
773                .append(U64::new(2))
774                .merkleize(&db, Some(U64::new(22)), floor)
775                .await;
776            let expected_root = child.root();
777
778            db.apply_batch(parent).unwrap();
779            db.apply_batch(child).unwrap();
780            db.sync().await.unwrap();
781
782            assert_eq!(db.root(), expected_root);
783
784            db.destroy().await.unwrap();
785        });
786    }
787
788    #[test_traced("INFO")]
789    fn test_compact_floor_regressed() {
790        deterministic::Runner::default().start(|context| async move {
791            let mut db =
792                open_db::<mmr::Family>(context.child("db"), "keyless-floor-regressed").await;
793
794            let advance_floor = db.new_batch().append(U64::new(1));
795            let advance_floor = advance_floor.merkleize(&db, None, Location::new(1)).await;
796            db.apply_batch(advance_floor).unwrap();
797
798            let regressed = db
799                .new_batch()
800                .append(U64::new(2))
801                .merkleize(&db, None, Location::new(0))
802                .await;
803
804            assert!(matches!(
805                db.apply_batch(regressed),
806                Err(Error::FloorRegressed(new, current))
807                    if new == Location::new(0) && current == Location::new(1)
808            ));
809
810            db.destroy().await.unwrap();
811        });
812    }
813
814    // A chained batch whose tip floor is below its parent's floor must be rejected:
815    // the parent's Commit participates in the per-commit monotonicity invariant even
816    // before it is applied.
817    #[test_traced("INFO")]
818    fn test_compact_ancestor_floor_regressed() {
819        deterministic::Runner::default().start(|context| async move {
820            let mut db =
821                open_db::<mmr::Family>(context.child("db"), "keyless-ancestor-floor-regressed")
822                    .await;
823
824            // parent: append + commit at loc 2 with floor=2.
825            let parent = db
826                .new_batch()
827                .append(U64::new(1))
828                .merkleize(&db, None, Location::new(2))
829                .await;
830            // child: append + commit at loc 4 with floor=1 (regressed from parent's floor=2).
831            let child = parent
832                .new_batch::<Sha256>()
833                .append(U64::new(2))
834                .merkleize(&db, None, Location::new(1))
835                .await;
836
837            assert!(matches!(
838                db.apply_batch(child),
839                Err(Error::FloorRegressed(new, prev))
840                    if new == Location::new(1) && prev == Location::new(2)
841            ));
842
843            db.destroy().await.unwrap();
844        });
845    }
846
847    #[test_traced("INFO")]
848    fn test_compact_rewind_restores_commit_metadata_and_floor() {
849        deterministic::Runner::default().start(|context| async move {
850            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-rewind-meta").await;
851
852            let v1 = U64::new(1);
853            let meta1 = U64::new(11);
854            let floor1 = Location::new(0);
855            db.apply_batch(
856                db.new_batch()
857                    .append(v1)
858                    .merkleize(&db, Some(meta1.clone()), floor1)
859                    .await,
860            )
861            .unwrap();
862            db.sync().await.unwrap();
863            let root_after_first = db.root();
864            let size_after_first = db.size();
865
866            let v2 = U64::new(2);
867            let meta2 = U64::new(22);
868            let floor2 = Location::new(1);
869            db.apply_batch(
870                db.new_batch()
871                    .append(v2)
872                    .merkleize(&db, Some(meta2.clone()), floor2)
873                    .await,
874            )
875            .unwrap();
876            db.sync().await.unwrap();
877            assert_eq!(db.get_metadata(), Some(meta2));
878            assert_eq!(db.inactivity_floor_loc(), floor2);
879
880            db.rewind(size_after_first).await.unwrap();
881            assert_eq!(db.root(), root_after_first);
882            assert_eq!(db.get_metadata(), Some(meta1));
883            assert_eq!(db.inactivity_floor_loc(), floor1);
884
885            db.destroy().await.unwrap();
886        });
887    }
888
889    #[test_traced("INFO")]
890    fn test_compact_rewind_persists_across_reopen() {
891        deterministic::Runner::default().start(|context| async move {
892            let partition = "keyless-rewind-reopen";
893            let meta1 = U64::new(11);
894            let floor1 = Location::new(0);
895            let meta2 = U64::new(22);
896            let floor2 = Location::new(1);
897
898            let root_after_first = {
899                let mut db = open_db::<mmr::Family>(context.child("first"), partition).await;
900                db.apply_batch(
901                    db.new_batch()
902                        .append(U64::new(1))
903                        .merkleize(&db, Some(meta1.clone()), floor1)
904                        .await,
905                )
906                .unwrap();
907                db.sync().await.unwrap();
908                let root = db.root();
909                let size_after_first = db.size();
910
911                db.apply_batch(
912                    db.new_batch()
913                        .append(U64::new(2))
914                        .merkleize(&db, Some(meta2), floor2)
915                        .await,
916                )
917                .unwrap();
918                db.sync().await.unwrap();
919
920                db.rewind(size_after_first).await.unwrap();
921                root
922            };
923
924            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
925            assert_eq!(db.root(), root_after_first);
926            assert_eq!(db.get_metadata(), Some(meta1));
927            assert_eq!(db.inactivity_floor_loc(), floor1);
928
929            db.destroy().await.unwrap();
930        });
931    }
932
933    #[test_traced("INFO")]
934    fn test_compact_commit_persists_across_reopen() {
935        deterministic::Runner::default().start(|context| async move {
936            let partition = "keyless-commit-reopen";
937            let meta1 = U64::new(11);
938            let meta2 = U64::new(22);
939
940            let root_after_second = {
941                let mut db = open_db::<mmr::Family>(context.child("first"), partition).await;
942                db.apply_batch(
943                    db.new_batch()
944                        .append(U64::new(1))
945                        .merkleize(&db, Some(meta1), Location::new(0))
946                        .await,
947                )
948                .unwrap();
949                db.commit().await.unwrap();
950
951                db.apply_batch(
952                    db.new_batch()
953                        .append(U64::new(2))
954                        .merkleize(&db, Some(meta2.clone()), Location::new(1))
955                        .await,
956                )
957                .unwrap();
958                db.commit().await.unwrap();
959                db.root()
960            };
961
962            // Reopen recovers the committed tip even though the journal was never synced.
963            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
964            assert_eq!(db.root(), root_after_second);
965            assert_eq!(db.get_metadata(), Some(meta2));
966            assert_eq!(db.inactivity_floor_loc(), Location::new(1));
967            db.destroy().await.unwrap();
968        });
969    }
970
971    #[test_traced("INFO")]
972    fn test_compact_rewind_to_committed_entry_after_reopen() {
973        deterministic::Runner::default().start(|context| async move {
974            let partition = "keyless-commit-rewind-reopen";
975            let meta1 = U64::new(11);
976            let meta2 = U64::new(22);
977
978            let (root_a, size_a) = {
979                let mut db = open_db::<mmr::Family>(context.child("first"), partition).await;
980                db.apply_batch(
981                    db.new_batch()
982                        .append(U64::new(1))
983                        .merkleize(&db, Some(meta1.clone()), Location::new(0))
984                        .await,
985                )
986                .unwrap();
987                db.commit().await.unwrap();
988                let root_a = db.root();
989                let size_a = db.size();
990
991                db.apply_batch(
992                    db.new_batch()
993                        .append(U64::new(2))
994                        .merkleize(&db, Some(meta2), Location::new(1))
995                        .await,
996                )
997                .unwrap();
998                db.commit().await.unwrap();
999                (root_a, size_a)
1000            };
1001
1002            // Both committed witnesses survive the crash: reopen recovers the tip, and the
1003            // earlier commit remains a valid rewind target.
1004            let mut db = open_db::<mmr::Family>(context.child("second"), partition).await;
1005            db.rewind(size_a).await.unwrap();
1006            assert_eq!(db.root(), root_a);
1007            assert_eq!(db.get_metadata(), Some(meta1));
1008            db.destroy().await.unwrap();
1009        });
1010    }
1011
1012    #[test_traced("INFO")]
1013    fn test_compact_sync_after_commit() {
1014        deterministic::Runner::default().start(|context| async move {
1015            let partition = "keyless-sync-after-commit";
1016            let meta = U64::new(11);
1017
1018            let root = {
1019                let mut db = open_db::<mmr::Family>(context.child("first"), partition).await;
1020                db.apply_batch(
1021                    db.new_batch()
1022                        .append(U64::new(1))
1023                        .merkleize(&db, Some(meta.clone()), Location::new(0))
1024                        .await,
1025                )
1026                .unwrap();
1027                db.commit().await.unwrap();
1028                // The commit already made the state durable, so this is a no-op.
1029                db.sync().await.unwrap();
1030                db.root()
1031            };
1032
1033            let db = open_db::<mmr::Family>(context.child("second"), partition).await;
1034            assert_eq!(db.root(), root);
1035            assert_eq!(db.get_metadata(), Some(meta));
1036            db.destroy().await.unwrap();
1037        });
1038    }
1039
1040    #[test_traced("INFO")]
1041    fn test_compact_import_persists_with_commit() {
1042        deterministic::Runner::default().start(|context| async move {
1043            let dst = "keyless-import-commit-dst";
1044            let src = "keyless-import-commit-src";
1045            let meta_a = U64::new(11);
1046            let meta_b = U64::new(22);
1047
1048            // Build state B in a separate source partition and capture its validated state.
1049            let target_b = {
1050                let mut source = open_db::<mmr::Family>(context.child("src"), src).await;
1051                source
1052                    .apply_batch(
1053                        source
1054                            .new_batch()
1055                            .append(U64::new(2))
1056                            .merkleize(&source, Some(meta_b.clone()), Location::new(0))
1057                            .await,
1058                    )
1059                    .unwrap();
1060                source.sync().await.unwrap();
1061                source.target()
1062            };
1063            let (_, proof_b, pinned_b) = {
1064                let journal = open_witness_journal(context.child("src_tip"), src).await;
1065                witness::tests::tip(&journal).await
1066            };
1067            let validated = compact_sync::ValidatedState {
1068                state: compact_sync::State {
1069                    leaf_count: target_b.leaf_count,
1070                    pinned_nodes: pinned_b,
1071                    last_commit_op: Operation::Commit(Some(meta_b.clone()), Location::new(0)),
1072                    last_commit_proof: proof_b,
1073                },
1074                root: target_b.root,
1075            };
1076
1077            // Seed the destination partition with a different committed state A.
1078            {
1079                let mut seeded = open_db::<mmr::Family>(context.child("seed"), dst).await;
1080                seeded
1081                    .apply_batch(
1082                        seeded
1083                            .new_batch()
1084                            .append(U64::new(1))
1085                            .merkleize(&seeded, Some(meta_a), Location::new(0))
1086                            .await,
1087                    )
1088                    .unwrap();
1089                seeded.sync().await.unwrap();
1090                assert_ne!(seeded.target(), target_b);
1091            }
1092
1093            // Import state B over the destination and make it durable with commit (not sync).
1094            {
1095                let journal = open_witness_journal(context.child("import"), dst).await;
1096                let mut imported = TestDb::<mmr::Family>::init_from_validated_state(
1097                    Sequential,
1098                    journal,
1099                    (),
1100                    validated,
1101                )
1102                .unwrap();
1103                assert_eq!(imported.target(), target_b);
1104                imported.commit().await.unwrap();
1105            }
1106
1107            // Reopen recovers the committed import, replacing state A even though the journal was
1108            // never synced.
1109            let db = open_db::<mmr::Family>(context.child("reopen"), dst).await;
1110            assert_eq!(db.target(), target_b);
1111            assert_eq!(db.root(), target_b.root);
1112            assert_eq!(db.get_metadata(), Some(meta_b));
1113            db.destroy().await.unwrap();
1114        });
1115    }
1116
1117    #[test_traced("INFO")]
1118    fn test_compact_reopen_rejects_tampered_witness() {
1119        deterministic::Runner::default().start(|context| async move {
1120            let partition = "keyless-witness-tamper";
1121            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1122            db.apply_batch(
1123                db.new_batch()
1124                    .append(U64::new(7))
1125                    .merkleize(&db, Some(U64::new(11)), Location::new(1))
1126                    .await,
1127            )
1128            .unwrap();
1129            db.sync().await.unwrap();
1130            drop(db);
1131
1132            // Corrupt the persisted proof so it no longer verifies against the stored root.
1133            let mut journal = open_witness_journal(context.child("tamper"), partition).await;
1134            let (op_bytes, mut proof, pinned_nodes) = witness::tests::tip(&journal).await;
1135            if let Some(digest) = proof.digests.first_mut() {
1136                *digest = Sha256::fill(0xff);
1137            } else {
1138                proof.leaves = Location::new(*proof.leaves + 1);
1139            }
1140            witness::tests::overwrite_tip(&mut journal, op_bytes, proof, pinned_nodes).await;
1141            drop(journal);
1142
1143            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1144            let reopened = TestDb::<mmr::Family>::init_from_merkle(
1145                merkle,
1146                context.child("reopen_witness"),
1147                witness_config(partition, &context),
1148                (),
1149            )
1150            .await;
1151            assert!(matches!(reopened, Err(Error::DataCorrupted(_))));
1152        });
1153    }
1154
1155    #[test_traced("INFO")]
1156    fn test_compact_rewind_rejects_corrupt_target_entry() {
1157        deterministic::Runner::default().start(|context| async move {
1158            let partition = "keyless-corrupt-rewind-target";
1159            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1160            db.apply_batch(
1161                db.new_batch()
1162                    .append(U64::new(1))
1163                    .merkleize(&db, None, Location::new(1))
1164                    .await,
1165            )
1166            .unwrap();
1167            db.sync().await.unwrap();
1168            let rewind_target = db.target().leaf_count;
1169            db.apply_batch(
1170                db.new_batch()
1171                    .append(U64::new(2))
1172                    .merkleize(&db, None, Location::new(1))
1173                    .await,
1174            )
1175            .unwrap();
1176            db.sync().await.unwrap();
1177            let tip_target = db.target();
1178            drop(db);
1179
1180            // Corrupt the rewind target's entry (the journal holds bootstrap, target, tip).
1181            let mut journal = open_witness_journal(context.child("corrupt"), partition).await;
1182            witness::tests::corrupt_entry(&mut journal, 1, |entry| {
1183                entry.pinned_nodes[0] = Sha256::fill(0xff);
1184            })
1185            .await;
1186            drop(journal);
1187
1188            // The tip entry is intact, so reopen succeeds.
1189            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1190            let mut reopened = TestDb::<mmr::Family>::init_from_merkle(
1191                merkle,
1192                context.child("reopen"),
1193                witness_config(partition, &context),
1194                (),
1195            )
1196            .await
1197            .unwrap();
1198            assert_eq!(reopened.target(), tip_target);
1199
1200            // The corrupt entry fails the rewind before any truncation.
1201            assert!(matches!(
1202                reopened.rewind(rewind_target).await,
1203                Err(Error::DataCorrupted(_))
1204            ));
1205            drop(reopened);
1206
1207            // The newer history survives: reopen still lands on the original tip.
1208            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1209            let reopened = TestDb::<mmr::Family>::init_from_merkle(
1210                merkle,
1211                context.child("reopen2"),
1212                witness_config(partition, &context),
1213                (),
1214            )
1215            .await
1216            .unwrap();
1217            assert_eq!(reopened.target(), tip_target);
1218            reopened.destroy().await.unwrap();
1219        });
1220    }
1221
1222    #[test_traced("INFO")]
1223    fn test_compact_reopen_rejects_interrupted_import() {
1224        deterministic::Runner::default().start(|context| async move {
1225            let partition = "keyless-interrupted-import";
1226            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1227            db.apply_batch(
1228                db.new_batch()
1229                    .append(U64::new(7))
1230                    .merkleize(&db, Some(U64::new(11)), Location::new(1))
1231                    .await,
1232            )
1233            .unwrap();
1234            db.sync().await.unwrap();
1235            drop(db);
1236
1237            // Simulate a crash between an import's journal clear and its entry append: the
1238            // journal is empty but its size is nonzero.
1239            let mut journal = open_witness_journal(context.child("clear"), partition).await;
1240            let size = journal.size();
1241            journal.clear_to_size(size.max(1)).await.unwrap();
1242            drop(journal);
1243
1244            // Reopen must fail rather than bootstrap a fresh db.
1245            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1246            let reopened = TestDb::<mmr::Family>::init_from_merkle(
1247                merkle,
1248                context.child("reopen_witness"),
1249                witness_config(partition, &context),
1250                (),
1251            )
1252            .await;
1253            assert!(matches!(reopened, Err(Error::Journal(_))));
1254        });
1255    }
1256
1257    #[test_traced("INFO")]
1258    fn test_compact_reopen_rejects_commit_floor_beyond_tip() {
1259        deterministic::Runner::default().start(|context| async move {
1260            let partition = "keyless-invalid-persisted-floor";
1261            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1262            db.apply_batch(
1263                db.new_batch()
1264                    .append(U64::new(7))
1265                    .merkleize(&db, Some(U64::new(11)), Location::new(1))
1266                    .await,
1267            )
1268            .unwrap();
1269            db.sync().await.unwrap();
1270            drop(db);
1271            let oversized_floor = Location::new(10);
1272
1273            // Overwrite the persisted commit op with a floor beyond its own commit location.
1274            let mut journal = open_witness_journal(context.child("tamper"), partition).await;
1275            let (_, proof, pinned_nodes) = witness::tests::tip(&journal).await;
1276            let bad_op = Operation::<mmr::Family, FixedEncoding<U64>>::Commit(
1277                Some(U64::new(11)),
1278                oversized_floor,
1279            )
1280            .encode()
1281            .to_vec();
1282            witness::tests::overwrite_tip(&mut journal, bad_op, proof, pinned_nodes).await;
1283            drop(journal);
1284
1285            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1286            let reopened = TestDb::<mmr::Family>::init_from_merkle(
1287                merkle,
1288                context.child("reopen_witness"),
1289                witness_config(partition, &context),
1290                (),
1291            )
1292            .await;
1293            assert!(matches!(
1294                reopened,
1295                Err(Error::DataCorrupted("invalid compact witness"))
1296            ));
1297        });
1298    }
1299
1300    #[test_traced("INFO")]
1301    fn test_compact_reopen_rejects_tampered_pinned_nodes() {
1302        deterministic::Runner::default().start(|context| async move {
1303            let partition = "keyless-pins-tamper";
1304            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1305            db.apply_batch(
1306                db.new_batch()
1307                    .append(U64::new(7))
1308                    .merkleize(&db, Some(U64::new(11)), Location::new(1))
1309                    .await,
1310            )
1311            .unwrap();
1312            db.sync().await.unwrap();
1313            drop(db);
1314
1315            // Corrupt one pinned frontier node: the root recomputed from the rebuilt Merkle no
1316            // longer matches the proof stored in the same entry.
1317            let mut journal = open_witness_journal(context.child("tamper"), partition).await;
1318            let (op_bytes, proof, mut pinned_nodes) = witness::tests::tip(&journal).await;
1319            pinned_nodes[0] = Sha256::fill(0xff);
1320            witness::tests::overwrite_tip(&mut journal, op_bytes, proof, pinned_nodes).await;
1321            drop(journal);
1322
1323            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1324            let reopened = TestDb::<mmr::Family>::init_from_merkle(
1325                merkle,
1326                context.child("reopen_witness"),
1327                witness_config(partition, &context),
1328                (),
1329            )
1330            .await;
1331            assert!(matches!(reopened, Err(Error::DataCorrupted(_))));
1332        });
1333    }
1334
1335    #[test_traced("INFO")]
1336    fn test_compact_rewind_to_current_is_noop() {
1337        deterministic::Runner::default().start(|context| async move {
1338            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-rewind-noop").await;
1339            db.apply_batch(
1340                db.new_batch()
1341                    .append(U64::new(1))
1342                    .merkleize(&db, Some(U64::new(11)), Location::new(0))
1343                    .await,
1344            )
1345            .unwrap();
1346            db.sync().await.unwrap();
1347            let root = db.root();
1348            let size = db.size();
1349
1350            db.rewind(size).await.unwrap();
1351            assert_eq!(db.root(), root);
1352            assert_eq!(db.size(), size);
1353            db.destroy().await.unwrap();
1354        });
1355    }
1356
1357    #[test_traced("INFO")]
1358    fn test_compact_prune_past_tip_keeps_tip() {
1359        deterministic::Runner::default().start(|context| async move {
1360            let partition = "keyless-prune-past-tip";
1361            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1362            db.apply_batch(
1363                db.new_batch()
1364                    .append(U64::new(1))
1365                    .merkleize(&db, Some(U64::new(11)), Location::new(0))
1366                    .await,
1367            )
1368            .unwrap();
1369            db.sync().await.unwrap();
1370            let target = db.target();
1371
1372            // Prune with a boundary beyond the tip: the tip entry must survive.
1373            db.prune(Location::new(*db.size() + 100)).await.unwrap();
1374            assert_eq!(db.target(), target);
1375            drop(db);
1376
1377            let reopened = open_db::<mmr::Family>(context.child("reopen"), partition).await;
1378            assert_eq!(reopened.target(), target);
1379            reopened.destroy().await.unwrap();
1380        });
1381    }
1382
1383    #[test_traced("INFO")]
1384    fn test_compact_rewind_beyond_history() {
1385        deterministic::Runner::default().start(|context| async move {
1386            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-rewind-beyond").await;
1387            // The bootstrap commit is the oldest retained state (one leaf); no commit with zero
1388            // operations exists to rewind to.
1389            assert!(matches!(
1390                db.rewind(Location::new(0)).await,
1391                Err(Error::Merkle(crate::merkle::Error::RewindBeyondHistory))
1392            ));
1393            // A target past the tip is not a commit either.
1394            assert!(matches!(
1395                db.rewind(Location::new(*db.size() + 100)).await,
1396                Err(Error::Merkle(crate::merkle::Error::RewindBeyondHistory))
1397            ));
1398            db.destroy().await.unwrap();
1399        });
1400    }
1401
1402    #[test_traced("INFO")]
1403    fn test_compact_rewind_between_commits() {
1404        deterministic::Runner::default().start(|context| async move {
1405            let mut db =
1406                open_db::<mmr::Family>(context.child("db"), "keyless-rewind-between").await;
1407            let floor = db.inactivity_floor_loc();
1408
1409            // A multi-op commit jumps the committed size from 1 (bootstrap) to 4.
1410            db.apply_batch(
1411                db.new_batch()
1412                    .append(U64::new(1))
1413                    .append(U64::new(2))
1414                    .merkleize(&db, Some(U64::new(11)), floor)
1415                    .await,
1416            )
1417            .unwrap();
1418            db.sync().await.unwrap();
1419            let root_a = db.root();
1420            let size_a = db.size();
1421            assert_eq!(size_a, Location::new(4));
1422
1423            // A second commit moves the size to 6.
1424            db.apply_batch(
1425                db.new_batch()
1426                    .append(U64::new(3))
1427                    .merkleize(&db, Some(U64::new(22)), floor)
1428                    .await,
1429            )
1430            .unwrap();
1431            db.sync().await.unwrap();
1432            let root_b = db.root();
1433
1434            // Targets inside a commit's span match no entry, even though entries exist on
1435            // both sides.
1436            for target in [2u64, 3, 5] {
1437                assert!(matches!(
1438                    db.rewind(Location::new(target)).await,
1439                    Err(Error::Merkle(crate::merkle::Error::RewindBeyondHistory))
1440                ));
1441            }
1442            assert_eq!(db.root(), root_b);
1443
1444            // The exact commit boundary remains a valid target.
1445            db.rewind(size_a).await.unwrap();
1446            assert_eq!(db.root(), root_a);
1447            assert_eq!(db.get_metadata(), Some(U64::new(11)));
1448            db.destroy().await.unwrap();
1449        });
1450    }
1451
1452    /// A witness entry appended but not synced (a commit interrupted before its journal sync)
1453    /// must be dropped on reopen, recovering the last synced commit.
1454    #[test_traced("INFO")]
1455    fn test_compact_reopen_drops_unsynced_witness() {
1456        deterministic::Runner::default().start(|context| async move {
1457            let partition = "keyless-witness-unsynced";
1458            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1459
1460            // Commit state A.
1461            db.apply_batch(
1462                db.new_batch()
1463                    .append(U64::new(1))
1464                    .merkleize(&db, Some(U64::new(11)), Location::new(1))
1465                    .await,
1466            )
1467            .unwrap();
1468            db.sync().await.unwrap();
1469            let target_a = db.target();
1470            drop(db);
1471
1472            // Simulate the crash window: append an entry ahead of the tip without syncing it,
1473            // then drop the journal. The unsynced tail must not survive reopen.
1474            let mut journal = open_witness_journal(context.child("crash"), partition).await;
1475            let (op_bytes, mut proof, pinned_nodes) = witness::tests::tip(&journal).await;
1476            proof.leaves = Location::new(*proof.leaves + 2);
1477            witness::tests::append_unsynced(&mut journal, op_bytes, proof, pinned_nodes).await;
1478            drop(journal);
1479
1480            // Reopen must drop the unsynced entry and recover state A.
1481            let reopened = open_db::<mmr::Family>(context.child("reopen"), partition).await;
1482            assert_eq!(reopened.target(), target_a);
1483            reopened.destroy().await.unwrap();
1484        });
1485    }
1486
1487    #[test_traced("INFO")]
1488    fn test_compact_rewind_multiple_commits() {
1489        deterministic::Runner::default().start(|context| async move {
1490            let partition = "keyless-rewind-multi";
1491            let mut db = open_db::<mmr::Family>(context.child("db"), partition).await;
1492
1493            // Commit A, B, C, recording the state after A.
1494            db.apply_batch(
1495                db.new_batch()
1496                    .append(U64::new(1))
1497                    .merkleize(&db, Some(U64::new(11)), Location::new(0))
1498                    .await,
1499            )
1500            .unwrap();
1501            db.sync().await.unwrap();
1502            let root_a = db.root();
1503            let size_a = db.size();
1504            let target_a = db.target();
1505
1506            for i in [2u64, 3] {
1507                db.apply_batch(
1508                    db.new_batch()
1509                        .append(U64::new(i))
1510                        .merkleize(&db, Some(U64::new(i * 11)), Location::new(0))
1511                        .await,
1512                )
1513                .unwrap();
1514                db.sync().await.unwrap();
1515            }
1516            assert_ne!(db.root(), root_a);
1517
1518            // Rewind two commits in one call.
1519            db.rewind(size_a).await.unwrap();
1520            assert_eq!(db.root(), root_a);
1521            assert_eq!(db.size(), size_a);
1522            assert_eq!(db.get_metadata(), Some(U64::new(11)));
1523            assert_eq!(db.target(), target_a);
1524            drop(db);
1525
1526            // The rewind is durable: reopen recovers state A.
1527            let db = open_db::<mmr::Family>(context.child("reopen"), partition).await;
1528            assert_eq!(db.root(), root_a);
1529            assert_eq!(db.target(), target_a);
1530            db.destroy().await.unwrap();
1531        });
1532    }
1533
1534    #[test_traced("INFO")]
1535    fn test_compact_prune_then_rewind() {
1536        deterministic::Runner::default().start(|context| async move {
1537            // One entry per section so pruning takes effect at entry granularity (pruning is
1538            // section-aligned and never drops a partial section).
1539            let mut witness_cfg = witness_config("keyless-prune-rewind", &context);
1540            witness_cfg.items_per_section = NZU64!(1);
1541            let merkle = crate::merkle::compact::Merkle::new(Sequential);
1542            let mut db: TestDb<mmr::Family> =
1543                Db::init_from_merkle(merkle, context.child("witness"), witness_cfg, ())
1544                    .await
1545                    .unwrap();
1546
1547            // Commit A, B, C.
1548            let mut sizes = Vec::new();
1549            for i in [1u64, 2, 3] {
1550                db.apply_batch(
1551                    db.new_batch()
1552                        .append(U64::new(i))
1553                        .merkleize(&db, Some(U64::new(i * 11)), Location::new(0))
1554                        .await,
1555                )
1556                .unwrap();
1557                db.sync().await.unwrap();
1558                sizes.push(db.size());
1559            }
1560
1561            // Prune history below B: rewinding to B still works, rewinding to A does not.
1562            db.prune(sizes[1]).await.unwrap();
1563            assert!(matches!(
1564                db.rewind(sizes[0]).await,
1565                Err(Error::Merkle(crate::merkle::Error::RewindBeyondHistory))
1566            ));
1567            db.rewind(sizes[1]).await.unwrap();
1568            assert_eq!(db.size(), sizes[1]);
1569            assert_eq!(db.get_metadata(), Some(U64::new(22)));
1570
1571            db.destroy().await.unwrap();
1572        });
1573    }
1574
1575    #[test_traced("INFO")]
1576    fn test_compact_rewind_preserves_pre_advance_batch() {
1577        deterministic::Runner::default().start(|context| async move {
1578            let mut db =
1579                open_db::<mmr::Family>(context.child("db"), "keyless-rewind-preserves-pre-advance")
1580                    .await;
1581
1582            db.apply_batch(
1583                db.new_batch()
1584                    .append(U64::new(1))
1585                    .merkleize(&db, None, Location::new(0))
1586                    .await,
1587            )
1588            .unwrap();
1589            db.sync().await.unwrap();
1590            let size_after_first = db.size();
1591
1592            // Merkleize a batch against the post-commit-A state.
1593            let held = db
1594                .new_batch()
1595                .append(U64::new(2))
1596                .merkleize(&db, None, Location::new(0))
1597                .await;
1598
1599            // Advance past that state and commit, then rewind back to it.
1600            db.apply_batch(
1601                db.new_batch()
1602                    .append(U64::new(3))
1603                    .merkleize(&db, None, Location::new(0))
1604                    .await,
1605            )
1606            .unwrap();
1607            db.sync().await.unwrap();
1608            db.rewind(size_after_first).await.unwrap();
1609
1610            // The rewind restored the state that `held` was merkleized against, so it still
1611            // matches the Merkle size and applies cleanly.
1612            db.apply_batch(held).unwrap();
1613
1614            db.destroy().await.unwrap();
1615        });
1616    }
1617
1618    #[test_traced("INFO")]
1619    fn test_compact_noop_commit_after_commit() {
1620        deterministic::Runner::default().start(|context| async move {
1621            let mut db =
1622                open_db::<mmr::Family>(context.child("db"), "keyless-noop-after-commit").await;
1623
1624            db.apply_batch(
1625                db.new_batch()
1626                    .append(U64::new(1))
1627                    .append(U64::new(2))
1628                    .merkleize(&db, Some(U64::new(11)), Location::new(0))
1629                    .await,
1630            )
1631            .unwrap();
1632            db.sync().await.unwrap();
1633            let root_after_first = db.root();
1634            assert_eq!(db.size(), Location::new(4));
1635
1636            db.sync().await.unwrap();
1637            assert_eq!(db.size(), Location::new(4));
1638            assert_eq!(db.root(), root_after_first);
1639            assert_eq!(db.target().root, db.root());
1640
1641            db.destroy().await.unwrap();
1642        });
1643    }
1644
1645    #[test_traced("INFO")]
1646    fn test_compact_noop_commit_after_reopen() {
1647        deterministic::Runner::default().start(|context| async move {
1648            let partition = "keyless-noop-after-reopen";
1649
1650            let root_before_drop = {
1651                let mut db = open_db::<mmr::Family>(context.child("first"), partition).await;
1652                db.apply_batch(
1653                    db.new_batch()
1654                        .append(U64::new(1))
1655                        .append(U64::new(2))
1656                        .merkleize(&db, Some(U64::new(11)), Location::new(0))
1657                        .await,
1658                )
1659                .unwrap();
1660                db.sync().await.unwrap();
1661                let root = db.root();
1662                assert_eq!(db.size(), Location::new(4));
1663                root
1664            };
1665
1666            let mut db = open_db::<mmr::Family>(context.child("second"), partition).await;
1667            assert_eq!(db.root(), root_before_drop);
1668            assert_eq!(db.size(), Location::new(4));
1669
1670            db.sync().await.unwrap();
1671            assert_eq!(db.size(), Location::new(4));
1672            assert_eq!(db.root(), root_before_drop);
1673            assert_eq!(db.target().root, db.root());
1674
1675            db.destroy().await.unwrap();
1676        });
1677    }
1678
1679    #[test_traced("INFO")]
1680    fn test_compact_noop_commit_after_rewind() {
1681        deterministic::Runner::default().start(|context| async move {
1682            let mut db =
1683                open_db::<mmr::Family>(context.child("db"), "keyless-noop-after-rewind").await;
1684
1685            db.apply_batch(
1686                db.new_batch()
1687                    .append(U64::new(1))
1688                    .append(U64::new(2))
1689                    .merkleize(&db, Some(U64::new(11)), Location::new(0))
1690                    .await,
1691            )
1692            .unwrap();
1693            db.sync().await.unwrap();
1694            let root_after_first = db.root();
1695
1696            db.apply_batch(
1697                db.new_batch()
1698                    .append(U64::new(3))
1699                    .merkleize(&db, Some(U64::new(22)), Location::new(1))
1700                    .await,
1701            )
1702            .unwrap();
1703            db.sync().await.unwrap();
1704
1705            db.rewind(Location::new(4)).await.unwrap();
1706            assert_eq!(db.size(), Location::new(4));
1707            assert_eq!(db.root(), root_after_first);
1708
1709            db.sync().await.unwrap();
1710            assert_eq!(db.size(), Location::new(4));
1711            assert_eq!(db.root(), root_after_first);
1712            assert_eq!(db.target().root, db.root());
1713
1714            db.destroy().await.unwrap();
1715        });
1716    }
1717
1718    #[test_traced("INFO")]
1719    fn test_compact_rewind_makes_post_advance_batch_stale() {
1720        deterministic::Runner::default().start(|context| async move {
1721            let mut db =
1722                open_db::<mmr::Family>(context.child("db"), "keyless-rewind-makes-stale").await;
1723
1724            db.apply_batch(
1725                db.new_batch()
1726                    .append(U64::new(1))
1727                    .merkleize(&db, None, Location::new(0))
1728                    .await,
1729            )
1730            .unwrap();
1731            db.sync().await.unwrap();
1732            let size_after_first = db.size();
1733
1734            db.apply_batch(
1735                db.new_batch()
1736                    .append(U64::new(2))
1737                    .merkleize(&db, None, Location::new(0))
1738                    .await,
1739            )
1740            .unwrap();
1741            db.sync().await.unwrap();
1742
1743            // Merkleize a batch against the post-commit-B state, which the rewind will discard.
1744            let held = db
1745                .new_batch()
1746                .append(U64::new(3))
1747                .merkleize(&db, None, Location::new(0))
1748                .await;
1749
1750            db.rewind(size_after_first).await.unwrap();
1751
1752            // After rewind, mem.size reflects post-commit-A, but the held batch starts after
1753            // post-commit-B. Apply must be rejected with StaleBatch.
1754            assert!(matches!(
1755                db.apply_batch(held),
1756                Err(Error::StaleBatch { .. })
1757            ));
1758
1759            db.destroy().await.unwrap();
1760        });
1761    }
1762
1763    #[test_traced("INFO")]
1764    fn test_compact_floor_beyond_size() {
1765        deterministic::Runner::default().start(|context| async move {
1766            let mut db = open_db::<mmr::Family>(context.child("db"), "keyless-floor-beyond").await;
1767
1768            let batch = db.new_batch().merkleize(&db, None, Location::new(2)).await;
1769
1770            assert!(matches!(
1771                db.apply_batch(batch),
1772                Err(Error::FloorBeyondSize(floor, tip))
1773                    if floor == Location::new(2) && tip == Location::new(1)
1774            ));
1775
1776            db.destroy().await.unwrap();
1777        });
1778    }
1779
1780    // A chained batch whose ancestor's floor exceeds that ancestor's own commit location
1781    // must be rejected, identifying the ancestor's bound rather than the tip's.
1782    #[test_traced("INFO")]
1783    fn test_compact_ancestor_floor_beyond_size() {
1784        deterministic::Runner::default().start(|context| async move {
1785            let mut db =
1786                open_db::<mmr::Family>(context.child("db"), "keyless-ancestor-floor-beyond").await;
1787
1788            // parent: append + commit at loc 2, floor=3 (one past parent's commit).
1789            let parent = db
1790                .new_batch()
1791                .append(U64::new(1))
1792                .merkleize(&db, None, Location::new(3))
1793                .await;
1794            // child: valid on its own (floor=0), but parent's floor is bad.
1795            let child = parent
1796                .new_batch::<Sha256>()
1797                .append(U64::new(2))
1798                .merkleize(&db, None, Location::new(0))
1799                .await;
1800
1801            assert!(matches!(
1802                db.apply_batch(child),
1803                Err(Error::FloorBeyondSize(floor, commit))
1804                    if floor == Location::new(3) && commit == Location::new(2)
1805            ));
1806
1807            db.destroy().await.unwrap();
1808        });
1809    }
1810}