Skip to main content

commonware_storage/journal/
authenticated.rs

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