Skip to main content

commonware_storage/qmdb/keyless/
mod.rs

1//! The [Keyless] qmdb allows for append-only storage of data that can later be retrieved by its
2//! location. Both fixed-size and variable-size values are supported via the [fixed] and [variable]
3//! submodules.
4//!
5//! # Examples
6//!
7//! ```ignore
8//! // Simple mode: apply a batch, then durably commit it.
9//! let batch = db.new_batch().append(value);
10//! let merkleized = batch.merkleize(&db, None, db.inactivity_floor_loc()).await;
11//! db.apply_batch(merkleized).await?;
12//! db.commit().await?;
13//! ```
14//!
15//! ```ignore
16//! // Batches can still fork before you apply them.
17//! let floor = db.inactivity_floor_loc();
18//! let parent = db.new_batch().append(value_a);
19//! let parent = parent.merkleize(&db, None, floor).await;
20//!
21//! let child_a = parent.new_batch();
22//! let child_a = child_a.append(value_b);
23//! let child_a = child_a.merkleize(&db, None, floor).await;
24//!
25//! let child_b = parent.new_batch();
26//! let child_b = child_b.append(value_c);
27//! let child_b = child_b.merkleize(&db, None, floor).await;
28//!
29//! db.apply_batch(child_a).await?;
30//! db.commit().await?;
31//! ```
32//!
33//! ```ignore
34//! // Sequential commit: apply parent then child.
35//! let floor = db.inactivity_floor_loc();
36//! let parent = db.new_batch().append(value_a);
37//! let parent_m = parent.merkleize(&db, None, floor).await;
38//! let child = parent_m.new_batch().append(value_b);
39//! let child_m = child.merkleize(&db, None, floor).await;
40//!
41//! db.apply_batch(parent_m).await?;
42//! db.apply_batch(child_m).await?;
43//! db.commit().await?;
44//! ```
45
46use crate::{
47    journal::{
48        authenticated,
49        contiguous::{Contiguous, Mutable},
50    },
51    merkle::{full::Config as MerkleConfig, Family, Location, Proof},
52    qmdb::{
53        any::value::ValueEncoding, batch_chain, metrics::Metrics, single_operation_root, Error,
54    },
55    Context,
56};
57use commonware_codec::EncodeShared;
58use commonware_cryptography::Hasher;
59use commonware_macros::boxed;
60use commonware_parallel::Strategy;
61use std::{num::NonZeroU64, sync::Arc};
62use tracing::{debug, warn};
63
64pub mod batch;
65mod compact;
66pub mod fixed;
67mod operation;
68pub(crate) mod sync;
69pub mod variable;
70pub use compact::{
71    Config as CompactConfig, Db as CompactDb, MerkleizedBatch as CompactMerkleizedBatch,
72    UnmerkleizedBatch as CompactUnmerkleizedBatch,
73};
74pub use operation::Operation;
75
76/// Compute the authenticated root of a newly initialized database without opening storage.
77///
78/// The initial commit never carries metadata, so this root always represents `Commit(None, 0)`.
79pub fn initial_root<F, V, H>() -> H::Digest
80where
81    F: Family,
82    V: ValueEncoding,
83    H: Hasher,
84    Operation<F, V>: EncodeShared,
85{
86    single_operation_root::<F, H>(&Operation::<F, V>::Commit(None, Location::new(0)))
87}
88
89/// Configuration for a [Keyless] authenticated db.
90#[derive(Clone)]
91pub struct Config<J, S: Strategy> {
92    /// Configuration for the Merkle structure backing the authenticated journal.
93    pub merkle: MerkleConfig<S>,
94
95    /// Configuration for the operations log journal.
96    pub log: J,
97}
98
99/// A keyless authenticated database.
100pub struct Keyless<F, E, V, C, H, S>
101where
102    F: Family,
103    E: Context,
104    V: ValueEncoding,
105    C: Contiguous<Item = Operation<F, V>>,
106    H: Hasher,
107    S: Strategy,
108    Operation<F, V>: EncodeShared,
109{
110    /// Authenticated journal of operations.
111    journal: authenticated::Journal<F, E, C, H, S>,
112
113    /// Cached canonical operations root.
114    root: H::Digest,
115
116    /// The location of the last commit, if any.
117    last_commit_loc: Location<F>,
118
119    /// The inactivity floor declared by the last committed batch. Operations at locations below
120    /// this value are considered inactive by the application and may be pruned.
121    inactivity_floor_loc: Location<F>,
122
123    /// Metrics for this database.
124    metrics: Metrics<E>,
125}
126
127impl<F, E, V, C, H, S> Keyless<F, E, V, C, H, S>
128where
129    F: Family,
130    E: Context,
131    V: ValueEncoding,
132    C: Mutable<Item = Operation<F, V>>,
133    H: Hasher,
134    S: Strategy,
135    Operation<F, V>: EncodeShared,
136{
137    #[boxed]
138    pub(crate) async fn init_from_journal(
139        mut journal: authenticated::Journal<F, E, C, H, S>,
140        context: E,
141    ) -> Result<Self, Error<F>> {
142        let metrics = Metrics::new(context);
143        if journal.size() == 0 {
144            warn!("no operations found in log, creating initial commit");
145            journal
146                .append(&Operation::Commit(None, Location::new(0)))
147                .await?;
148            journal.sync().await?;
149        }
150
151        let (last_commit_loc, inactivity_floor_loc) = {
152            let bounds = journal.bounds();
153            let last_commit_loc = Location::new(
154                bounds
155                    .end
156                    .checked_sub(1)
157                    .expect("at least one commit should exist"),
158            );
159            let op = journal.read(*last_commit_loc).await?;
160            let inactivity_floor_loc = op
161                .has_floor()
162                .expect("last operation should be a commit with floor");
163            (last_commit_loc, inactivity_floor_loc)
164        };
165        let inactive_peaks = F::inactive_peaks(
166            F::location_to_position(Location::new(*last_commit_loc + 1)),
167            inactivity_floor_loc,
168        );
169        let root = journal.root(inactive_peaks)?;
170
171        let db = Self {
172            journal,
173            root,
174            last_commit_loc,
175            inactivity_floor_loc,
176            metrics,
177        };
178        db.update_metrics();
179        Ok(db)
180    }
181
182    /// Get the value at location `loc` in the database.
183    ///
184    /// # Errors
185    ///
186    /// Returns [`Error::LocationOutOfBounds`] if `loc` >=
187    /// `self.bounds().end`.
188    pub async fn get(&self, loc: Location<F>) -> Result<Option<V::Value>, Error<F>> {
189        let _timer = self.metrics.get_timer();
190        self.metrics.get_calls.inc();
191        self.metrics.lookups_requested.inc();
192        let op_count = self.journal.bounds().end;
193        if loc >= op_count {
194            return Err(Error::LocationOutOfBounds(loc, Location::new(op_count)));
195        }
196        let op = self.journal.read(*loc).await?;
197
198        let result = op.into_value();
199        Ok(result)
200    }
201
202    /// Batch read values at multiple locations.
203    ///
204    /// Locations must be strictly increasing.
205    /// Returns results in the same order as the input locations.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`Error::LocationOutOfBounds`] if any location >= `bounds().end`.
210    pub async fn get_many(&self, locs: &[Location<F>]) -> Result<Vec<Option<V::Value>>, Error<F>> {
211        if locs.is_empty() {
212            return Ok(Vec::new());
213        }
214
215        let _timer = self.metrics.get_many_timer();
216        self.metrics.get_many_calls.inc();
217        self.metrics.lookups_requested.inc_by(locs.len() as u64);
218        assert!(
219            locs.is_sorted_by(|a, b| a < b),
220            "locations must be strictly increasing"
221        );
222        let op_count = self.journal.bounds().end;
223        for &loc in locs {
224            if loc >= op_count {
225                return Err(Error::LocationOutOfBounds(loc, Location::new(op_count)));
226            }
227        }
228        let positions: Vec<u64> = locs.iter().map(|loc| **loc).collect();
229        let ops = self.journal.read_many(&positions).await?;
230        let result = ops.into_iter().map(|op| op.into_value()).collect();
231        Ok(result)
232    }
233
234    /// Returns the location of the last commit.
235    pub const fn last_commit_loc(&self) -> Location<F> {
236        self.last_commit_loc
237    }
238
239    /// Returns the inactivity floor declared by the last committed batch.
240    pub const fn inactivity_floor_loc(&self) -> Location<F> {
241        self.inactivity_floor_loc
242    }
243
244    /// Return [start, end) where `start` and `end - 1` are the Locations of the oldest and newest
245    /// retained operations respectively.
246    pub fn bounds(&self) -> std::ops::Range<Location<F>> {
247        let bounds = self.journal.bounds();
248        Location::new(bounds.start)..Location::new(bounds.end)
249    }
250
251    /// Update state gauges from the current database state.
252    fn update_metrics(&self) {
253        let bounds = self.journal.bounds();
254        self.metrics.update(
255            bounds.end,
256            bounds.start,
257            *self.inactivity_floor_loc,
258            *self.last_commit_loc,
259        );
260    }
261
262    /// Return the most recent location from which this database can safely be synced, and the
263    /// upper bound on [`Self::prune`]'s `loc`. For keyless databases, this equals the
264    /// inactivity floor declared by the last committed batch.
265    pub const fn sync_boundary(&self) -> Location<F> {
266        self.inactivity_floor_loc
267    }
268
269    /// Get the metadata associated with the last commit.
270    pub async fn get_metadata(&self) -> Result<Option<V::Value>, Error<F>> {
271        let op = self.journal.read(*self.last_commit_loc).await?;
272        let Operation::Commit(metadata, _floor) = op else {
273            return Ok(None);
274        };
275
276        Ok(metadata)
277    }
278
279    /// Return the root of the db.
280    pub const fn root(&self) -> H::Digest {
281        self.root
282    }
283
284    /// Return a reference to the merkleization strategy.
285    pub const fn strategy(&self) -> &S {
286        self.journal.strategy()
287    }
288
289    /// Generate and return:
290    ///  1. a proof of all operations applied to the db in the range starting at (and including)
291    ///     location `start_loc`, and ending at the first of either:
292    ///     - the last operation performed, or
293    ///     - the operation `max_ops` from the start.
294    ///  2. the operations corresponding to the leaves in this range.
295    ///
296    /// # Errors
297    ///
298    /// - Returns [`Error::Merkle`] with [`crate::merkle::Error::RangeOutOfBounds`] if `start_loc`
299    ///   >= the number of operations.
300    /// - Returns [`Error::Journal`] with [`crate::journal::Error::ItemPruned`] if `start_loc` has
301    ///   been pruned.
302    pub async fn proof(
303        &self,
304        start_loc: Location<F>,
305        max_ops: NonZeroU64,
306    ) -> Result<(Proof<F, H::Digest>, Vec<Operation<F, V>>), Error<F>> {
307        self.historical_proof(self.bounds().end, start_loc, max_ops)
308            .await
309    }
310
311    /// Analogous to proof, but with respect to the state of the Merkle structure when it had
312    /// `op_count` operations.
313    ///
314    /// `op_count` must be the size of a commit boundary.
315    ///
316    /// # Errors
317    ///
318    /// - Returns [`Error::Merkle`] with [`crate::merkle::Error::RangeOutOfBounds`] if `start_loc`
319    ///   >= `op_count` or `op_count` > number of operations.
320    /// - Returns [`Error::Journal`] with [`crate::journal::Error::ItemPruned`] if `start_loc` has
321    ///   been pruned.
322    /// - Returns [`Error::HistoricalFloorPruned`] if `op_count - 1` is retained but is not a commit
323    ///   op.
324    #[allow(clippy::type_complexity)]
325    #[tracing::instrument(
326        name = "qmdb.keyless.db.historical_proof",
327        level = "info",
328        skip_all,
329        fields(
330            op_count = *op_count,
331            start_loc = *start_loc,
332            max_ops = max_ops.get(),
333        ),
334    )]
335    pub async fn historical_proof(
336        &self,
337        op_count: Location<F>,
338        start_loc: Location<F>,
339        max_ops: NonZeroU64,
340    ) -> Result<(Proof<F, H::Digest>, Vec<Operation<F, V>>), Error<F>> {
341        if op_count > self.journal.size() {
342            return Err(crate::merkle::Error::RangeOutOfBounds(op_count).into());
343        }
344
345        let inactive_peaks =
346            crate::qmdb::inactive_peaks_at::<F, _>(&self.journal, op_count, |op| op.has_floor())
347                .await?;
348
349        Ok(self
350            .journal
351            .historical_proof(op_count, start_loc, max_ops, inactive_peaks)
352            .await?)
353    }
354
355    /// Return the pinned Merkle nodes for a lower operation boundary of `loc`.
356    pub async fn pinned_nodes_at(&self, loc: Location<F>) -> Result<Vec<H::Digest>, Error<F>> {
357        self.journal
358            .merkle
359            .pinned_nodes_at(loc)
360            .await
361            .map_err(Into::into)
362    }
363
364    /// Prune historical operations prior to `loc`. This does not affect the db's root.
365    ///
366    /// `prune` requires no prior commit. After a crash, the database remains recoverable;
367    /// uncommitted operations are not guaranteed to survive.
368    ///
369    /// # Errors
370    ///
371    /// - Returns [`Error::PruneBeyondMinRequired`] if `loc` > the inactivity floor.
372    #[tracing::instrument(name = "qmdb.keyless.db.prune", level = "info", skip_all)]
373    pub async fn prune(&mut self, loc: Location<F>) -> Result<(), Error<F>> {
374        let _timer = self.metrics.prune_timer();
375        self.metrics.prune_calls.inc();
376        if loc > self.inactivity_floor_loc {
377            return Err(Error::PruneBeyondMinRequired(
378                loc,
379                self.inactivity_floor_loc,
380            ));
381        }
382        self.journal.prune(loc).await?;
383        self.update_metrics();
384        Ok(())
385    }
386
387    /// Rewind the database to `size` operations, where `size` is the location of the next append.
388    ///
389    /// This rewinds both the operations journal and its Merkle structure to the historical state
390    /// at `size`. The inactivity floor is restored from the rewind target commit operation, so
391    /// the post-rewind floor matches the floor that was in effect at that commit.
392    ///
393    /// # Errors
394    ///
395    /// - Returns [`Error::Journal`] with [`crate::journal::Error::InvalidRewind`] if `size` is 0
396    ///   or exceeds the current committed size.
397    /// - Returns [`Error::Journal`] with [`crate::journal::Error::ItemPruned`] if the operation at
398    ///   `size - 1` has been pruned.
399    /// - Returns [`Error::UnexpectedData`] if the operation at `size - 1` is not a commit.
400    ///
401    /// Any error from this method is fatal for this handle. Rewind may mutate journal state
402    /// before this method finishes updating in-memory rewind state. Callers must drop this
403    /// database handle after any `Err` from `rewind` and reopen from storage.
404    ///
405    /// A successful rewind is not restart-stable until a subsequent [`Self::commit`] or
406    /// [`Self::sync`].
407    #[tracing::instrument(name = "qmdb.keyless.db.rewind", level = "info", skip_all)]
408    pub async fn rewind(&mut self, size: Location<F>) -> Result<(), Error<F>> {
409        let rewind_size = *size;
410        let current_size = *self.last_commit_loc + 1;
411        if rewind_size == current_size {
412            return Ok(());
413        }
414        if rewind_size == 0 || rewind_size > current_size {
415            return Err(Error::Journal(crate::journal::Error::InvalidRewind(
416                rewind_size,
417            )));
418        }
419
420        let rewind_last_loc = Location::new(rewind_size - 1);
421        let rewind_floor = {
422            let bounds = self.journal.bounds();
423            if rewind_size <= bounds.start {
424                return Err(Error::Journal(crate::journal::Error::ItemPruned(
425                    *rewind_last_loc,
426                )));
427            }
428            let rewind_last_op = self.journal.read(*rewind_last_loc).await?;
429            let Operation::Commit(_, floor) = rewind_last_op else {
430                return Err(Error::UnexpectedData(rewind_last_loc));
431            };
432            floor
433        };
434
435        // Journal rewind happens before in-memory commit-location updates. If a later step fails,
436        // this handle may be internally diverged and must be dropped by the caller.
437        self.journal.rewind(rewind_size).await?;
438        self.last_commit_loc = rewind_last_loc;
439        self.inactivity_floor_loc = rewind_floor;
440        let inactive_peaks = F::inactive_peaks(F::location_to_position(size), rewind_floor);
441        self.root = self.journal.root(inactive_peaks)?;
442        self.update_metrics();
443        Ok(())
444    }
445
446    /// Sync all database state to disk. While this isn't necessary to ensure durability of
447    /// committed operations, periodic invocation may reduce memory usage and the time required to
448    /// recover the database on restart.
449    #[tracing::instrument(name = "qmdb.keyless.db.sync", level = "info", skip_all)]
450    pub async fn sync(&mut self) -> Result<(), Error<F>> {
451        let _timer = self.metrics.sync_timer();
452        self.metrics.sync_calls.inc();
453        self.journal.sync().await?;
454        Ok(())
455    }
456
457    /// Durably commit the journal state published by prior [`Keyless::apply_batch`] calls.
458    #[tracing::instrument(name = "qmdb.keyless.db.commit", level = "info", skip_all)]
459    pub async fn commit(&mut self) -> Result<(), Error<F>> {
460        let _timer = self.metrics.commit_timer();
461        self.metrics.commit_calls.inc();
462        self.journal.commit().await?;
463        Ok(())
464    }
465
466    /// Destroy the db, removing all data from disk.
467    #[boxed]
468    pub async fn destroy(self) -> Result<(), Error<F>> {
469        Ok(self.journal.destroy().await?)
470    }
471
472    /// Create a new speculative batch of operations with this database as its parent.
473    pub fn new_batch(&self) -> batch::UnmerkleizedBatch<F, H, V, S> {
474        let journal_size = *self.last_commit_loc + 1;
475        batch::UnmerkleizedBatch::new(self, journal_size)
476    }
477
478    /// Create an initial [`batch::MerkleizedBatch`] from the committed DB state.
479    pub fn to_batch(&self) -> Arc<batch::MerkleizedBatch<F, H::Digest, V, S>> {
480        let journal_size = *self.last_commit_loc + 1;
481        Arc::new(batch::MerkleizedBatch {
482            journal_batch: self.journal.to_merkleized_batch(),
483            root: self.root,
484            parent: None,
485            bounds: batch_chain::Bounds {
486                base_size: journal_size,
487                db_size: journal_size,
488                total_size: journal_size,
489                ancestors: Vec::new(),
490                inactivity_floor: self.inactivity_floor_loc,
491            },
492        })
493    }
494
495    /// Apply a [`batch::MerkleizedBatch`] to the database.
496    ///
497    /// A batch is valid only if every batch applied to the database since this batch's
498    /// ancestor chain was created is an ancestor of this batch. Applying a batch from a
499    /// different fork returns [`Error::StaleBatch`] (see [`crate::qmdb::batch_chain`] for
500    /// more details).
501    ///
502    /// Every commit operation in the batch chain (each unapplied ancestor's commit plus the
503    /// tip's) must satisfy two per-commit invariants:
504    ///
505    /// 1. The floor is monotonically non-decreasing across the chain, starting from the
506    ///    database's current inactivity floor.
507    /// 2. The floor is at most the commit operation's own location (`total_size - 1` at that
508    ///    point). A floor past the commit would let a later `prune(floor)` remove the last
509    ///    readable commit from the journal.
510    ///
511    /// Violations return [`Error::FloorRegressed`] or [`Error::FloorBeyondSize`] identifying
512    /// the offending floor and the bound it crossed (the prior validated floor, or the commit
513    /// location, respectively). Floor validation happens before any journal mutation, so the
514    /// database is untouched on floor errors.
515    ///
516    /// Returns the range of locations written.
517    ///
518    /// This publishes the batch to the in-memory database state and appends it to the
519    /// journal, but does not durably commit it. Call [`Keyless::commit`] or
520    /// [`Keyless::sync`] to guarantee durability.
521    #[tracing::instrument(name = "qmdb.keyless.db.apply_batch", level = "info", skip_all)]
522    pub async fn apply_batch(
523        &mut self,
524        batch: Arc<batch::MerkleizedBatch<F, H::Digest, V, S>>,
525    ) -> Result<core::ops::Range<Location<F>>, Error<F>> {
526        let _timer = self.metrics.apply_batch_timer();
527        self.metrics.apply_batch_calls.inc();
528        let db_size = *self.last_commit_loc + 1;
529        batch
530            .bounds
531            .validate_apply_to(db_size, self.inactivity_floor_loc)?;
532        let start_loc = self.last_commit_loc + 1;
533
534        self.journal.apply_batch(&batch.journal_batch).await?;
535
536        self.last_commit_loc = Location::new(batch.bounds.total_size - 1);
537        self.inactivity_floor_loc = batch.bounds.inactivity_floor;
538        self.root = batch.root;
539        let end_loc = Location::new(batch.bounds.total_size);
540        debug!(size = ?end_loc, "applied batch");
541        let range = start_loc..end_loc;
542        self.update_metrics();
543        self.metrics
544            .operations_applied
545            .inc_by(*range.end - *range.start);
546        Ok(range)
547    }
548}
549
550#[cfg(test)]
551pub(crate) mod tests {
552    use super::*;
553    use crate::{journal::contiguous::Mutable, qmdb::verify_proof};
554    use commonware_cryptography::Sha256;
555    use commonware_parallel::Strategy;
556    use commonware_runtime::{deterministic, Supervisor as _};
557    use commonware_utils::NZU64;
558    use std::{future::Future, pin::Pin};
559
560    pub(crate) type Reopen<D> =
561        Box<dyn Fn(deterministic::Context) -> Pin<Box<dyn Future<Output = D> + Send>>>;
562
563    type TestKeyless<F, V, C, H, S> = Keyless<F, deterministic::Context, V, C, H, S>;
564
565    /// Test value factory: creates distinct values from an index.
566    pub(crate) trait TestValue: Clone + PartialEq + std::fmt::Debug + Send + Sync {
567        fn make(i: u64) -> Self;
568    }
569
570    impl TestValue for Vec<u8> {
571        fn make(i: u64) -> Self {
572            vec![(i % 255) as u8; ((i % 13) + 7) as usize]
573        }
574    }
575
576    impl TestValue for commonware_utils::sequence::U64 {
577        fn make(i: u64) -> Self {
578            Self::new(i * 10 + 1)
579        }
580    }
581
582    #[boxed]
583    pub(crate) async fn test_keyless_db_empty<F: Family, V, C, H, S: Strategy>(
584        context: deterministic::Context,
585        db: TestKeyless<F, V, C, H, S>,
586        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
587    ) where
588        V: ValueEncoding<Value: TestValue>,
589        C: Mutable<Item = Operation<F, V>>,
590        H: Hasher,
591        Operation<F, V>: EncodeShared,
592    {
593        let bounds = db.bounds();
594        assert_eq!(bounds.end, 1); // initial commit should exist
595        assert_eq!(bounds.start, Location::new(0));
596        assert_eq!(db.get_metadata().await.unwrap(), None);
597        assert_eq!(db.last_commit_loc(), Location::new(0));
598
599        // Make sure closing/reopening gets us back to the same state, even after adding an uncommitted op.
600        let root = db.root();
601        {
602            db.new_batch().append(V::Value::make(1));
603            // Don't merkleize/apply -- simulate failed commit
604        }
605        drop(db);
606
607        let mut db = reopen(context.child("db").with_attribute("index", 2)).await;
608        assert_eq!(db.root(), root);
609        assert_eq!(db.bounds().end, 1);
610        assert_eq!(db.get_metadata().await.unwrap(), None);
611
612        // Test calling commit on an empty db which should make it (durably) non-empty.
613        let metadata = V::Value::make(99);
614        let merkleized = db
615            .new_batch()
616            .merkleize(&db, Some(metadata.clone()), db.inactivity_floor_loc())
617            .await;
618        db.apply_batch(merkleized).await.unwrap();
619        db.commit().await.unwrap();
620        assert_eq!(db.bounds().end, 2); // 2 commit ops
621        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
622        assert_eq!(
623            db.get(Location::new(1)).await.unwrap(),
624            Some(metadata.clone())
625        ); // the commit op
626        let root = db.root();
627
628        // Commit op should remain after reopen even without clean shutdown.
629        let db = reopen(context.child("db").with_attribute("index", 3)).await;
630        assert_eq!(db.bounds().end, 2); // commit op should remain after re-open.
631        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata));
632        assert_eq!(db.root(), root);
633        assert_eq!(db.last_commit_loc(), Location::new(1));
634
635        db.destroy().await.unwrap();
636    }
637
638    #[boxed]
639    pub(crate) async fn test_keyless_db_commit_after_sync_recovery<
640        F: Family,
641        V,
642        C,
643        H,
644        S: Strategy,
645    >(
646        context: deterministic::Context,
647        mut db: TestKeyless<F, V, C, H, S>,
648        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
649    ) where
650        V: ValueEncoding<Value: TestValue>,
651        C: Mutable<Item = Operation<F, V>>,
652        H: Hasher,
653        Operation<F, V>: EncodeShared,
654    {
655        let value0 = V::Value::make(10);
656        let value1 = V::Value::make(20);
657
658        // Commit and sync the first append so recovery has an older durable boundary.
659        let first_loc = Location::new(1);
660        let merkleized = db
661            .new_batch()
662            .append(value0.clone())
663            .merkleize(&db, None, db.inactivity_floor_loc())
664            .await;
665        db.apply_batch(merkleized).await.unwrap();
666        db.commit().await.unwrap();
667        db.sync().await.unwrap();
668
669        // Commit the next append without syncing; reopen must replay it from the journal.
670        let second_loc = db.bounds().end;
671        let merkleized = db
672            .new_batch()
673            .append(value1.clone())
674            .merkleize(&db, None, db.inactivity_floor_loc())
675            .await;
676        db.apply_batch(merkleized).await.unwrap();
677        db.commit().await.unwrap();
678        let committed_bounds = db.bounds();
679        let committed_root = db.root();
680        drop(db);
681
682        let db = reopen(context.child("db").with_attribute("index", 2)).await;
683        assert_eq!(db.bounds(), committed_bounds);
684        assert_eq!(db.root(), committed_root);
685        assert_eq!(db.get(first_loc).await.unwrap(), Some(value0));
686        assert_eq!(db.get(second_loc).await.unwrap(), Some(value1));
687
688        db.destroy().await.unwrap();
689    }
690
691    #[boxed]
692    pub(crate) async fn test_keyless_db_build_basic<F: Family, V, C, H, S: Strategy>(
693        context: deterministic::Context,
694        mut db: TestKeyless<F, V, C, H, S>,
695        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
696    ) where
697        V: ValueEncoding<Value: TestValue>,
698        C: Mutable<Item = Operation<F, V>>,
699        H: Hasher,
700        Operation<F, V>: EncodeShared,
701    {
702        // Build a db with 2 values and make sure we can get them back.
703        let v1 = V::Value::make(1);
704        let v2 = V::Value::make(2);
705
706        {
707            let batch = db.new_batch();
708            let loc1 = batch.size();
709            let batch = batch.append(v1.clone());
710            let loc2 = batch.size();
711            let batch = batch.append(v2.clone());
712            assert_eq!(loc1, Location::new(1));
713            assert_eq!(loc2, Location::new(2));
714            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
715                .await
716                .unwrap();
717        }
718
719        // Make sure closing/reopening gets us back to the same state.
720        assert_eq!(db.bounds().end, 4); // 2 appends, 1 commit + 1 initial commit
721        assert_eq!(db.get_metadata().await.unwrap(), None);
722        assert_eq!(db.get(Location::new(3)).await.unwrap(), None); // the commit op
723        let root = db.root();
724        db.sync().await.unwrap();
725        drop(db);
726
727        let db = reopen(context.child("db").with_attribute("index", 2)).await;
728        assert_eq!(db.bounds().end, 4);
729        assert_eq!(db.root(), root);
730        assert_eq!(db.get(Location::new(1)).await.unwrap().unwrap(), v1);
731        assert_eq!(db.get(Location::new(2)).await.unwrap().unwrap(), v2);
732
733        // Make sure commit operation remains after drop/reopen.
734        drop(db);
735        let db = reopen(context.child("db").with_attribute("index", 3)).await;
736        assert_eq!(db.bounds().end, 4);
737        assert_eq!(db.root(), root);
738
739        db.destroy().await.unwrap();
740    }
741
742    #[boxed]
743    pub(crate) async fn test_keyless_db_recovery<F: Family, V, C, H, S: Strategy>(
744        context: deterministic::Context,
745        db: TestKeyless<F, V, C, H, S>,
746        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
747    ) where
748        V: ValueEncoding<Value: TestValue>,
749        C: Mutable<Item = Operation<F, V>>,
750        H: Hasher,
751        Operation<F, V>: EncodeShared,
752    {
753        let root = db.root();
754        const ELEMENTS: u64 = 100;
755
756        // Create uncommitted appends then simulate failure.
757        {
758            let mut batch = db.new_batch();
759            for i in 0..ELEMENTS {
760                batch = batch.append(V::Value::make(i));
761            }
762            // Don't merkleize/apply -- simulate failed commit
763        }
764        drop(db);
765        // Should rollback to the previous root.
766        let mut db = reopen(context.child("db").with_attribute("index", 2)).await;
767        assert_eq!(root, db.root());
768
769        // Apply the updates and commit them this time.
770        {
771            let mut batch = db.new_batch();
772            for i in 0..ELEMENTS {
773                batch = batch.append(V::Value::make(i + 100));
774            }
775            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
776                .await
777                .unwrap();
778        }
779        db.commit().await.unwrap();
780        let root = db.root();
781
782        // Create uncommitted appends then simulate failure.
783        {
784            let mut batch = db.new_batch();
785            for i in 0..ELEMENTS {
786                batch = batch.append(V::Value::make(i + 200));
787            }
788            // Don't merkleize/apply -- simulate failed commit
789        }
790        drop(db);
791        // Should rollback to the previous root.
792        let mut db = reopen(context.child("db").with_attribute("index", 3)).await;
793        assert_eq!(root, db.root());
794
795        // Apply the updates and commit them this time.
796        {
797            let mut batch = db.new_batch();
798            for i in 0..ELEMENTS {
799                batch = batch.append(V::Value::make(i + 300));
800            }
801            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
802                .await
803                .unwrap();
804        }
805        db.commit().await.unwrap();
806        let root = db.root();
807
808        // Make sure we can reopen and get back to the same state.
809        drop(db);
810        let db = reopen(context.child("db").with_attribute("index", 4)).await;
811        assert_eq!(db.bounds().end, 2 * ELEMENTS + 3);
812        assert_eq!(db.root(), root);
813
814        db.destroy().await.unwrap();
815    }
816
817    #[boxed]
818    pub(crate) async fn test_keyless_db_proof<F: Family, V, C, S: Strategy>(
819        mut db: TestKeyless<F, V, C, Sha256, S>,
820    ) where
821        V: ValueEncoding<Value: TestValue>,
822        C: Mutable<Item = Operation<F, V>>,
823        Operation<F, V>: EncodeShared + std::fmt::Debug,
824    {
825        const ELEMENTS: u64 = 50;
826
827        {
828            let mut batch = db.new_batch();
829            for i in 0..ELEMENTS {
830                batch = batch.append(V::Value::make(i));
831            }
832            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
833                .await
834                .unwrap();
835        }
836        let root = db.root();
837
838        let (proof, ops) = db.proof(Location::new(0), NZU64!(100)).await.unwrap();
839        assert!(verify_proof::<Sha256, _, _>(
840            &proof,
841            Location::new(0),
842            &ops,
843            &root,
844        ));
845        assert_eq!(ops.len() as u64, 1 + ELEMENTS + 1);
846
847        let (proof, ops) = db.proof(Location::new(10), NZU64!(5)).await.unwrap();
848        assert!(verify_proof::<Sha256, _, _>(
849            &proof,
850            Location::new(10),
851            &ops,
852            &root,
853        ));
854        assert_eq!(ops.len(), 5);
855
856        db.destroy().await.unwrap();
857    }
858
859    #[boxed]
860    pub(crate) async fn test_keyless_db_metadata<F: Family, V, C, H, S: Strategy>(
861        mut db: TestKeyless<F, V, C, H, S>,
862    ) where
863        V: ValueEncoding<Value: TestValue>,
864        C: Mutable<Item = Operation<F, V>>,
865        H: Hasher,
866        Operation<F, V>: EncodeShared,
867    {
868        let metadata = V::Value::make(99);
869        let merkleized = db
870            .new_batch()
871            .append(V::Value::make(1))
872            .merkleize(&db, Some(metadata.clone()), db.inactivity_floor_loc())
873            .await;
874        db.apply_batch(merkleized).await.unwrap();
875        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata));
876
877        let merkleized = db
878            .new_batch()
879            .merkleize(&db, None, db.inactivity_floor_loc())
880            .await;
881        db.apply_batch(merkleized).await.unwrap();
882        assert_eq!(db.get_metadata().await.unwrap(), None);
883
884        db.destroy().await.unwrap();
885    }
886
887    #[boxed]
888    pub(crate) async fn test_keyless_db_pruning<F: Family, V, C, H, S: Strategy>(
889        mut db: TestKeyless<F, V, C, H, S>,
890    ) where
891        V: ValueEncoding<Value: TestValue>,
892        C: Mutable<Item = Operation<F, V>>,
893        H: Hasher,
894        Operation<F, V>: EncodeShared,
895    {
896        // Initial floor is 0, so pruning past 0 should fail.
897        assert_eq!(db.inactivity_floor_loc(), Location::new(0));
898        let result = db.prune(Location::new(1)).await;
899        assert!(
900            matches!(result, Err(Error::PruneBeyondMinRequired(prune_loc, floor))
901                if prune_loc == Location::new(1) && floor == Location::new(0))
902        );
903
904        // Add values and commit, advancing the floor to the new commit location.
905        let first_commit_loc = Location::<F>::new(3);
906        let merkleized = db
907            .new_batch()
908            .append(V::Value::make(1))
909            .append(V::Value::make(2))
910            .merkleize(&db, None, first_commit_loc)
911            .await;
912        db.apply_batch(merkleized).await.unwrap();
913        assert_eq!(db.last_commit_loc(), first_commit_loc);
914        assert_eq!(db.inactivity_floor_loc(), first_commit_loc);
915
916        // Append one more, advancing the floor with it.
917        let second_commit_loc = Location::<F>::new(5);
918        let merkleized = db
919            .new_batch()
920            .append(V::Value::make(3))
921            .merkleize(&db, None, second_commit_loc)
922            .await;
923        db.apply_batch(merkleized).await.unwrap();
924
925        // Valid prune: up to the floor (previous commit location).
926        let root = db.root();
927        assert!(db.prune(first_commit_loc).await.is_ok());
928        assert_eq!(db.root(), root);
929
930        // Pruning beyond the current floor fails.
931        let new_floor = db.inactivity_floor_loc();
932        let beyond = Location::new(*new_floor + 1);
933        let result = db.prune(beyond).await;
934        assert!(
935            matches!(result, Err(Error::PruneBeyondMinRequired(prune_loc, floor))
936                if prune_loc == beyond && floor == new_floor)
937        );
938
939        db.destroy().await.unwrap();
940    }
941
942    #[boxed]
943    pub(crate) async fn test_keyless_db_empty_db_recovery<F: Family, V, C, H, S: Strategy>(
944        context: deterministic::Context,
945        db: TestKeyless<F, V, C, H, S>,
946        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
947    ) where
948        V: ValueEncoding<Value: TestValue>,
949        C: Mutable<Item = Operation<F, V>>,
950        H: Hasher,
951        Operation<F, V>: EncodeShared,
952    {
953        let root = db.root();
954        const ELEMENTS: u64 = 200;
955
956        // Reopen DB without clean shutdown and make sure the state is the same.
957        let db = reopen(context.child("db").with_attribute("index", 2)).await;
958        assert_eq!(db.bounds().end, 1); // initial commit should exist
959        assert_eq!(db.root(), root);
960
961        // Simulate failure after inserting operations without a commit.
962        {
963            let mut batch = db.new_batch();
964            for i in 0..ELEMENTS {
965                batch = batch.append(V::Value::make(i));
966            }
967            // Don't merkleize/apply -- simulate failed commit
968        }
969        drop(db);
970        let db = reopen(context.child("db").with_attribute("index", 3)).await;
971        assert_eq!(db.bounds().end, 1); // initial commit should exist
972        assert_eq!(db.root(), root);
973
974        // Repeat: simulate failure after inserting operations without a commit.
975        {
976            let mut batch = db.new_batch();
977            for i in 0..ELEMENTS {
978                batch = batch.append(V::Value::make(i + 500));
979            }
980            // Don't merkleize/apply -- simulate failed commit
981        }
982        drop(db);
983        let db = reopen(context.child("db").with_attribute("index", 4)).await;
984        assert_eq!(db.bounds().end, 1); // initial commit should exist
985        assert_eq!(db.root(), root);
986
987        // One last check: multiple batches of uncommitted appends.
988        {
989            let mut batch = db.new_batch();
990            for i in 0..ELEMENTS * 3 {
991                batch = batch.append(V::Value::make(i + 1000));
992            }
993            // Don't merkleize/apply -- simulate failed commit
994        }
995        drop(db);
996        let mut db = reopen(context.child("db").with_attribute("index", 5)).await;
997        assert_eq!(db.bounds().end, 1); // initial commit should exist
998        assert_eq!(db.root(), root);
999        assert_eq!(db.last_commit_loc(), Location::new(0));
1000
1001        // Apply the ops one last time but fully commit them this time, then clean up.
1002        {
1003            let mut batch = db.new_batch();
1004            for i in 0..ELEMENTS {
1005                batch = batch.append(V::Value::make(i + 2000));
1006            }
1007            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1008                .await
1009                .unwrap();
1010        }
1011        db.commit().await.unwrap();
1012        let db = reopen(context.child("db").with_attribute("index", 6)).await;
1013        assert!(db.bounds().end > 1);
1014        assert_ne!(db.root(), root);
1015
1016        db.destroy().await.unwrap();
1017    }
1018
1019    #[boxed]
1020    pub(crate) async fn test_keyless_db_replay_with_trailing_appends<
1021        F: Family,
1022        V,
1023        C,
1024        H,
1025        S: Strategy,
1026    >(
1027        context: deterministic::Context,
1028        mut db: TestKeyless<F, V, C, H, S>,
1029        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
1030    ) where
1031        V: ValueEncoding<Value: TestValue>,
1032        C: Mutable<Item = Operation<F, V>>,
1033        H: Hasher,
1034        Operation<F, V>: EncodeShared,
1035    {
1036        // Add some initial operations and commit.
1037        {
1038            let mut batch = db.new_batch();
1039            for i in 0..10u64 {
1040                batch = batch.append(V::Value::make(i));
1041            }
1042            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1043                .await
1044                .unwrap();
1045        }
1046        db.commit().await.unwrap();
1047        let committed_root = db.root();
1048        let committed_size = db.bounds().end;
1049
1050        // Add exactly one more append (uncommitted).
1051        {
1052            db.new_batch().append(V::Value::make(99));
1053            // Don't merkleize/apply -- simulate failed commit
1054        }
1055        drop(db);
1056
1057        // Reopen and verify correct recovery.
1058        let mut db = reopen(context.child("db").with_attribute("index", 2)).await;
1059        assert_eq!(
1060            db.bounds().end,
1061            committed_size,
1062            "Should rewind to last commit"
1063        );
1064        assert_eq!(db.root(), committed_root, "Root should match last commit");
1065        assert_eq!(
1066            db.last_commit_loc(),
1067            committed_size - 1,
1068            "Last commit location should be correct"
1069        );
1070
1071        // Verify we can append and commit new data after recovery.
1072        let new_value = V::Value::make(77);
1073        {
1074            let batch = db.new_batch();
1075            let loc = batch.size();
1076            let batch = batch.append(new_value.clone());
1077            assert_eq!(
1078                loc, committed_size,
1079                "New append should get the expected location"
1080            );
1081            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1082                .await
1083                .unwrap();
1084        }
1085        db.commit().await.unwrap();
1086
1087        assert_eq!(db.get(committed_size).await.unwrap(), Some(new_value));
1088
1089        let new_committed_root = db.root();
1090        let new_committed_size = db.bounds().end;
1091
1092        // Add multiple uncommitted appends.
1093        {
1094            let mut batch = db.new_batch();
1095            for i in 0..5u64 {
1096                batch = batch.append(V::Value::make(200 + i));
1097            }
1098            // Don't merkleize/apply -- simulate failed commit
1099        }
1100        drop(db);
1101
1102        // Reopen and verify correct recovery.
1103        let db = reopen(context.child("db").with_attribute("index", 3)).await;
1104        assert_eq!(
1105            db.bounds().end,
1106            new_committed_size,
1107            "Should rewind to last commit with multiple trailing appends"
1108        );
1109        assert_eq!(
1110            db.root(),
1111            new_committed_root,
1112            "Root should match last commit after multiple appends"
1113        );
1114        assert_eq!(
1115            db.last_commit_loc(),
1116            new_committed_size - 1,
1117            "Last commit location should be correct after multiple appends"
1118        );
1119
1120        db.destroy().await.unwrap();
1121    }
1122
1123    /// `get_many` on the DB and on unmerkleized/merkleized batches returns
1124    /// results consistent with individual `get` calls.
1125    #[boxed]
1126    pub(crate) async fn test_keyless_get_many<F: Family, V, C, S: Strategy>(
1127        mut db: TestKeyless<F, V, C, Sha256, S>,
1128    ) where
1129        V: ValueEncoding<Value: TestValue>,
1130        C: Mutable<Item = Operation<F, V>>,
1131        Operation<F, V>: EncodeShared,
1132    {
1133        let v1 = V::Value::make(1);
1134        let v2 = V::Value::make(2);
1135        let v3 = V::Value::make(3);
1136
1137        // Commit v1 and v2 to disk.
1138        let batch = db.new_batch();
1139        let loc1 = batch.size();
1140        let batch = batch.append(v1.clone());
1141        let loc2 = batch.size();
1142        let batch = batch.append(v2.clone());
1143        db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1144            .await
1145            .unwrap();
1146        db.commit().await.unwrap();
1147
1148        // DB-level get_many.
1149        let results = db.get_many(&[loc1, loc2]).await.unwrap();
1150        assert_eq!(results, vec![Some(v1.clone()), Some(v2.clone())]);
1151
1152        // Empty input.
1153        let results = db.get_many(&[]).await.unwrap();
1154        assert!(results.is_empty());
1155
1156        // Unmerkleized batch: pending appends + DB fallthrough.
1157        let batch = db.new_batch();
1158        let loc3 = batch.size();
1159        let batch = batch.append(v3.clone());
1160        let results = batch.get_many(&[loc1, loc3], &db).await.unwrap();
1161        assert_eq!(results, vec![Some(v1.clone()), Some(v3.clone())]);
1162
1163        // Merkleized batch: parent chain + DB fallthrough.
1164        let parent = db
1165            .new_batch()
1166            .append(v3.clone())
1167            .merkleize(&db, None, db.inactivity_floor_loc())
1168            .await;
1169        let child = parent.new_batch::<Sha256>().append(V::Value::make(4));
1170        let results = child.get_many(&[loc1, loc2], &db).await.unwrap();
1171        assert_eq!(results, vec![Some(v1.clone()), Some(v2.clone())]);
1172
1173        db.destroy().await.unwrap();
1174    }
1175
1176    #[boxed]
1177    pub(crate) async fn test_keyless_batch_chained<F: Family, V, C, S: Strategy>(
1178        mut db: TestKeyless<F, V, C, Sha256, S>,
1179    ) where
1180        V: ValueEncoding<Value: TestValue>,
1181        C: Mutable<Item = Operation<F, V>>,
1182        Operation<F, V>: EncodeShared,
1183    {
1184        let v1 = V::Value::make(10);
1185        let v2 = V::Value::make(20);
1186        let v3 = V::Value::make(30);
1187
1188        let parent = db.new_batch();
1189        let loc1 = parent.size();
1190        let parent = parent.append(v1.clone());
1191        let parent_m = parent.merkleize(&db, None, db.inactivity_floor_loc()).await;
1192
1193        let child = parent_m.new_batch::<Sha256>();
1194        let loc2 = child.size();
1195        let child = child.append(v2.clone());
1196        let loc3 = child.size();
1197        let child = child.append(v3.clone());
1198        let child_m = child.merkleize(&db, None, db.inactivity_floor_loc()).await;
1199        let child_root = child_m.root();
1200
1201        db.apply_batch(child_m).await.unwrap();
1202        db.commit().await.unwrap();
1203
1204        assert_eq!(db.root(), child_root);
1205        assert_eq!(db.get(loc1).await.unwrap(), Some(v1));
1206        assert_eq!(db.get(loc2).await.unwrap(), Some(v2));
1207        assert_eq!(db.get(loc3).await.unwrap(), Some(v3));
1208
1209        db.destroy().await.unwrap();
1210    }
1211
1212    #[boxed]
1213    pub(crate) async fn test_keyless_stale_batch<F: Family, V, C, H, S: Strategy>(
1214        mut db: TestKeyless<F, V, C, H, S>,
1215    ) where
1216        V: ValueEncoding<Value: TestValue>,
1217        C: Mutable<Item = Operation<F, V>>,
1218        H: Hasher,
1219        Operation<F, V>: EncodeShared,
1220    {
1221        let batch_a = db
1222            .new_batch()
1223            .append(V::Value::make(10))
1224            .merkleize(&db, None, db.inactivity_floor_loc())
1225            .await;
1226        let batch_b = db
1227            .new_batch()
1228            .append(V::Value::make(20))
1229            .merkleize(&db, None, db.inactivity_floor_loc())
1230            .await;
1231
1232        db.apply_batch(batch_a).await.unwrap();
1233
1234        let result = db.apply_batch(batch_b).await;
1235        assert!(matches!(result, Err(Error::StaleBatch { .. })));
1236
1237        db.destroy().await.unwrap();
1238    }
1239
1240    #[boxed]
1241    pub(crate) async fn test_keyless_partial_ancestor_commit<F: Family, V, C, H, S: Strategy>(
1242        mut db: TestKeyless<F, V, C, H, S>,
1243    ) where
1244        V: ValueEncoding<Value: TestValue>,
1245        C: Mutable<Item = Operation<F, V>>,
1246        H: Hasher,
1247        Operation<F, V>: EncodeShared,
1248    {
1249        // Chain: DB <- A <- B <- C
1250        let a = db
1251            .new_batch()
1252            .append(V::Value::make(10))
1253            .merkleize(&db, None, db.inactivity_floor_loc())
1254            .await;
1255        let b = a
1256            .new_batch::<H>()
1257            .append(V::Value::make(20))
1258            .merkleize(&db, None, db.inactivity_floor_loc())
1259            .await;
1260        let c = b
1261            .new_batch::<H>()
1262            .append(V::Value::make(30))
1263            .merkleize(&db, None, db.inactivity_floor_loc())
1264            .await;
1265
1266        let expected_root = c.root();
1267
1268        // Apply only A, then apply C directly (B's items applied via ancestor batches).
1269        db.apply_batch(a).await.unwrap();
1270        db.apply_batch(c).await.unwrap();
1271
1272        // Root must match what the full chain produces.
1273        assert_eq!(db.root(), expected_root);
1274
1275        db.destroy().await.unwrap();
1276    }
1277
1278    #[boxed]
1279    pub(crate) async fn test_keyless_to_batch<F: Family, V, C, S: Strategy>(
1280        mut db: TestKeyless<F, V, C, Sha256, S>,
1281    ) where
1282        V: ValueEncoding<Value: TestValue>,
1283        C: Mutable<Item = Operation<F, V>>,
1284        Operation<F, V>: EncodeShared,
1285    {
1286        let batch = db.new_batch();
1287        let loc1 = batch.size();
1288        let batch = batch.append(V::Value::make(10));
1289        db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1290            .await
1291            .unwrap();
1292
1293        let snapshot = db.to_batch();
1294        assert_eq!(snapshot.root(), db.root());
1295
1296        let child_batch = snapshot.new_batch::<Sha256>();
1297        let loc2 = child_batch.size();
1298        let child_batch = child_batch.append(V::Value::make(20));
1299        db.apply_batch(
1300            child_batch
1301                .merkleize(&db, None, db.inactivity_floor_loc())
1302                .await,
1303        )
1304        .await
1305        .unwrap();
1306
1307        assert_eq!(db.get(loc1).await.unwrap(), Some(V::Value::make(10)));
1308        assert_eq!(db.get(loc2).await.unwrap(), Some(V::Value::make(20)));
1309
1310        db.destroy().await.unwrap();
1311    }
1312
1313    #[boxed]
1314    pub(crate) async fn test_keyless_db_non_empty_recovery<F: Family, V, C, H, S: Strategy>(
1315        context: deterministic::Context,
1316        mut db: TestKeyless<F, V, C, H, S>,
1317        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
1318    ) where
1319        V: ValueEncoding<Value: TestValue>,
1320        C: Mutable<Item = Operation<F, V>>,
1321        H: Hasher,
1322        Operation<F, V>: EncodeShared,
1323    {
1324        // Append many values then commit, advancing the floor to the new commit so we can
1325        // later prune up to it.
1326        const ELEMENTS: u64 = 200;
1327        {
1328            let mut batch = db.new_batch();
1329            for i in 0..ELEMENTS {
1330                batch = batch.append(V::Value::make(i));
1331            }
1332            let new_commit = Location::new(*db.last_commit_loc() + 1 + ELEMENTS);
1333            db.apply_batch(batch.merkleize(&db, None, new_commit).await)
1334                .await
1335                .unwrap();
1336        }
1337        db.commit().await.unwrap();
1338        let root = db.root();
1339        let op_count = db.bounds().end;
1340
1341        // Reopen DB without clean shutdown and make sure the state is the same.
1342        let db = reopen(context.child("db").with_attribute("index", 2)).await;
1343        assert_eq!(db.bounds().end, op_count);
1344        assert_eq!(db.root(), root);
1345        assert_eq!(db.last_commit_loc(), op_count - 1);
1346        drop(db);
1347
1348        // Insert many operations without commit, then simulate failure.
1349        let db = reopen(context.child("recovery_a")).await;
1350        {
1351            let mut batch = db.new_batch();
1352            for i in 0..ELEMENTS {
1353                batch = batch.append(V::Value::make(i + 1000));
1354            }
1355            // Don't merkleize/apply -- simulate failed commit
1356        }
1357        drop(db);
1358        let db = reopen(context.child("recovery_b")).await;
1359        assert_eq!(db.bounds().end, op_count);
1360        assert_eq!(db.root(), root);
1361        drop(db);
1362
1363        // Repeat after pruning to the last commit.
1364        let mut db = reopen(context.child("db").with_attribute("index", 3)).await;
1365        db.prune(db.last_commit_loc()).await.unwrap();
1366        assert_eq!(db.bounds().end, op_count);
1367        assert_eq!(db.root(), root);
1368        db.sync().await.unwrap();
1369        drop(db);
1370
1371        let db = reopen(context.child("recovery_c")).await;
1372        {
1373            let mut batch = db.new_batch();
1374            for i in 0..ELEMENTS {
1375                batch = batch.append(V::Value::make(i + 2000));
1376            }
1377        }
1378        drop(db);
1379        let db = reopen(context.child("recovery_d")).await;
1380        assert_eq!(db.bounds().end, op_count);
1381        assert_eq!(db.root(), root);
1382        drop(db);
1383
1384        // Apply the ops one last time but fully commit them this time, then clean up.
1385        let mut db = reopen(context.child("db").with_attribute("index", 4)).await;
1386        {
1387            let mut batch = db.new_batch();
1388            for i in 0..ELEMENTS {
1389                batch = batch.append(V::Value::make(i + 3000));
1390            }
1391            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1392                .await
1393                .unwrap();
1394        }
1395        db.commit().await.unwrap();
1396        let db = reopen(context.child("db").with_attribute("index", 5)).await;
1397        let bounds = db.bounds();
1398        assert!(bounds.end > op_count);
1399        assert_ne!(db.root(), root);
1400        assert_eq!(db.last_commit_loc(), bounds.end - 1);
1401
1402        db.destroy().await.unwrap();
1403    }
1404
1405    #[boxed]
1406    pub(crate) async fn test_keyless_db_proof_comprehensive<F: Family, V, C, S: Strategy>(
1407        mut db: TestKeyless<F, V, C, Sha256, S>,
1408    ) where
1409        V: ValueEncoding<Value: TestValue>,
1410        C: Mutable<Item = Operation<F, V>>,
1411        Operation<F, V>: EncodeShared + std::fmt::Debug,
1412    {
1413        // Build a db with some values.
1414        const ELEMENTS: u64 = 100;
1415        {
1416            let mut batch = db.new_batch();
1417            for i in 0u64..ELEMENTS {
1418                batch = batch.append(V::Value::make(i));
1419            }
1420            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1421                .await
1422                .unwrap();
1423        }
1424
1425        // Test that historical proof fails with op_count > number of operations.
1426        assert!(matches!(
1427            db.historical_proof(db.bounds().end + 1, Location::new(5), NZU64!(10))
1428                .await,
1429            Err(Error::<F>::Merkle(crate::merkle::Error::RangeOutOfBounds(
1430                _
1431            )))
1432        ));
1433
1434        let root = db.root();
1435
1436        for (start_loc, max_ops) in [
1437            (0, 10),
1438            (10, 5),
1439            (50, 20),
1440            (90, 15),
1441            (0, 1),
1442            (ELEMENTS - 1, 1),
1443            (ELEMENTS, 1),
1444        ] {
1445            let (proof, ops) = db
1446                .proof(Location::new(start_loc), NZU64!(max_ops))
1447                .await
1448                .unwrap();
1449            assert!(
1450                verify_proof::<Sha256, _, _>(&proof, Location::new(start_loc), &ops, &root,),
1451                "Failed to verify proof for range starting at {start_loc} with max {max_ops} ops",
1452            );
1453            let expected_ops = std::cmp::min(max_ops, *db.bounds().end - start_loc);
1454            assert_eq!(ops.len() as u64, expected_ops);
1455
1456            let wrong_root = Sha256::hash(&[0xFF; 32]);
1457            assert!(!verify_proof::<Sha256, _, _>(
1458                &proof,
1459                Location::new(start_loc),
1460                &ops,
1461                &wrong_root,
1462            ));
1463            if start_loc > 0 {
1464                assert!(!verify_proof::<Sha256, _, _>(
1465                    &proof,
1466                    Location::new(start_loc - 1),
1467                    &ops,
1468                    &root,
1469                ));
1470            }
1471        }
1472
1473        db.destroy().await.unwrap();
1474    }
1475
1476    #[boxed]
1477    pub(crate) async fn test_keyless_db_proof_with_pruning<F: Family, V, C, S: Strategy>(
1478        context: deterministic::Context,
1479        mut db: TestKeyless<F, V, C, Sha256, S>,
1480        reopen: Reopen<TestKeyless<F, V, C, Sha256, S>>,
1481    ) where
1482        V: ValueEncoding<Value: TestValue>,
1483        C: Mutable<Item = Operation<F, V>>,
1484        Operation<F, V>: EncodeShared + std::fmt::Debug,
1485    {
1486        const ELEMENTS: u64 = 100;
1487        {
1488            let mut batch = db.new_batch();
1489            for i in 0u64..ELEMENTS {
1490                batch = batch.append(V::Value::make(i));
1491            }
1492            let new_commit = Location::new(*db.last_commit_loc() + 1 + ELEMENTS);
1493            db.apply_batch(batch.merkleize(&db, None, new_commit).await)
1494                .await
1495                .unwrap();
1496        }
1497
1498        {
1499            let mut batch = db.new_batch();
1500            for i in ELEMENTS..ELEMENTS * 2 {
1501                batch = batch.append(V::Value::make(i));
1502            }
1503            let new_commit = Location::new(*db.last_commit_loc() + 1 + ELEMENTS);
1504            db.apply_batch(batch.merkleize(&db, None, new_commit).await)
1505                .await
1506                .unwrap();
1507        }
1508        let root = db.root();
1509
1510        const PRUNE_LOC: u64 = 30;
1511        db.prune(Location::new(PRUNE_LOC)).await.unwrap();
1512        let oldest_retained = db.bounds().start;
1513        assert_eq!(db.root(), root);
1514
1515        db.sync().await.unwrap();
1516        drop(db);
1517        let mut db = reopen(context).await;
1518        assert_eq!(db.root(), root);
1519
1520        for (start_loc, max_ops) in [
1521            (oldest_retained, 10),
1522            (Location::new(50), 20),
1523            (Location::new(150), 10),
1524            (Location::new(190), 15),
1525        ] {
1526            if start_loc < oldest_retained {
1527                continue;
1528            }
1529            let (proof, ops) = db.proof(start_loc, NZU64!(max_ops)).await.unwrap();
1530            assert!(verify_proof::<Sha256, _, _>(&proof, start_loc, &ops, &root,));
1531        }
1532
1533        let aggressive_prune: Location<F> = Location::new(150);
1534        db.prune(aggressive_prune).await.unwrap();
1535
1536        let new_oldest = db.bounds().start;
1537        let (proof, ops) = db.proof(new_oldest, NZU64!(20)).await.unwrap();
1538        assert!(verify_proof::<Sha256, _, _>(
1539            &proof, new_oldest, &ops, &root,
1540        ));
1541
1542        let almost_all = db.bounds().end - 5;
1543        db.prune(almost_all).await.unwrap();
1544        let final_oldest = db.bounds().start;
1545        if final_oldest < db.bounds().end {
1546            let (final_proof, final_ops) = db.proof(final_oldest, NZU64!(10)).await.unwrap();
1547            assert!(verify_proof::<Sha256, _, _>(
1548                &final_proof,
1549                final_oldest,
1550                &final_ops,
1551                &root,
1552            ));
1553        }
1554
1555        db.destroy().await.unwrap();
1556    }
1557
1558    #[boxed]
1559    pub(crate) async fn test_keyless_db_get_out_of_bounds<F: Family, V, C, H, S: Strategy>(
1560        mut db: TestKeyless<F, V, C, H, S>,
1561    ) where
1562        V: ValueEncoding<Value: TestValue>,
1563        C: Mutable<Item = Operation<F, V>>,
1564        H: Hasher,
1565        Operation<F, V>: EncodeShared,
1566    {
1567        assert!(db.get(Location::new(0)).await.unwrap().is_none());
1568
1569        let merkleized = db
1570            .new_batch()
1571            .append(V::Value::make(1))
1572            .append(V::Value::make(2))
1573            .merkleize(&db, None, db.inactivity_floor_loc())
1574            .await;
1575        db.apply_batch(merkleized).await.unwrap();
1576
1577        assert_eq!(
1578            db.get(Location::new(1)).await.unwrap(),
1579            Some(V::Value::make(1))
1580        );
1581        assert!(db.get(Location::new(3)).await.unwrap().is_none());
1582        assert!(matches!(
1583            db.get(Location::new(4)).await,
1584            Err(Error::LocationOutOfBounds(loc, size))
1585                if loc == Location::new(4) && size == Location::new(4)
1586        ));
1587
1588        db.destroy().await.unwrap();
1589    }
1590
1591    #[boxed]
1592    pub(crate) async fn test_keyless_batch_get<F: Family, V, C, H, S: Strategy>(
1593        mut db: TestKeyless<F, V, C, H, S>,
1594    ) where
1595        V: ValueEncoding<Value: TestValue>,
1596        C: Mutable<Item = Operation<F, V>>,
1597        H: Hasher,
1598        Operation<F, V>: EncodeShared,
1599    {
1600        let base_vals: Vec<V::Value> = (0..3).map(|i| V::Value::make(10 + i)).collect();
1601        let mut base_locs = Vec::new();
1602        {
1603            let mut batch = db.new_batch();
1604            for v in &base_vals {
1605                let loc = batch.size();
1606                batch = batch.append(v.clone());
1607                base_locs.push(loc);
1608            }
1609            db.apply_batch(batch.merkleize(&db, None, db.inactivity_floor_loc()).await)
1610                .await
1611                .unwrap();
1612        }
1613
1614        let batch = db.new_batch();
1615        for (i, loc) in base_locs.iter().enumerate() {
1616            assert_eq!(
1617                batch.get(*loc, &db).await.unwrap(),
1618                Some(base_vals[i].clone()),
1619            );
1620        }
1621
1622        let new_val = V::Value::make(99);
1623        let new_loc = batch.size();
1624        let batch = batch.append(new_val.clone());
1625        assert_eq!(batch.get(new_loc, &db).await.unwrap(), Some(new_val));
1626        assert_eq!(
1627            batch.get(Location::new(*new_loc + 1), &db).await.unwrap(),
1628            None
1629        );
1630
1631        db.destroy().await.unwrap();
1632    }
1633
1634    #[boxed]
1635    pub(crate) async fn test_keyless_batch_stacked_get<F: Family, V, C, S: Strategy>(
1636        db: TestKeyless<F, V, C, Sha256, S>,
1637    ) where
1638        V: ValueEncoding<Value: TestValue>,
1639        C: Mutable<Item = Operation<F, V>>,
1640        Operation<F, V>: EncodeShared,
1641    {
1642        let v1 = V::Value::make(1);
1643        let v2 = V::Value::make(2);
1644
1645        let parent = db.new_batch();
1646        let loc1 = parent.size();
1647        let parent = parent.append(v1.clone());
1648        let parent_m = parent.merkleize(&db, None, db.inactivity_floor_loc()).await;
1649
1650        let child = parent_m.new_batch::<Sha256>();
1651        assert_eq!(child.get(loc1, &db).await.unwrap(), Some(v1));
1652
1653        let loc2 = child.size();
1654        let child = child.append(v2.clone());
1655        assert_eq!(child.get(loc2, &db).await.unwrap(), Some(v2));
1656        assert_eq!(child.get(Location::new(9999), &db).await.unwrap(), None);
1657
1658        db.destroy().await.unwrap();
1659    }
1660
1661    #[boxed]
1662    pub(crate) async fn test_keyless_batch_speculative_root<F: Family, V, C, H, S: Strategy>(
1663        mut db: TestKeyless<F, V, C, H, S>,
1664    ) where
1665        V: ValueEncoding<Value: TestValue>,
1666        C: Mutable<Item = Operation<F, V>>,
1667        H: Hasher,
1668        Operation<F, V>: EncodeShared,
1669    {
1670        let mut batch = db.new_batch();
1671        for i in 0u64..10 {
1672            batch = batch.append(V::Value::make(i));
1673        }
1674        let merkleized = batch.merkleize(&db, None, db.inactivity_floor_loc()).await;
1675        let speculative = merkleized.root();
1676        db.apply_batch(merkleized).await.unwrap();
1677        assert_eq!(db.root(), speculative);
1678
1679        let merkleized = db
1680            .new_batch()
1681            .append(V::Value::make(100))
1682            .merkleize(&db, Some(V::Value::make(55)), db.inactivity_floor_loc())
1683            .await;
1684        let speculative = merkleized.root();
1685        db.apply_batch(merkleized).await.unwrap();
1686        assert_eq!(db.root(), speculative);
1687
1688        db.destroy().await.unwrap();
1689    }
1690
1691    #[boxed]
1692    pub(crate) async fn test_keyless_merkleized_batch_get<F: Family, V, C, S: Strategy>(
1693        mut db: TestKeyless<F, V, C, Sha256, S>,
1694    ) where
1695        V: ValueEncoding<Value: TestValue>,
1696        C: Mutable<Item = Operation<F, V>>,
1697        Operation<F, V>: EncodeShared,
1698    {
1699        let base_val = V::Value::make(10);
1700        let merkleized = db
1701            .new_batch()
1702            .append(base_val.clone())
1703            .merkleize(&db, None, db.inactivity_floor_loc())
1704            .await;
1705        db.apply_batch(merkleized).await.unwrap();
1706
1707        let new_val = V::Value::make(20);
1708        let merkleized = db
1709            .new_batch()
1710            .append(new_val.clone())
1711            .merkleize(&db, None, db.inactivity_floor_loc())
1712            .await;
1713
1714        assert_eq!(
1715            merkleized.get(Location::new(1), &db).await.unwrap(),
1716            Some(base_val),
1717        );
1718        assert_eq!(
1719            merkleized.get(Location::new(3), &db).await.unwrap(),
1720            Some(new_val),
1721        );
1722        assert_eq!(merkleized.get(Location::new(4), &db).await.unwrap(), None);
1723
1724        db.destroy().await.unwrap();
1725    }
1726
1727    #[boxed]
1728    pub(crate) async fn test_keyless_batch_chained_apply_sequential<
1729        F: Family,
1730        V,
1731        C,
1732        H,
1733        S: Strategy,
1734    >(
1735        mut db: TestKeyless<F, V, C, H, S>,
1736    ) where
1737        V: ValueEncoding<Value: TestValue>,
1738        C: Mutable<Item = Operation<F, V>>,
1739        H: Hasher,
1740        Operation<F, V>: EncodeShared,
1741    {
1742        let v1 = V::Value::make(1);
1743        let v2 = V::Value::make(2);
1744
1745        let parent = db.new_batch();
1746        let loc1 = parent.size();
1747        let parent = parent.append(v1.clone());
1748        let parent_m = parent.merkleize(&db, None, db.inactivity_floor_loc()).await;
1749        let parent_root = parent_m.root();
1750
1751        db.apply_batch(parent_m).await.unwrap();
1752        assert_eq!(db.root(), parent_root);
1753        assert_eq!(db.get(loc1).await.unwrap(), Some(v1));
1754
1755        let batch2 = db.new_batch();
1756        let loc2 = batch2.size();
1757        let batch2 = batch2.append(v2.clone());
1758        let batch2_m = batch2.merkleize(&db, None, db.inactivity_floor_loc()).await;
1759        let batch2_root = batch2_m.root();
1760        db.apply_batch(batch2_m).await.unwrap();
1761        assert_eq!(db.root(), batch2_root);
1762        assert_eq!(db.get(loc2).await.unwrap(), Some(v2));
1763
1764        db.destroy().await.unwrap();
1765    }
1766
1767    #[boxed]
1768    pub(crate) async fn test_keyless_batch_many_sequential<F: Family, V, C, S: Strategy>(
1769        mut db: TestKeyless<F, V, C, Sha256, S>,
1770    ) where
1771        V: ValueEncoding<Value: TestValue>,
1772        C: Mutable<Item = Operation<F, V>>,
1773        Operation<F, V>: EncodeShared + std::fmt::Debug,
1774    {
1775        const BATCHES: u64 = 20;
1776        const APPENDS_PER_BATCH: u64 = 5;
1777        let mut all_values: Vec<V::Value> = Vec::new();
1778        let mut all_locs: Vec<Location<F>> = Vec::new();
1779
1780        for batch_idx in 0..BATCHES {
1781            let mut batch = db.new_batch();
1782            for j in 0..APPENDS_PER_BATCH {
1783                let v = V::Value::make(batch_idx * 10 + j);
1784                let loc = batch.size();
1785                batch = batch.append(v.clone());
1786                all_values.push(v);
1787                all_locs.push(loc);
1788            }
1789            let merkleized = batch.merkleize(&db, None, db.inactivity_floor_loc()).await;
1790            db.apply_batch(merkleized).await.unwrap();
1791        }
1792
1793        for (i, loc) in all_locs.iter().enumerate() {
1794            assert_eq!(db.get(*loc).await.unwrap(), Some(all_values[i].clone()));
1795        }
1796
1797        let root = db.root();
1798        let (proof, ops) = db.proof(Location::new(0), NZU64!(1000)).await.unwrap();
1799        assert!(verify_proof::<Sha256, _, _>(
1800            &proof,
1801            Location::new(0),
1802            &ops,
1803            &root,
1804        ));
1805        assert_eq!(db.bounds().end, 1 + BATCHES * (APPENDS_PER_BATCH + 1));
1806
1807        db.destroy().await.unwrap();
1808    }
1809
1810    #[boxed]
1811    pub(crate) async fn test_keyless_batch_empty<F: Family, V, C, H, S: Strategy>(
1812        mut db: TestKeyless<F, V, C, H, S>,
1813    ) where
1814        V: ValueEncoding<Value: TestValue>,
1815        C: Mutable<Item = Operation<F, V>>,
1816        H: Hasher,
1817        Operation<F, V>: EncodeShared,
1818    {
1819        let merkleized = db
1820            .new_batch()
1821            .append(V::Value::make(1))
1822            .merkleize(&db, None, db.inactivity_floor_loc())
1823            .await;
1824        db.apply_batch(merkleized).await.unwrap();
1825        let root_before = db.root();
1826        let size_before = db.bounds().end;
1827
1828        let merkleized = db
1829            .new_batch()
1830            .merkleize(&db, None, db.inactivity_floor_loc())
1831            .await;
1832        let speculative = merkleized.root();
1833        db.apply_batch(merkleized).await.unwrap();
1834
1835        assert_ne!(db.root(), root_before);
1836        assert_eq!(db.root(), speculative);
1837        assert_eq!(db.bounds().end, size_before + 1);
1838
1839        db.destroy().await.unwrap();
1840    }
1841
1842    #[boxed]
1843    pub(crate) async fn test_keyless_batch_chained_merkleized_get<F: Family, V, C, S: Strategy>(
1844        mut db: TestKeyless<F, V, C, Sha256, S>,
1845    ) where
1846        V: ValueEncoding<Value: TestValue>,
1847        C: Mutable<Item = Operation<F, V>>,
1848        Operation<F, V>: EncodeShared,
1849    {
1850        let base_val = V::Value::make(10);
1851        let floor = db.inactivity_floor_loc();
1852        db.apply_batch(
1853            db.new_batch()
1854                .append(base_val.clone())
1855                .merkleize(&db, None, floor)
1856                .await,
1857        )
1858        .await
1859        .unwrap();
1860
1861        let v1 = V::Value::make(1);
1862        let parent = db.new_batch();
1863        let loc1 = parent.size();
1864        let parent_m = parent
1865            .append(v1.clone())
1866            .merkleize(&db, None, db.inactivity_floor_loc())
1867            .await;
1868
1869        let v2 = V::Value::make(2);
1870        let child = parent_m.new_batch::<Sha256>();
1871        let loc2 = child.size();
1872        let child_m = child
1873            .append(v2.clone())
1874            .merkleize(&db, None, db.inactivity_floor_loc())
1875            .await;
1876
1877        assert_eq!(
1878            child_m.get(Location::new(1), &db).await.unwrap(),
1879            Some(base_val),
1880        );
1881        assert_eq!(child_m.get(loc1, &db).await.unwrap(), Some(v1));
1882        assert_eq!(child_m.get(loc2, &db).await.unwrap(), Some(v2));
1883
1884        db.destroy().await.unwrap();
1885    }
1886
1887    #[boxed]
1888    pub(crate) async fn test_keyless_batch_large<F: Family, V, C, S: Strategy>(
1889        mut db: TestKeyless<F, V, C, Sha256, S>,
1890    ) where
1891        V: ValueEncoding<Value: TestValue>,
1892        C: Mutable<Item = Operation<F, V>>,
1893        Operation<F, V>: EncodeShared + std::fmt::Debug,
1894    {
1895        const N: u64 = 500;
1896        let mut values = Vec::new();
1897        let mut locs = Vec::new();
1898
1899        let mut batch = db.new_batch();
1900        for i in 0..N {
1901            let v = V::Value::make(i);
1902            locs.push(batch.size());
1903            batch = batch.append(v.clone());
1904            values.push(v);
1905        }
1906        let merkleized = batch.merkleize(&db, None, db.inactivity_floor_loc()).await;
1907        db.apply_batch(merkleized).await.unwrap();
1908
1909        for (i, loc) in locs.iter().enumerate() {
1910            assert_eq!(db.get(*loc).await.unwrap(), Some(values[i].clone()));
1911        }
1912
1913        let root = db.root();
1914        let (proof, ops) = db.proof(Location::new(0), NZU64!(1000)).await.unwrap();
1915        assert!(verify_proof::<Sha256, _, _>(
1916            &proof,
1917            Location::new(0),
1918            &ops,
1919            &root,
1920        ));
1921        assert_eq!(db.bounds().end, 1 + N + 1);
1922
1923        db.destroy().await.unwrap();
1924    }
1925
1926    #[boxed]
1927    pub(crate) async fn test_keyless_stale_batch_chained<F: Family, V, C, S: Strategy>(
1928        mut db: TestKeyless<F, V, C, Sha256, S>,
1929    ) where
1930        V: ValueEncoding<Value: TestValue>,
1931        C: Mutable<Item = Operation<F, V>>,
1932        Operation<F, V>: EncodeShared,
1933    {
1934        let parent = db
1935            .new_batch()
1936            .append(V::Value::make(1))
1937            .merkleize(&db, None, db.inactivity_floor_loc())
1938            .await;
1939        let child_a = parent
1940            .new_batch::<Sha256>()
1941            .append(V::Value::make(2))
1942            .merkleize(&db, None, db.inactivity_floor_loc())
1943            .await;
1944        let child_b = parent
1945            .new_batch::<Sha256>()
1946            .append(V::Value::make(3))
1947            .merkleize(&db, None, db.inactivity_floor_loc())
1948            .await;
1949
1950        db.apply_batch(child_a).await.unwrap();
1951        assert!(matches!(
1952            db.apply_batch(child_b).await,
1953            Err(Error::StaleBatch { .. })
1954        ));
1955
1956        db.destroy().await.unwrap();
1957    }
1958
1959    #[boxed]
1960    pub(crate) async fn test_keyless_sequential_commit_parent_then_child<
1961        F: Family,
1962        V,
1963        C,
1964        S: Strategy,
1965    >(
1966        mut db: TestKeyless<F, V, C, Sha256, S>,
1967    ) where
1968        V: ValueEncoding<Value: TestValue>,
1969        C: Mutable<Item = Operation<F, V>>,
1970        Operation<F, V>: EncodeShared,
1971    {
1972        let parent = db
1973            .new_batch()
1974            .append(V::Value::make(1))
1975            .merkleize(&db, None, db.inactivity_floor_loc())
1976            .await;
1977        let child = parent
1978            .new_batch::<Sha256>()
1979            .append(V::Value::make(2))
1980            .merkleize(&db, None, db.inactivity_floor_loc())
1981            .await;
1982
1983        db.apply_batch(parent).await.unwrap();
1984        db.apply_batch(child).await.unwrap();
1985
1986        db.destroy().await.unwrap();
1987    }
1988
1989    #[boxed]
1990    pub(crate) async fn test_keyless_stale_batch_child_before_parent<F: Family, V, C, S: Strategy>(
1991        mut db: TestKeyless<F, V, C, Sha256, S>,
1992    ) where
1993        V: ValueEncoding<Value: TestValue>,
1994        C: Mutable<Item = Operation<F, V>>,
1995        Operation<F, V>: EncodeShared,
1996    {
1997        let parent = db
1998            .new_batch()
1999            .append(V::Value::make(1))
2000            .merkleize(&db, None, db.inactivity_floor_loc())
2001            .await;
2002        let child = parent
2003            .new_batch::<Sha256>()
2004            .append(V::Value::make(2))
2005            .merkleize(&db, None, db.inactivity_floor_loc())
2006            .await;
2007
2008        db.apply_batch(child).await.unwrap();
2009        assert!(matches!(
2010            db.apply_batch(parent).await,
2011            Err(Error::StaleBatch { .. })
2012        ));
2013
2014        db.destroy().await.unwrap();
2015    }
2016
2017    #[boxed]
2018    pub(crate) async fn test_keyless_child_root_matches_pending_and_committed<
2019        F: Family,
2020        V,
2021        C,
2022        S: Strategy,
2023    >(
2024        mut db: TestKeyless<F, V, C, Sha256, S>,
2025    ) where
2026        V: ValueEncoding<Value: TestValue>,
2027        C: Mutable<Item = Operation<F, V>>,
2028        Operation<F, V>: EncodeShared,
2029    {
2030        // Build the child while the parent is still pending.
2031        let parent = db
2032            .new_batch()
2033            .append(V::Value::make(1))
2034            .merkleize(&db, None, db.inactivity_floor_loc())
2035            .await;
2036        let pending_child = parent
2037            .new_batch::<Sha256>()
2038            .append(V::Value::make(2))
2039            .merkleize(&db, None, db.inactivity_floor_loc())
2040            .await;
2041
2042        // Commit the parent, then rebuild the same logical child from the
2043        // committed DB state and compare roots.
2044        db.apply_batch(parent).await.unwrap();
2045        db.commit().await.unwrap();
2046
2047        let committed_child = db
2048            .new_batch()
2049            .append(V::Value::make(2))
2050            .merkleize(&db, None, db.inactivity_floor_loc())
2051            .await;
2052
2053        assert_eq!(pending_child.root(), committed_child.root());
2054
2055        db.destroy().await.unwrap();
2056    }
2057
2058    async fn commit_appends<F: Family, V, C, H, S: Strategy>(
2059        db: &mut TestKeyless<F, V, C, H, S>,
2060        values: impl IntoIterator<Item = V::Value>,
2061        metadata: Option<V::Value>,
2062    ) -> core::ops::Range<Location<F>>
2063    where
2064        V: ValueEncoding<Value: TestValue>,
2065        C: Mutable<Item = Operation<F, V>>,
2066        H: Hasher,
2067        Operation<F, V>: EncodeShared,
2068    {
2069        // Tests that don't specifically exercise floor behavior advance the floor to the new
2070        // commit location, so pruning up to the last commit works analogously to the pre-floor
2071        // semantics.
2072        let base_size = *db.last_commit_loc() + 1;
2073        let appends_iter: Vec<_> = values.into_iter().collect();
2074        let new_commit_loc = Location::new(base_size + appends_iter.len() as u64);
2075        let mut batch = db.new_batch();
2076        for value in appends_iter {
2077            batch = batch.append(value);
2078        }
2079        let range = db
2080            .apply_batch(batch.merkleize(db, metadata, new_commit_loc).await)
2081            .await
2082            .unwrap();
2083        db.commit().await.unwrap();
2084        range
2085    }
2086
2087    #[boxed]
2088    pub(crate) async fn test_keyless_db_rewind_recovery<F: Family, V, C, H, S: Strategy>(
2089        context: deterministic::Context,
2090        mut db: TestKeyless<F, V, C, H, S>,
2091        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
2092    ) where
2093        V: ValueEncoding<Value: TestValue>,
2094        C: Mutable<Item = Operation<F, V>>,
2095        H: Hasher,
2096        Operation<F, V>: EncodeShared,
2097    {
2098        let initial_root = db.root();
2099        let initial_size = db.bounds().end;
2100
2101        let value_a = V::Value::make(1);
2102        let value_b = V::Value::make(2);
2103        let metadata_a = V::Value::make(3);
2104        let first_range = commit_appends(
2105            &mut db,
2106            [value_a.clone(), value_b.clone()],
2107            Some(metadata_a.clone()),
2108        )
2109        .await;
2110
2111        let root_before = db.root();
2112        let size_before = db.bounds().end;
2113        let commit_before = db.last_commit_loc();
2114        assert_eq!(size_before, first_range.end);
2115
2116        let value_c = V::Value::make(4);
2117        let metadata_b = V::Value::make(5);
2118        let second_range =
2119            commit_appends(&mut db, [value_c.clone()], Some(metadata_b.clone())).await;
2120        assert_eq!(second_range.start, size_before);
2121        assert_ne!(db.root(), root_before);
2122        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata_b));
2123
2124        db.rewind(size_before).await.unwrap();
2125        assert_eq!(db.root(), root_before);
2126        assert_eq!(db.bounds().end, size_before);
2127        assert_eq!(db.last_commit_loc(), commit_before);
2128        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata_a.clone()));
2129        assert_eq!(
2130            db.get(Location::new(1)).await.unwrap(),
2131            Some(value_a.clone())
2132        );
2133        assert_eq!(
2134            db.get(Location::new(2)).await.unwrap(),
2135            Some(value_b.clone())
2136        );
2137        assert!(
2138            matches!(
2139                db.get(Location::new(4)).await,
2140                Err(Error::LocationOutOfBounds(_, size)) if size == size_before
2141            ),
2142            "rewound append should be out of bounds",
2143        );
2144
2145        db.commit().await.unwrap();
2146        drop(db);
2147        let mut db = reopen(context.child("reopen")).await;
2148        assert_eq!(db.root(), root_before);
2149        assert_eq!(db.bounds().end, size_before);
2150        assert_eq!(db.last_commit_loc(), commit_before);
2151        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata_a));
2152        assert_eq!(
2153            db.get(Location::new(1)).await.unwrap(),
2154            Some(value_a.clone())
2155        );
2156        assert_eq!(
2157            db.get(Location::new(2)).await.unwrap(),
2158            Some(value_b.clone())
2159        );
2160        assert!(matches!(
2161            db.get(Location::new(4)).await,
2162            Err(Error::LocationOutOfBounds(_, size)) if size == size_before
2163        ));
2164
2165        db.rewind(initial_size).await.unwrap();
2166        assert_eq!(db.root(), initial_root);
2167        assert_eq!(db.bounds().end, initial_size);
2168        assert_eq!(db.get_metadata().await.unwrap(), None);
2169        assert!(matches!(
2170            db.get(Location::new(1)).await,
2171            Err(Error::LocationOutOfBounds(_, size)) if size == initial_size
2172        ));
2173
2174        db.commit().await.unwrap();
2175        drop(db);
2176        let db = reopen(context.child("reopen_initial_boundary")).await;
2177        assert_eq!(db.root(), initial_root);
2178        assert_eq!(db.bounds().end, initial_size);
2179        assert_eq!(db.get_metadata().await.unwrap(), None);
2180        assert!(matches!(
2181            db.get(Location::new(1)).await,
2182            Err(Error::LocationOutOfBounds(_, size)) if size == initial_size
2183        ));
2184
2185        db.destroy().await.unwrap();
2186    }
2187
2188    #[boxed]
2189    pub(crate) async fn test_keyless_db_rewind_pruned_target_errors<
2190        F: Family,
2191        V,
2192        C,
2193        H,
2194        S: Strategy,
2195    >(
2196        mut db: TestKeyless<F, V, C, H, S>,
2197    ) where
2198        V: ValueEncoding<Value: TestValue>,
2199        C: Mutable<Item = Operation<F, V>>,
2200        H: Hasher,
2201        Operation<F, V>: EncodeShared,
2202    {
2203        let first_range = commit_appends(&mut db, (0..16).map(V::Value::make), None).await;
2204
2205        let mut round = 0u64;
2206        loop {
2207            round += 1;
2208            assert!(
2209                round <= 64,
2210                "failed to prune enough history for rewind test"
2211            );
2212
2213            commit_appends(
2214                &mut db,
2215                (0..16).map(|i| V::Value::make(round * 100 + i)),
2216                None,
2217            )
2218            .await;
2219            db.prune(db.last_commit_loc()).await.unwrap();
2220
2221            if db.bounds().start > first_range.start {
2222                break;
2223            }
2224        }
2225
2226        let oldest_retained = db.bounds().start;
2227        let boundary_err = db.rewind(oldest_retained).await.unwrap_err();
2228        assert!(
2229            matches!(
2230                boundary_err,
2231                Error::Journal(crate::journal::Error::ItemPruned(_))
2232            ),
2233            "unexpected rewind error at retained boundary: {boundary_err:?}"
2234        );
2235
2236        let err = db.rewind(first_range.start).await.unwrap_err();
2237        assert!(
2238            matches!(err, Error::Journal(crate::journal::Error::ItemPruned(_))),
2239            "unexpected rewind error: {err:?}"
2240        );
2241
2242        db.destroy().await.unwrap();
2243    }
2244
2245    #[boxed]
2246    pub(crate) async fn test_keyless_db_floor_tracking<F: Family, V, C, H, S: Strategy>(
2247        context: deterministic::Context,
2248        mut db: TestKeyless<F, V, C, H, S>,
2249        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
2250    ) where
2251        V: ValueEncoding<Value: TestValue>,
2252        C: Mutable<Item = Operation<F, V>>,
2253        H: Hasher,
2254        Operation<F, V>: EncodeShared,
2255    {
2256        // Freshly created db has floor = 0.
2257        assert_eq!(db.inactivity_floor_loc(), Location::new(0));
2258
2259        // Apply a batch with a declared floor; the db's floor should update.
2260        let floor_a = Location::<F>::new(2);
2261        let merkleized = db
2262            .new_batch()
2263            .append(V::Value::make(1))
2264            .append(V::Value::make(2))
2265            .merkleize(&db, None, floor_a)
2266            .await;
2267        db.apply_batch(merkleized).await.unwrap();
2268        db.commit().await.unwrap();
2269        assert_eq!(db.inactivity_floor_loc(), floor_a);
2270
2271        // Reopen: floor should survive restart (it's part of the last commit operation).
2272        drop(db);
2273        let mut db = reopen(context.child("reopen")).await;
2274        assert_eq!(db.inactivity_floor_loc(), floor_a);
2275
2276        // Floor may stay the same across a commit (monotonic non-decreasing).
2277        let merkleized = db
2278            .new_batch()
2279            .append(V::Value::make(3))
2280            .merkleize(&db, None, floor_a)
2281            .await;
2282        db.apply_batch(merkleized).await.unwrap();
2283        assert_eq!(db.inactivity_floor_loc(), floor_a);
2284
2285        // Floor may advance further.
2286        let floor_b = Location::<F>::new(5);
2287        let merkleized = db
2288            .new_batch()
2289            .append(V::Value::make(4))
2290            .merkleize(&db, None, floor_b)
2291            .await;
2292        db.apply_batch(merkleized).await.unwrap();
2293        assert_eq!(db.inactivity_floor_loc(), floor_b);
2294
2295        db.destroy().await.unwrap();
2296    }
2297
2298    #[boxed]
2299    pub(crate) async fn test_keyless_db_floor_regression_rejected<F: Family, V, C, H, S: Strategy>(
2300        mut db: TestKeyless<F, V, C, H, S>,
2301    ) where
2302        V: ValueEncoding<Value: TestValue>,
2303        C: Mutable<Item = Operation<F, V>>,
2304        H: Hasher,
2305        Operation<F, V>: EncodeShared,
2306    {
2307        // Advance floor to 3.
2308        let merkleized = db
2309            .new_batch()
2310            .append(V::Value::make(1))
2311            .append(V::Value::make(2))
2312            .merkleize(&db, None, Location::new(3))
2313            .await;
2314        db.apply_batch(merkleized).await.unwrap();
2315        assert_eq!(db.inactivity_floor_loc(), Location::new(3));
2316        let root_before = db.root();
2317        let last_commit_before = db.last_commit_loc();
2318
2319        // Try to commit with a lower floor; apply_batch rejects.
2320        let merkleized = db
2321            .new_batch()
2322            .append(V::Value::make(3))
2323            .merkleize(&db, None, Location::new(1))
2324            .await;
2325        let err = db.apply_batch(merkleized).await.unwrap_err();
2326        assert!(
2327            matches!(err, Error::FloorRegressed(new, current) if *new == 1 && *current == 3),
2328            "unexpected error: {err:?}"
2329        );
2330
2331        // DB state must be untouched: floor, last_commit_loc, and root unchanged.
2332        assert_eq!(db.inactivity_floor_loc(), Location::new(3));
2333        assert_eq!(db.last_commit_loc(), last_commit_before);
2334        assert_eq!(db.root(), root_before);
2335
2336        db.destroy().await.unwrap();
2337    }
2338
2339    #[boxed]
2340    pub(crate) async fn test_keyless_db_floor_beyond_commit_loc_rejected<
2341        F: Family,
2342        V,
2343        C,
2344        H,
2345        S: Strategy,
2346    >(
2347        mut db: TestKeyless<F, V, C, H, S>,
2348    ) where
2349        V: ValueEncoding<Value: TestValue>,
2350        C: Mutable<Item = Operation<F, V>>,
2351        H: Hasher,
2352        Operation<F, V>: EncodeShared,
2353    {
2354        // Batch of 2 appends + 1 commit lands at locations [1..4); commit at 3, total_size = 4.
2355        // A floor > 3 (the commit location) is invalid — even floor == 4 (one past the commit)
2356        // is rejected so a subsequent prune cannot remove the last readable commit.
2357        let merkleized = db
2358            .new_batch()
2359            .append(V::Value::make(1))
2360            .append(V::Value::make(2))
2361            .merkleize(&db, None, Location::new(999))
2362            .await;
2363        let err = db.apply_batch(merkleized).await.unwrap_err();
2364        assert!(
2365            matches!(err, Error::FloorBeyondSize(floor, commit) if *floor == 999 && *commit == 3),
2366            "unexpected error: {err:?}"
2367        );
2368
2369        // Boundary: floor == total_size (= commit_loc + 1) is also rejected.
2370        let merkleized = db
2371            .new_batch()
2372            .append(V::Value::make(3))
2373            .append(V::Value::make(4))
2374            .merkleize(&db, None, Location::new(4))
2375            .await;
2376        let err = db.apply_batch(merkleized).await.unwrap_err();
2377        assert!(
2378            matches!(err, Error::FloorBeyondSize(floor, commit) if *floor == 4 && *commit == 3),
2379            "unexpected error: {err:?}"
2380        );
2381
2382        db.destroy().await.unwrap();
2383    }
2384
2385    #[boxed]
2386    pub(crate) async fn test_keyless_db_rewind_restores_floor<F: Family, V, C, H, S: Strategy>(
2387        mut db: TestKeyless<F, V, C, H, S>,
2388    ) where
2389        V: ValueEncoding<Value: TestValue>,
2390        C: Mutable<Item = Operation<F, V>>,
2391        H: Hasher,
2392        Operation<F, V>: EncodeShared,
2393    {
2394        // First commit: floor advances to 3 (= commit location).
2395        let floor_a = Location::<F>::new(3);
2396        let merkleized = db
2397            .new_batch()
2398            .append(V::Value::make(1))
2399            .append(V::Value::make(2))
2400            .merkleize(&db, None, floor_a)
2401            .await;
2402        db.apply_batch(merkleized).await.unwrap();
2403        db.commit().await.unwrap();
2404        let rewind_target = Location::new(*db.last_commit_loc() + 1);
2405
2406        // Second commit: floor advances to 6.
2407        let floor_b = Location::<F>::new(6);
2408        let merkleized = db
2409            .new_batch()
2410            .append(V::Value::make(3))
2411            .append(V::Value::make(4))
2412            .merkleize(&db, None, floor_b)
2413            .await;
2414        db.apply_batch(merkleized).await.unwrap();
2415        db.commit().await.unwrap();
2416        assert_eq!(db.inactivity_floor_loc(), floor_b);
2417
2418        // Rewind to the first commit; floor should restore to floor_a.
2419        db.rewind(rewind_target).await.unwrap();
2420        assert_eq!(db.inactivity_floor_loc(), floor_a);
2421
2422        // Prune is now gated at floor_a. Pruning past it fails.
2423        let beyond = Location::new(*floor_a + 1);
2424        let err = db.prune(beyond).await.unwrap_err();
2425        assert!(matches!(err, Error::PruneBeyondMinRequired(_, _)));
2426
2427        // Pruning up to the floor works.
2428        db.prune(floor_a).await.unwrap();
2429
2430        db.destroy().await.unwrap();
2431    }
2432
2433    /// Floor is embedded in the Commit operation and therefore in the Merkle root: two databases
2434    /// with identical appends but different floors must produce different roots.
2435    #[boxed]
2436    pub(crate) async fn test_keyless_db_floor_changes_root<F: Family, V, C, H, S: Strategy>(
2437        mut db_a: TestKeyless<F, V, C, H, S>,
2438        mut db_b: TestKeyless<F, V, C, H, S>,
2439    ) where
2440        V: ValueEncoding<Value: TestValue>,
2441        C: Mutable<Item = Operation<F, V>>,
2442        H: Hasher,
2443        Operation<F, V>: EncodeShared,
2444    {
2445        let appends = [V::Value::make(1), V::Value::make(2)];
2446
2447        // db_a commits with floor=0.
2448        let mut batch_a = db_a.new_batch();
2449        for v in appends.iter() {
2450            batch_a = batch_a.append(v.clone());
2451        }
2452        db_a.apply_batch(batch_a.merkleize(&db_a, None, Location::new(0)).await)
2453            .await
2454            .unwrap();
2455
2456        // db_b commits the same appends but with floor=3 (= commit location).
2457        let mut batch_b = db_b.new_batch();
2458        for v in appends.iter() {
2459            batch_b = batch_b.append(v.clone());
2460        }
2461        db_b.apply_batch(batch_b.merkleize(&db_b, None, Location::new(3)).await)
2462            .await
2463            .unwrap();
2464
2465        assert_ne!(db_a.root(), db_b.root());
2466
2467        db_a.destroy().await.unwrap();
2468        db_b.destroy().await.unwrap();
2469    }
2470
2471    /// A floor equal to the commit operation's location is on the tight boundary of acceptance.
2472    #[boxed]
2473    pub(crate) async fn test_keyless_db_floor_at_commit_loc_accepted<
2474        F: Family,
2475        V,
2476        C,
2477        H,
2478        S: Strategy,
2479    >(
2480        mut db: TestKeyless<F, V, C, H, S>,
2481    ) where
2482        V: ValueEncoding<Value: TestValue>,
2483        C: Mutable<Item = Operation<F, V>>,
2484        H: Hasher,
2485        Operation<F, V>: EncodeShared,
2486    {
2487        // 2 appends + 1 commit on top of the initial commit: commit lands at location 3.
2488        // floor == 3 (= commit_loc) is the maximum accepted value under the per-commit bound.
2489        let commit_loc = Location::<F>::new(3);
2490        db.apply_batch(
2491            db.new_batch()
2492                .append(V::Value::make(1))
2493                .append(V::Value::make(2))
2494                .merkleize(&db, None, commit_loc)
2495                .await,
2496        )
2497        .await
2498        .unwrap();
2499        assert_eq!(db.inactivity_floor_loc(), commit_loc);
2500
2501        db.destroy().await.unwrap();
2502    }
2503
2504    /// End-to-end: commit → drop → reopen → rewind → verify floor restored after a crash.
2505    #[boxed]
2506    pub(crate) async fn test_keyless_db_rewind_after_reopen_with_floor<
2507        F: Family,
2508        V,
2509        C,
2510        H,
2511        S: Strategy,
2512    >(
2513        context: deterministic::Context,
2514        mut db: TestKeyless<F, V, C, H, S>,
2515        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
2516    ) where
2517        V: ValueEncoding<Value: TestValue>,
2518        C: Mutable<Item = Operation<F, V>>,
2519        H: Hasher,
2520        Operation<F, V>: EncodeShared,
2521    {
2522        // First commit: 2 appends + commit, floor advances to 3.
2523        let floor_a = Location::<F>::new(3);
2524        db.apply_batch(
2525            db.new_batch()
2526                .append(V::Value::make(1))
2527                .append(V::Value::make(2))
2528                .merkleize(&db, None, floor_a)
2529                .await,
2530        )
2531        .await
2532        .unwrap();
2533        db.commit().await.unwrap();
2534        let rewind_target = Location::new(*db.last_commit_loc() + 1);
2535
2536        // Second commit: 2 appends + commit, floor advances to 6.
2537        let floor_b = Location::<F>::new(6);
2538        db.apply_batch(
2539            db.new_batch()
2540                .append(V::Value::make(3))
2541                .append(V::Value::make(4))
2542                .merkleize(&db, None, floor_b)
2543                .await,
2544        )
2545        .await
2546        .unwrap();
2547        db.commit().await.unwrap();
2548
2549        // Drop & reopen to simulate a crash after both commits were durable.
2550        drop(db);
2551        let mut db = reopen(context.child("reopen")).await;
2552        assert_eq!(db.inactivity_floor_loc(), floor_b);
2553
2554        // Rewind to the first commit; floor should restore to floor_a.
2555        db.rewind(rewind_target).await.unwrap();
2556        assert_eq!(db.inactivity_floor_loc(), floor_a);
2557        assert_eq!(db.last_commit_loc(), Location::new(3));
2558
2559        // Commit the rewind so it's durable, then reopen and confirm the floor again.
2560        db.commit().await.unwrap();
2561        drop(db);
2562        let db = reopen(context.child("reopen").with_attribute("index", 2)).await;
2563        assert_eq!(db.inactivity_floor_loc(), floor_a);
2564
2565        db.destroy().await.unwrap();
2566    }
2567
2568    /// A chained batch that applies a tip with a floor *lower than* its parent's floor must
2569    /// be rejected — the parent's `Commit` is written to the journal by the same
2570    /// `journal.apply_batch` call, so its floor participates in the per-commit monotonicity
2571    /// invariant.
2572    #[boxed]
2573    pub(crate) async fn test_keyless_db_ancestor_floor_regression_rejected<
2574        F,
2575        V,
2576        C,
2577        H,
2578        S: Strategy,
2579    >(
2580        mut db: TestKeyless<F, V, C, H, S>,
2581    ) where
2582        F: Family,
2583        V: ValueEncoding<Value: TestValue>,
2584        C: Mutable<Item = Operation<F, V>>,
2585        H: Hasher,
2586        Operation<F, V>: EncodeShared,
2587    {
2588        // parent: 1 append + commit at loc 2 with floor=2 (the parent's commit_loc).
2589        let parent = db
2590            .new_batch()
2591            .append(V::Value::make(1))
2592            .merkleize(&db, None, Location::new(2))
2593            .await;
2594        // child: 1 append + commit at loc 4 with floor=1 (regressed from parent's floor=2).
2595        let child = parent
2596            .new_batch::<H>()
2597            .append(V::Value::make(2))
2598            .merkleize(&db, None, Location::new(1))
2599            .await;
2600
2601        let root_before = db.root();
2602        let last_commit_before = db.last_commit_loc();
2603        let floor_before = db.inactivity_floor_loc();
2604
2605        let err = db.apply_batch(child).await.unwrap_err();
2606        assert!(
2607            matches!(err, Error::FloorRegressed(new, prev) if *new == 1 && *prev == 2),
2608            "unexpected error: {err:?}"
2609        );
2610
2611        // DB state untouched by the rejected chain.
2612        assert_eq!(db.root(), root_before);
2613        assert_eq!(db.last_commit_loc(), last_commit_before);
2614        assert_eq!(db.inactivity_floor_loc(), floor_before);
2615
2616        db.destroy().await.unwrap();
2617    }
2618
2619    /// A chained batch where an *ancestor's* floor exceeds its own commit location must be
2620    /// rejected — identifying the ancestor's bound, not the tip's.
2621    #[boxed]
2622    pub(crate) async fn test_keyless_db_ancestor_floor_beyond_commit_loc_rejected<
2623        F,
2624        V,
2625        C,
2626        H,
2627        S: Strategy,
2628    >(
2629        mut db: TestKeyless<F, V, C, H, S>,
2630    ) where
2631        F: Family,
2632        V: ValueEncoding<Value: TestValue>,
2633        C: Mutable<Item = Operation<F, V>>,
2634        H: Hasher,
2635        Operation<F, V>: EncodeShared,
2636    {
2637        // parent: 1 append + commit at loc 2. Declare floor = 3 (one past the commit).
2638        let parent = db
2639            .new_batch()
2640            .append(V::Value::make(1))
2641            .merkleize(&db, None, Location::new(3))
2642            .await;
2643        // child: valid on its own (floor = 0 ≤ child's commit_loc), but parent's floor is bad.
2644        let child = parent
2645            .new_batch::<H>()
2646            .append(V::Value::make(2))
2647            .merkleize(&db, None, Location::new(0))
2648            .await;
2649
2650        let err = db.apply_batch(child).await.unwrap_err();
2651        // Error should identify the ancestor's commit_loc (2), not the tip's.
2652        assert!(
2653            matches!(err, Error::FloorBeyondSize(floor, commit) if *floor == 3 && *commit == 2),
2654            "unexpected error: {err:?}"
2655        );
2656
2657        db.destroy().await.unwrap();
2658    }
2659
2660    /// After committing with `floor = commit_loc` and pruning down to it, the live set is
2661    /// exactly one operation — the commit itself. This is the minimum non-empty live set
2662    /// achievable under the per-commit bound. The DB must remain fully usable: the commit is
2663    /// readable, the root is preserved, reopen recovers `inactivity_floor_loc` from the sole
2664    /// remaining op, and a follow-on batch applies cleanly on top.
2665    #[boxed]
2666    pub(crate) async fn test_keyless_db_single_commit_live_set<F, V, C, H, S: Strategy>(
2667        context: deterministic::Context,
2668        mut db: TestKeyless<F, V, C, H, S>,
2669        reopen: Reopen<TestKeyless<F, V, C, H, S>>,
2670    ) where
2671        F: Family,
2672        V: ValueEncoding<Value: TestValue>,
2673        C: Mutable<Item = Operation<F, V>>,
2674        H: Hasher,
2675        Operation<F, V>: EncodeShared,
2676    {
2677        // Initial commit is at loc 0. 3 appends + commit → commit lands at loc 4.
2678        // Declare floor = 4 (= commit_loc), the tight maximum.
2679        let metadata = V::Value::make(42);
2680        let commit_loc = Location::<F>::new(4);
2681        db.apply_batch(
2682            db.new_batch()
2683                .append(V::Value::make(1))
2684                .append(V::Value::make(2))
2685                .append(V::Value::make(3))
2686                .merkleize(&db, Some(metadata.clone()), commit_loc)
2687                .await,
2688        )
2689        .await
2690        .unwrap();
2691        db.commit().await.unwrap();
2692        assert_eq!(db.last_commit_loc(), commit_loc);
2693        assert_eq!(db.inactivity_floor_loc(), commit_loc);
2694        let root_after_commit = db.root();
2695
2696        // Prune at the floor — the maximum prune allowed under the invariant.
2697        // Pruning is blob-aligned, so `bounds.start` may not physically advance all the way
2698        // to `commit_loc`; what matters semantically is that the floor has authorized pruning
2699        // of everything below the commit and that any further prune is rejected.
2700        db.prune(commit_loc).await.unwrap();
2701        let bounds = db.bounds();
2702        assert!(
2703            bounds.start <= commit_loc,
2704            "prune must not advance bounds.start past the floor"
2705        );
2706        assert_eq!(bounds.end, Location::new(*commit_loc + 1));
2707
2708        // Pruning one past the floor must be rejected — the floor is the hard ceiling.
2709        let err = db.prune(Location::new(*commit_loc + 1)).await.unwrap_err();
2710        assert!(matches!(err, Error::PruneBeyondMinRequired(p, f)
2711                if *p == *commit_loc + 1 && *f == *commit_loc));
2712
2713        // The commit op remains readable; its metadata is intact.
2714        assert_eq!(db.get(commit_loc).await.unwrap(), Some(metadata.clone()));
2715        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
2716        assert_eq!(db.last_commit_loc(), commit_loc);
2717        assert_eq!(db.inactivity_floor_loc(), commit_loc);
2718        // Prune does not affect the root (documented invariant on `prune`).
2719        assert_eq!(db.root(), root_after_commit);
2720
2721        // Persist the prune, then reopen: `init_from_journal` must recover the floor from
2722        // the last commit op.
2723        db.sync().await.unwrap();
2724        drop(db);
2725        let mut db = reopen(context.child("reopened")).await;
2726        let reopened_bounds = db.bounds();
2727        assert_eq!(reopened_bounds.end, Location::new(*commit_loc + 1));
2728        assert_eq!(db.last_commit_loc(), commit_loc);
2729        assert_eq!(db.inactivity_floor_loc(), commit_loc);
2730        assert_eq!(db.root(), root_after_commit);
2731        assert_eq!(db.get_metadata().await.unwrap(), Some(metadata.clone()));
2732
2733        // A follow-on batch applies on top. Monotonicity requires the new floor to be at
2734        // least `commit_loc` (= 4); advancing to the new tight max (= 7) exercises the
2735        // ancestor-to-tip floor transition from a minimum-live-set starting point.
2736        let next_commit_loc = Location::<F>::new(7);
2737        let v5 = V::Value::make(5);
2738        let v6 = V::Value::make(6);
2739        db.apply_batch(
2740            db.new_batch()
2741                .append(v5.clone())
2742                .append(v6.clone())
2743                .merkleize(&db, None, next_commit_loc)
2744                .await,
2745        )
2746        .await
2747        .unwrap();
2748        db.commit().await.unwrap();
2749        assert_eq!(db.last_commit_loc(), next_commit_loc);
2750        assert_eq!(db.inactivity_floor_loc(), next_commit_loc);
2751
2752        // New appends readable; the original commit op is also still in the live set (not
2753        // re-pruned), so reading it still returns its metadata.
2754        assert_eq!(db.get(Location::new(5)).await.unwrap(), Some(v5));
2755        assert_eq!(db.get(Location::new(6)).await.unwrap(), Some(v6));
2756        assert_eq!(db.get(commit_loc).await.unwrap(), Some(metadata));
2757
2758        db.destroy().await.unwrap();
2759    }
2760
2761    /// A multi-level chain with strictly-monotonic, within-bounds floors applies cleanly.
2762    #[boxed]
2763    pub(crate) async fn test_keyless_db_chained_apply_with_valid_floors_succeeds<
2764        F,
2765        V,
2766        C,
2767        H,
2768        S: Strategy,
2769    >(
2770        mut db: TestKeyless<F, V, C, H, S>,
2771    ) where
2772        F: Family,
2773        V: ValueEncoding<Value: TestValue>,
2774        C: Mutable<Item = Operation<F, V>>,
2775        H: Hasher,
2776        Operation<F, V>: EncodeShared,
2777    {
2778        // parent:     1 append, commit at loc 2, floor = 2.
2779        // child:      1 append, commit at loc 4, floor = 3.
2780        // grandchild: 1 append, commit at loc 6, floor = 5.
2781        let parent = db
2782            .new_batch()
2783            .append(V::Value::make(1))
2784            .merkleize(&db, None, Location::new(2))
2785            .await;
2786        let child = parent
2787            .new_batch::<H>()
2788            .append(V::Value::make(2))
2789            .merkleize(&db, None, Location::new(3))
2790            .await;
2791        let grandchild = child
2792            .new_batch::<H>()
2793            .append(V::Value::make(3))
2794            .merkleize(&db, None, Location::new(5))
2795            .await;
2796
2797        db.apply_batch(grandchild).await.unwrap();
2798
2799        // Grandchild's commit is the last op; tip's floor is the live floor.
2800        assert_eq!(db.last_commit_loc(), Location::new(6));
2801        assert_eq!(db.inactivity_floor_loc(), Location::new(5));
2802
2803        db.destroy().await.unwrap();
2804    }
2805}