Skip to main content

commonware_storage/journal/
authenticated.rs

1//! Authenticated journal implementation.
2//!
3//! An authenticated journal maintains a contiguous journal of items alongside a Merkle-family
4//! structure. The item at index i in the journal corresponds to the leaf at Location i in the
5//! Merkle structure. This structure enables efficient proofs that an item is included in the
6//! journal at a specific location.
7//!
8//! # Ownership
9//!
10//! Mutating methods take the journal by value and return it on success. If a mutating
11//! method returns an error, or its future is dropped before it finishes, the journal is
12//! gone: state that was not yet durable is discarded, but everything already on disk stays
13//! recoverable.
14
15use crate::{
16    Context,
17    journal::{
18        Error as JournalError,
19        contiguous::{Contiguous, Many, Mutable},
20    },
21    merkle::{
22        self, Bagging, Family, Location, Position, Proof, Readable, batch, full::Merkle,
23        hasher::Standard as StandardHasher, mem::Mem,
24    },
25};
26use alloc::{
27    sync::{Arc, Weak},
28    vec::Vec,
29};
30use commonware_codec::{Encode, EncodeShared};
31use commonware_cryptography::{Digest, Hasher};
32use commonware_macros::boxed;
33use commonware_parallel::Strategy;
34use commonware_runtime::{Handle, ReadOptions};
35use core::{
36    num::{NonZeroU64, NonZeroUsize},
37    ops::Range,
38};
39use futures::{Stream, TryFutureExt as _, try_join};
40use thiserror::Error;
41use tracing::{debug, warn};
42
43/// Errors that can occur when interacting with an authenticated journal.
44#[derive(Error, Debug)]
45pub enum Error<F: Family> {
46    #[error("merkle error: {0}")]
47    Merkle(#[from] merkle::Error<F>),
48
49    #[error("journal error: {0}")]
50    Journal(#[from] super::Error),
51}
52
53/// Strong ref to an ancestor [`MerkleizedBatch`] in the journal-batch chain.
54type MerkleizedParent<F, H, Item, S> = Arc<MerkleizedBatch<F, <H as Hasher>::Digest, Item, S>>;
55
56/// A speculative batch whose root digest has not yet been computed,
57/// in contrast to [`MerkleizedBatch`].
58pub struct UnmerkleizedBatch<F: Family, H: Hasher, Item: Send + Sync, S: Strategy> {
59    // The inner batch of Merkle leaf digests.
60    inner: batch::UnmerkleizedBatch<F, H::Digest, S>,
61    // The hasher to use for hashing the items.
62    hasher: StandardHasher<H>,
63    // The items to append from this batch.
64    items: Vec<Item>,
65    // This batch's parent, or None if the parent is the journal itself.
66    parent: Option<MerkleizedParent<F, H, Item, S>>,
67}
68
69type MerkleizedBatchArc<F, H, Item, S> = Arc<MerkleizedBatch<F, <H as Hasher>::Digest, Item, S>>;
70
71impl<F: Family, H: Hasher, Item: Encode + Send + Sync, S: Strategy>
72    UnmerkleizedBatch<F, H, Item, S>
73{
74    /// Add an item to the batch.
75    #[allow(clippy::should_implement_trait)]
76    pub fn add(mut self, item: Item) -> Self {
77        let encoded = item.encode();
78        self.inner = self.inner.add(&self.hasher, &encoded);
79        self.items.push(item);
80        self
81    }
82
83    /// Collect ancestor items and the leaf count before the oldest retained ancestor.
84    fn collect_ancestor_items(
85        parent: &MerkleizedParent<F, H, Item, S>,
86    ) -> (u64, Vec<Arc<Vec<Item>>>) {
87        let mut items = Vec::new();
88        let mut base_leaves = parent.as_ref().size() - parent.items.len() as u64;
89        if !parent.items.is_empty() {
90            items.push(Arc::clone(&parent.items));
91        }
92        let mut current = parent.parent.as_ref().and_then(Weak::upgrade);
93        while let Some(batch) = current {
94            base_leaves = batch.as_ref().size() - batch.items.len() as u64;
95            if !batch.items.is_empty() {
96                items.push(Arc::clone(&batch.items));
97            }
98            current = batch.parent.as_ref().and_then(Weak::upgrade);
99        }
100        items.reverse();
101        (base_leaves, items)
102    }
103
104    /// Merkleize the batch.
105    /// `base` provides committed node data as fallback during hash computation.
106    pub fn merkleize(self, base: &Mem<F, H::Digest>) -> MerkleizedBatchArc<F, H, Item, S> {
107        let Self {
108            inner,
109            hasher,
110            items,
111            parent,
112        } = self;
113
114        let (ancestor_base_leaves, ancestor_items) = parent.as_ref().map_or_else(
115            || (*inner.leaves() - items.len() as u64, Vec::new()),
116            Self::collect_ancestor_items,
117        );
118        let items = Arc::new(items);
119        let merkle = inner.merkleize(base, &hasher);
120        Arc::new(MerkleizedBatch {
121            inner: merkle,
122            bagging: hasher.root_bagging(),
123            items,
124            parent: parent.as_ref().map(Arc::downgrade),
125            ancestor_base_leaves,
126            ancestor_items,
127        })
128    }
129
130    /// Add caller-supplied items to the batch.
131    ///
132    /// # Panics
133    ///
134    /// Panics if items were previously added via [`add`](Self::add).
135    pub(crate) fn add_many(mut self, items: Vec<Item>) -> Self {
136        assert!(
137            self.items.is_empty(),
138            "add_many expects no items added via add"
139        );
140
141        self.inner = self.inner.add_many(&self.hasher, &items);
142        self.items = items;
143        self
144    }
145}
146
147/// A speculative batch whose root digest has been computed, in contrast to [`UnmerkleizedBatch`].
148#[derive(Clone, Debug)]
149pub struct MerkleizedBatch<F: Family, D: Digest, Item: Send + Sync, S: Strategy> {
150    /// The inner batch of Merkle leaf digests.
151    pub(crate) inner: Arc<batch::MerkleizedBatch<F, D, S>>,
152    /// The peak bagging policy inherited from the parent journal or batch.
153    bagging: Bagging,
154    /// The items to append from this batch.
155    items: Arc<Vec<Item>>,
156    /// This batch's parent, or None if the parent is the journal itself.
157    parent: Option<Weak<Self>>,
158    /// Number of leaves before the oldest retained ancestor batch.
159    pub(crate) ancestor_base_leaves: u64,
160    /// Ancestor item batches collected at merkleize time (root-to-tip order).
161    pub(crate) ancestor_items: Vec<Arc<Vec<Item>>>,
162}
163
164impl<F: Family, D: Digest, Item: Send + Sync, S: Strategy> MerkleizedBatch<F, D, Item, S> {
165    /// The number of items visible through this batch, including ancestors.
166    pub(crate) fn size(&self) -> u64 {
167        *self.inner.leaves()
168    }
169
170    /// Compute the root digest after this batch is applied using `inactive_peaks` and the bagging
171    /// carried by `hasher`.
172    ///
173    /// This recomputes the root rather than reading a cache.
174    pub fn root(
175        &self,
176        base: &Mem<F, D>,
177        hasher: &impl merkle::hasher::Hasher<F, Digest = D>,
178        inactive_peaks: usize,
179    ) -> Result<D, merkle::Error<F>> {
180        self.inner.root(base, hasher, inactive_peaks)
181    }
182
183    /// Inclusion proof for the element at `loc`.
184    pub fn proof(
185        &self,
186        base: &Mem<F, D>,
187        hasher: &impl merkle::hasher::Hasher<F, Digest = D>,
188        loc: Location<F>,
189        inactive_peaks: usize,
190    ) -> Result<Proof<F, D>, merkle::Error<F>> {
191        self.inner.proof(base, hasher, loc, inactive_peaks)
192    }
193
194    /// Inclusion proof for all elements in `range`.
195    pub fn range_proof(
196        &self,
197        base: &Mem<F, D>,
198        hasher: &impl merkle::hasher::Hasher<F, Digest = D>,
199        range: core::ops::Range<Location<F>>,
200        inactive_peaks: usize,
201    ) -> Result<Proof<F, D>, merkle::Error<F>> {
202        self.inner.range_proof(base, hasher, range, inactive_peaks)
203    }
204
205    /// The items added in this batch.
206    pub(crate) const fn items(&self) -> &Arc<Vec<Item>> {
207        &self.items
208    }
209
210    /// Create a new speculative batch of operations with this batch as its parent.
211    ///
212    /// The batch becomes invalid if any ancestor is dropped before being applied, or a sibling
213    /// fork has been applied.
214    pub fn new_batch<H: Hasher<Digest = D>>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, Item, S>
215    where
216        Item: Encode,
217    {
218        UnmerkleizedBatch {
219            inner: self.inner.new_batch(),
220            hasher: StandardHasher::new(self.bagging),
221            items: Vec::new(),
222            parent: Some(Arc::clone(self)),
223        }
224    }
225}
226
227impl<F: Family, D: Digest, Item: Send + Sync, S: Strategy> Readable
228    for MerkleizedBatch<F, D, Item, S>
229{
230    type Family = F;
231    type Digest = D;
232
233    fn size(&self) -> Position<F> {
234        self.inner.size()
235    }
236
237    fn get_node(&self, pos: Position<F>) -> Option<D> {
238        self.inner.get_node(pos)
239    }
240}
241
242/// An append-only data structure that maintains a sequential journal of items alongside a
243/// Merkle-family structure. The item at index i in the journal corresponds to the leaf at Location
244/// i in the Merkle structure. This structure enables efficient proofs that an item is included in
245/// the journal at a specific location.
246pub struct Journal<F, E, C, H, S>
247where
248    F: Family,
249    E: Context,
250    C: Contiguous<Item: EncodeShared>,
251    H: Hasher,
252    S: Strategy,
253{
254    /// Merkle structure where each leaf is an item digest.
255    /// Invariant: leaf i corresponds to item i in the journal.
256    pub(crate) merkle: Merkle<F, E, H::Digest, S>,
257
258    /// Journal of items.
259    /// Invariant: item i corresponds to leaf i in the Merkle structure.
260    pub(crate) journal: C,
261
262    pub(crate) hasher: StandardHasher<H>,
263}
264
265impl<F, E, C, H, S> core::fmt::Debug for Journal<F, E, C, H, S>
266where
267    F: Family,
268    E: Context,
269    C: Contiguous<Item: EncodeShared>,
270    H: Hasher,
271    S: Strategy,
272{
273    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
274        f.debug_struct("Journal")
275            .field("size", &self.size())
276            .finish_non_exhaustive()
277    }
278}
279
280impl<F, E, C, H, S> Journal<F, E, C, H, S>
281where
282    F: Family,
283    E: Context,
284    C: Contiguous<Item: EncodeShared>,
285    H: Hasher,
286    S: Strategy,
287{
288    /// Returns the Location of the next item appended to the journal.
289    pub fn size(&self) -> Location<F> {
290        Location::new(self.journal.bounds().end)
291    }
292
293    /// Compute the root of the Merkle structure using `inactive_peaks` and the bagging carried by
294    /// the journal's hasher.
295    pub fn root(&self, inactive_peaks: usize) -> Result<H::Digest, Error<F>> {
296        self.merkle
297            .root(&self.hasher, inactive_peaks)
298            .map_err(Into::into)
299    }
300
301    /// Convert authenticated-journal errors to the contiguous journal trait error type.
302    fn map_error(error: Error<F>) -> JournalError {
303        match error {
304            Error::Journal(inner) => inner,
305            Error::Merkle(inner) => JournalError::Merkle(anyhow::Error::from(inner)),
306        }
307    }
308
309    /// Return a reference to the merkleization strategy.
310    pub const fn strategy(&self) -> &S {
311        self.merkle.strategy()
312    }
313
314    /// Create a speculative batch atop this journal.
315    pub fn new_batch(&self) -> UnmerkleizedBatch<F, H, C::Item, S>
316    where
317        C::Item: Encode,
318    {
319        let root = self.merkle.to_batch();
320        UnmerkleizedBatch {
321            inner: root.new_batch(),
322            hasher: StandardHasher::new(self.hasher.root_bagging()),
323            items: Vec::new(),
324            parent: None,
325        }
326    }
327
328    /// Add `items` to `batch`, merkleize, and compute the post-apply root, all as one CPU-bound job
329    /// submitted through [`Strategy::spawn`].
330    ///
331    /// The job hashes against an immutable snapshot of the committed Merkle state, so a parallel
332    /// strategy can host the batch's dominant CPU phase on its own pool instead of occupying the
333    /// calling task. If the job's caller is cancelled, the job still runs to completion
334    /// against its snapshot and the result is discarded.
335    pub(crate) async fn merkleize(
336        &self,
337        batch: UnmerkleizedBatch<F, H, C::Item, S>,
338        items: Vec<C::Item>,
339        inactive_peaks: usize,
340    ) -> Result<(MerkleizedBatchArc<F, H, C::Item, S>, H::Digest), merkle::Error<F>>
341    where
342        C::Item: 'static,
343    {
344        let ancestors = batch.inner.retain_ancestors();
345        let mem = self.merkle.snapshot();
346        let hasher = self.hasher.clone();
347        let strategy = self.strategy().clone();
348        strategy
349            .spawn(items.len(), move |_| {
350                let merkleized = batch.add_many(items).merkleize(&mem);
351                let root = merkleized.root(&mem, &hasher, inactive_peaks)?;
352                drop(ancestors);
353                Ok((merkleized, root))
354            })
355            .await
356    }
357
358    /// Create an owned [`MerkleizedBatch`] representing the current committed state.
359    ///
360    /// The batch has no items (the committed items are on disk, not in memory).
361    /// This is the starting point for building owned batch chains.
362    pub(crate) fn to_merkleized_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, C::Item, S>> {
363        Arc::new(MerkleizedBatch {
364            inner: self.merkle.to_batch(),
365            bagging: self.hasher.root_bagging(),
366            items: Arc::new(Vec::new()),
367            parent: None,
368            ancestor_base_leaves: *self.size(),
369            ancestor_items: Vec::new(),
370        })
371    }
372}
373
374impl<F, E, C, H, S> Journal<F, E, C, H, S>
375where
376    F: Family,
377    E: Context,
378    C: Mutable<Item: EncodeShared>,
379    H: Hasher,
380    S: Strategy,
381{
382    /// Begin durably persisting the journal.
383    ///
384    /// Awaiting the returned [Handle] provides the same durability guarantee as [Self::commit].
385    /// Also tries to advance the recovery watermarks to bound startup recovery. Use
386    /// [Self::sync] to guarantee no recovery is needed.
387    pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error<F>> {
388        let (journal_handle, merkle_handle);
389        ((self.journal, journal_handle), (self.merkle, merkle_handle)) = try_join!(
390            self.journal.start_sync().map_err(Error::Journal),
391            self.merkle.start_sync().map_err(Error::Merkle)
392        )?;
393
394        let handle =
395            Handle::from_future(
396                async move { try_join!(journal_handle, merkle_handle).map(|_| ()) },
397            );
398        Ok((self, handle))
399    }
400
401    /// Durably persist the journal. This is faster than `sync()` but does not guarantee that the
402    /// Merkle structure is durably persisted, meaning recovery may be required on startup in the
403    /// event of a crash.
404    pub async fn commit(mut self) -> Result<Self, Error<F>> {
405        // Though not necessary for recovery, we flush the merkle structure (without syncing it) to
406        // limit memory bloat.
407        (self.journal, self.merkle) = try_join!(
408            self.journal.commit().map_err(Error::Journal),
409            self.merkle.flush().map_err(Error::Merkle)
410        )?;
411
412        Ok(self)
413    }
414}
415
416impl<F, E, C, H, S> Journal<F, E, C, H, S>
417where
418    F: Family,
419    E: Context,
420    C: Mutable<Item: EncodeShared>,
421    H: Hasher,
422    S: Strategy,
423{
424    /// Create a new [Journal] from the given components after aligning the Merkle structure with
425    /// the journal.
426    #[boxed]
427    pub async fn from_components(
428        merkle: Merkle<F, E, H::Digest, S>,
429        journal: C,
430        hasher: StandardHasher<H>,
431        apply_batch_size: u64,
432    ) -> Result<Self, Error<F>> {
433        let merkle = Self::align(merkle, &journal, &hasher, apply_batch_size).await?;
434
435        // Sync the Merkle structure to disk to avoid having to repeat any recovery that may have
436        // been performed on next startup.
437        let merkle = merkle.sync().await?;
438
439        Ok(Self {
440            merkle,
441            journal,
442            hasher,
443        })
444    }
445
446    /// Align the Merkle structure to be consistent with the journal. Any items in the structure
447    /// that are not in the journal are popped, and any items in the journal that are not in the
448    /// structure are added. Items are added in batches of size `apply_batch_size` to bound peak
449    /// memory use: each batch's items are buffered in memory so their leaves can be hashed
450    /// across the strategy.
451    async fn align(
452        mut merkle: Merkle<F, E, H::Digest, S>,
453        journal: &C,
454        hasher: &StandardHasher<H>,
455        apply_batch_size: u64,
456    ) -> Result<Merkle<F, E, H::Digest, S>, Error<F>> {
457        // Rewind Merkle structure elements that are ahead of the journal.
458        let journal_size = journal.bounds().end;
459        let mut merkle_leaves = merkle.leaves();
460        if merkle_leaves > journal_size {
461            let rewind_count = merkle_leaves - journal_size;
462            warn!(
463                journal_size,
464                ?rewind_count,
465                "rewinding Merkle structure to match journal"
466            );
467            merkle = merkle.rewind(*rewind_count as usize).await?;
468            merkle_leaves = Location::new(journal_size);
469        }
470
471        // If the Merkle structure is behind, replay journal items to catch up.
472        if merkle_leaves < journal_size {
473            let replay_count = journal_size - *merkle_leaves;
474            warn!(
475                ?journal_size,
476                replay_count, "Merkle structure lags behind journal, replaying journal to catch up"
477            );
478
479            while merkle_leaves < journal_size {
480                let count = apply_batch_size.min(journal_size - *merkle_leaves);
481                let mut items = Vec::with_capacity(count as usize);
482                for _ in 0..count {
483                    items.push(journal.read(*merkle_leaves).await?);
484                    merkle_leaves += 1;
485                }
486
487                let batch = merkle.new_batch().add_many(hasher, &items);
488                let batch = merkle.with_mem(|mem| batch.merkleize(mem, hasher));
489                merkle = merkle.apply_batch(&batch)?;
490            }
491            return Ok(merkle);
492        }
493
494        // At this point the Merkle structure and journal should be consistent.
495        assert_eq!(journal.bounds().end, *merkle.leaves());
496
497        Ok(merkle)
498    }
499
500    /// Append an item to the journal and update the Merkle structure.
501    pub async fn append(mut self, item: &C::Item) -> Result<(Self, Location<F>), Error<F>> {
502        let encoded_item = item.encode();
503
504        // Append item to the journal, then update the Merkle structure state.
505        let loc;
506        (self.journal, loc) = self.journal.append(item).await?;
507        let unmerkleized_batch = self.merkle.new_batch().add(&self.hasher, &encoded_item);
508        let batch = self
509            .merkle
510            .with_mem(|mem| unmerkleized_batch.merkleize(mem, &self.hasher));
511        self.merkle = self.merkle.apply_batch(&batch)?;
512
513        Ok((self, Location::new(loc)))
514    }
515
516    /// Apply a batch to the journal.
517    ///
518    /// A batch is valid if the journal has not been modified since the batch
519    /// chain was created, or if only ancestors of this batch have been applied.
520    /// Already-committed ancestors are skipped automatically.
521    /// Applying a batch from a different fork returns an error.
522    pub async fn apply_batch(
523        mut self,
524        batch: &MerkleizedBatch<F, H::Digest, C::Item, S>,
525    ) -> Result<Self, Error<F>> {
526        let merkle_size = self.merkle.size();
527        let base_size = batch.inner.base_size();
528
529        // Determine whether ancestors have already been committed.
530        // `base_size` is the merkle size when the batch chain was forked.
531        // If the merkle has advanced past the fork point, ancestors are
532        // already on disk; check that the current size is reachable from
533        // the batch chain before skipping them.
534        let skip_ancestors = if merkle_size == base_size {
535            false
536        } else if merkle_size > base_size && merkle_size < batch.inner.size() {
537            true
538        } else {
539            // Merkle is at an incompatible position (a sibling or unrelated
540            // fork was committed). Eagerly reject to avoid mutating the journal.
541            return Err(merkle::Error::StaleBatch {
542                expected: base_size,
543                actual: merkle_size,
544            }
545            .into());
546        };
547
548        // Apply ancestor item batches in root-to-tip order. Already-committed
549        // batches are skipped by tracking cumulative leaf count.
550        // Batches are collected into a single append_many call to acquire the
551        // journal's write lock once instead of per-batch.
552        let committed_leaves = self.journal.bounds().end;
553        if committed_leaves < batch.ancestor_base_leaves {
554            return Err(merkle::Error::AncestorDropped {
555                expected: batch.inner.size(),
556                actual: merkle_size,
557            }
558            .into());
559        }
560
561        let mut batch_leaf_end = batch.ancestor_base_leaves;
562        let mut batches: Vec<&[C::Item]> = Vec::with_capacity(batch.ancestor_items.len() + 1);
563        for ancestor in &batch.ancestor_items {
564            batch_leaf_end += ancestor.len() as u64;
565            if skip_ancestors && batch_leaf_end <= committed_leaves {
566                continue;
567            }
568            batches.push(ancestor);
569        }
570        if !batch.items.is_empty() {
571            batches.push(&batch.items);
572        }
573        if !batches.is_empty() {
574            (self.journal, _) = self.journal.append_many(Many::Nested(&batches)).await?;
575        }
576
577        self.merkle = self.merkle.apply_batch(&batch.inner)?;
578        assert_eq!(*self.merkle.leaves(), self.journal.bounds().end);
579        Ok(self)
580    }
581
582    /// Rewind the journal and Merkle structure.
583    #[boxed]
584    pub async fn rewind(mut self, size: u64) -> Result<Self, Error<F>> {
585        self.journal = self.journal.rewind(size).await?;
586
587        let leaves = *self.merkle.leaves();
588        if leaves > size {
589            self.merkle = self.merkle.rewind((leaves - size) as usize).await?;
590        }
591
592        Ok(self)
593    }
594
595    /// Prune both the Merkle structure and journal to the given location.
596    ///
597    /// # Returns
598    /// The new pruning boundary, which may be less than the requested `prune_loc`.
599    #[boxed]
600    pub async fn prune(self, prune_loc: Location<F>) -> Result<(Self, Location<F>), Error<F>> {
601        let (journal, boundary, _) = self.prune_inner(prune_loc).await?;
602        Ok((journal, boundary))
603    }
604
605    async fn prune_inner(
606        mut self,
607        prune_loc: Location<F>,
608    ) -> Result<(Self, Location<F>, bool), Error<F>> {
609        if self.merkle.size() == 0 {
610            // DB is empty, nothing to prune.
611            let boundary = Location::new(self.journal.bounds().start);
612            return Ok((self, boundary, false));
613        }
614
615        // Sync the Merkle structure before pruning the journal, otherwise its last element could
616        // end up behind the journal's first element after a crash, and there would be no way to
617        // replay the items between the structure's last element and the journal's first element.
618        // Commit the journal alongside: the prune target may be justified by a buffered append
619        // (e.g. a commit operation), and pruning does not guarantee buffered appends are durable.
620        (self.journal, self.merkle) = try_join!(
621            self.journal.commit().map_err(Error::Journal),
622            self.merkle.sync().map_err(Error::Merkle)
623        )?;
624
625        let journal_pruned;
626        (self.journal, journal_pruned) = self.journal.prune(*prune_loc).await?;
627        let bounds = self.journal.bounds();
628        let boundary = Location::new(bounds.start);
629        let merkle_boundary = self.merkle.bounds().start;
630
631        if boundary > merkle_boundary {
632            debug!(size = ?bounds.end, ?prune_loc, boundary = ?bounds.start, "pruned inactive ops");
633            self.merkle = self.merkle.prune(boundary).await?;
634        }
635
636        Ok((self, boundary, journal_pruned || boundary > merkle_boundary))
637    }
638}
639
640impl<F, E, C, H, S> Journal<F, E, C, H, S>
641where
642    F: Family,
643    E: Context,
644    C: Contiguous<Item: EncodeShared>,
645    H: Hasher,
646    S: Strategy,
647{
648    /// Generate a proof of inclusion for items starting at `start_loc`.
649    ///
650    /// Returns a proof and the items corresponding to the leaves in the range `start_loc..end_loc`,
651    /// where `end_loc` is the minimum of the current item count and `start_loc + max_ops`.
652    ///
653    /// # Errors
654    ///
655    /// - Returns [Error::Merkle] with [merkle::Error::LocationOverflow] if `start_loc` >
656    ///   [Family::MAX_LEAVES].
657    /// - Returns [Error::Merkle] with [merkle::Error::RangeOutOfBounds] if `start_loc` >= current
658    ///   item count.
659    /// - Returns [Error::Journal] with [crate::journal::Error::ItemPruned] if `start_loc` has been
660    ///   pruned.
661    pub async fn proof(
662        &self,
663        start_loc: Location<F>,
664        max_ops: NonZeroU64,
665        inactive_peaks: usize,
666    ) -> Result<(Proof<F, H::Digest>, Vec<C::Item>), Error<F>> {
667        self.historical_proof(self.size(), start_loc, max_ops, inactive_peaks)
668            .await
669    }
670
671    /// Inclusion proof for the items `batch` appends, anchored at the batch's speculative tip.
672    ///
673    /// Nodes below the batch chain are read from this journal's
674    /// [Merkle store][crate::merkle::mem::Mem], which retains them at least until
675    /// the batch's changes are flushed.
676    pub fn speculative_proof(
677        &self,
678        batch: &MerkleizedBatch<F, H::Digest, C::Item, S>,
679        inactive_peaks: usize,
680    ) -> Result<Proof<F, H::Digest>, Error<F>> {
681        let end = batch.size();
682        let start = Location::new(end - batch.items().len() as u64);
683        self.merkle
684            .with_mem(|mem| {
685                batch.range_proof(mem, &self.hasher, start..Location::new(end), inactive_peaks)
686            })
687            .map_err(Error::Merkle)
688    }
689
690    /// Merkle frontier at the first item `batch` appends ([`Family::nodes_to_pin`]).
691    ///
692    /// Nodes below the batch chain are read from this journal's
693    /// [Merkle store][crate::merkle::mem::Mem], which retains them at least until
694    /// the batch's changes are flushed.
695    pub fn speculative_pinned_nodes(
696        &self,
697        batch: &MerkleizedBatch<F, H::Digest, C::Item, S>,
698    ) -> Result<Vec<H::Digest>, Error<F>> {
699        let start = Location::new(batch.size() - batch.items().len() as u64);
700        self.merkle
701            .with_mem(|mem| {
702                F::nodes_to_pin(start)
703                    .map(|pos| {
704                        batch
705                            .get_node(pos)
706                            .or_else(|| mem.get_node(pos))
707                            .ok_or(merkle::Error::ElementPruned(pos))
708                    })
709                    .collect::<Result<Vec<_>, _>>()
710            })
711            .map_err(Error::Merkle)
712    }
713
714    /// Generate a historical proof with respect to the state of the Merkle structure when it had
715    /// `historical_leaves` leaves.
716    ///
717    /// Returns a proof and the items corresponding to the leaves in the range `start_loc..end_loc`,
718    /// where `end_loc` is the minimum of `historical_leaves` and `start_loc + max_ops`.
719    ///
720    /// # Errors
721    ///
722    /// - Returns [Error::Merkle] with [merkle::Error::RangeOutOfBounds] if `start_loc` >=
723    ///   `historical_leaves` or `historical_leaves` > number of items in the journal.
724    /// - Returns [Error::Journal] with [crate::journal::Error::ItemPruned] if `start_loc` has been
725    ///   pruned.
726    pub async fn historical_proof(
727        &self,
728        historical_leaves: Location<F>,
729        start_loc: Location<F>,
730        max_ops: NonZeroU64,
731        inactive_peaks: usize,
732    ) -> Result<(Proof<F, H::Digest>, Vec<C::Item>), Error<F>> {
733        let bounds = self.journal.bounds();
734
735        if *historical_leaves > bounds.end {
736            return Err(merkle::Error::RangeOutOfBounds(Location::new(bounds.end)).into());
737        }
738        if start_loc >= historical_leaves {
739            return Err(merkle::Error::RangeOutOfBounds(start_loc).into());
740        }
741
742        let end_loc = std::cmp::min(historical_leaves, start_loc.saturating_add(max_ops.get()));
743
744        let hasher = self.hasher.clone();
745        let proof = self
746            .merkle
747            .historical_range_proof(
748                &hasher,
749                historical_leaves,
750                start_loc..end_loc,
751                inactive_peaks,
752            )
753            .await?;
754
755        let positions: Vec<u64> = (*start_loc..*end_loc).collect();
756        let ops = self.journal.read_many(&positions).await?;
757
758        Ok((proof, ops))
759    }
760}
761
762impl<F, E, C, H, S> Journal<F, E, C, H, S>
763where
764    F: Family,
765    E: Context,
766    C: Mutable<Item: EncodeShared>,
767    H: Hasher,
768    S: Strategy,
769{
770    /// Destroy the authenticated journal, removing all data from disk.
771    #[boxed]
772    pub async fn destroy(self) -> Result<(), Error<F>> {
773        // `try_join!` contains an await boundary, so destructure first to avoid
774        // stack growth from retaining the entire `self` in the future.
775        let Self {
776            journal, merkle, ..
777        } = self;
778        try_join!(
779            journal.destroy().map_err(Error::Journal),
780            merkle.destroy().map_err(Error::Merkle),
781        )?;
782
783        Ok(())
784    }
785
786    /// Durably persist the journal, ensuring no recovery is required on startup.
787    pub async fn sync(mut self) -> Result<Self, Error<F>> {
788        (self.journal, self.merkle) = try_join!(
789            self.journal.sync().map_err(Error::Journal),
790            self.merkle.sync().map_err(Error::Merkle)
791        )?;
792
793        Ok(self)
794    }
795}
796
797/// The number of items to apply to the Merkle structure in a single batch.
798const APPLY_BATCH_SIZE: u64 = 1 << 16;
799
800impl<F, E, C, H, S> Journal<F, E, C, H, S>
801where
802    F: Family,
803    E: Context,
804    C: Backing<E, Item: EncodeShared>,
805    H: Hasher,
806    S: Strategy,
807{
808    /// Create a new authenticated [Journal].
809    ///
810    /// The backing journal will be rewound to the last item matching `rewind_predicate`,
811    /// and the merkle structure will be aligned to match.
812    #[boxed]
813    pub async fn new(
814        context: E,
815        merkle_cfg: merkle::full::Config<S>,
816        journal_cfg: C::Config,
817        rewind_predicate: fn(&C::Item) -> bool,
818        bagging: merkle::Bagging,
819    ) -> Result<Self, Error<F>> {
820        let journal = C::init(context.child("journal"), journal_cfg).await?;
821        let (journal, _) = journal.rewind_to(rewind_predicate).await?;
822
823        let hasher = StandardHasher::<H>::new(bagging);
824        let merkle = Merkle::init(context.child("merkle"), &hasher, merkle_cfg).await?;
825        let merkle = Self::align(merkle, &journal, &hasher, APPLY_BATCH_SIZE).await?;
826
827        let journal = journal.sync().await?;
828        let merkle = merkle.sync().await?;
829
830        Ok(Self {
831            merkle,
832            journal,
833            hasher,
834        })
835    }
836}
837
838impl<F, E, C, H, S> Journal<F, E, C, H, S>
839where
840    F: Family,
841    E: Context,
842    C: Contiguous<Item: EncodeShared>,
843    H: Hasher,
844    S: Strategy,
845{
846    /// Like [`Contiguous::read_many`], but returns the items partitioned into the shards the
847    /// probe ran with. Concatenating the shards yields the items in `positions` order.
848    ///
849    /// Large batches shard the page-cache probe across the strategy pool. Each shard
850    /// assembles its own hits while they are still cache-hot on the probing worker, so bulk
851    /// callers that can consume partitioned results (e.g. the floor raise, which classifies
852    /// candidates in chunks) skip the serial reassembly a flat result would require.
853    pub(crate) async fn read_many_sharded(
854        &self,
855        positions: &[u64],
856    ) -> Result<Vec<Vec<C::Item>>, JournalError> {
857        // An empty batch cannot shard: the parallel arm's chunk math needs a non-zero chunk
858        // size, and the policy may explore that arm at any batch size.
859        if positions.is_empty() {
860            return Ok(Vec::new());
861        }
862
863        // Probe page-cache hits synchronously and complete the misses with one batched read.
864        // The strategy policy decides per batch size whether the probe runs on the calling
865        // thread or sharded across the pool (one scratch buffer per shard and one cache-lock
866        // acquisition per blob a shard touches). The sortedness assert keeps contract
867        // violations deterministic: past it, a non-increasing batch would only trip per-shard
868        // validation when an inversion lands inside a single shard.
869        assert!(
870            positions.is_sorted_by(|a, b| a < b),
871            "positions must be strictly increasing"
872        );
873        let strategy = self.strategy();
874        let journal = &self.journal;
875
876        // Each shard yields its hits densely plus the shard-local indices it declined.
877        let probe = |positions: &[u64]| -> (Vec<C::Item>, Vec<usize>) {
878            let probed = journal.try_read_many_sync(positions);
879            let mut hits = Vec::with_capacity(probed.len());
880            let mut missed = Vec::new();
881            for (idx, item) in probed.into_iter().enumerate() {
882                match item {
883                    Some(item) => hits.push(item),
884                    None => missed.push(idx),
885                }
886            }
887            (hits, missed)
888        };
889        let shards: Vec<(Vec<C::Item>, Vec<usize>)> = strategy.run(
890            positions.len(),
891            || vec![probe(positions)],
892            || {
893                let manual = strategy.manual();
894                let shard_len = positions.len().div_ceil(manual.parallelism());
895                manual.map_collect_vec(positions.chunks(shard_len).collect::<Vec<_>>(), &probe)
896            },
897        );
898
899        // The declined positions are a strictly increasing subsequence of `positions`, so one
900        // batched read serves them all. Each shard covers the slice of `positions` starting
901        // at the previous shards' total item count, whatever geometry the probe ran with.
902        let mut misses: Vec<u64> = Vec::new();
903        let mut offset = 0;
904        for (hits, missed) in &shards {
905            misses.extend(missed.iter().map(|idx| positions[offset + idx]));
906            offset += hits.len() + missed.len();
907        }
908        if misses.is_empty() {
909            return Ok(shards.into_iter().map(|(hits, _)| hits).collect());
910        }
911        let mut fetched = journal.read_many(&misses).await?.into_iter();
912
913        // Weave the fetched items back into each shard that declined positions.
914        let mut result = Vec::with_capacity(shards.len());
915        for (hits, missed) in shards {
916            if missed.is_empty() {
917                result.push(hits);
918                continue;
919            }
920            let total = hits.len() + missed.len();
921            let mut woven = Vec::with_capacity(total);
922            let mut hits = hits.into_iter();
923            let mut missed = missed.into_iter().peekable();
924            for idx in 0..total {
925                if missed.next_if_eq(&idx).is_some() {
926                    woven.push(fetched.next().expect("one fetched item per miss"));
927                } else {
928                    woven.push(hits.next().expect("one probed item per hit"));
929                }
930            }
931            result.push(woven);
932        }
933        Ok(result)
934    }
935}
936
937impl<F, E, C, H, S> Contiguous for Journal<F, E, C, H, S>
938where
939    F: Family,
940    E: Context,
941    C: Contiguous<Item: EncodeShared>,
942    H: Hasher,
943    S: Strategy,
944{
945    type Item = C::Item;
946
947    fn bounds(&self) -> Range<u64> {
948        self.journal.bounds()
949    }
950
951    async fn read(&self, position: u64) -> Result<C::Item, JournalError> {
952        self.journal.read(position).await
953    }
954
955    async fn read_many(&self, positions: &[u64]) -> Result<Vec<C::Item>, JournalError> {
956        let mut shards = self.read_many_sharded(positions).await?;
957        if shards.len() == 1 {
958            return Ok(shards.pop().expect("length checked"));
959        }
960        let mut items = Vec::with_capacity(positions.len());
961        for shard in shards {
962            items.extend(shard);
963        }
964        Ok(items)
965    }
966
967    fn try_read_sync(&self, position: u64) -> Option<C::Item> {
968        self.journal.try_read_sync(position)
969    }
970
971    fn try_read_many_sync(&self, positions: &[u64]) -> Vec<Option<C::Item>> {
972        self.journal.try_read_many_sync(positions)
973    }
974
975    async fn replay(
976        &self,
977        start_pos: u64,
978        buffer: NonZeroUsize,
979        read_options: ReadOptions,
980    ) -> Result<impl Stream<Item = Result<(u64, C::Item), JournalError>> + Send, JournalError> {
981        self.journal.replay(start_pos, buffer, read_options).await
982    }
983}
984
985impl<F, E, C, H, S> Mutable for Journal<F, E, C, H, S>
986where
987    F: Family,
988    E: Context,
989    C: Mutable<Item: EncodeShared>,
990    H: Hasher,
991    S: Strategy,
992{
993    async fn append(self, item: &Self::Item) -> Result<(Self, u64), JournalError> {
994        let (journal, loc) = Self::append(self, item).await.map_err(Self::map_error)?;
995
996        Ok((journal, *loc))
997    }
998
999    async fn append_many(
1000        mut self,
1001        items: Many<'_, Self::Item>,
1002    ) -> Result<(Self, u64), JournalError> {
1003        // The per-item loop below never reaches the backing journal's shared empty check, so the
1004        // trait's EmptyAppend contract must be enforced here.
1005        if items.is_empty() {
1006            return Err(JournalError::EmptyAppend);
1007        }
1008
1009        // Every append must also update the Merkle structure, so items append one at a time.
1010        // Batched appends of already-merkleized items go through `apply_batch`, which batches
1011        // the backing journal writes instead.
1012        let mut last_pos = self.journal.bounds().end;
1013        match items {
1014            Many::Flat(items) => {
1015                for item in items {
1016                    let (journal, loc) = Self::append(self, item).await.map_err(Self::map_error)?;
1017                    self = journal;
1018                    last_pos = *loc;
1019                }
1020            }
1021            Many::Nested(nested_items) => {
1022                for items in nested_items {
1023                    for item in *items {
1024                        let (journal, loc) =
1025                            Self::append(self, item).await.map_err(Self::map_error)?;
1026                        self = journal;
1027                        last_pos = *loc;
1028                    }
1029                }
1030            }
1031        }
1032        Ok((self, last_pos))
1033    }
1034
1035    async fn prune(self, min_position: u64) -> Result<(Self, bool), JournalError> {
1036        let prune_to = {
1037            let bounds = self.journal.bounds();
1038            min_position.min(bounds.end)
1039        };
1040
1041        let (journal, _, pruned) = self
1042            .prune_inner(Location::new(prune_to))
1043            .await
1044            .map_err(Self::map_error)?;
1045        Ok((journal, pruned))
1046    }
1047
1048    async fn rewind(self, size: u64) -> Result<Self, JournalError> {
1049        Self::rewind(self, size).await.map_err(Self::map_error)
1050    }
1051
1052    async fn start_sync(self) -> Result<(Self, Handle<()>), JournalError> {
1053        Self::start_sync(self).await.map_err(Self::map_error)
1054    }
1055
1056    async fn commit(self) -> Result<Self, JournalError> {
1057        Self::commit(self).await.map_err(Self::map_error)
1058    }
1059
1060    async fn sync(self) -> Result<Self, JournalError> {
1061        Self::sync(self).await.map_err(Self::map_error)
1062    }
1063
1064    async fn destroy(self) -> Result<(), JournalError> {
1065        Self::destroy(self).await.map_err(Self::map_error)
1066    }
1067}
1068
1069/// A [Mutable] journal that can back an authenticated [Journal].
1070pub trait Backing<E: Context>: Mutable {
1071    /// The configuration needed to initialize this journal.
1072    type Config: Clone + Send;
1073
1074    /// Initialize the journal from its configuration.
1075    fn init(
1076        context: E,
1077        cfg: Self::Config,
1078    ) -> impl core::future::Future<Output = Result<Self, JournalError>> + Send
1079    where
1080        Self: Sized;
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086    use crate::{
1087        journal::contiguous::fixed::{Config as JConfig, Journal as ContiguousJournal},
1088        merkle::{
1089            Bagging::{BackwardFold, ForwardFold},
1090            full::{Config as MerkleConfig, Merkle},
1091            mmb, mmr,
1092        },
1093        qmdb::{
1094            any::{
1095                operation::{Unordered as Op, update::Unordered as Update},
1096                value::FixedEncoding,
1097            },
1098            operation::Committable,
1099        },
1100        utils::detached::{DropMonitor, block_strategy},
1101    };
1102    use commonware_codec::Encode;
1103    use commonware_cryptography::{Sha256, sha256::Digest};
1104    use commonware_macros::test_traced;
1105    use commonware_parallel::{Manual, Rayon, Sequential};
1106    use commonware_runtime::{
1107        BufferPooler, Runner as _, Spawner as _, Strategizer as _, Supervisor as _,
1108        buffer::paged::CacheRef,
1109        deterministic::{self, Context},
1110        mocks::{
1111            DelayedSyncContext, PendingSyncs, RecordingContext, drive_pending_syncs,
1112            fail_pending_syncs, next_pending_sync,
1113        },
1114        reschedule,
1115    };
1116    use commonware_utils::{NZU16, NZU64, NZUsize};
1117    use futures::StreamExt as _;
1118    use std::{
1119        future::Future,
1120        num::{NonZeroU16, NonZeroUsize},
1121        time::Duration,
1122    };
1123
1124    const PAGE_SIZE: NonZeroU16 = NZU16!(101);
1125    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(11);
1126
1127    /// Generic operation type for testing, parameterized by Merkle family.
1128    type TestOp<F> = Op<F, Digest, FixedEncoding<Digest>>;
1129
1130    /// Generic authenticated journal type for testing, parameterized by Merkle family.
1131    type TestJournal<F> = Journal<
1132        F,
1133        deterministic::Context,
1134        ContiguousJournal<deterministic::Context, TestOp<F>>,
1135        Sha256,
1136        Sequential,
1137    >;
1138
1139    type RecordingTestJournal<F> = Journal<
1140        F,
1141        RecordingContext<deterministic::Context>,
1142        ContiguousJournal<RecordingContext<deterministic::Context>, TestOp<F>>,
1143        Sha256,
1144        Sequential,
1145    >;
1146
1147    fn journal_root<F: Family>(journal: &TestJournal<F>) -> Digest {
1148        journal.root(0).unwrap()
1149    }
1150
1151    fn batch_root<F: Family>(
1152        journal: &TestJournal<F>,
1153        batch: &MerkleizedBatch<F, Digest, TestOp<F>, Sequential>,
1154    ) -> Digest {
1155        journal
1156            .merkle
1157            .with_mem(|mem| batch.root(mem, &journal.hasher, 0))
1158            .unwrap()
1159    }
1160
1161    fn merkleize_with<F: Family + PartialEq>(
1162        batch: UnmerkleizedBatch<F, Sha256, TestOp<F>, Sequential>,
1163        base: &Mem<F, Digest>,
1164        items: Vec<TestOp<F>>,
1165    ) -> MerkleizedBatchArc<F, Sha256, TestOp<F>, Sequential> {
1166        batch.add_many(items).merkleize(base)
1167    }
1168
1169    /// Create Merkle configuration for tests with the given strategy.
1170    fn merkle_config_with<S: Strategy>(
1171        suffix: &str,
1172        pooler: &impl BufferPooler,
1173        strategy: S,
1174    ) -> MerkleConfig<S> {
1175        MerkleConfig {
1176            journal_partition: format!("mmr-journal-{suffix}"),
1177            metadata_partition: format!("mmr-metadata-{suffix}"),
1178            items_per_blob: NZU64!(11),
1179            write_buffer: NZUsize!(1024),
1180            replay_buffer: NZUsize!(1024),
1181            strategy,
1182            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1183        }
1184    }
1185
1186    /// Create Merkle configuration for tests.
1187    fn merkle_config(suffix: &str, pooler: &impl BufferPooler) -> MerkleConfig<Sequential> {
1188        merkle_config_with(suffix, pooler, Sequential)
1189    }
1190
1191    /// Create journal configuration for tests.
1192    fn journal_config(suffix: &str, pooler: &impl BufferPooler) -> JConfig {
1193        JConfig {
1194            partition: format!("journal-{suffix}"),
1195            items_per_blob: NZU64!(7),
1196            write_buffer: NZUsize!(1024),
1197            replay_buffer: NZUsize!(1024),
1198            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
1199        }
1200    }
1201
1202    /// Create a new empty authenticated journal.
1203    async fn create_empty_journal<F: Family + PartialEq>(
1204        context: Context,
1205        suffix: &str,
1206    ) -> TestJournal<F> {
1207        let merkle_cfg = merkle_config(suffix, &context);
1208        let journal_cfg = journal_config(suffix, &context);
1209        TestJournal::<F>::new(
1210            context,
1211            merkle_cfg,
1212            journal_cfg,
1213            |op: &TestOp<F>| op.is_commit(),
1214            ForwardFold,
1215        )
1216        .await
1217        .unwrap()
1218    }
1219
1220    #[test]
1221    fn test_batches_inherit_journal_bagging() {
1222        deterministic::Runner::default().start(|context| async move {
1223            let merkle_cfg = merkle_config("batch-bagging", &context);
1224            let journal_cfg = journal_config("batch-bagging", &context);
1225            let journal = TestJournal::<mmr::Family>::new(
1226                context,
1227                merkle_cfg,
1228                journal_cfg,
1229                |op: &TestOp<mmr::Family>| op.is_commit(),
1230                BackwardFold,
1231            )
1232            .await
1233            .unwrap();
1234
1235            let batch = journal.new_batch();
1236            assert_eq!(batch.hasher.root_bagging(), BackwardFold);
1237
1238            let merkleized = journal.merkle.with_mem(|mem| batch.merkleize(mem));
1239            let child: UnmerkleizedBatch<mmr::Family, Sha256, TestOp<mmr::Family>, Sequential> =
1240                merkleized.new_batch();
1241            assert_eq!(child.hasher.root_bagging(), BackwardFold);
1242        });
1243    }
1244
1245    /// Large batched reads shard across the strategy pool and match per-position reads.
1246    #[test]
1247    fn test_read_many_shards_across_strategy_pool() {
1248        deterministic::Runner::default().start(|context| async move {
1249            // A parallelism > 1 strategy with more positions than the shard threshold
1250            // exercises the sharded sync path. The tiny test page cache pushes most
1251            // positions through the batched miss fallback while the write buffer serves
1252            // the tail synchronously.
1253            let strategy = context.strategy(NZUsize!(2));
1254            let merkle_cfg = merkle_config_with("shard", &context, strategy);
1255            let journal_cfg = journal_config("shard", &context);
1256            type RayonJournal = Journal<
1257                mmr::Family,
1258                Context,
1259                ContiguousJournal<Context, TestOp<mmr::Family>>,
1260                Sha256,
1261                Rayon,
1262            >;
1263            let mut journal = RayonJournal::new(
1264                context,
1265                merkle_cfg,
1266                journal_cfg,
1267                |op: &TestOp<mmr::Family>| op.is_commit(),
1268                ForwardFold,
1269            )
1270            .await
1271            .unwrap();
1272
1273            let count = 4200u64;
1274            for i in 0..count {
1275                let op = create_operation::<mmr::Family>((i % 251) as u8);
1276                (journal, _) = journal.append(&op).await.unwrap();
1277            }
1278            let journal = journal.sync().await.unwrap();
1279
1280            let positions: Vec<u64> = (0..count).collect();
1281            let batch = Contiguous::read_many(&journal, &positions).await.unwrap();
1282            assert_eq!(batch.len(), positions.len());
1283            for &pos in &positions {
1284                let single = Contiguous::read(&journal, pos).await.unwrap();
1285                assert_eq!(batch[pos as usize], single);
1286            }
1287
1288            // An empty batch is a no-op, even with a multi-threaded strategy.
1289            assert!(
1290                Contiguous::read_many(&journal, &[])
1291                    .await
1292                    .unwrap()
1293                    .is_empty()
1294            );
1295        });
1296    }
1297
1298    /// A non-increasing batch panics deterministically, even when fully cached.
1299    #[test]
1300    #[should_panic(expected = "positions must be strictly increasing")]
1301    fn test_read_many_rejects_unsorted_positions() {
1302        deterministic::Runner::default().start(|context| async move {
1303            let mut journal = create_empty_journal::<mmr::Family>(context, "unsorted").await;
1304            for i in 0..2u8 {
1305                let op = create_operation::<mmr::Family>(i);
1306                (journal, _) = journal.append(&op).await.unwrap();
1307            }
1308            let journal = journal.sync().await.unwrap();
1309
1310            let _ = Contiguous::read_many(&journal, &[1, 0]).await;
1311        });
1312    }
1313
1314    /// Create a test operation with predictable values based on index.
1315    fn create_operation<F: Family + PartialEq>(index: u8) -> TestOp<F> {
1316        TestOp::<F>::Update(Update(
1317            Sha256::fill(index),
1318            Sha256::fill(index.wrapping_add(1)),
1319        ))
1320    }
1321
1322    /// Create an authenticated journal with N committed operations.
1323    ///
1324    /// Operations are added and then synced to ensure they are committed.
1325    async fn create_journal_with_ops<F: Family + PartialEq>(
1326        context: Context,
1327        suffix: &str,
1328        count: usize,
1329    ) -> TestJournal<F> {
1330        let mut journal = create_empty_journal::<F>(context, suffix).await;
1331
1332        for i in 0..count {
1333            let op = create_operation::<F>(i as u8);
1334            let loc;
1335            (journal, loc) = journal.append(&op).await.unwrap();
1336            assert_eq!(loc, Location::<F>::new(i as u64));
1337        }
1338
1339        journal = journal.sync().await.unwrap();
1340        journal
1341    }
1342
1343    /// Create separate Merkle and journal components for testing alignment.
1344    ///
1345    /// These components are created independently and can be manipulated separately to test
1346    /// scenarios where the Merkle structure and journal are out of sync (e.g., one ahead of the
1347    /// other).
1348    async fn create_components<F: Family + PartialEq>(
1349        context: Context,
1350        suffix: &str,
1351    ) -> (
1352        Merkle<F, deterministic::Context, Digest, Sequential>,
1353        ContiguousJournal<deterministic::Context, TestOp<F>>,
1354        StandardHasher<Sha256>,
1355    ) {
1356        let hasher = StandardHasher::new(ForwardFold);
1357        let merkle = Merkle::<F, _, Digest, Sequential>::init(
1358            context.child("mmr"),
1359            &hasher,
1360            merkle_config(suffix, &context),
1361        )
1362        .await
1363        .unwrap();
1364        let journal =
1365            ContiguousJournal::init(context.child("journal"), journal_config(suffix, &context))
1366                .await
1367                .unwrap();
1368        (merkle, journal, hasher)
1369    }
1370
1371    /// Verify that a proof correctly proves the given operations are included in the Merkle
1372    /// structure.
1373    fn verify_proof<F: Family + PartialEq>(
1374        proof: &Proof<F, <Sha256 as commonware_cryptography::Hasher>::Digest>,
1375        operations: &[TestOp<F>],
1376        start_loc: Location<F>,
1377        root: &<Sha256 as commonware_cryptography::Hasher>::Digest,
1378        hasher: &StandardHasher<Sha256>,
1379    ) -> bool {
1380        let encoded_ops: Vec<_> = operations.iter().map(|op| op.encode()).collect();
1381        proof.verify_range_inclusion(hasher, &encoded_ops, start_loc, root)
1382    }
1383
1384    /// Verify that new() creates an empty authenticated journal.
1385    async fn test_new_creates_empty_journal_inner<F: Family + PartialEq>(context: Context) {
1386        let journal = create_empty_journal::<F>(context, "new-empty").await;
1387
1388        let bounds = journal.bounds();
1389        assert_eq!(bounds.end, 0);
1390        assert_eq!(bounds.start, 0);
1391        assert!(bounds.is_empty());
1392    }
1393
1394    #[test_traced("INFO")]
1395    fn test_new_creates_empty_journal_mmr() {
1396        let executor = deterministic::Runner::default();
1397        executor.start(test_new_creates_empty_journal_inner::<mmr::Family>);
1398    }
1399
1400    #[test_traced("INFO")]
1401    fn test_new_creates_empty_journal_mmb() {
1402        let executor = deterministic::Runner::default();
1403        executor.start(test_new_creates_empty_journal_inner::<mmb::Family>);
1404    }
1405
1406    /// Verify that align() correctly handles empty Merkle and journal components.
1407    async fn test_align_with_empty_mmr_and_journal_inner<F: Family + PartialEq>(context: Context) {
1408        let (merkle, journal, hasher) = create_components::<F>(context, "align-empty").await;
1409
1410        let merkle = TestJournal::<F>::align(merkle, &journal, &hasher, APPLY_BATCH_SIZE)
1411            .await
1412            .unwrap();
1413
1414        assert_eq!(merkle.leaves(), Location::<F>::new(0));
1415        assert_eq!(journal.size(), 0);
1416    }
1417
1418    #[test_traced("INFO")]
1419    fn test_align_with_empty_mmr_and_journal_mmr() {
1420        let executor = deterministic::Runner::default();
1421        executor.start(test_align_with_empty_mmr_and_journal_inner::<mmr::Family>);
1422    }
1423
1424    #[test_traced("INFO")]
1425    fn test_align_with_empty_mmr_and_journal_mmb() {
1426        let executor = deterministic::Runner::default();
1427        executor.start(test_align_with_empty_mmr_and_journal_inner::<mmb::Family>);
1428    }
1429
1430    /// Verify that align() pops Merkle elements when Merkle is ahead of the journal.
1431    async fn test_align_when_mmr_ahead_inner<F: Family + PartialEq>(context: Context) {
1432        let (mut merkle, mut journal, hasher) = create_components::<F>(context, "mmr-ahead").await;
1433
1434        // Add 20 operations to both Merkle and journal
1435        {
1436            let batch = {
1437                let mut batch = merkle.new_batch();
1438                for i in 0..20 {
1439                    let op = create_operation::<F>(i as u8);
1440                    let encoded = op.encode();
1441                    batch = batch.add(&hasher, &encoded);
1442                    (journal, _) = journal.append(&op).await.unwrap();
1443                }
1444                batch
1445            };
1446            let batch = merkle.with_mem(|mem| batch.merkleize(mem, &hasher));
1447            merkle = merkle.apply_batch(&batch).unwrap();
1448        }
1449
1450        // Add commit operation to journal only (making journal ahead)
1451        let commit_op = TestOp::<F>::CommitFloor(None, Location::<F>::new(0));
1452        let (journal, _) = journal.append(&commit_op).await.unwrap();
1453        let journal = journal.sync().await.unwrap();
1454
1455        // Merkle has 20 leaves, journal has 21 operations (20 ops + 1 commit)
1456        let merkle = TestJournal::<F>::align(merkle, &journal, &hasher, APPLY_BATCH_SIZE)
1457            .await
1458            .unwrap();
1459
1460        // Merkle should have been aligned to match journal
1461        assert_eq!(merkle.leaves(), Location::<F>::new(21));
1462        assert_eq!(journal.size(), 21);
1463    }
1464
1465    #[test_traced("WARN")]
1466    fn test_align_when_mmr_ahead_mmr() {
1467        let executor = deterministic::Runner::default();
1468        executor.start(test_align_when_mmr_ahead_inner::<mmr::Family>);
1469    }
1470
1471    #[test_traced("WARN")]
1472    fn test_align_when_mmr_ahead_mmb() {
1473        let executor = deterministic::Runner::default();
1474        executor.start(test_align_when_mmr_ahead_inner::<mmb::Family>);
1475    }
1476
1477    /// Verify that align() replays journal operations when journal is ahead of Merkle.
1478    async fn test_align_when_journal_ahead_inner<F: Family + PartialEq>(context: Context) {
1479        let (merkle, mut journal, hasher) = create_components::<F>(context, "journal-ahead").await;
1480
1481        // Add 20 operations to journal only
1482        for i in 0..20 {
1483            let op = create_operation::<F>(i as u8);
1484            (journal, _) = journal.append(&op).await.unwrap();
1485        }
1486
1487        // Add commit
1488        let commit_op = TestOp::<F>::CommitFloor(None, Location::<F>::new(0));
1489        let (journal, _) = journal.append(&commit_op).await.unwrap();
1490        let journal = journal.sync().await.unwrap();
1491
1492        // Journal has 21 operations, Merkle has 0 leaves
1493        let merkle = TestJournal::<F>::align(merkle, &journal, &hasher, APPLY_BATCH_SIZE)
1494            .await
1495            .unwrap();
1496
1497        // Merkle should have been replayed to match journal
1498        assert_eq!(merkle.leaves(), Location::<F>::new(21));
1499        assert_eq!(journal.size(), 21);
1500    }
1501
1502    #[test_traced("WARN")]
1503    fn test_align_when_journal_ahead_mmr() {
1504        let executor = deterministic::Runner::default();
1505        executor.start(test_align_when_journal_ahead_inner::<mmr::Family>);
1506    }
1507
1508    #[test_traced("WARN")]
1509    fn test_align_when_journal_ahead_mmb() {
1510        let executor = deterministic::Runner::default();
1511        executor.start(test_align_when_journal_ahead_inner::<mmb::Family>);
1512    }
1513
1514    /// Verify that align()'s parallel replay produces the same Merkle state as the serial path.
1515    async fn test_align_replay_parallel_matches_serial_inner<F: Family + PartialEq>(
1516        context: Context,
1517    ) {
1518        type ParallelJournal<F> = Journal<
1519            F,
1520            deterministic::Context,
1521            ContiguousJournal<deterministic::Context, TestOp<F>>,
1522            Sha256,
1523            Manual<Rayon>,
1524        >;
1525
1526        // Build a journal that is ahead of both Merkle structures.
1527        let mut journal = ContiguousJournal::init(
1528            context.child("journal"),
1529            journal_config("replay-strategies", &context),
1530        )
1531        .await
1532        .unwrap();
1533        for i in 0..20 {
1534            (journal, _) = journal
1535                .append(&create_operation::<F>(i as u8))
1536                .await
1537                .unwrap();
1538        }
1539        let commit_op = TestOp::<F>::CommitFloor(None, Location::<F>::new(0));
1540        let (journal, _) = journal.append(&commit_op).await.unwrap();
1541        let journal = journal.sync().await.unwrap();
1542
1543        // Replay with a batch size that forces multiple batches on each side. `Sequential`
1544        // hashes each batch serially, and a `Manual`-wrapped strategy runs the batch hashing
1545        // across its pool without any adaptive policy, so the two replays deterministically
1546        // exercise both the serial and parallel hashing paths.
1547        let hasher = StandardHasher::<Sha256>::new(ForwardFold);
1548        let serial = Merkle::<F, _, Digest, Sequential>::init(
1549            context.child("mmr_serial"),
1550            &hasher,
1551            merkle_config("replay-serial", &context),
1552        )
1553        .await
1554        .unwrap();
1555        let serial = TestJournal::<F>::align(serial, &journal, &hasher, 7)
1556            .await
1557            .unwrap();
1558
1559        let parallel = Merkle::<F, _, Digest, Manual<Rayon>>::init(
1560            context.child("mmr_parallel"),
1561            &hasher,
1562            merkle_config_with(
1563                "replay-parallel",
1564                &context,
1565                Rayon::new(NZUsize!(2)).unwrap().manual(),
1566            ),
1567        )
1568        .await
1569        .unwrap();
1570        let parallel = ParallelJournal::<F>::align(parallel, &journal, &hasher, 7)
1571            .await
1572            .unwrap();
1573
1574        assert_eq!(serial.leaves(), Location::<F>::new(21));
1575        assert_eq!(parallel.leaves(), Location::<F>::new(21));
1576        assert_eq!(
1577            serial.root(&hasher, 0).unwrap(),
1578            parallel.root(&hasher, 0).unwrap()
1579        );
1580    }
1581
1582    #[test_traced("WARN")]
1583    fn test_align_replay_parallel_matches_serial_mmr() {
1584        let executor = deterministic::Runner::default();
1585        executor.start(test_align_replay_parallel_matches_serial_inner::<mmr::Family>);
1586    }
1587
1588    #[test_traced("WARN")]
1589    fn test_align_replay_parallel_matches_serial_mmb() {
1590        let executor = deterministic::Runner::default();
1591        executor.start(test_align_replay_parallel_matches_serial_inner::<mmb::Family>);
1592    }
1593
1594    /// Verify that align() discards uncommitted operations.
1595    async fn test_align_with_mismatched_committed_ops_inner<F: Family + PartialEq>(
1596        context: Context,
1597    ) {
1598        let mut journal = create_empty_journal::<F>(context.child("first"), "mismatched").await;
1599
1600        // Add 20 uncommitted operations
1601        for i in 0..20 {
1602            let loc;
1603            (journal, loc) = journal
1604                .append(&create_operation::<F>(i as u8))
1605                .await
1606                .unwrap();
1607            assert_eq!(loc, Location::<F>::new(i as u64));
1608        }
1609
1610        // Don't sync - these are uncommitted
1611        // After alignment, they should be discarded
1612        let size_before = journal.size();
1613        assert_eq!(size_before, 20);
1614
1615        // Drop and recreate to simulate restart (which calls align internally)
1616        journal.sync().await.unwrap();
1617        let journal = create_empty_journal::<F>(context.child("second"), "mismatched").await;
1618
1619        // Uncommitted operations should be gone
1620        assert_eq!(journal.size(), 0);
1621    }
1622
1623    #[test_traced("INFO")]
1624    fn test_align_with_mismatched_committed_ops_mmr() {
1625        let executor = deterministic::Runner::default();
1626        executor.start(|context| {
1627            test_align_with_mismatched_committed_ops_inner::<mmr::Family>(context)
1628        });
1629    }
1630
1631    #[test_traced("INFO")]
1632    fn test_align_with_mismatched_committed_ops_mmb() {
1633        let executor = deterministic::Runner::default();
1634        executor.start(|context| {
1635            test_align_with_mismatched_committed_ops_inner::<mmb::Family>(context)
1636        });
1637    }
1638
1639    async fn test_rewind_inner<F: Family + PartialEq>(context: Context) {
1640        // Test 1: Matching operation is kept
1641        {
1642            let mut journal = ContiguousJournal::init(
1643                context.child("rewind_match"),
1644                journal_config("rewind-match", &context),
1645            )
1646            .await
1647            .unwrap();
1648
1649            // Add operations where operation 3 is a commit
1650            for i in 0..3 {
1651                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1652            }
1653            (journal, _) = journal
1654                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
1655                .await
1656                .unwrap();
1657            for i in 4..7 {
1658                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1659            }
1660
1661            // Rewind to last commit
1662            let final_size;
1663            (journal, final_size) = journal.rewind_to(|op| op.is_commit()).await.unwrap();
1664            assert_eq!(final_size, 4);
1665            assert_eq!(journal.size(), 4);
1666
1667            // Verify the commit operation is still there
1668            let op = journal.read(3).await.unwrap();
1669            assert!(op.is_commit());
1670        }
1671
1672        // Test 2: Last matching operation is chosen when multiple match
1673        {
1674            let mut journal = ContiguousJournal::init(
1675                context.child("rewind_multiple"),
1676                journal_config("rewind-multiple", &context),
1677            )
1678            .await
1679            .unwrap();
1680
1681            // Add multiple commits
1682            (journal, _) = journal.append(&create_operation::<F>(0)).await.unwrap();
1683            (journal, _) = journal
1684                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
1685                .await
1686                .unwrap(); // pos 1
1687            (journal, _) = journal.append(&create_operation::<F>(2)).await.unwrap();
1688            (journal, _) = journal
1689                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(1)))
1690                .await
1691                .unwrap(); // pos 3
1692            (journal, _) = journal.append(&create_operation::<F>(4)).await.unwrap();
1693
1694            // Should rewind to last commit (pos 3)
1695            let final_size;
1696            (journal, final_size) = journal.rewind_to(|op| op.is_commit()).await.unwrap();
1697            assert_eq!(final_size, 4);
1698
1699            // Verify the last commit is still there
1700            let op = journal.read(3).await.unwrap();
1701            assert!(op.is_commit());
1702
1703            // Verify we can't read pos 4
1704            assert!(journal.read(4).await.is_err());
1705        }
1706
1707        // Test 3: Rewind to pruning boundary when no match
1708        {
1709            let mut journal = ContiguousJournal::init(
1710                context.child("rewind_no_match"),
1711                journal_config("rewind-no-match", &context),
1712            )
1713            .await
1714            .unwrap();
1715
1716            // Add operations with no commits
1717            for i in 0..10 {
1718                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1719            }
1720
1721            // Rewind should go to pruning boundary (0 for unpruned)
1722            let final_size;
1723            (journal, final_size) = journal.rewind_to(|op| op.is_commit()).await.unwrap();
1724            assert_eq!(final_size, 0, "Should rewind to pruning boundary (0)");
1725            assert_eq!(journal.size(), 0);
1726        }
1727
1728        // Test 4: Rewind with existing pruning boundary
1729        {
1730            let mut journal = ContiguousJournal::init(
1731                context.child("rewind_with_pruning"),
1732                journal_config("rewind-with-pruning", &context),
1733            )
1734            .await
1735            .unwrap();
1736
1737            // Add operations and a commit at position 10 (past first section boundary of 7)
1738            for i in 0..10 {
1739                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1740            }
1741            (journal, _) = journal
1742                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
1743                .await
1744                .unwrap(); // pos 10
1745            for i in 11..15 {
1746                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1747            }
1748            journal = journal.sync().await.unwrap();
1749
1750            // Prune up to position 8 (this will prune section 0, items 0-6, keeping 7+)
1751            (journal, _) = journal.prune(8).await.unwrap();
1752            assert_eq!(journal.bounds().start, 7);
1753
1754            // Add more uncommitted operations
1755            for i in 15..20 {
1756                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1757            }
1758
1759            // Rewind should keep the commit at position 10
1760            let final_size;
1761            (journal, final_size) = journal.rewind_to(|op| op.is_commit()).await.unwrap();
1762            assert_eq!(final_size, 11);
1763
1764            // Verify commit is still there
1765            let op = journal.read(10).await.unwrap();
1766            assert!(op.is_commit());
1767        }
1768
1769        // Test 5: Rewind with no matches after pruning boundary
1770        {
1771            let mut journal = ContiguousJournal::init(
1772                context.child("rewind_no_match_pruned"),
1773                journal_config("rewind-no-match-pruned", &context),
1774            )
1775            .await
1776            .unwrap();
1777
1778            // Add operations with a commit at position 5 (in section 0: 0-6)
1779            for i in 0..5 {
1780                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1781            }
1782            (journal, _) = journal
1783                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
1784                .await
1785                .unwrap(); // pos 5
1786            for i in 6..10 {
1787                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1788            }
1789            journal = journal.sync().await.unwrap();
1790
1791            // Prune up to position 8 (this prunes section 0, including the commit at pos 5)
1792            // Pruning boundary will be at position 7 (start of section 1)
1793            (journal, _) = journal.prune(8).await.unwrap();
1794            assert_eq!(journal.bounds().start, 7);
1795
1796            // Add uncommitted operations with no commits (in section 1: 7-13)
1797            for i in 10..14 {
1798                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1799            }
1800
1801            // Rewind with no matching commits after the pruning boundary
1802            // Should rewind to the pruning boundary at position 7
1803            let (_, final_size) = journal.rewind_to(|op| op.is_commit()).await.unwrap();
1804            assert_eq!(final_size, 7);
1805        }
1806
1807        // Test 6: Empty journal
1808        {
1809            let mut journal = ContiguousJournal::init(
1810                context.child("rewind_empty"),
1811                journal_config("rewind-empty", &context),
1812            )
1813            .await
1814            .unwrap();
1815
1816            // Rewind empty journal should be no-op
1817            let final_size;
1818            (journal, final_size) = journal
1819                .rewind_to(|op: &TestOp<F>| op.is_commit())
1820                .await
1821                .unwrap();
1822            assert_eq!(final_size, 0);
1823            assert_eq!(journal.size(), 0);
1824        }
1825
1826        // Test 7: Position based authenticated journal rewind.
1827        {
1828            let merkle_cfg = merkle_config("rewind", &context);
1829            let journal_cfg = journal_config("rewind", &context);
1830            let mut journal = TestJournal::<F>::new(
1831                context.child("rewind"),
1832                merkle_cfg,
1833                journal_cfg,
1834                |op| op.is_commit(),
1835                ForwardFold,
1836            )
1837            .await
1838            .unwrap();
1839
1840            // Add operations with a commit at position 5 (in section 0: 0-6)
1841            for i in 0..5 {
1842                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1843            }
1844            (journal, _) = journal
1845                .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
1846                .await
1847                .unwrap(); // pos 5
1848            for i in 6..10 {
1849                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1850            }
1851            assert_eq!(journal.size(), 10);
1852
1853            journal = journal.rewind(2).await.unwrap();
1854            assert_eq!(journal.size(), 2);
1855            assert_eq!(journal.merkle.leaves(), 2);
1856            assert_eq!(journal.merkle.size(), 3);
1857            let bounds = journal.bounds();
1858            assert_eq!(bounds.start, 0);
1859            assert!(!bounds.is_empty());
1860
1861            journal = journal.rewind(0).await.unwrap();
1862            assert_eq!(journal.size(), 0);
1863            assert_eq!(journal.merkle.leaves(), 0);
1864            assert_eq!(journal.merkle.size(), 0);
1865            let bounds = journal.bounds();
1866            assert_eq!(bounds.start, 0);
1867            assert!(bounds.is_empty());
1868
1869            // Test rewinding after pruning.
1870            for i in 0..255 {
1871                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1872            }
1873            (journal, _) = journal.prune(Location::<F>::new(100)).await.unwrap();
1874            assert_eq!(journal.bounds().start, 98);
1875            journal = journal.rewind(98).await.unwrap();
1876            let bounds = journal.bounds();
1877            assert_eq!(bounds.end, 98);
1878            assert_eq!(journal.merkle.leaves(), 98);
1879            assert_eq!(bounds.start, 98);
1880            assert!(bounds.is_empty());
1881
1882            // Rewinding into the pruned region fails.
1883            let res = journal.rewind(97).await;
1884            assert!(matches!(
1885                res,
1886                Err(Error::Journal(JournalError::ItemPruned(97)))
1887            ));
1888        }
1889
1890        // Test 8: Rewind target beyond current size fails.
1891        {
1892            let merkle_cfg = merkle_config("rewind-invalid", &context);
1893            let journal_cfg = journal_config("rewind-invalid", &context);
1894            let mut journal = TestJournal::<F>::new(
1895                context,
1896                merkle_cfg,
1897                journal_cfg,
1898                |op| op.is_commit(),
1899                ForwardFold,
1900            )
1901            .await
1902            .unwrap();
1903
1904            for i in 0..2 {
1905                (journal, _) = journal.append(&create_operation::<F>(i)).await.unwrap();
1906            }
1907            assert!(matches!(
1908                journal.rewind(3).await,
1909                Err(Error::Journal(JournalError::InvalidRewind(_)))
1910            ));
1911        }
1912    }
1913
1914    #[test_traced("INFO")]
1915    fn test_rewind_mmr() {
1916        let executor = deterministic::Runner::default();
1917        executor.start(test_rewind_inner::<mmr::Family>);
1918    }
1919
1920    #[test_traced("INFO")]
1921    fn test_rewind_mmb() {
1922        let executor = deterministic::Runner::default();
1923        executor.start(test_rewind_inner::<mmb::Family>);
1924    }
1925
1926    /// Verify that append() increments the operation count, returns correct locations, and
1927    /// operations can be read back correctly.
1928    async fn test_apply_op_and_read_operations_inner<F: Family + PartialEq>(context: Context) {
1929        let mut journal = create_empty_journal::<F>(context, "apply_op").await;
1930
1931        assert_eq!(journal.size(), 0);
1932
1933        // Add 50 operations
1934        let expected_ops: Vec<_> = (0..50).map(|i| create_operation::<F>(i as u8)).collect();
1935        for (i, op) in expected_ops.iter().enumerate() {
1936            let loc;
1937            (journal, loc) = journal.append(op).await.unwrap();
1938            assert_eq!(loc, Location::<F>::new(i as u64));
1939            assert_eq!(journal.size(), (i + 1) as u64);
1940        }
1941
1942        assert_eq!(journal.size(), 50);
1943
1944        // Verify all operations can be read back correctly
1945        journal = journal.sync().await.unwrap();
1946        for (i, expected_op) in expected_ops.iter().enumerate() {
1947            let read_op = journal.read(*Location::<F>::new(i as u64)).await.unwrap();
1948            assert_eq!(read_op, *expected_op);
1949        }
1950    }
1951
1952    #[test_traced("INFO")]
1953    fn test_apply_op_and_read_operations_mmr() {
1954        let executor = deterministic::Runner::default();
1955        executor.start(test_apply_op_and_read_operations_inner::<mmr::Family>);
1956    }
1957
1958    #[test_traced("INFO")]
1959    fn test_apply_op_and_read_operations_mmb() {
1960        let executor = deterministic::Runner::default();
1961        executor.start(test_apply_op_and_read_operations_inner::<mmb::Family>);
1962    }
1963
1964    /// Verify that read() returns correct operations at various positions.
1965    async fn test_read_operations_at_various_positions_inner<F: Family + PartialEq>(
1966        context: Context,
1967    ) {
1968        let journal = create_journal_with_ops::<F>(context, "read", 50).await;
1969
1970        // Verify reading first operation
1971        let first_op = journal.read(*Location::<F>::new(0)).await.unwrap();
1972        assert_eq!(first_op, create_operation::<F>(0));
1973
1974        // Verify reading middle operation
1975        let middle_op = journal.read(*Location::<F>::new(25)).await.unwrap();
1976        assert_eq!(middle_op, create_operation::<F>(25));
1977
1978        // Verify reading last operation
1979        let last_op = journal.read(*Location::<F>::new(49)).await.unwrap();
1980        assert_eq!(last_op, create_operation::<F>(49));
1981
1982        // Verify all operations match expected values
1983        for i in 0..50 {
1984            let op = journal.read(*Location::<F>::new(i)).await.unwrap();
1985            assert_eq!(op, create_operation::<F>(i as u8));
1986        }
1987    }
1988
1989    #[test_traced("INFO")]
1990    fn test_read_operations_at_various_positions_mmr() {
1991        let executor = deterministic::Runner::default();
1992        executor.start(|context| {
1993            test_read_operations_at_various_positions_inner::<mmr::Family>(context)
1994        });
1995    }
1996
1997    #[test_traced("INFO")]
1998    fn test_read_operations_at_various_positions_mmb() {
1999        let executor = deterministic::Runner::default();
2000        executor.start(|context| {
2001            test_read_operations_at_various_positions_inner::<mmb::Family>(context)
2002        });
2003    }
2004
2005    /// Verify that read() returns an error for pruned operations.
2006    async fn test_read_pruned_operation_returns_error_inner<F: Family + PartialEq>(
2007        context: Context,
2008    ) {
2009        let mut journal = create_journal_with_ops::<F>(context, "read_pruned", 100).await;
2010
2011        // Add commit and prune
2012        (journal, _) = journal
2013            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2014            .await
2015            .unwrap();
2016        journal = journal.sync().await.unwrap();
2017        let pruned_boundary;
2018        (journal, pruned_boundary) = journal.prune(Location::<F>::new(50)).await.unwrap();
2019
2020        // Try to read an operation before the pruned boundary
2021        let read_loc = Location::<F>::new(0);
2022        if read_loc < pruned_boundary {
2023            let result = journal.read(*read_loc).await;
2024            assert!(matches!(result, Err(crate::journal::Error::ItemPruned(_))));
2025        }
2026    }
2027
2028    #[test_traced("INFO")]
2029    fn test_read_pruned_operation_returns_error_mmr() {
2030        let executor = deterministic::Runner::default();
2031        executor.start(|context| {
2032            test_read_pruned_operation_returns_error_inner::<mmr::Family>(context)
2033        });
2034    }
2035
2036    #[test_traced("INFO")]
2037    fn test_read_pruned_operation_returns_error_mmb() {
2038        let executor = deterministic::Runner::default();
2039        executor.start(|context| {
2040            test_read_pruned_operation_returns_error_inner::<mmb::Family>(context)
2041        });
2042    }
2043
2044    /// Verify that read() returns an error for out-of-range locations.
2045    async fn test_read_out_of_range_returns_error_inner<F: Family + PartialEq>(context: Context) {
2046        let journal = create_journal_with_ops::<F>(context, "read_oob", 3).await;
2047
2048        // Try to read beyond the end
2049        let result = journal.read(*Location::<F>::new(10)).await;
2050        assert!(matches!(
2051            result,
2052            Err(crate::journal::Error::ItemOutOfRange(_))
2053        ));
2054    }
2055
2056    #[test_traced("INFO")]
2057    fn test_read_out_of_range_returns_error_mmr() {
2058        let executor = deterministic::Runner::default();
2059        executor.start(test_read_out_of_range_returns_error_inner::<mmr::Family>);
2060    }
2061
2062    #[test_traced("INFO")]
2063    fn test_read_out_of_range_returns_error_mmb() {
2064        let executor = deterministic::Runner::default();
2065        executor.start(test_read_out_of_range_returns_error_inner::<mmb::Family>);
2066    }
2067
2068    /// Verify that we can read all operations back correctly.
2069    async fn test_read_all_operations_back_correctly_inner<F: Family + PartialEq>(
2070        context: Context,
2071    ) {
2072        let journal = create_journal_with_ops::<F>(context, "read_all", 50).await;
2073
2074        assert_eq!(journal.size(), 50);
2075
2076        // Verify all operations can be read back and match expected values
2077        for i in 0..50 {
2078            let op = journal.read(*Location::<F>::new(i)).await.unwrap();
2079            assert_eq!(op, create_operation::<F>(i as u8));
2080        }
2081    }
2082
2083    #[test_traced("INFO")]
2084    fn test_read_all_operations_back_correctly_mmr() {
2085        let executor = deterministic::Runner::default();
2086        executor.start(test_read_all_operations_back_correctly_inner::<mmr::Family>);
2087    }
2088
2089    #[test_traced("INFO")]
2090    fn test_read_all_operations_back_correctly_mmb() {
2091        let executor = deterministic::Runner::default();
2092        executor.start(test_read_all_operations_back_correctly_inner::<mmb::Family>);
2093    }
2094
2095    /// Verify that sync() persists operations.
2096    async fn test_sync_inner<F: Family + PartialEq>(context: Context) {
2097        let mut journal = create_empty_journal::<F>(context.child("first"), "close_pending").await;
2098
2099        // Add 20 operations
2100        let expected_ops: Vec<_> = (0..20).map(|i| create_operation::<F>(i as u8)).collect();
2101        for (i, op) in expected_ops.iter().enumerate() {
2102            let loc;
2103            (journal, loc) = journal.append(op).await.unwrap();
2104            assert_eq!(loc, Location::<F>::new(i as u64),);
2105        }
2106
2107        // Add commit operation to commit the operations
2108        let commit_loc;
2109        (journal, commit_loc) = journal
2110            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
2111            .await
2112            .unwrap();
2113        assert_eq!(
2114            commit_loc,
2115            Location::<F>::new(20),
2116            "commit should be at location 20"
2117        );
2118        journal.sync().await.unwrap();
2119
2120        // Reopen and verify the operations persisted
2121        let journal = create_empty_journal::<F>(context.child("second"), "close_pending").await;
2122        assert_eq!(journal.size(), 21);
2123
2124        // Verify all operations can be read back
2125        for (i, expected_op) in expected_ops.iter().enumerate() {
2126            let read_op = journal.read(*Location::<F>::new(i as u64)).await.unwrap();
2127            assert_eq!(read_op, *expected_op);
2128        }
2129    }
2130
2131    #[test_traced("INFO")]
2132    fn test_sync_mmr() {
2133        let executor = deterministic::Runner::default();
2134        executor.start(test_sync_inner::<mmr::Family>);
2135    }
2136
2137    #[test_traced("INFO")]
2138    fn test_sync_mmb() {
2139        let executor = deterministic::Runner::default();
2140        executor.start(test_sync_inner::<mmb::Family>);
2141    }
2142
2143    /// Awaiting a start_sync handle provides commit-level durability: committed operations
2144    /// survive a reopen, with recovery re-aligning the Merkle structure.
2145    async fn test_start_sync_durability_inner<F: Family + PartialEq>(context: Context) {
2146        let mut journal = create_empty_journal::<F>(context.child("first"), "start_sync").await;
2147        let expected_ops: Vec<_> = (0..5).map(|i| create_operation::<F>(i as u8)).collect();
2148        for op in expected_ops.iter() {
2149            (journal, _) = journal.append(op).await.unwrap();
2150        }
2151        (journal, _) = journal
2152            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(0)))
2153            .await
2154            .unwrap();
2155
2156        let handle;
2157        (journal, handle) = journal.start_sync().await.unwrap();
2158        handle.await.unwrap();
2159        let root = journal_root(&journal);
2160        drop(journal);
2161
2162        let journal = create_empty_journal::<F>(context.child("second"), "start_sync").await;
2163        assert_eq!(journal.size(), 6);
2164        assert_eq!(journal_root(&journal), root);
2165        for (i, expected_op) in expected_ops.iter().enumerate() {
2166            let read_op = journal.read(*Location::<F>::new(i as u64)).await.unwrap();
2167            assert_eq!(read_op, *expected_op);
2168        }
2169    }
2170
2171    #[test_traced("INFO")]
2172    fn test_start_sync_durability_mmr() {
2173        let executor = deterministic::Runner::default();
2174        executor.start(test_start_sync_durability_inner::<mmr::Family>);
2175    }
2176
2177    #[test_traced("INFO")]
2178    fn test_start_sync_durability_mmb() {
2179        let executor = deterministic::Runner::default();
2180        executor.start(test_start_sync_durability_inner::<mmb::Family>);
2181    }
2182
2183    /// Delayed-sync context for exercising in-flight sync handles.
2184    type DelayedCtx = DelayedSyncContext<deterministic::Context>;
2185
2186    /// Authenticated journal over a delayed-sync storage backend.
2187    type DelayedTestJournal<F> =
2188        Journal<F, DelayedCtx, ContiguousJournal<DelayedCtx, TestOp<F>>, Sha256, Sequential>;
2189
2190    /// Open an authenticated journal whose blob syncs park on `pending`.
2191    ///
2192    /// `new` durably persists the recovered journal, so while syncs park the returned future
2193    /// must be driven with [drive_pending_syncs] (or the mock unblocked first).
2194    fn open_delayed_journal(
2195        context: &Context,
2196        label: &'static str,
2197        suffix: &str,
2198        pending: &PendingSyncs,
2199    ) -> impl Future<Output = Result<DelayedTestJournal<mmr::Family>, Error<mmr::Family>>> {
2200        DelayedTestJournal::<mmr::Family>::new(
2201            DelayedCtx {
2202                inner: context.child(label),
2203                pending: pending.clone(),
2204            },
2205            merkle_config(suffix, context),
2206            journal_config(suffix, context),
2207            |op: &TestOp<mmr::Family>| op.is_commit(),
2208            ForwardFold,
2209        )
2210    }
2211
2212    /// A sync handle must not block journal use while the backend sync is pending.
2213    #[test_traced("INFO")]
2214    fn test_start_sync_overlaps_work() {
2215        let executor = deterministic::Runner::default();
2216        executor.start(|context| async move {
2217            let pending = PendingSyncs::default();
2218            let open = open_delayed_journal(&context, "first", "start_sync_overlap", &pending);
2219            let mut journal = drive_pending_syncs(&pending, open).await.unwrap();
2220            for i in 0..4 {
2221                (journal, _) = journal
2222                    .append(&create_operation::<mmr::Family>(i))
2223                    .await
2224                    .unwrap();
2225            }
2226
2227            let starts_before = pending.starts();
2228            let entered_before = pending.entered();
2229            let completions_before = pending.completions();
2230            let handle;
2231            (journal, handle) = journal.start_sync().await.unwrap();
2232            assert!(pending.starts() > starts_before);
2233            assert_eq!(pending.completions(), completions_before);
2234
2235            // Observe the sync while the journal keeps working.
2236            let waiter = context
2237                .child("await_sync")
2238                .spawn(|_| async move { handle.await.unwrap() });
2239            while pending.entered() == entered_before {
2240                reschedule().await;
2241            }
2242
2243            // Appends and reads complete before the sync does.
2244            (journal, _) = journal
2245                .append(&create_operation::<mmr::Family>(4))
2246                .await
2247                .unwrap();
2248            let read_op = journal.read(0).await.unwrap();
2249            assert_eq!(read_op, create_operation::<mmr::Family>(0));
2250            assert_eq!(
2251                pending.completions(),
2252                completions_before,
2253                "the journal made progress while the sync was still in flight"
2254            );
2255
2256            pending.unblock();
2257            waiter.await.unwrap();
2258
2259            // The mid-sync append is durable after the next sync.
2260            (journal, _) = journal
2261                .append(&TestOp::<mmr::Family>::CommitFloor(None, Location::new(0)))
2262                .await
2263                .unwrap();
2264            let handle;
2265            (journal, handle) = journal.start_sync().await.unwrap();
2266            handle.await.unwrap();
2267            let root = journal.root(0).unwrap();
2268            drop(journal);
2269
2270            let journal = open_delayed_journal(&context, "second", "start_sync_overlap", &pending)
2271                .await
2272                .unwrap();
2273            assert_eq!(journal.size(), 6);
2274            assert_eq!(journal.root(0).unwrap(), root);
2275        });
2276    }
2277
2278    /// A sync begun by `start_sync` that fails in flight surfaces the error through both the
2279    /// returned handle and the next durability operation.
2280    #[test_traced("INFO")]
2281    fn test_start_sync_failure_propagates() {
2282        let executor = deterministic::Runner::default();
2283        executor.start(|context| async move {
2284            // Pass syncs through so opening the journal doesn't park.
2285            let pending = PendingSyncs::default();
2286            pending.unblock();
2287            let mut journal = open_delayed_journal(&context, "first", "start_sync_fail", &pending)
2288                .await
2289                .unwrap();
2290            for i in 0..4 {
2291                (journal, _) = journal
2292                    .append(&create_operation::<mmr::Family>(i))
2293                    .await
2294                    .unwrap();
2295            }
2296
2297            // Arm all future syncs to resolve to an injected error.
2298            pending.arm_fail();
2299
2300            let handle;
2301            (journal, handle) = journal.start_sync().await.unwrap();
2302            assert!(
2303                handle.await.is_err(),
2304                "the sync handle surfaces the failure"
2305            );
2306            let starts_before = pending.starts();
2307            // A failed mutable method consumes the journal per the failures-are-fatal contract.
2308            assert!(
2309                matches!(
2310                    journal.commit().await,
2311                    Err(Error::Journal(JournalError::Runtime(_)))
2312                ),
2313                "the next durability op surfaces the failed in-flight sync"
2314            );
2315            assert_eq!(
2316                pending.starts(),
2317                starts_before,
2318                "the surfaced error is the retained failure, not a fresh sync's"
2319            );
2320        });
2321    }
2322
2323    /// A merkle-only sync failure fails the joined handle even though the operation log's own
2324    /// sync succeeded.
2325    #[test_traced("INFO")]
2326    fn test_start_sync_merkle_failure_fails_handle() {
2327        let executor = deterministic::Runner::default();
2328        executor.start(|context| async move {
2329            let pending = PendingSyncs::default();
2330            let open = open_delayed_journal(&context, "first", "merkle_fail", &pending);
2331            let mut journal = drive_pending_syncs(&pending, open).await.unwrap();
2332            for i in 0..4 {
2333                (journal, _) = journal
2334                    .append(&create_operation::<mmr::Family>(i))
2335                    .await
2336                    .unwrap();
2337            }
2338
2339            // Prove the appends durable, then dirty only the merkle journal: commit syncs the
2340            // operation log but merely flushes merkle nodes.
2341            let handle;
2342            (journal, handle) = journal.start_sync().await.unwrap();
2343            drive_pending_syncs(&pending, handle).await.unwrap();
2344            for i in 4..6 {
2345                (journal, _) = journal
2346                    .append(&create_operation::<mmr::Family>(i))
2347                    .await
2348                    .unwrap();
2349            }
2350            journal = drive_pending_syncs(&pending, journal.commit())
2351                .await
2352                .unwrap();
2353
2354            // The operation log's data is already durable, so its only parked sync is the
2355            // watermark advance: release it, then fail the merkle journal's syncs.
2356            let handle;
2357            (journal, handle) = journal.start_sync().await.unwrap();
2358            let ops_watermark = next_pending_sync(&pending);
2359            ops_watermark.release.send(Ok(())).unwrap();
2360            fail_pending_syncs(&pending);
2361            assert!(
2362                handle.await.is_err(),
2363                "a merkle-only failure surfaces on the joined handle"
2364            );
2365
2366            // The merkle journal retained the failure: the next sync resurfaces it.
2367            assert!(drive_pending_syncs(&pending, journal.sync()).await.is_err());
2368        });
2369    }
2370
2371    /// Verify that pruning an empty journal returns the boundary.
2372    async fn test_prune_empty_journal_inner<F: Family + PartialEq>(context: Context) {
2373        let journal = create_empty_journal::<F>(context, "prune_empty").await;
2374
2375        let (_, boundary) = journal.prune(Location::<F>::new(0)).await.unwrap();
2376
2377        assert_eq!(boundary, Location::<F>::new(0));
2378    }
2379
2380    #[test_traced("INFO")]
2381    fn test_prune_empty_journal_mmr() {
2382        let executor = deterministic::Runner::default();
2383        executor.start(test_prune_empty_journal_inner::<mmr::Family>);
2384    }
2385
2386    #[test_traced("INFO")]
2387    fn test_prune_empty_journal_mmb() {
2388        let executor = deterministic::Runner::default();
2389        executor.start(test_prune_empty_journal_inner::<mmb::Family>);
2390    }
2391
2392    /// Verify that pruning to a specific location works correctly.
2393    async fn test_prune_to_location_inner<F: Family + PartialEq>(context: Context) {
2394        let mut journal = create_journal_with_ops::<F>(context, "prune_to", 100).await;
2395
2396        // Add commit at position 50
2397        (journal, _) = journal
2398            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2399            .await
2400            .unwrap();
2401        journal = journal.sync().await.unwrap();
2402
2403        let (_, boundary) = journal.prune(Location::<F>::new(50)).await.unwrap();
2404
2405        // Boundary should be <= requested location (may align to section boundary)
2406        assert!(boundary <= Location::<F>::new(50));
2407    }
2408
2409    #[test_traced("INFO")]
2410    fn test_prune_to_location_mmr() {
2411        let executor = deterministic::Runner::default();
2412        executor.start(test_prune_to_location_inner::<mmr::Family>);
2413    }
2414
2415    #[test_traced("INFO")]
2416    fn test_prune_to_location_mmb() {
2417        let executor = deterministic::Runner::default();
2418        executor.start(test_prune_to_location_inner::<mmb::Family>);
2419    }
2420
2421    /// Verify that prune() returns the actual boundary (which may differ from requested).
2422    async fn test_prune_returns_actual_boundary_inner<F: Family + PartialEq>(context: Context) {
2423        let mut journal = create_journal_with_ops::<F>(context, "prune_boundary", 100).await;
2424
2425        (journal, _) = journal
2426            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2427            .await
2428            .unwrap();
2429        journal = journal.sync().await.unwrap();
2430
2431        let requested = Location::<F>::new(50);
2432        let (journal, actual) = journal.prune(requested).await.unwrap();
2433
2434        // Actual boundary should match bounds.start
2435        let bounds = journal.bounds();
2436        assert!(!bounds.is_empty());
2437        assert_eq!(actual, bounds.start);
2438
2439        // Actual may be <= requested due to section alignment
2440        assert!(actual <= requested);
2441    }
2442
2443    #[test_traced("INFO")]
2444    fn test_prune_returns_actual_boundary_mmr() {
2445        let executor = deterministic::Runner::default();
2446        executor.start(test_prune_returns_actual_boundary_inner::<mmr::Family>);
2447    }
2448
2449    #[test_traced("INFO")]
2450    fn test_prune_returns_actual_boundary_mmb() {
2451        let executor = deterministic::Runner::default();
2452        executor.start(test_prune_returns_actual_boundary_inner::<mmb::Family>);
2453    }
2454
2455    /// Verify that pruning through the Mutable trait also prunes authenticated Merkle state.
2456    async fn test_mutable_prune_updates_merkle_boundary_inner<F: Family + PartialEq>(
2457        context: Context,
2458    ) {
2459        let mut journal = create_journal_with_ops::<F>(context, "trait_prune", 100).await;
2460
2461        (journal, _) = journal
2462            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2463            .await
2464            .unwrap();
2465        journal = journal.sync().await.unwrap();
2466
2467        let (journal, pruned) = <TestJournal<F> as Mutable>::prune(journal, 50)
2468            .await
2469            .unwrap();
2470        assert!(pruned);
2471
2472        let item_boundary = journal.bounds().start;
2473        let merkle_boundary = journal.merkle.bounds().start;
2474        assert_eq!(Location::<F>::new(item_boundary), merkle_boundary);
2475        assert!(merkle_boundary > Location::<F>::new(0));
2476
2477        let (journal, pruned) = <TestJournal<F> as Mutable>::prune(journal, 50)
2478            .await
2479            .unwrap();
2480        assert!(!pruned);
2481        assert_eq!(journal.bounds().start, item_boundary);
2482        assert_eq!(journal.merkle.bounds().start, merkle_boundary);
2483    }
2484
2485    #[test_traced("INFO")]
2486    fn test_mutable_prune_updates_merkle_boundary_mmr() {
2487        let executor = deterministic::Runner::default();
2488        executor.start(test_mutable_prune_updates_merkle_boundary_inner::<mmr::Family>);
2489    }
2490
2491    #[test_traced("INFO")]
2492    fn test_mutable_prune_updates_merkle_boundary_mmb() {
2493        let executor = deterministic::Runner::default();
2494        executor.start(test_mutable_prune_updates_merkle_boundary_inner::<mmb::Family>);
2495    }
2496
2497    /// Verify that pruning doesn't change the operation count.
2498    async fn test_prune_preserves_operation_count_inner<F: Family + PartialEq>(context: Context) {
2499        let mut journal = create_journal_with_ops::<F>(context, "prune_count", 100).await;
2500
2501        (journal, _) = journal
2502            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2503            .await
2504            .unwrap();
2505        journal = journal.sync().await.unwrap();
2506
2507        let count_before = journal.size();
2508        let (journal, _) = journal.prune(Location::<F>::new(50)).await.unwrap();
2509        let count_after = journal.size();
2510
2511        assert_eq!(count_before, count_after);
2512    }
2513
2514    #[test_traced("INFO")]
2515    fn test_prune_preserves_operation_count_mmr() {
2516        let executor = deterministic::Runner::default();
2517        executor.start(test_prune_preserves_operation_count_inner::<mmr::Family>);
2518    }
2519
2520    #[test_traced("INFO")]
2521    fn test_prune_preserves_operation_count_mmb() {
2522        let executor = deterministic::Runner::default();
2523        executor.start(test_prune_preserves_operation_count_inner::<mmb::Family>);
2524    }
2525
2526    /// Verify bounds() for empty journal, no pruning, and after pruning.
2527    async fn test_bounds_empty_and_pruned_inner<F: Family + PartialEq>(context: Context) {
2528        // Test empty journal
2529        let journal = create_empty_journal::<F>(context.child("empty"), "oldest").await;
2530        assert!(journal.bounds().is_empty());
2531        journal.destroy().await.unwrap();
2532
2533        // Test no pruning
2534        let journal = create_journal_with_ops::<F>(context.child("no_prune"), "oldest", 100).await;
2535        let bounds = journal.bounds();
2536        assert!(!bounds.is_empty());
2537        assert_eq!(bounds.start, 0);
2538        journal.destroy().await.unwrap();
2539
2540        // Test after pruning
2541        let mut journal =
2542            create_journal_with_ops::<F>(context.child("pruned"), "oldest", 100).await;
2543        (journal, _) = journal
2544            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2545            .await
2546            .unwrap();
2547        journal = journal.sync().await.unwrap();
2548
2549        let (journal, pruned_boundary) = journal.prune(Location::<F>::new(50)).await.unwrap();
2550
2551        // Should match the pruned boundary (may be <= 50 due to section alignment)
2552        let bounds = journal.bounds();
2553        assert!(!bounds.is_empty());
2554        assert_eq!(bounds.start, pruned_boundary);
2555        // Should be <= requested location (50)
2556        assert!(pruned_boundary <= 50);
2557        journal.destroy().await.unwrap();
2558    }
2559
2560    #[test_traced("INFO")]
2561    fn test_bounds_empty_and_pruned_mmr() {
2562        let executor = deterministic::Runner::default();
2563        executor.start(test_bounds_empty_and_pruned_inner::<mmr::Family>);
2564    }
2565
2566    #[test_traced("INFO")]
2567    fn test_bounds_empty_and_pruned_mmb() {
2568        let executor = deterministic::Runner::default();
2569        executor.start(test_bounds_empty_and_pruned_inner::<mmb::Family>);
2570    }
2571
2572    /// Verify bounds().start for empty journal, no pruning, and after pruning.
2573    async fn test_bounds_start_after_prune_inner<F: Family + PartialEq>(context: Context) {
2574        // Test empty journal
2575        let journal = create_empty_journal::<F>(context.child("empty"), "boundary").await;
2576        assert_eq!(journal.bounds().start, 0);
2577
2578        // Test no pruning
2579        let journal =
2580            create_journal_with_ops::<F>(context.child("no_prune"), "boundary", 100).await;
2581        assert_eq!(journal.bounds().start, 0);
2582
2583        // Test after pruning
2584        let mut journal =
2585            create_journal_with_ops::<F>(context.child("pruned"), "boundary", 100).await;
2586        (journal, _) = journal
2587            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(50)))
2588            .await
2589            .unwrap();
2590        journal = journal.sync().await.unwrap();
2591
2592        let (journal, pruned_boundary) = journal.prune(Location::<F>::new(50)).await.unwrap();
2593
2594        assert_eq!(journal.bounds().start, pruned_boundary);
2595    }
2596
2597    #[test_traced("INFO")]
2598    fn test_bounds_start_after_prune_mmr() {
2599        let executor = deterministic::Runner::default();
2600        executor.start(test_bounds_start_after_prune_inner::<mmr::Family>);
2601    }
2602
2603    #[test_traced("INFO")]
2604    fn test_bounds_start_after_prune_mmb() {
2605        let executor = deterministic::Runner::default();
2606        executor.start(test_bounds_start_after_prune_inner::<mmb::Family>);
2607    }
2608
2609    /// Verify that Merkle prunes to the journal's actual boundary, not the requested location.
2610    async fn test_mmr_prunes_to_journal_boundary_inner<F: Family + PartialEq>(context: Context) {
2611        let mut journal = create_journal_with_ops::<F>(context, "mmr_boundary", 50).await;
2612
2613        (journal, _) = journal
2614            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(25)))
2615            .await
2616            .unwrap();
2617        journal = journal.sync().await.unwrap();
2618
2619        let (journal, pruned_boundary) = journal.prune(Location::<F>::new(25)).await.unwrap();
2620
2621        // Verify Merkle and journal remain in sync
2622        let bounds = journal.bounds();
2623        assert!(!bounds.is_empty());
2624        assert_eq!(pruned_boundary, bounds.start);
2625
2626        // Verify boundary is at or before requested (due to section alignment)
2627        assert!(pruned_boundary <= Location::<F>::new(25));
2628
2629        // Verify operation count is unchanged
2630        assert_eq!(journal.size(), 51);
2631    }
2632
2633    #[test_traced("INFO")]
2634    fn test_mmr_prunes_to_journal_boundary_mmr() {
2635        let executor = deterministic::Runner::default();
2636        executor.start(test_mmr_prunes_to_journal_boundary_inner::<mmr::Family>);
2637    }
2638
2639    #[test_traced("INFO")]
2640    fn test_mmr_prunes_to_journal_boundary_mmb() {
2641        let executor = deterministic::Runner::default();
2642        executor.start(test_mmr_prunes_to_journal_boundary_inner::<mmb::Family>);
2643    }
2644
2645    /// Verify proof() for multiple operations.
2646    async fn test_proof_multiple_operations_inner<F: Family + PartialEq>(context: Context) {
2647        let journal = create_journal_with_ops::<F>(context, "proof_multi", 50).await;
2648
2649        let (proof, ops) = journal
2650            .proof(Location::<F>::new(0), NZU64!(50), 0)
2651            .await
2652            .unwrap();
2653
2654        assert_eq!(ops.len(), 50);
2655        for (i, op) in ops.iter().enumerate() {
2656            assert_eq!(*op, create_operation::<F>(i as u8));
2657        }
2658
2659        // Verify the proof is valid
2660        let hasher = StandardHasher::new(ForwardFold);
2661        let root = journal_root(&journal);
2662        assert!(verify_proof(
2663            &proof,
2664            &ops,
2665            Location::<F>::new(0),
2666            &root,
2667            &hasher
2668        ));
2669    }
2670
2671    #[test_traced("INFO")]
2672    fn test_proof_multiple_operations_mmr() {
2673        let executor = deterministic::Runner::default();
2674        executor.start(test_proof_multiple_operations_inner::<mmr::Family>);
2675    }
2676
2677    #[test_traced("INFO")]
2678    fn test_proof_multiple_operations_mmb() {
2679        let executor = deterministic::Runner::default();
2680        executor.start(test_proof_multiple_operations_inner::<mmb::Family>);
2681    }
2682
2683    /// Verify that historical_proof() respects the max_ops limit.
2684    async fn test_historical_proof_limited_by_max_ops_inner<F: Family + PartialEq>(
2685        context: Context,
2686    ) {
2687        let journal = create_journal_with_ops::<F>(context, "proof_limit", 50).await;
2688
2689        let size = journal.size();
2690        let (proof, ops) = journal
2691            .historical_proof(size, Location::<F>::new(0), NZU64!(20), 0)
2692            .await
2693            .unwrap();
2694
2695        // Should return only 20 operations despite 50 being available
2696        assert_eq!(ops.len(), 20);
2697        for (i, op) in ops.iter().enumerate() {
2698            assert_eq!(*op, create_operation::<F>(i as u8));
2699        }
2700
2701        // Verify the proof is valid
2702        let hasher = StandardHasher::new(ForwardFold);
2703        let root = journal_root(&journal);
2704        assert!(verify_proof(
2705            &proof,
2706            &ops,
2707            Location::<F>::new(0),
2708            &root,
2709            &hasher
2710        ));
2711    }
2712
2713    #[test_traced("INFO")]
2714    fn test_historical_proof_limited_by_max_ops_mmr() {
2715        let executor = deterministic::Runner::default();
2716        executor.start(|context| {
2717            test_historical_proof_limited_by_max_ops_inner::<mmr::Family>(context)
2718        });
2719    }
2720
2721    #[test_traced("INFO")]
2722    fn test_historical_proof_limited_by_max_ops_mmb() {
2723        let executor = deterministic::Runner::default();
2724        executor.start(|context| {
2725            test_historical_proof_limited_by_max_ops_inner::<mmb::Family>(context)
2726        });
2727    }
2728
2729    /// Verify historical_proof() at the end of the journal.
2730    async fn test_historical_proof_at_end_of_journal_inner<F: Family + PartialEq>(
2731        context: Context,
2732    ) {
2733        let journal = create_journal_with_ops::<F>(context, "proof_end", 50).await;
2734
2735        let size = journal.size();
2736        // Request proof starting near the end
2737        let (proof, ops) = journal
2738            .historical_proof(size, Location::<F>::new(40), NZU64!(20), 0)
2739            .await
2740            .unwrap();
2741
2742        // Should return only 10 operations (positions 40-49)
2743        assert_eq!(ops.len(), 10);
2744        for (i, op) in ops.iter().enumerate() {
2745            assert_eq!(*op, create_operation::<F>((40 + i) as u8));
2746        }
2747
2748        // Verify the proof is valid
2749        let hasher = StandardHasher::new(ForwardFold);
2750        let root = journal_root(&journal);
2751        assert!(verify_proof(
2752            &proof,
2753            &ops,
2754            Location::<F>::new(40),
2755            &root,
2756            &hasher
2757        ));
2758    }
2759
2760    #[test_traced("INFO")]
2761    fn test_historical_proof_at_end_of_journal_mmr() {
2762        let executor = deterministic::Runner::default();
2763        executor.start(test_historical_proof_at_end_of_journal_inner::<mmr::Family>);
2764    }
2765
2766    #[test_traced("INFO")]
2767    fn test_historical_proof_at_end_of_journal_mmb() {
2768        let executor = deterministic::Runner::default();
2769        executor.start(test_historical_proof_at_end_of_journal_inner::<mmb::Family>);
2770    }
2771
2772    /// Verify that historical_proof() returns an error for invalid size.
2773    async fn test_historical_proof_out_of_range_returns_error_inner<F: Family + PartialEq>(
2774        context: Context,
2775    ) {
2776        let journal = create_journal_with_ops::<F>(context, "proof_oob", 5).await;
2777
2778        // Request proof with size > actual journal size
2779        let result = journal
2780            .historical_proof(Location::<F>::new(10), Location::<F>::new(0), NZU64!(1), 0)
2781            .await;
2782
2783        assert!(matches!(
2784            result,
2785            Err(Error::Merkle(merkle::Error::RangeOutOfBounds(_)))
2786        ));
2787    }
2788
2789    #[test_traced("INFO")]
2790    fn test_historical_proof_out_of_range_returns_error_mmr() {
2791        let executor = deterministic::Runner::default();
2792        executor.start(|context| {
2793            test_historical_proof_out_of_range_returns_error_inner::<mmr::Family>(context)
2794        });
2795    }
2796
2797    #[test_traced("INFO")]
2798    fn test_historical_proof_out_of_range_returns_error_mmb() {
2799        let executor = deterministic::Runner::default();
2800        executor.start(|context| {
2801            test_historical_proof_out_of_range_returns_error_inner::<mmb::Family>(context)
2802        });
2803    }
2804
2805    /// Verify that historical_proof() returns an error when start_loc >= size.
2806    async fn test_historical_proof_start_too_large_returns_error_inner<F: Family + PartialEq>(
2807        context: Context,
2808    ) {
2809        let journal = create_journal_with_ops::<F>(context, "proof_start_oob", 5).await;
2810
2811        let size = journal.size();
2812        // Request proof starting at size (should fail)
2813        let result = journal.historical_proof(size, size, NZU64!(1), 0).await;
2814
2815        assert!(matches!(
2816            result,
2817            Err(Error::Merkle(merkle::Error::RangeOutOfBounds(_)))
2818        ));
2819    }
2820
2821    #[test_traced("INFO")]
2822    fn test_historical_proof_start_too_large_returns_error_mmr() {
2823        let executor = deterministic::Runner::default();
2824        executor.start(|context| {
2825            test_historical_proof_start_too_large_returns_error_inner::<mmr::Family>(context)
2826        });
2827    }
2828
2829    #[test_traced("INFO")]
2830    fn test_historical_proof_start_too_large_returns_error_mmb() {
2831        let executor = deterministic::Runner::default();
2832        executor.start(|context| {
2833            test_historical_proof_start_too_large_returns_error_inner::<mmb::Family>(context)
2834        });
2835    }
2836
2837    /// Verify historical_proof() for a truly historical state (before more operations added).
2838    async fn test_historical_proof_truly_historical_inner<F: Family + PartialEq>(context: Context) {
2839        // Create journal with initial operations
2840        let mut journal = create_journal_with_ops::<F>(context, "proof_historical", 50).await;
2841
2842        // Capture root at historical state
2843        let hasher = StandardHasher::new(ForwardFold);
2844        let historical_root = journal_root(&journal);
2845        let historical_size = journal.size();
2846
2847        // Add more operations after the historical state
2848        for i in 50..100 {
2849            (journal, _) = journal
2850                .append(&create_operation::<F>(i as u8))
2851                .await
2852                .unwrap();
2853        }
2854        let journal = journal.sync().await.unwrap();
2855
2856        // Generate proof for the historical state
2857        let (proof, ops) = journal
2858            .historical_proof(historical_size, Location::<F>::new(0), NZU64!(50), 0)
2859            .await
2860            .unwrap();
2861
2862        // Verify operations match expected historical operations
2863        assert_eq!(ops.len(), 50);
2864        for (i, op) in ops.iter().enumerate() {
2865            assert_eq!(*op, create_operation::<F>(i as u8));
2866        }
2867
2868        // Verify the proof is valid against the historical root
2869        assert!(verify_proof(
2870            &proof,
2871            &ops,
2872            Location::<F>::new(0),
2873            &historical_root,
2874            &hasher
2875        ));
2876    }
2877
2878    #[test_traced("INFO")]
2879    fn test_historical_proof_truly_historical_mmr() {
2880        let executor = deterministic::Runner::default();
2881        executor.start(test_historical_proof_truly_historical_inner::<mmr::Family>);
2882    }
2883
2884    #[test_traced("INFO")]
2885    fn test_historical_proof_truly_historical_mmb() {
2886        let executor = deterministic::Runner::default();
2887        executor.start(test_historical_proof_truly_historical_inner::<mmb::Family>);
2888    }
2889
2890    /// Verify that historical_proof() returns an error when start_loc is pruned.
2891    async fn test_historical_proof_pruned_location_returns_error_inner<F: Family + PartialEq>(
2892        context: Context,
2893    ) {
2894        let mut journal = create_journal_with_ops::<F>(context, "proof_pruned", 50).await;
2895
2896        (journal, _) = journal
2897            .append(&TestOp::<F>::CommitFloor(None, Location::<F>::new(25)))
2898            .await
2899            .unwrap();
2900        journal = journal.sync().await.unwrap();
2901        let pruned_boundary;
2902        (journal, pruned_boundary) = journal.prune(Location::<F>::new(25)).await.unwrap();
2903
2904        // Try to get proof starting at a location before the pruned boundary
2905        let size = journal.size();
2906        let start_loc = Location::<F>::new(0);
2907        if start_loc < pruned_boundary {
2908            let result = journal
2909                .historical_proof(size, start_loc, NZU64!(1), 0)
2910                .await;
2911
2912            // Should fail when trying to read pruned operations
2913            assert!(result.is_err());
2914        }
2915    }
2916
2917    #[test_traced("INFO")]
2918    fn test_historical_proof_pruned_location_returns_error_mmr() {
2919        let executor = deterministic::Runner::default();
2920        executor.start(|context| {
2921            test_historical_proof_pruned_location_returns_error_inner::<mmr::Family>(context)
2922        });
2923    }
2924
2925    #[test_traced("INFO")]
2926    fn test_historical_proof_pruned_location_returns_error_mmb() {
2927        let executor = deterministic::Runner::default();
2928        executor.start(|context| {
2929            test_historical_proof_pruned_location_returns_error_inner::<mmb::Family>(context)
2930        });
2931    }
2932
2933    /// Verify replay() with empty journal and multiple operations.
2934    async fn test_replay_operations_inner<F: Family + PartialEq>(context: Context) {
2935        // Test empty journal
2936        let journal = create_empty_journal::<F>(context.child("empty"), "replay").await;
2937        let stream = journal
2938            .replay(0, NZUsize!(10), ReadOptions::default())
2939            .await
2940            .unwrap();
2941        futures::pin_mut!(stream);
2942        assert!(stream.next().await.is_none());
2943
2944        // Test replaying all operations
2945        let journal = create_journal_with_ops::<F>(context.child("with_ops"), "replay", 50).await;
2946        let stream = journal
2947            .replay(0, NZUsize!(100), ReadOptions::default())
2948            .await
2949            .unwrap();
2950        futures::pin_mut!(stream);
2951
2952        for i in 0..50 {
2953            let (pos, op) = stream.next().await.unwrap().unwrap();
2954            assert_eq!(pos, i);
2955            assert_eq!(op, create_operation::<F>(i as u8));
2956        }
2957
2958        assert!(stream.next().await.is_none());
2959    }
2960
2961    #[test_traced("INFO")]
2962    fn test_replay_operations_mmr() {
2963        let executor = deterministic::Runner::default();
2964        executor.start(test_replay_operations_inner::<mmr::Family>);
2965    }
2966
2967    #[test_traced("INFO")]
2968    fn test_replay_operations_mmb() {
2969        let executor = deterministic::Runner::default();
2970        executor.start(test_replay_operations_inner::<mmb::Family>);
2971    }
2972
2973    #[test_traced("INFO")]
2974    fn test_replay_propagates_read_options_to_backing_journal() {
2975        let executor = deterministic::Runner::default();
2976        executor.start(|context| async move {
2977            let (context, recordings) = RecordingContext::new(context);
2978            let merkle_cfg = merkle_config("replay-options", &context);
2979            let journal_cfg = journal_config("replay-options", &context);
2980            let page_cache = journal_cfg.page_cache.clone();
2981            let mut journal = RecordingTestJournal::<mmr::Family>::new(
2982                context,
2983                merkle_cfg,
2984                journal_cfg,
2985                |op| op.is_commit(),
2986                ForwardFold,
2987            )
2988            .await
2989            .unwrap();
2990
2991            for i in 0..8 {
2992                let operation = create_operation::<mmr::Family>(i);
2993                (journal, _) = journal.append(&operation).await.unwrap();
2994            }
2995            journal = journal.sync().await.unwrap();
2996
2997            // Evict cached pages so the first replay item requires a backing read through the
2998            // authenticated wrapper.
2999            page_cache.clear();
3000            let stream = journal
3001                .replay(0, NZUsize!(100), ReadOptions::DONT_CACHE)
3002                .await
3003                .unwrap();
3004            recordings.clear();
3005            futures::pin_mut!(stream);
3006            let (position, operation) = stream.next().await.unwrap().unwrap();
3007            assert_eq!(position, 0);
3008            assert_eq!(operation, create_operation::<mmr::Family>(0));
3009
3010            // The authenticated wrapper forwards DONT_CACHE unchanged to the backing journal.
3011            let reads = recordings.snapshot().reads;
3012            assert!(!reads.is_empty());
3013            assert!(
3014                reads
3015                    .iter()
3016                    .all(|options| *options == ReadOptions::DONT_CACHE)
3017            );
3018        });
3019    }
3020
3021    /// Verify replay() starting from a middle location.
3022    async fn test_replay_from_middle_inner<F: Family + PartialEq>(context: Context) {
3023        let journal = create_journal_with_ops::<F>(context, "replay_middle", 50).await;
3024        let stream = journal
3025            .replay(25, NZUsize!(100), ReadOptions::default())
3026            .await
3027            .unwrap();
3028        futures::pin_mut!(stream);
3029
3030        let mut count = 0;
3031        while let Some(result) = stream.next().await {
3032            let (pos, op) = result.unwrap();
3033            assert_eq!(pos, 25 + count);
3034            assert_eq!(op, create_operation::<F>((25 + count) as u8));
3035            count += 1;
3036        }
3037
3038        // Should have replayed positions 25-49 (25 operations)
3039        assert_eq!(count, 25);
3040    }
3041
3042    #[test_traced("INFO")]
3043    fn test_replay_from_middle_mmr() {
3044        let executor = deterministic::Runner::default();
3045        executor.start(test_replay_from_middle_inner::<mmr::Family>);
3046    }
3047
3048    #[test_traced("INFO")]
3049    fn test_replay_from_middle_mmb() {
3050        let executor = deterministic::Runner::default();
3051        executor.start(test_replay_from_middle_inner::<mmb::Family>);
3052    }
3053
3054    /// Verify the speculative batch API: fork two batches, verify independent roots, apply one.
3055    async fn test_speculative_batch_inner<F: Family + PartialEq>(context: Context) {
3056        let mut journal = create_journal_with_ops::<F>(context, "speculative_batch", 10).await;
3057        let original_root = journal_root(&journal);
3058
3059        // Fork two independent speculative batches.
3060        let b1 = journal.new_batch();
3061        let b2 = journal.new_batch();
3062
3063        // Add different items to each batch.
3064        let op_a = create_operation::<F>(100);
3065        let op_b = create_operation::<F>(200);
3066        let b1 = b1.add(op_a.clone());
3067        let b2 = b2.add(op_b);
3068
3069        // Merkleize and verify independent roots.
3070        let m1 = journal.merkle.with_mem(|mem| b1.merkleize(mem));
3071        let m2 = journal.merkle.with_mem(|mem| b2.merkleize(mem));
3072        assert_ne!(batch_root(&journal, &m1), batch_root(&journal, &m2));
3073        assert_ne!(batch_root(&journal, &m1), original_root);
3074        assert_ne!(batch_root(&journal, &m2), original_root);
3075
3076        // Journal root should be unchanged (batches are speculative).
3077        assert_eq!(journal_root(&journal), original_root);
3078
3079        // Apply batch 1.
3080        let expected_root = batch_root(&journal, &m1);
3081        journal = journal.apply_batch(&m1).await.unwrap();
3082
3083        // Journal should now match the applied batch's root.
3084        assert_eq!(journal_root(&journal), expected_root);
3085        assert_eq!(*journal.size(), 11);
3086    }
3087
3088    #[test_traced("INFO")]
3089    fn test_speculative_batch_mmr() {
3090        let executor = deterministic::Runner::default();
3091        executor.start(test_speculative_batch_inner::<mmr::Family>);
3092    }
3093
3094    #[test_traced("INFO")]
3095    fn test_speculative_batch_mmb() {
3096        let executor = deterministic::Runner::default();
3097        executor.start(test_speculative_batch_inner::<mmb::Family>);
3098    }
3099
3100    /// Verify stacking: create batch A, merkleize, create batch B from merkleized A,
3101    /// merkleize, and apply. Verify root and items.
3102    async fn test_speculative_batch_stacking_inner<F: Family + PartialEq>(context: Context) {
3103        let mut journal = create_journal_with_ops::<F>(context, "batch_stacking", 10).await;
3104
3105        let op_a = create_operation::<F>(100);
3106        let op_b = create_operation::<F>(200);
3107
3108        let (merkleized_a, merkleized_b) = {
3109            let batch_a = journal.new_batch().add(op_a.clone());
3110            let merkleized_a = journal.merkle.with_mem(|mem| batch_a.merkleize(mem));
3111
3112            let batch_b = merkleized_a.new_batch::<Sha256>().add(op_b.clone());
3113            let merkleized_b = journal.merkle.with_mem(|mem| batch_b.merkleize(mem));
3114            (merkleized_a, merkleized_b)
3115        };
3116
3117        let expected_root = batch_root(&journal, &merkleized_b);
3118        journal = journal.apply_batch(&merkleized_b).await.unwrap();
3119        drop(merkleized_a);
3120
3121        assert_eq!(journal_root(&journal), expected_root);
3122        assert_eq!(*journal.size(), 12);
3123
3124        // Verify both items were appended correctly.
3125        let read_a = journal.read(*Location::<F>::new(10)).await.unwrap();
3126        assert_eq!(read_a, op_a);
3127        let read_b = journal.read(*Location::<F>::new(11)).await.unwrap();
3128        assert_eq!(read_b, op_b);
3129    }
3130
3131    #[test_traced("INFO")]
3132    fn test_speculative_batch_stacking_mmr() {
3133        let executor = deterministic::Runner::default();
3134        executor.start(test_speculative_batch_stacking_inner::<mmr::Family>);
3135    }
3136
3137    #[test_traced("INFO")]
3138    fn test_speculative_batch_stacking_mmb() {
3139        let executor = deterministic::Runner::default();
3140        executor.start(test_speculative_batch_stacking_inner::<mmb::Family>);
3141    }
3142
3143    /// Verify sequential batch application: apply batch A, then build and apply batch B
3144    /// from the committed state. Verify root and items.
3145    async fn test_speculative_batch_sequential_inner<F: Family + PartialEq>(context: Context) {
3146        let mut journal = create_journal_with_ops::<F>(context, "batch_sequential", 10).await;
3147
3148        let op_a = create_operation::<F>(100);
3149        let op_b = create_operation::<F>(200);
3150
3151        // Apply batch A.
3152        let batch_a = journal.new_batch().add(op_a.clone());
3153        let merkleized_a = journal.merkle.with_mem(|mem| batch_a.merkleize(mem));
3154        journal = journal.apply_batch(&merkleized_a).await.unwrap();
3155        assert_eq!(*journal.size(), 11);
3156
3157        // Apply batch B (built on top of the committed A).
3158        let batch_b = journal.new_batch().add(op_b.clone());
3159        let merkleized_b = journal.merkle.with_mem(|mem| batch_b.merkleize(mem));
3160        let expected_root = batch_root(&journal, &merkleized_b);
3161        journal = journal.apply_batch(&merkleized_b).await.unwrap();
3162
3163        assert_eq!(journal_root(&journal), expected_root);
3164        assert_eq!(*journal.size(), 12);
3165
3166        // Verify both items were appended correctly.
3167        let read_a = journal.read(*Location::<F>::new(10)).await.unwrap();
3168        assert_eq!(read_a, op_a);
3169        let read_b = journal.read(*Location::<F>::new(11)).await.unwrap();
3170        assert_eq!(read_b, op_b);
3171    }
3172
3173    #[test_traced("INFO")]
3174    fn test_speculative_batch_sequential_mmr() {
3175        let executor = deterministic::Runner::default();
3176        executor.start(test_speculative_batch_sequential_inner::<mmr::Family>);
3177    }
3178
3179    #[test_traced("INFO")]
3180    fn test_speculative_batch_sequential_mmb() {
3181        let executor = deterministic::Runner::default();
3182        executor.start(test_speculative_batch_sequential_inner::<mmb::Family>);
3183    }
3184
3185    async fn test_stale_batch_sibling_inner<F: Family + PartialEq>(context: Context) {
3186        let mut journal = create_empty_journal::<F>(context.child("open"), "stale-sibling").await;
3187        let op_a = create_operation::<F>(1);
3188        let op_b = create_operation::<F>(2);
3189
3190        // Create two batches from the same base.
3191        let batch_a = journal.new_batch().add(op_a.clone());
3192        let merkleized_a = journal.merkle.with_mem(|mem| batch_a.merkleize(mem));
3193        let batch_b = journal.new_batch().add(op_b);
3194        let merkleized_b = journal.merkle.with_mem(|mem| batch_b.merkleize(mem));
3195
3196        // Apply A, then commit and sync so the recovered state below includes it (reopen
3197        // rewinds to the last commit operation).
3198        journal = journal.apply_batch(&merkleized_a).await.unwrap();
3199        let commit_op = TestOp::<F>::CommitFloor(None, Location::<F>::new(0));
3200        (journal, _) = journal.append(&commit_op).await.unwrap();
3201        journal = journal.sync().await.unwrap();
3202        let root_a = journal_root(&journal);
3203        let size_a = journal.size();
3204        let (_, ops) = journal
3205            .proof(Location::<F>::new(0), NZU64!(1), 0)
3206            .await
3207            .unwrap();
3208        assert_eq!(ops, vec![op_a.clone()]);
3209
3210        // Apply B -- should fail (stale).
3211        let result = journal.apply_batch(&merkleized_b).await;
3212        assert!(
3213            matches!(
3214                result,
3215                Err(super::Error::Merkle(merkle::Error::StaleBatch { .. }))
3216            ),
3217            "expected StaleBatch, got {result:?}"
3218        );
3219
3220        // The eager reject mutated nothing: reopening recovers exactly A's state.
3221        let journal = create_empty_journal::<F>(context.child("reopen"), "stale-sibling").await;
3222        assert_eq!(journal_root(&journal), root_a);
3223        assert_eq!(journal.size(), size_a);
3224        let (_, ops) = journal
3225            .proof(Location::<F>::new(0), NZU64!(1), 0)
3226            .await
3227            .unwrap();
3228        assert_eq!(ops, vec![op_a]);
3229        journal.destroy().await.unwrap();
3230    }
3231
3232    #[test_traced("INFO")]
3233    fn test_stale_batch_sibling_mmr() {
3234        let executor = deterministic::Runner::default();
3235        executor.start(test_stale_batch_sibling_inner::<mmr::Family>);
3236    }
3237
3238    #[test_traced("INFO")]
3239    fn test_stale_batch_sibling_mmb() {
3240        let executor = deterministic::Runner::default();
3241        executor.start(test_stale_batch_sibling_inner::<mmb::Family>);
3242    }
3243
3244    async fn test_stale_batch_chained_inner<F: Family + PartialEq>(context: Context) {
3245        let mut journal = create_journal_with_ops::<F>(context, "stale-chained", 5).await;
3246
3247        // Parent batch, then fork two children.
3248        let parent_batch = journal.new_batch().add(create_operation::<F>(10));
3249        let parent = journal.merkle.with_mem(|mem| parent_batch.merkleize(mem));
3250        let batch_a = parent.new_batch::<Sha256>().add(create_operation::<F>(20));
3251        let child_a = journal.merkle.with_mem(|mem| batch_a.merkleize(mem));
3252        let batch_b = parent.new_batch::<Sha256>().add(create_operation::<F>(30));
3253        let child_b = journal.merkle.with_mem(|mem| batch_b.merkleize(mem));
3254
3255        // Apply child_a, then child_b should be stale.
3256        journal = journal.apply_batch(&child_a).await.unwrap();
3257        let result = journal.apply_batch(&child_b).await;
3258        drop(parent);
3259        assert!(
3260            matches!(
3261                result,
3262                Err(super::Error::Merkle(merkle::Error::StaleBatch { .. }))
3263            ),
3264            "expected StaleBatch for sibling, got {result:?}"
3265        );
3266    }
3267
3268    #[test_traced("INFO")]
3269    fn test_stale_batch_chained_mmr() {
3270        let executor = deterministic::Runner::default();
3271        executor.start(test_stale_batch_chained_inner::<mmr::Family>);
3272    }
3273
3274    #[test_traced("INFO")]
3275    fn test_stale_batch_chained_mmb() {
3276        let executor = deterministic::Runner::default();
3277        executor.start(test_stale_batch_chained_inner::<mmb::Family>);
3278    }
3279
3280    async fn test_stale_batch_parent_before_child_inner<F: Family + PartialEq>(context: Context) {
3281        let mut journal = create_empty_journal::<F>(context, "stale-parent-first").await;
3282
3283        // Create parent, then child.
3284        let parent_batch = journal.new_batch().add(create_operation::<F>(1));
3285        let parent = journal.merkle.with_mem(|mem| parent_batch.merkleize(mem));
3286        let child_batch = parent.new_batch::<Sha256>().add(create_operation::<F>(2));
3287        let child = journal.merkle.with_mem(|mem| child_batch.merkleize(mem));
3288
3289        let expected_root = batch_root(&journal, &child);
3290
3291        // Apply parent, then child (sequential commit).
3292        journal = journal.apply_batch(&parent).await.unwrap();
3293        journal = journal.apply_batch(&child).await.unwrap();
3294
3295        assert_eq!(journal_root(&journal), expected_root);
3296        assert_eq!(*journal.size(), 2);
3297    }
3298
3299    #[test_traced("INFO")]
3300    fn test_stale_batch_parent_before_child_mmr() {
3301        let executor = deterministic::Runner::default();
3302        executor.start(test_stale_batch_parent_before_child_inner::<mmr::Family>);
3303    }
3304
3305    #[test_traced("INFO")]
3306    fn test_stale_batch_parent_before_child_mmb() {
3307        let executor = deterministic::Runner::default();
3308        executor.start(test_stale_batch_parent_before_child_inner::<mmb::Family>);
3309    }
3310
3311    async fn test_stale_batch_child_before_parent_inner<F: Family + PartialEq>(context: Context) {
3312        let mut journal = create_empty_journal::<F>(context, "stale-child-first").await;
3313
3314        // Create parent, then child.
3315        let parent_batch = journal.new_batch().add(create_operation::<F>(1));
3316        let parent = journal.merkle.with_mem(|mem| parent_batch.merkleize(mem));
3317        let child_batch = parent.new_batch::<Sha256>().add(create_operation::<F>(2));
3318        let child = journal.merkle.with_mem(|mem| child_batch.merkleize(mem));
3319
3320        // Apply child first (full chain) -- parent should now be stale.
3321        journal = journal.apply_batch(&child).await.unwrap();
3322        let result = journal.apply_batch(&parent).await;
3323        assert!(
3324            matches!(
3325                result,
3326                Err(super::Error::Merkle(merkle::Error::StaleBatch { .. }))
3327            ),
3328            "expected StaleBatch for parent after child applied, got {result:?}"
3329        );
3330    }
3331
3332    #[test_traced("INFO")]
3333    fn test_stale_batch_child_before_parent_mmr() {
3334        let executor = deterministic::Runner::default();
3335        executor.start(test_stale_batch_child_before_parent_inner::<mmr::Family>);
3336    }
3337
3338    #[test_traced("INFO")]
3339    fn test_stale_batch_child_before_parent_mmb() {
3340        let executor = deterministic::Runner::default();
3341        executor.start(test_stale_batch_child_before_parent_inner::<mmb::Family>);
3342    }
3343
3344    /// Apply parent then child: child skips already-committed ancestor items.
3345    async fn test_apply_batch_skip_ancestor_items_inner<F: Family + PartialEq>(context: Context) {
3346        let mut journal = create_journal_with_ops::<F>(context, "rp-skip", 3).await;
3347
3348        // Parent: 2 items.
3349        let parent_batch = journal
3350            .new_batch()
3351            .add(create_operation::<F>(10))
3352            .add(create_operation::<F>(11));
3353        let parent = journal.merkle.with_mem(|mem| parent_batch.merkleize(mem));
3354
3355        // Child: 3 more items.
3356        let child_batch = parent
3357            .new_batch::<Sha256>()
3358            .add(create_operation::<F>(20))
3359            .add(create_operation::<F>(21))
3360            .add(create_operation::<F>(22));
3361        let child = journal.merkle.with_mem(|mem| child_batch.merkleize(mem));
3362
3363        // Apply parent.
3364        journal = journal.apply_batch(&parent).await.unwrap();
3365
3366        // Apply child (ancestor items already committed, skipped automatically).
3367        journal = journal.apply_batch(&child).await.unwrap();
3368
3369        // Verify all items are present.
3370        let (_, ops) = journal
3371            .proof(Location::<F>::new(3), NZU64!(5), 0)
3372            .await
3373            .unwrap();
3374        assert_eq!(ops.len(), 5);
3375    }
3376
3377    #[test_traced("INFO")]
3378    fn test_apply_batch_skip_ancestor_items_mmr() {
3379        let executor = deterministic::Runner::default();
3380        executor.start(test_apply_batch_skip_ancestor_items_inner::<mmr::Family>);
3381    }
3382
3383    #[test_traced("INFO")]
3384    fn test_apply_batch_skip_ancestor_items_mmb() {
3385        let executor = deterministic::Runner::default();
3386        executor.start(test_apply_batch_skip_ancestor_items_inner::<mmb::Family>);
3387    }
3388
3389    /// `apply_batch` works correctly across a 3-level chain.
3390    async fn test_apply_batch_cross_batch_inner<F: Family + PartialEq>(context: Context) {
3391        let mut journal = create_journal_with_ops::<F>(context, "rp-cross", 2).await;
3392
3393        // Grandparent: 3 items.
3394        let grandparent_batch = journal
3395            .new_batch()
3396            .add(create_operation::<F>(3))
3397            .add(create_operation::<F>(4))
3398            .add(create_operation::<F>(5));
3399        let grandparent = journal
3400            .merkle
3401            .with_mem(|mem| grandparent_batch.merkleize(mem));
3402
3403        // Parent: 2 items.
3404        let parent_batch = grandparent
3405            .new_batch::<Sha256>()
3406            .add(create_operation::<F>(6))
3407            .add(create_operation::<F>(7));
3408        let parent = journal.merkle.with_mem(|mem| parent_batch.merkleize(mem));
3409
3410        // Child: 1 item.
3411        let child_batch = parent.new_batch::<Sha256>().add(create_operation::<F>(8));
3412        let child = journal.merkle.with_mem(|mem| child_batch.merkleize(mem));
3413
3414        // Apply grandparent, then parent, then child sequentially.
3415        journal = journal.apply_batch(&grandparent).await.unwrap();
3416
3417        // Apply parent (ancestor items already committed, skipped automatically).
3418        journal = journal.apply_batch(&parent).await.unwrap();
3419
3420        // Apply child (ancestor items already committed, skipped automatically).
3421        journal = journal.apply_batch(&child).await.unwrap();
3422
3423        // All 8 items (2 base + 3 + 2 + 1) should be present.
3424        assert_eq!(*journal.size(), 8);
3425
3426        // Verify the actual items at each location.
3427        let (_, ops) = journal
3428            .proof(Location::<F>::new(2), NZU64!(6), 0)
3429            .await
3430            .unwrap();
3431        for (i, op) in ops.iter().enumerate() {
3432            assert_eq!(*op, create_operation::<F>((i + 3) as u8));
3433        }
3434    }
3435
3436    #[test_traced("INFO")]
3437    fn test_apply_batch_cross_batch_mmr() {
3438        let executor = deterministic::Runner::default();
3439        executor.start(test_apply_batch_cross_batch_inner::<mmr::Family>);
3440    }
3441
3442    #[test_traced("INFO")]
3443    fn test_apply_batch_cross_batch_mmb() {
3444        let executor = deterministic::Runner::default();
3445        executor.start(test_apply_batch_cross_batch_inner::<mmb::Family>);
3446    }
3447
3448    /// merkleize_with produces the same root as add + merkleize.
3449    async fn test_merkleize_with_matches_add_inner<F: Family + PartialEq>(context: Context) {
3450        let journal = create_journal_with_ops::<F>(context, "mw-matches", 5).await;
3451
3452        let ops = vec![
3453            create_operation::<F>(10),
3454            create_operation::<F>(11),
3455            create_operation::<F>(12),
3456        ];
3457
3458        // add + merkleize
3459        let mut batch = journal.new_batch();
3460        for op in &ops {
3461            batch = batch.add(op.clone());
3462        }
3463        let expected = journal.merkle.with_mem(|mem| batch.merkleize(mem));
3464
3465        // merkleize_with
3466        let batch = journal.new_batch();
3467        let actual = journal
3468            .merkle
3469            .with_mem(|mem| merkleize_with(batch, mem, ops));
3470
3471        assert_eq!(
3472            batch_root(&journal, &actual),
3473            batch_root(&journal, &expected)
3474        );
3475    }
3476
3477    #[test_traced("INFO")]
3478    fn test_merkleize_with_matches_add_mmr() {
3479        let executor = deterministic::Runner::default();
3480        executor.start(test_merkleize_with_matches_add_inner::<mmr::Family>);
3481    }
3482
3483    #[test_traced("INFO")]
3484    fn test_merkleize_with_matches_add_mmb() {
3485        let executor = deterministic::Runner::default();
3486        executor.start(test_merkleize_with_matches_add_inner::<mmb::Family>);
3487    }
3488
3489    /// merkleize_with items are readable after apply.
3490    async fn test_merkleize_with_apply_inner<F: Family + PartialEq>(context: Context) {
3491        let mut journal = create_journal_with_ops::<F>(context, "mw-apply", 5).await;
3492
3493        let ops = vec![create_operation::<F>(10), create_operation::<F>(11)];
3494        let batch = journal.new_batch();
3495        let merkleized = journal
3496            .merkle
3497            .with_mem(|mem| merkleize_with(batch, mem, ops.clone()));
3498
3499        let expected_root = batch_root(&journal, &merkleized);
3500        journal = journal.apply_batch(&merkleized).await.unwrap();
3501
3502        assert_eq!(journal_root(&journal), expected_root);
3503        assert_eq!(*journal.size(), 7);
3504
3505        assert_eq!(journal.read(5).await.unwrap(), ops[0]);
3506        assert_eq!(journal.read(6).await.unwrap(), ops[1]);
3507    }
3508
3509    #[test_traced("INFO")]
3510    fn test_merkleize_with_apply_mmr() {
3511        let executor = deterministic::Runner::default();
3512        executor.start(test_merkleize_with_apply_inner::<mmr::Family>);
3513    }
3514
3515    #[test_traced("INFO")]
3516    fn test_merkleize_with_apply_mmb() {
3517        let executor = deterministic::Runner::default();
3518        executor.start(test_merkleize_with_apply_inner::<mmb::Family>);
3519    }
3520
3521    /// Apply C (grandchild of A) after only A is committed. B's journal items
3522    /// must still be applied -- skip only A's items.
3523    async fn test_apply_batch_skips_only_committed_ancestor_items_inner<F: Family + PartialEq>(
3524        context: Context,
3525    ) {
3526        let mut journal = create_empty_journal::<F>(context.child("storage"), "skip-partial").await;
3527
3528        // Build chain: A -> B -> C
3529        let a_batch = journal.new_batch().add(create_operation::<F>(1));
3530        let a = journal.merkle.with_mem(|mem| a_batch.merkleize(mem));
3531        let b_batch = a.new_batch::<Sha256>().add(create_operation::<F>(2));
3532        let b = journal.merkle.with_mem(|mem| b_batch.merkleize(mem));
3533        let c_batch = b.new_batch::<Sha256>().add(create_operation::<F>(3));
3534        let c = journal.merkle.with_mem(|mem| c_batch.merkleize(mem));
3535
3536        // Apply A, then apply C directly (skipping B's apply_batch).
3537        journal = journal.apply_batch(&a).await.unwrap();
3538        journal = journal.apply_batch(&c).await.unwrap();
3539
3540        // All 3 items should be in the journal.
3541        assert_eq!(*journal.size(), 3);
3542
3543        // Build a reference that applies all three sequentially.
3544        let mut reference =
3545            create_empty_journal::<F>(context.child("ref"), "skip-partial-ref").await;
3546        for i in 1..=3u8 {
3547            (reference, _) = reference.append(&create_operation::<F>(i)).await.unwrap();
3548        }
3549        assert_eq!(journal_root(&journal), journal_root(&reference));
3550    }
3551
3552    #[test_traced("INFO")]
3553    fn test_apply_batch_skips_only_committed_ancestor_items_mmr() {
3554        let executor = deterministic::Runner::default();
3555        executor.start(test_apply_batch_skips_only_committed_ancestor_items_inner::<mmr::Family>);
3556    }
3557
3558    #[test_traced("INFO")]
3559    fn test_apply_batch_skips_only_committed_ancestor_items_mmb() {
3560        let executor = deterministic::Runner::default();
3561        executor.start(test_apply_batch_skips_only_committed_ancestor_items_inner::<mmb::Family>);
3562    }
3563
3564    /// A descendant whose uncommitted ancestor was dropped must fail before
3565    /// appending any of its retained journal items.
3566    async fn test_apply_batch_detects_dropped_uncommitted_ancestor_inner<F: Family + PartialEq>(
3567        context: Context,
3568    ) {
3569        let journal =
3570            create_empty_journal::<F>(context.child("storage"), "dropped-uncommitted").await;
3571
3572        let a_batch = journal.new_batch().add(create_operation::<F>(1));
3573        let a = journal.merkle.with_mem(|mem| a_batch.merkleize(mem));
3574        let b_batch = a.new_batch::<Sha256>().add(create_operation::<F>(2));
3575        let b = journal.merkle.with_mem(|mem| b_batch.merkleize(mem));
3576
3577        drop(a);
3578        let c_batch = b.new_batch::<Sha256>().add(create_operation::<F>(3));
3579        let c = journal.merkle.with_mem(|mem| c_batch.merkleize(mem));
3580        drop(b);
3581
3582        assert_eq!(c.ancestor_base_leaves, 1);
3583        assert_eq!(c.ancestor_items.len(), 1);
3584
3585        let result = journal.apply_batch(&c).await;
3586        assert!(
3587            matches!(
3588                result,
3589                Err(super::Error::Merkle(merkle::Error::AncestorDropped { expected, .. }))
3590                    if expected == c.inner.size()
3591            ),
3592            "expected AncestorDropped, got {result:?}"
3593        );
3594    }
3595
3596    #[test_traced("INFO")]
3597    fn test_apply_batch_detects_dropped_uncommitted_ancestor_mmb() {
3598        let executor = deterministic::Runner::default();
3599        executor.start(test_apply_batch_detects_dropped_uncommitted_ancestor_inner::<mmb::Family>);
3600    }
3601
3602    /// A dropped committed prefix must not shift the remaining uncommitted
3603    /// ancestor items back to the original fork point.
3604    async fn test_apply_batch_after_committed_ancestor_dropped_inner<F: Family + PartialEq>(
3605        context: Context,
3606    ) {
3607        let mut journal =
3608            create_empty_journal::<F>(context.child("storage"), "dropped-committed").await;
3609
3610        let mut a_batch = journal.new_batch();
3611        for i in 0..8u8 {
3612            a_batch = a_batch.add(create_operation::<F>(i));
3613        }
3614        let a = journal.merkle.with_mem(|mem| a_batch.merkleize(mem));
3615        let b_batch = a.new_batch::<Sha256>().add(create_operation::<F>(8));
3616        let b = journal.merkle.with_mem(|mem| b_batch.merkleize(mem));
3617
3618        journal = journal.apply_batch(&a).await.unwrap();
3619        drop(a);
3620
3621        let c_batch = b.new_batch::<Sha256>().add(create_operation::<F>(9));
3622        let c = journal.merkle.with_mem(|mem| c_batch.merkleize(mem));
3623
3624        // Only B remains in the retained ancestor suffix.
3625        assert_eq!(c.ancestor_items.len(), 1);
3626        assert_eq!(c.ancestor_base_leaves, *journal.size());
3627        assert_eq!(c.inner.ancestor_base_size, journal.merkle.size());
3628
3629        drop(b);
3630        journal = journal.apply_batch(&c).await.unwrap();
3631        assert_eq!(*journal.size(), 10);
3632
3633        let mut reference =
3634            create_empty_journal::<F>(context.child("reference"), "dropped-committed-ref").await;
3635        for i in 0..10u8 {
3636            (reference, _) = reference.append(&create_operation::<F>(i)).await.unwrap();
3637        }
3638        assert_eq!(journal_root(&journal), journal_root(&reference));
3639    }
3640
3641    #[test_traced("INFO")]
3642    fn test_apply_batch_after_committed_ancestor_dropped_mmb() {
3643        let executor = deterministic::Runner::default();
3644        executor.start(test_apply_batch_after_committed_ancestor_dropped_inner::<mmb::Family>);
3645    }
3646
3647    /// Merkleization retains a speculative suffix after its committed prefix is released.
3648    async fn test_merkleize_after_committed_prefix_dropped_inner<F: Family + PartialEq>(
3649        context: Context,
3650    ) {
3651        let mut journal =
3652            create_empty_journal::<F>(context.child("storage"), "committed-prefix").await;
3653
3654        // Build a speculative suffix over a prefix that will be committed independently.
3655        let prefix_items = (0..8u8).map(create_operation::<F>).collect();
3656        let (prefix, _) = journal
3657            .merkleize(journal.new_batch(), prefix_items, 0)
3658            .await
3659            .unwrap();
3660        let pending_items = (8..10u8).map(create_operation::<F>).collect();
3661        let (pending, _) = journal
3662            .merkleize(prefix.new_batch::<Sha256>(), pending_items, 0)
3663            .await
3664            .unwrap();
3665
3666        // Commit and release the prefix. Its Merkle nodes now resolve through the snapshot.
3667        journal = journal.apply_batch(&prefix).await.unwrap();
3668        drop(prefix);
3669
3670        // The child batch is the pending suffix's only remaining owner. Merkleization must retain
3671        // that suffix through root computation.
3672        let child_batch = pending.new_batch::<Sha256>();
3673        drop(pending);
3674        let (child, expected_root) = journal
3675            .merkleize(child_batch, vec![create_operation::<F>(10)], 0)
3676            .await
3677            .unwrap();
3678        journal = journal.apply_batch(&child).await.unwrap();
3679
3680        assert_eq!(journal_root(&journal), expected_root);
3681        assert_eq!(*journal.size(), 11);
3682    }
3683
3684    #[test_traced("INFO")]
3685    fn test_merkleize_after_committed_prefix_dropped_mmr() {
3686        let executor = deterministic::Runner::default();
3687        executor.start(test_merkleize_after_committed_prefix_dropped_inner::<mmr::Family>);
3688    }
3689
3690    #[test_traced("INFO")]
3691    fn test_merkleize_after_committed_prefix_dropped_mmb() {
3692        let executor = deterministic::Runner::default();
3693        executor.start(test_merkleize_after_committed_prefix_dropped_inner::<mmb::Family>);
3694    }
3695
3696    /// A detached merkleization job owns the full ancestor chain after its waiter is dropped.
3697    #[test_traced("INFO")]
3698    fn test_merkleize_retains_ancestors_after_cancellation() {
3699        deterministic::Runner::default().start(|context| async move {
3700            let strategy = Rayon::new(NZUsize!(2)).unwrap();
3701            let merkle_cfg = merkle_config_with("cancelled-merkleize", &context, strategy);
3702            let journal_cfg = journal_config("cancelled-merkleize", &context);
3703            type RayonJournal = Journal<
3704                mmr::Family,
3705                Context,
3706                ContiguousJournal<Context, DropMonitor<TestOp<mmr::Family>>>,
3707                Sha256,
3708                Rayon,
3709            >;
3710            let journal = RayonJournal::new(
3711                context,
3712                merkle_cfg,
3713                journal_cfg,
3714                |_: &DropMonitor<TestOp<mmr::Family>>| false,
3715                ForwardFold,
3716            )
3717            .await
3718            .unwrap();
3719
3720            let a_items = (0..8u8)
3721                .map(create_operation::<mmr::Family>)
3722                .map(DropMonitor::untracked)
3723                .collect();
3724            let a_batch = journal.new_batch().add_many(a_items);
3725            let a = journal.merkle.with_mem(|mem| a_batch.merkleize(mem));
3726            let b_items = (8..10u8)
3727                .map(create_operation::<mmr::Family>)
3728                .map(DropMonitor::untracked)
3729                .collect();
3730            let b_batch = a.new_batch::<Sha256>().add_many(b_items);
3731            let b = journal.merkle.with_mem(|mem| b_batch.merkleize(mem));
3732
3733            let ancestor = Arc::downgrade(&a.inner);
3734            let c_batch = b.new_batch::<Sha256>();
3735            drop(b);
3736
3737            let release = block_strategy(journal.strategy(), 2);
3738            let (item, clean_drop) = DropMonitor::tracked(create_operation::<mmr::Family>(10));
3739            let mut merkleize = Box::pin(journal.merkleize(c_batch, vec![item], 0));
3740            assert!(futures::poll!(merkleize.as_mut()).is_pending());
3741            drop(merkleize);
3742            drop(a);
3743
3744            assert!(ancestor.upgrade().is_some());
3745            drop(release);
3746            assert!(
3747                clean_drop
3748                    .recv_timeout(Duration::from_secs(10))
3749                    .expect("detached merkleization did not finish"),
3750                "detached merkleization panicked"
3751            );
3752            assert!(ancestor.upgrade().is_none());
3753        });
3754    }
3755}