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