Skip to main content

commonware_storage/qmdb/current/
batch.rs

1//! Batch mutation API for Current QMDBs.
2//!
3//! Wraps the [`any::batch`] API.
4
5use crate::{
6    index::Unordered as UnorderedIndex,
7    journal::contiguous::{Contiguous, Mutable},
8    merkle::{
9        self, batch::MerkleizedBatch as GenericMerkleizedBatch, mem::Mem,
10        storage::Storage as MerkleStorage, Graftable, Location, Position, Readable,
11    },
12    qmdb::{
13        any::{
14            self,
15            batch::{DiffCursors, DiffEntry, Staged as AnyStaged, StagedUpdates},
16            operation::{update, Operation},
17            ValueEncoding,
18        },
19        batch_chain::Bounds,
20        bitmap::Shared,
21        current::{
22            db::{compute_db_root, read_graft_inputs},
23            grafting,
24        },
25        operation::Key,
26        Error,
27    },
28    Context,
29};
30use ahash::AHashMap;
31use commonware_codec::Codec;
32use commonware_cryptography::{Digest, Hasher};
33use commonware_parallel::Strategy;
34use commonware_utils::bitmap::{self, Readable as _};
35use core::ops::Range;
36use std::sync::Arc;
37
38/// Speculative chunk-level bitmap overlay.
39///
40/// Instead of tracking individual pushed bits and cleared locations, maintains materialized chunk
41/// bytes for every chunk that differs from the parent bitmap. This directly produces the chunk data
42/// needed for grafted MMR leaf computation.
43#[derive(Clone, Debug, Default)]
44pub(crate) struct ChunkOverlay<const N: usize> {
45    /// Dirty chunks: chunk_idx -> materialized chunk bytes.
46    ///
47    /// Iteration order is not observed by any consumer.
48    pub(crate) chunks: AHashMap<usize, [u8; N]>,
49    /// Total number of bits (parent + new operations).
50    pub(crate) len: u64,
51}
52
53impl<const N: usize> ChunkOverlay<N> {
54    const CHUNK_BITS: u64 = bitmap::Prunable::<N>::CHUNK_SIZE_BITS;
55
56    fn new(len: u64, capacity: usize) -> Self {
57        Self {
58            chunks: AHashMap::with_capacity(capacity),
59            len,
60        }
61    }
62
63    /// Load-or-create a chunk: returns a mutable reference to the materialized chunk bytes. On
64    /// first access for an existing chunk, reads from `base`.
65    fn chunk_mut<B: bitmap::Readable<N>>(&mut self, base: &B, idx: usize) -> &mut [u8; N] {
66        self.chunks.entry(idx).or_insert_with(|| {
67            let base_len = base.len();
68            let base_complete = base.complete_chunks();
69            let base_has_partial = !base_len.is_multiple_of(Self::CHUNK_BITS);
70            if idx < base_complete {
71                base.get_chunk(idx)
72            } else if idx == base_complete && base_has_partial {
73                base.last_chunk().0
74            } else {
75                bitmap::BitMap::<N>::EMPTY_CHUNK
76            }
77        })
78    }
79
80    /// Set a single bit (used for pushes and active operations).
81    fn set_bit<B: bitmap::Readable<N>>(&mut self, base: &B, loc: u64) {
82        let idx = bitmap::Prunable::<N>::to_chunk_index(loc);
83        let rel = (loc % Self::CHUNK_BITS) as usize;
84        let chunk = self.chunk_mut(base, idx);
85        chunk[rel / 8] |= 1 << (rel % 8);
86    }
87
88    /// Clear a single bit (used for superseded locations). `pruned_chunks` is passed in by the
89    /// caller so the hot loop in `build_chunk_overlay` reads it once rather than per call.
90    /// Skips locations in pruned chunks since those bits are already inactive.
91    fn clear_bit<B: bitmap::Readable<N>>(&mut self, base: &B, pruned_chunks: usize, loc: u64) {
92        let idx = bitmap::Prunable::<N>::to_chunk_index(loc);
93        if idx < pruned_chunks {
94            return;
95        }
96        let rel = (loc % Self::CHUNK_BITS) as usize;
97        let chunk = self.chunk_mut(base, idx);
98        chunk[rel / 8] &= !(1 << (rel % 8));
99    }
100
101    /// Get a dirty chunk's bytes, or `None` if unmodified.
102    pub(crate) fn get(&self, idx: usize) -> Option<&[u8; N]> {
103        self.chunks.get(&idx)
104    }
105
106    /// Number of complete chunks.
107    pub(crate) const fn complete_chunks(&self) -> usize {
108        (self.len / Self::CHUNK_BITS) as usize
109    }
110}
111
112/// Bitmap-accelerated floor scan over a layered `BitmapBatch` chain. Skips locations where the
113/// bitmap bit is unset, avoiding I/O reads for inactive operations.
114///
115/// Mirrors the contract on `any::batch::fill_candidates`: may return only locations that are
116/// *possibly* active in `[floor, tip)`, may skip locations only when known inactive. The
117/// floor-raise loop revalidates each candidate, so false positives are tolerated; false
118/// negatives are forbidden.
119///
120/// False positives can arise two ways:
121/// - In the committed prefix, an uncommitted ancestor batch in the chain may have superseded
122///   the location -- the committed bitmap doesn't reflect uncommitted shadows.
123/// - Beyond the committed bitmap, locations are returned as sequential candidates (one per
124///   index) without per-location filtering, so any inactive uncommitted op shows up here.
125pub(crate) fn next_candidate<F: Graftable, B: bitmap::Readable<N>, const N: usize>(
126    bitmap: &B,
127    floor: Location<F>,
128    tip: u64,
129) -> Option<Location<F>> {
130    let floor = *floor;
131    let bitmap_len = bitmap.len();
132    let committed_end = bitmap_len.min(tip);
133    if floor < committed_end {
134        if let Some(idx) = bitmap.ones_iter_from(floor).next() {
135            if idx < committed_end {
136                return Some(Location::<F>::new(idx));
137            }
138        }
139    }
140    let candidate = floor.max(bitmap_len);
141    (candidate < tip).then(|| Location::<F>::new(candidate))
142}
143
144/// Fill `out` with up to `limit` floor-raise candidates in `[floor, tip)` over the layered
145/// `BitmapBatch` chain, returning the next `floor`. Produces the same sequence as repeatedly
146/// calling [`next_candidate`].
147pub(crate) fn fill_candidates<F: Graftable, B: bitmap::Readable<N>, const N: usize>(
148    bitmap: &B,
149    floor: Location<F>,
150    tip: u64,
151    limit: usize,
152    out: &mut Vec<Location<F>>,
153) -> Location<F> {
154    let mut scan = floor;
155    while out.len() < limit {
156        let Some(candidate) = next_candidate(bitmap, scan, tip) else {
157            break;
158        };
159        out.push(candidate);
160        scan = Location::<F>::new(*candidate + 1);
161    }
162    scan
163}
164
165/// Adapter that resolves ops MMR nodes for a batch's `compute_current_layer`.
166///
167/// Tries the batch chain's sync [`Readable`] first (which covers nodes appended or overwritten
168/// by the batch, plus anything still in the in-memory MMR). Falls through to the base's async
169/// [`MerkleStorage`].
170struct BatchStorageAdapter<
171    'a,
172    F: Graftable,
173    D: Digest,
174    R: Readable<Family = F, Digest = D, Error = merkle::Error<F>>,
175    S: MerkleStorage<F, Digest = D>,
176> {
177    batch: &'a R,
178    base: &'a S,
179    _phantom: core::marker::PhantomData<(F, D)>,
180}
181
182impl<
183        'a,
184        F: Graftable,
185        D: Digest,
186        R: Readable<Family = F, Digest = D, Error = merkle::Error<F>>,
187        S: MerkleStorage<F, Digest = D>,
188    > BatchStorageAdapter<'a, F, D, R, S>
189{
190    const fn new(batch: &'a R, base: &'a S) -> Self {
191        Self {
192            batch,
193            base,
194            _phantom: core::marker::PhantomData,
195        }
196    }
197}
198
199impl<
200        F: Graftable,
201        D: Digest,
202        R: Readable<Family = F, Digest = D, Error = merkle::Error<F>>,
203        S: MerkleStorage<F, Digest = D>,
204    > MerkleStorage<F> for BatchStorageAdapter<'_, F, D, R, S>
205{
206    type Digest = D;
207
208    fn size(&self) -> Position<F> {
209        self.batch.size()
210    }
211    async fn get_node(&self, pos: Position<F>) -> Result<Option<D>, merkle::Error<F>> {
212        if let Some(node) = self.batch.get_node(pos) {
213            return Ok(Some(node));
214        }
215        self.base.get_node(pos).await
216    }
217}
218
219/// Layers a [`GenericMerkleizedBatch`] over a [`Mem`] for node resolution.
220///
221/// [`GenericMerkleizedBatch::get_node`] only covers the batch chain; committed positions
222/// return `None`. This adapter falls through to the committed Mem for those positions.
223struct BatchOverMem<'a, F: Graftable, D: Digest, S: Strategy> {
224    batch: &'a GenericMerkleizedBatch<F, D, S>,
225    mem: &'a Mem<F, D>,
226}
227
228impl<F: Graftable, D: Digest, S: Strategy> Readable for BatchOverMem<'_, F, D, S> {
229    type Family = F;
230    type Digest = D;
231    type Error = merkle::Error<F>;
232
233    fn size(&self) -> Position<F> {
234        self.batch.size()
235    }
236
237    fn get_node(&self, pos: Position<F>) -> Option<D> {
238        if let Some(d) = self.batch.get_node(pos) {
239            return Some(d);
240        }
241        self.mem.get_node(pos)
242    }
243
244    fn pruning_boundary(&self) -> Location<F> {
245        self.batch.pruning_boundary()
246    }
247}
248
249/// A speculative batch of mutations whose root digest has not yet been computed,
250/// in contrast to [`MerkleizedBatch`].
251///
252/// Wraps a [`any::batch::UnmerkleizedBatch`] and adds bitmap and grafted MMR parent state
253/// needed to compute the current layer during [`merkleize`](Self::merkleize).
254pub struct UnmerkleizedBatch<F, H, U, const N: usize, S: Strategy>
255where
256    F: Graftable,
257    U: update::Update + Send + Sync,
258    H: Hasher,
259    Operation<F, U>: Codec,
260{
261    /// The inner any-layer batch that handles mutations, journal, and floor raise.
262    inner: any::batch::UnmerkleizedBatch<F, H, U, S>,
263
264    /// Parent's grafted MMR state.
265    grafted_parent: Arc<merkle::batch::MerkleizedBatch<F, H::Digest, S>>,
266
267    /// Parent's bitmap state (COW, Arc-based).
268    bitmap_parent: BitmapBatch<N>,
269}
270
271/// Staged batch returned by [`UnmerkleizedBatch::stage`].
272pub struct Staged<F, H, U, const N: usize, S: Strategy>
273where
274    F: Graftable,
275    U: update::Update + Send + Sync,
276    H: Hasher,
277    Operation<F, U>: Codec,
278{
279    inner: AnyStaged<F, H, U, S>,
280    grafted_parent: Arc<merkle::batch::MerkleizedBatch<F, H::Digest, S>>,
281    bitmap_parent: BitmapBatch<N>,
282}
283
284/// A speculative batch of operations whose root digest has been computed, in contrast to
285/// [`UnmerkleizedBatch`].
286///
287/// Wraps an [`any::batch::MerkleizedBatch`] and adds the bitmap and grafted MMR state needed to
288/// compute the canonical root.
289///
290/// # Branch validity
291///
292/// A `MerkleizedBatch` is a branch-scoped view rooted at a specific committed prefix of the DB. It
293/// is not an immutable snapshot.
294///
295/// Internally, the batch chain terminates in the DB's committed bitmap via `BitmapBatch::Base`.
296/// That committed bitmap evolves in place as [`Db::apply_batch`](super::db::Db::apply_batch),
297/// [`Db::prune`](super::db::Db::prune), and [`Db::rewind`](super::db::Db::rewind) update the DB.
298///
299/// Reads through this batch's chain, constructing child batches from it, and applying it later are
300/// only semantically correct while its ancestor chain is still the committed prefix of the DB. In
301/// other words, every successful [`apply_batch`](super::db::Db::apply_batch) since this batch was
302/// merkleized must have applied an ancestor of this batch.
303///
304/// Once a non-ancestor batch is applied, this batch and all of its descendants become invalid
305/// objects. The library does not guard against continued use after that point.
306///
307/// Applying an invalid batch is caught by the any-layer staleness check and returns
308/// [`Error::StaleBatch`] without mutating committed state, so `apply_batch` itself cannot corrupt
309/// the DB. The one exception is equal-size sibling branches (where both branches have the same
310/// total operation count): the staleness check is size-based and cannot distinguish them, so
311/// applying a descendant of one sibling after the other was already applied can silently corrupt
312/// snapshot/log state. Callers must not apply batches from an orphaned branch.
313///
314/// Rules of thumb:
315/// - Drop any `Arc<MerkleizedBatch>` you no longer intend to apply.
316/// - Extending a batch after `apply_batch` has consumed it (building a child off the just-applied
317///   parent) is safe. The committed bitmap now equals the parent's post-apply state, so child reads
318///   are consistent.
319/// - Extending a batch after a different branch has been applied is not safe. Do not call `get`,
320///   `new_batch`, or `apply_batch` on that branch again.
321pub struct MerkleizedBatch<
322    F: Graftable,
323    D: Digest,
324    U: update::Update + Send + Sync,
325    const N: usize,
326    S: Strategy,
327> where
328    Operation<F, U>: Send + Sync,
329{
330    /// Inner any-layer batch (ops MMR, diff, floor, commit loc, sizes).
331    pub(crate) inner: Arc<any::batch::MerkleizedBatch<F, D, U, S>>,
332
333    /// Grafted MMR state.
334    pub(crate) grafted: Arc<merkle::batch::MerkleizedBatch<F, D, S>>,
335
336    /// COW bitmap state (for use as a parent in speculative batches).
337    pub(crate) bitmap: BitmapBatch<N>,
338
339    /// The canonical root (ops root + grafted root + partial chunk).
340    pub(crate) canonical_root: D,
341}
342
343impl<F, H, U, const N: usize, S: Strategy> UnmerkleizedBatch<F, H, U, N, S>
344where
345    F: Graftable,
346    U: update::Update + Send + Sync,
347    H: Hasher,
348    Operation<F, U>: Codec,
349{
350    pub(super) const fn new(
351        inner: any::batch::UnmerkleizedBatch<F, H, U, S>,
352        grafted_parent: Arc<merkle::batch::MerkleizedBatch<F, H::Digest, S>>,
353        bitmap_parent: BitmapBatch<N>,
354    ) -> Self {
355        Self {
356            inner,
357            grafted_parent,
358            bitmap_parent,
359        }
360    }
361
362    /// Record a mutation. Use `Some(value)` for update/create, `None` for delete.
363    ///
364    /// If the same key is written multiple times within a batch, the last
365    /// value wins.
366    pub fn write(mut self, key: U::Key, value: Option<U::Value>) -> Self {
367        self.inner = self.inner.write(key, value);
368        self
369    }
370
371    /// Read through: mutations -> ancestor diffs -> committed DB.
372    pub async fn get<E, C, I>(
373        &self,
374        key: &U::Key,
375        db: &super::db::Db<F, E, C, I, H, U, N, S>,
376    ) -> Result<Option<U::Value>, Error<F>>
377    where
378        E: Context,
379        C: Contiguous<Item = Operation<F, U>>,
380        I: UnorderedIndex<Value = Location<F>> + 'static,
381    {
382        self.inner.get(key, &db.any).await
383    }
384
385    /// Batch read multiple keys.
386    ///
387    /// Returns results in the same order as the input keys.
388    pub async fn get_many<E, C, I>(
389        &self,
390        keys: &[&U::Key],
391        db: &super::db::Db<F, E, C, I, H, U, N, S>,
392    ) -> Result<Vec<Option<U::Value>>, Error<F>>
393    where
394        E: Context,
395        C: Contiguous<Item = Operation<F, U>>,
396        I: UnorderedIndex<Value = Location<F>> + 'static,
397    {
398        self.inner.get_many(keys, &db.any).await
399    }
400
401    /// Batch read multiple keys and return a staged batch for the same keys.
402    ///
403    /// Returns results in the same order as the input keys. The staged batch records updates by
404    /// read index: the initial keys occupy `0..keys.len()`, and each [`expand`](Staged::expand)
405    /// appends another index range.
406    pub async fn stage<E, C, I>(
407        self,
408        keys: &[&U::Key],
409        db: &super::db::Db<F, E, C, I, H, U, N, S>,
410    ) -> Result<(Vec<Option<U::Value>>, Staged<F, H, U, N, S>), Error<F>>
411    where
412        E: Context,
413        C: Contiguous<Item = Operation<F, U>>,
414        I: UnorderedIndex<Value = Location<F>> + 'static,
415    {
416        let Self {
417            inner,
418            grafted_parent,
419            bitmap_parent,
420        } = self;
421        let (values, inner) = inner.stage(keys, &db.any).await?;
422        Ok((
423            values,
424            Staged {
425                inner,
426                grafted_parent,
427                bitmap_parent,
428            },
429        ))
430    }
431}
432
433impl<F, H, U, const N: usize, S: Strategy> Staged<F, H, U, N, S>
434where
435    F: Graftable,
436    U: update::Update + Send + Sync,
437    H: Hasher,
438    Operation<F, U>: Codec,
439{
440    /// Expand this staged batch with more reads.
441    ///
442    /// Existing read indices remain stable. Newly read keys are appended to the staged read set and
443    /// assigned the returned range. The returned values are in the same order as `keys`.
444    ///
445    /// Expansion does not deduplicate against previously staged keys and does not observe values the
446    /// caller has computed for earlier staged slots but not yet passed to
447    /// [`merkleize`](Staged::merkleize).
448    pub async fn expand<E, C, I>(
449        self,
450        keys: &[&U::Key],
451        db: &super::db::Db<F, E, C, I, H, U, N, S>,
452    ) -> Result<(Range<usize>, Vec<Option<U::Value>>, Self), Error<F>>
453    where
454        E: Context,
455        C: Contiguous<Item = Operation<F, U>>,
456        I: UnorderedIndex<Value = Location<F>> + 'static,
457    {
458        let Self {
459            inner,
460            grafted_parent,
461            bitmap_parent,
462        } = self;
463        let (range, values, inner) = inner.expand(keys, &db.any).await?;
464        Ok((
465            range,
466            values,
467            Self {
468                inner,
469                grafted_parent,
470                bitmap_parent,
471            },
472        ))
473    }
474}
475
476impl<F, K, V, H, const N: usize, S: Strategy> Staged<F, H, update::Unordered<K, V>, N, S>
477where
478    F: Graftable,
479    K: Key,
480    V: ValueEncoding,
481    H: Hasher,
482    Operation<F, update::Unordered<K, V>>: Codec,
483{
484    /// Record updates for staged reads and upserts for unread keys, then merkleize.
485    ///
486    /// Consumes the staged handle and write vectors. Call [`expand`](Staged::expand) before this
487    /// method if more keys must be read into the staged index space.
488    ///
489    /// A `Some` value is an upsert. `None` is a delete. Update indices refer to the staged read
490    /// set: the initial `stage` input followed by any [`expand`](Staged::expand) ranges. `metadata`
491    /// is committed with the returned batch.
492    ///
493    /// # Panics
494    ///
495    /// Panics if any update's `read_index` is out of the staged read range.
496    #[allow(clippy::type_complexity)]
497    #[tracing::instrument(
498        name = "qmdb.current.unordered.batch.merkleize.staged",
499        level = "info",
500        skip_all,
501        fields(updates = updates.len() as u64, upserts = upserts.len() as u64),
502    )]
503    pub async fn merkleize<E, C, I>(
504        self,
505        updates: Vec<(usize, Option<V::Value>)>,
506        upserts: Vec<(K, Option<V::Value>)>,
507        metadata: Option<V::Value>,
508        db: &super::db::Db<F, E, C, I, H, update::Unordered<K, V>, N, S>,
509    ) -> Result<Arc<MerkleizedBatch<F, H::Digest, update::Unordered<K, V>, N, S>>, Error<F>>
510    where
511        E: Context,
512        C: Mutable<Item = Operation<F, update::Unordered<K, V>>>,
513        I: UnorderedIndex<Value = Location<F>> + 'static,
514    {
515        let Self {
516            inner,
517            grafted_parent,
518            bitmap_parent,
519        } = self;
520
521        // Overlap the update resolution with a committed-prefix candidate prefetch.
522        // Candidates come from the speculative `bitmap_parent` (the same source the floor
523        // raise scans below), clamped to the committed prefix inside the helper.
524        let (inner, staged_updates, prefetched) = inner
525            .resolve_updates_prefetched(updates, upserts, &db.any, |floor, tip, limit, out| {
526                fill_candidates(&bitmap_parent, floor, tip, limit, out)
527            })
528            .await?;
529        let inner = inner
530            .merkleize_with_floor_scan(
531                &db.any,
532                metadata,
533                staged_updates,
534                Some(prefetched),
535                |floor, tip, limit, out| fill_candidates(&bitmap_parent, floor, tip, limit, out),
536            )
537            .await?;
538        compute_current_layer(inner, db, &grafted_parent, &bitmap_parent).await
539    }
540}
541
542impl<F, K, V, H, const N: usize, S: Strategy> Staged<F, H, update::Ordered<K, V>, N, S>
543where
544    F: Graftable,
545    K: Key,
546    V: ValueEncoding,
547    H: Hasher,
548    Operation<F, update::Ordered<K, V>>: Codec,
549{
550    /// Record updates for staged reads and upserts for unread keys, then merkleize.
551    ///
552    /// Consumes the staged handle and write vectors. Call [`expand`](Staged::expand) before this
553    /// method if more keys must be read into the staged index space.
554    ///
555    /// A `Some` value is an upsert. `None` is a delete. Update indices refer to the staged read
556    /// set: the initial `stage` input followed by any [`expand`](Staged::expand) ranges. `metadata`
557    /// is committed with the returned batch.
558    ///
559    /// # Panics
560    ///
561    /// Panics if any update's `read_index` is out of the staged read range.
562    #[allow(clippy::type_complexity)]
563    #[tracing::instrument(
564        name = "qmdb.current.ordered.batch.merkleize.staged",
565        level = "info",
566        skip_all,
567        fields(updates = updates.len() as u64, upserts = upserts.len() as u64),
568    )]
569    pub async fn merkleize<E, C, I>(
570        self,
571        updates: Vec<(usize, Option<V::Value>)>,
572        upserts: Vec<(K, Option<V::Value>)>,
573        metadata: Option<V::Value>,
574        db: &super::db::Db<F, E, C, I, H, update::Ordered<K, V>, N, S>,
575    ) -> Result<Arc<MerkleizedBatch<F, H::Digest, update::Ordered<K, V>, N, S>>, Error<F>>
576    where
577        E: Context,
578        C: Mutable<Item = Operation<F, update::Ordered<K, V>>>,
579        I: crate::index::Ordered<Value = Location<F>> + 'static,
580    {
581        let Self {
582            inner,
583            grafted_parent,
584            bitmap_parent,
585        } = self;
586        let (inner, staged_updates) = inner.resolve_updates(updates, upserts, db.any.strategy());
587        let inner = inner
588            .merkleize_with_floor_scan(
589                &db.any,
590                metadata,
591                staged_updates,
592                |floor, tip, limit, out| fill_candidates(&bitmap_parent, floor, tip, limit, out),
593            )
594            .await?;
595        compute_current_layer(inner, db, &grafted_parent, &bitmap_parent).await
596    }
597}
598
599// Unordered merkleize.
600impl<F, K, V, H, const N: usize, S: Strategy> UnmerkleizedBatch<F, H, update::Unordered<K, V>, N, S>
601where
602    F: Graftable,
603    K: Key,
604    V: ValueEncoding,
605    H: Hasher,
606    Operation<F, update::Unordered<K, V>>: Codec,
607{
608    /// Resolve mutations into operations, merkleize, and return an `Arc<MerkleizedBatch>`.
609    #[allow(clippy::type_complexity)]
610    #[tracing::instrument(
611        name = "qmdb.current.unordered.batch.merkleize",
612        level = "info",
613        skip_all
614    )]
615    pub async fn merkleize<E, C, I>(
616        self,
617        db: &super::db::Db<F, E, C, I, H, update::Unordered<K, V>, N, S>,
618        metadata: Option<V::Value>,
619    ) -> Result<Arc<MerkleizedBatch<F, H::Digest, update::Unordered<K, V>, N, S>>, Error<F>>
620    where
621        E: Context,
622        C: Mutable<Item = Operation<F, update::Unordered<K, V>>>,
623        I: UnorderedIndex<Value = Location<F>> + 'static,
624    {
625        let Self {
626            inner,
627            grafted_parent,
628            bitmap_parent,
629        } = self;
630        // Use the speculative parent bitmap rather than the committed `any` bitmap.
631        let inner = inner
632            .merkleize_with_floor_scan(
633                &db.any,
634                metadata,
635                StagedUpdates::<F, update::Unordered<K, V>>::new(),
636                None,
637                |floor, tip, limit, out| fill_candidates(&bitmap_parent, floor, tip, limit, out),
638            )
639            .await?;
640        compute_current_layer(inner, db, &grafted_parent, &bitmap_parent).await
641    }
642}
643
644// Ordered merkleize.
645impl<F, K, V, H, const N: usize, S: Strategy> UnmerkleizedBatch<F, H, update::Ordered<K, V>, N, S>
646where
647    F: Graftable,
648    K: Key,
649    V: ValueEncoding,
650    H: Hasher,
651    Operation<F, update::Ordered<K, V>>: Codec,
652{
653    /// Resolve mutations into operations, merkleize, and return an `Arc<MerkleizedBatch>`.
654    #[allow(clippy::type_complexity)]
655    #[tracing::instrument(
656        name = "qmdb.current.ordered.batch.merkleize",
657        level = "info",
658        skip_all
659    )]
660    pub async fn merkleize<E, C, I>(
661        self,
662        db: &super::db::Db<F, E, C, I, H, update::Ordered<K, V>, N, S>,
663        metadata: Option<V::Value>,
664    ) -> Result<Arc<MerkleizedBatch<F, H::Digest, update::Ordered<K, V>, N, S>>, Error<F>>
665    where
666        E: Context,
667        C: Mutable<Item = Operation<F, update::Ordered<K, V>>>,
668        I: crate::index::Ordered<Value = Location<F>> + 'static,
669    {
670        let Self {
671            inner,
672            grafted_parent,
673            bitmap_parent,
674        } = self;
675        // Use the speculative parent bitmap rather than the committed `any` bitmap.
676        let inner = inner
677            .merkleize_with_floor_scan(
678                &db.any,
679                metadata,
680                StagedUpdates::<F, update::Ordered<K, V>>::new(),
681                |floor, tip, limit, out| fill_candidates(&bitmap_parent, floor, tip, limit, out),
682            )
683            .await?;
684        compute_current_layer(inner, db, &grafted_parent, &bitmap_parent).await
685    }
686}
687
688/// Derive all bitmap mutations (pushes + clears) for this batch in a single pass over the diff and
689/// ancestor diffs. Avoids iterating raw operations.
690///
691/// Pushes: one bit per operation in the batch. All false except active diff entries (whose `loc`
692/// falls in the batch) and the CommitFloor (last op).
693///
694/// Clears: previous CommitFloor, plus the most recent superseded location for each mutated key. We
695/// search back through ancestors to find the most recent active location; if none exists, we clear
696/// the committed DB location (`base_old_loc`).
697#[allow(clippy::type_complexity)]
698fn build_chunk_overlay<F: Graftable, U, B: bitmap::Readable<N>, const N: usize>(
699    base: &B,
700    batch_len: usize,
701    batch_base: u64,
702    diff: &[(U::Key, DiffEntry<F, U::Value>)],
703    ancestor_diffs: &[Arc<Vec<(U::Key, DiffEntry<F, U::Value>)>>],
704) -> ChunkOverlay<N>
705where
706    U: update::Update,
707{
708    let total_bits = base.len() + batch_len as u64;
709    let appended_chunks = (batch_len as u64).div_ceil(ChunkOverlay::<N>::CHUNK_BITS) as usize;
710    let mut overlay = ChunkOverlay::new(total_bits, diff.len() + appended_chunks + 1);
711    let pruned_chunks = base.pruned_chunks();
712
713    // 1. CommitFloor (last op) is always active.
714    let commit_loc = batch_base + batch_len as u64 - 1;
715    overlay.set_bit(base, commit_loc);
716
717    // 2. Inactivate previous CommitFloor.
718    overlay.clear_bit(base, pruned_chunks, batch_base - 1);
719
720    // 3. Set active bits + clear superseded locations from the diff. The diff is key-sorted,
721    // so ancestor resolution streams (one cursor per ancestor diff).
722    let mut ancestors = DiffCursors::new(ancestor_diffs.iter().map(|d| d.as_slice()));
723    for (key, entry) in diff {
724        // Set the active bit for this key's final location.
725        if let Some(loc) = entry.loc() {
726            if *loc >= batch_base && *loc < batch_base + batch_len as u64 {
727                overlay.set_bit(base, *loc);
728            }
729        }
730
731        // Clear the most recent superseded location. Older locations were already cleared by the
732        // ancestor batch that superseded them.
733        let mut prev_loc = entry.base_old_loc();
734        if let Some(ancestor_entry) = ancestors.resolve(key) {
735            prev_loc = ancestor_entry.loc();
736        }
737        if let Some(old) = prev_loc {
738            overlay.clear_bit(base, pruned_chunks, *old);
739        }
740    }
741
742    // Ensure all new complete chunks beyond the parent are materialized, so downstream consumers
743    // don't read from the parent and panic on out-of-range indices. Uses chunk_mut to inherit the
744    // parent's partial chunk data when idx == parent_complete (avoiding loss of existing bits).
745    let parent_complete = base.complete_chunks();
746    let new_complete = overlay.complete_chunks();
747    for idx in parent_complete..new_complete {
748        overlay.chunk_mut(base, idx);
749    }
750
751    overlay
752}
753
754/// Compute the current layer (bitmap + grafted MMR + canonical root) on top of a merkleized any
755/// batch.
756///
757/// Builds a chunk overlay from the diff, computes grafted MMR leaves from dirty chunks, and
758/// produces the `Arc<MerkleizedBatch>` directly.
759async fn compute_current_layer<F, E, U, C, I, H, const N: usize, S>(
760    inner: Arc<any::batch::MerkleizedBatch<F, H::Digest, U, S>>,
761    current_db: &super::db::Db<F, E, C, I, H, U, N, S>,
762    grafted_parent: &Arc<merkle::batch::MerkleizedBatch<F, H::Digest, S>>,
763    bitmap_parent: &BitmapBatch<N>,
764) -> Result<Arc<MerkleizedBatch<F, H::Digest, U, N, S>>, Error<F>>
765where
766    F: Graftable,
767    E: Context,
768    U: update::Update + Send + Sync,
769    C: Contiguous<Item = Operation<F, U>>,
770    I: UnorderedIndex<Value = Location<F>>,
771    H: Hasher,
772    S: Strategy,
773    Operation<F, U>: Codec,
774{
775    let batch_len = inner.journal_batch.items().len();
776    let batch_base = inner.bounds.total_size - batch_len as u64;
777
778    // Build chunk overlay: materialized bytes for every dirty chunk.
779    let overlay = build_chunk_overlay::<F, U, _, N>(
780        bitmap_parent,
781        batch_len,
782        batch_base,
783        &inner.diff,
784        &inner.ancestor_diffs,
785    );
786
787    let grafting_height = grafting::height::<N>();
788    let ops_tree_adapter =
789        BatchStorageAdapter::new(&inner.journal_batch, &current_db.any.log.merkle);
790
791    // Snapshot ops_leaves for the post-batch state (the canonical root we're about to compute
792    // sees this many ops). Thread it through `graftable_chunks` derivation and root computation.
793    let overlay_ops_leaves = Location::<F>::new(inner.bounds.total_size);
794
795    // Distinguish three counters:
796    //   - new_complete_chunks: chunks with all bits filled in the post-batch bitmap
797    //   - graftable_overlay:      chunks committed by the grafted tree (have a single h=G ancestor)
798    //   - graftable_parent:       grafted-tree leaf count from the parent (structural source of truth)
799    //
800    // The pending chunk (if any) sits at index `graftable_overlay` and is excluded from the
801    // grafted tree; its digest is hashed directly into the canonical root.
802    let new_complete_chunks = overlay.complete_chunks();
803    let graftable_overlay = grafting::graftable_chunks::<F>(*overlay_ops_leaves, grafting_height)
804        .min(new_complete_chunks as u64) as usize;
805    let graftable_parent = *grafted_parent.leaves() as usize;
806    let pruned_chunks = bitmap_parent.pruned_chunks();
807    assert!(
808        pruned_chunks <= graftable_parent && graftable_parent <= graftable_overlay && graftable_overlay <= new_complete_chunks,
809        "invariant violated: pruned={pruned_chunks} graftable_parent={graftable_parent} graftable_overlay={graftable_overlay} new_complete={new_complete_chunks}"
810    );
811
812    // Build the set of chunk indices whose grafted-leaf needs (re)computing:
813    //   1) Dirty chunks (bits changed in this batch) within the graftable range.
814    //   2) Pending -> graftable transitions: chunks newly graftable because the ops tree built
815    //      their h=G ancestor in this batch. Their bitmap bytes may not be dirty (the chunk
816    //      became graftable via ops growth alone) but they need a grafted-leaf entry now.
817    let mut chunk_indices_to_update: Vec<usize> = overlay
818        .chunks
819        .iter()
820        .filter(|(&idx, _)| idx < graftable_overlay && idx >= pruned_chunks)
821        .map(|(&idx, _)| idx)
822        .collect();
823    chunk_indices_to_update.extend(graftable_parent..graftable_overlay);
824    chunk_indices_to_update.sort_unstable();
825    chunk_indices_to_update.dedup();
826    let chunks_to_update = chunk_indices_to_update.into_iter().map(|idx| {
827        let chunk = overlay
828            .get(idx)
829            .copied()
830            .unwrap_or_else(|| bitmap_parent.get_chunk(idx));
831        (idx, chunk)
832    });
833
834    // Prefetch each chunk's covering ops-tree node, then run graft hashing and the grafted
835    // MMR build/merkleize as one job on the strategy (against a snapshot of the committed
836    // grafted tree) instead of occupying the calling task. An empty graft set hashes
837    // nothing, so it merkleizes inline rather than paying for a job handoff.
838    let graft_inputs = read_graft_inputs::<F, _, N>(&ops_tree_adapter, chunks_to_update).await?;
839    let grafted_hasher = grafting::hasher::<F, H>(grafting_height);
840    let grafted_batch = if graft_inputs.is_empty() {
841        grafted_parent
842            .new_batch()
843            .merkleize(&current_db.grafted_tree, &grafted_hasher)
844    } else {
845        let grafted_parent = Arc::clone(grafted_parent);
846        let grafted_tree = Arc::clone(&current_db.grafted_tree);
847        current_db
848            .strategy
849            .clone()
850            .spawn(move |strategy| {
851                let new_leaves = grafting::graft_chunk_digests::<H, _, N>(&strategy, graft_inputs);
852                let mut grafted_batch = grafted_parent.new_batch();
853                let old_grafted_leaves = *grafted_parent.leaves() as usize;
854                for (chunk_idx, digest) in new_leaves {
855                    if chunk_idx < old_grafted_leaves {
856                        grafted_batch = grafted_batch
857                            .update_leaf_digest(Location::<F>::new(chunk_idx as u64), digest)
858                            .expect("update_leaf_digest failed");
859                    } else {
860                        grafted_batch = grafted_batch.add_leaf_digest(digest);
861                    }
862                }
863                grafted_batch.merkleize(&grafted_tree, &grafted_hasher)
864            })
865            .await
866    };
867
868    // Build the layered bitmap (parent + overlay) before computing the canonical root, so that
869    // compute_db_root sees newly completed chunks. Using bitmap_parent alone would miss chunks
870    // that transitioned from partial to complete in this batch.
871    let bitmap_batch = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
872        parent: bitmap_parent.clone(),
873        overlay: Arc::new(overlay),
874        shared: Arc::clone(bitmap_parent.shared()),
875    }));
876
877    // Compute canonical root. The grafted batch alone cannot resolve committed nodes,
878    // so layer it over the committed grafted MMR.
879    let ops_root = inner.root();
880    let layered = BatchOverMem {
881        batch: &grafted_batch,
882        mem: &current_db.grafted_tree,
883    };
884    let grafted_storage =
885        grafting::Storage::<F, H, _, _>::new(&layered, grafting_height, &ops_tree_adapter);
886    // Compute partial chunk (last incomplete chunk, if any). The partial chunk lives at
887    // index `new_complete_chunks` (the chunk currently being filled with bits) -- distinct
888    // from `graftable_overlay` (the grafted-tree boundary). At gh >= 3, partial and pending can
889    // coexist; this branch only handles partial. The pending chunk (when present) is read
890    // from the bitmap inside `compute_db_root` via `pending_chunk()`.
891    let partial = {
892        let rem = bitmap_batch.len() % BitmapBatch::<N>::CHUNK_SIZE_BITS;
893        if rem == 0 {
894            None
895        } else {
896            let idx = new_complete_chunks;
897            let chunk = bitmap_batch.get_chunk(idx);
898            Some((chunk, rem))
899        }
900    };
901    let canonical_root = compute_db_root::<F, H, _, _, N>(
902        &bitmap_batch,
903        &grafted_storage,
904        overlay_ops_leaves,
905        partial,
906        inner.bounds.inactivity_floor,
907        &ops_root,
908    )
909    .await?;
910
911    Ok(Arc::new(MerkleizedBatch {
912        inner,
913        grafted: grafted_batch,
914        bitmap: bitmap_batch,
915        canonical_root,
916    }))
917}
918
919/// A view of the committed bitmap plus zero or more speculative overlay `Layer`s.
920///
921/// The chain terminates in a `Base` that references the shared committed bitmap. No validity
922/// check is performed. Callers must ensure they only read through batches whose chains are
923/// still valid prefixes of committed state (see [`Shared`]'s docs).
924#[derive(Clone, Debug)]
925pub(crate) enum BitmapBatch<const N: usize> {
926    /// Chain terminal: shared reference to the committed bitmap.
927    Base(Arc<Shared<N>>),
928    /// Speculative layer on top of a parent batch.
929    Layer(Arc<BitmapBatchLayer<N>>),
930}
931
932/// The data behind a [`BitmapBatch::Layer`].
933#[derive(Debug)]
934pub(crate) struct BitmapBatchLayer<const N: usize> {
935    pub(crate) parent: BitmapBatch<N>,
936    /// Chunk-level overlay: materialized bytes for every chunk that differs from parent.
937    pub(crate) overlay: Arc<ChunkOverlay<N>>,
938    /// Cached terminal [`Shared`] so [`BitmapBatch::shared`] and
939    /// [`BitmapBatch::pruned_chunks`] answer in O(1) instead of walking the chain.
940    pub(crate) shared: Arc<Shared<N>>,
941}
942
943impl<const N: usize> BitmapBatch<N> {
944    const CHUNK_SIZE_BITS: u64 = bitmap::Prunable::<N>::CHUNK_SIZE_BITS;
945
946    /// Return the terminal [`Shared`] at the bottom of the chain.
947    fn shared(&self) -> &Arc<Shared<N>> {
948        match self {
949            Self::Base(s) => s,
950            Self::Layer(layer) => &layer.shared,
951        }
952    }
953
954    /// Return a chain equivalent to `self` with any `Layer` whose overlay is now fully committed
955    /// replaced by a direct reference to the committed bitmap. Since `apply_batch` commits
956    /// contiguous prefixes, committed `Layer`s are always at the bottom of the chain.
957    fn trim_committed(&self) -> Self {
958        let shared = self.shared();
959        let committed = bitmap::Readable::<N>::len(shared.as_ref());
960        let mut kept = Vec::new();
961        let mut current = self;
962        while let Self::Layer(layer) = current {
963            if layer.overlay.len <= committed {
964                break;
965            }
966            kept.push(Arc::clone(&layer.overlay));
967            current = &layer.parent;
968        }
969        let mut result = Self::Base(Arc::clone(shared));
970        for overlay in kept.into_iter().rev() {
971            result = Self::Layer(Arc::new(BitmapBatchLayer {
972                parent: result,
973                overlay,
974                shared: Arc::clone(shared),
975            }));
976        }
977        result
978    }
979}
980
981impl<const N: usize> bitmap::Readable<N> for BitmapBatch<N> {
982    fn complete_chunks(&self) -> usize {
983        (self.len() / Self::CHUNK_SIZE_BITS) as usize
984    }
985
986    fn get_chunk(&self, idx: usize) -> [u8; N] {
987        // Walk the layer chain. Each layer's overlay either holds the chunk (return it) or
988        // doesn't (descend).
989        let mut current = self;
990        loop {
991            match current {
992                Self::Base(shared) => return shared.get_chunk(idx),
993                Self::Layer(layer) => {
994                    if let Some(&chunk) = layer.overlay.get(idx) {
995                        return chunk;
996                    }
997                    current = &layer.parent;
998                }
999            }
1000        }
1001    }
1002
1003    fn last_chunk(&self) -> ([u8; N], u64) {
1004        let total = self.len();
1005        if total == 0 {
1006            return (bitmap::BitMap::<N>::EMPTY_CHUNK, 0);
1007        }
1008        let rem = total % Self::CHUNK_SIZE_BITS;
1009        let bits_in_last = if rem == 0 { Self::CHUNK_SIZE_BITS } else { rem };
1010        let idx = if rem == 0 {
1011            self.complete_chunks().saturating_sub(1)
1012        } else {
1013            self.complete_chunks()
1014        };
1015        (self.get_chunk(idx), bits_in_last)
1016    }
1017
1018    fn pruned_chunks(&self) -> usize {
1019        self.shared().pruned_chunks()
1020    }
1021
1022    fn len(&self) -> u64 {
1023        match self {
1024            Self::Base(shared) => bitmap::Readable::<N>::len(shared.as_ref()),
1025            Self::Layer(layer) => layer.overlay.len,
1026        }
1027    }
1028}
1029
1030impl<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize, S: Strategy>
1031    MerkleizedBatch<F, D, U, N, S>
1032where
1033    Operation<F, U>: Send + Sync,
1034{
1035    /// Return the canonical root.
1036    pub const fn root(&self) -> D {
1037        self.canonical_root
1038    }
1039
1040    /// Return the QMDB ops-only root.
1041    pub fn ops_root(&self) -> D {
1042        self.inner.root()
1043    }
1044
1045    /// Return the [`Bounds`] of the batch.
1046    pub fn bounds(&self) -> &Bounds<F> {
1047        self.inner.bounds()
1048    }
1049
1050    /// Return the batch's safe sync boundary.
1051    ///
1052    /// This equals the boundary [`super::db::Db::sync_boundary`] reports once this batch is applied.
1053    pub fn sync_boundary(&self) -> Location<F> {
1054        // Derive from the commit's chunk-aligned inactivity floor, the same quantity the DB uses
1055        // after apply. Deliberately not the physical bitmap pruning boundary, which can lag the
1056        // inactivity floor when pruning has not run.
1057        super::db::sync_boundary::<F, N>(
1058            *self.inner.bounds().inactivity_floor / bitmap::Prunable::<N>::CHUNK_SIZE_BITS,
1059            self.inner.bounds().total_size,
1060        )
1061    }
1062}
1063
1064impl<F: Graftable, D: Digest, U: update::Update + Send + Sync, const N: usize, S: Strategy>
1065    MerkleizedBatch<F, D, U, N, S>
1066where
1067    Operation<F, U>: Codec,
1068{
1069    /// Create a new speculative batch of operations with this batch as its parent.
1070    ///
1071    /// All uncommitted ancestors in the chain must be kept alive until the child (or any
1072    /// descendant) is merkleized. Dropping an uncommitted ancestor causes data
1073    /// loss detected at `apply_batch` time.
1074    ///
1075    /// This is only valid while `self` is still on the winning branch. If a different branch has
1076    /// been applied since `self` was created, `self` is no longer a valid parent and must not be
1077    /// extended.
1078    pub fn new_batch<H>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, U, N, S>
1079    where
1080        H: Hasher<Digest = D>,
1081    {
1082        UnmerkleizedBatch::new(
1083            self.inner.new_batch::<H>(),
1084            Arc::clone(&self.grafted),
1085            self.bitmap.trim_committed(),
1086        )
1087    }
1088
1089    /// Read through: local diff -> ancestor diffs -> committed DB.
1090    ///
1091    /// This is only valid while `self` remains on the committed prefix. If a non-ancestor batch
1092    /// has been applied since `self` was merkleized, do not read through it.
1093    pub async fn get<E, C, I, H>(
1094        &self,
1095        key: &U::Key,
1096        db: &super::db::Db<F, E, C, I, H, U, N, S>,
1097    ) -> Result<Option<U::Value>, Error<F>>
1098    where
1099        E: Context,
1100        C: Contiguous<Item = Operation<F, U>>,
1101        I: UnorderedIndex<Value = Location<F>> + 'static,
1102        H: Hasher<Digest = D>,
1103    {
1104        self.inner.get(key, &db.any).await
1105    }
1106
1107    /// Batch read multiple keys.
1108    ///
1109    /// Returns results in the same order as the input keys.
1110    pub async fn get_many<E, C, I, H>(
1111        &self,
1112        keys: &[&U::Key],
1113        db: &super::db::Db<F, E, C, I, H, U, N, S>,
1114    ) -> Result<Vec<Option<U::Value>>, Error<F>>
1115    where
1116        E: Context,
1117        C: Contiguous<Item = Operation<F, U>>,
1118        I: UnorderedIndex<Value = Location<F>> + 'static,
1119        H: Hasher<Digest = D>,
1120    {
1121        self.inner.get_many(keys, &db.any).await
1122    }
1123}
1124
1125impl<F, E, C, I, H, U, const N: usize, S> super::db::Db<F, E, C, I, H, U, N, S>
1126where
1127    F: Graftable,
1128    E: Context,
1129    U: update::Update + Send + Sync,
1130    C: Contiguous<Item = Operation<F, U>>,
1131    I: UnorderedIndex<Value = Location<F>>,
1132    H: Hasher,
1133    S: Strategy,
1134    Operation<F, U>: Codec,
1135{
1136    /// Create an initial [`MerkleizedBatch`] from the current committed DB state.
1137    ///
1138    /// The returned batch is rooted at the current committed prefix, but it is not a persistent
1139    /// snapshot across later divergent commits. If some other branch is applied afterward, this
1140    /// batch is no longer valid and must not be read through, extended, or applied.
1141    pub fn to_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, U, N, S>> {
1142        let grafted = self.grafted_snapshot();
1143        Arc::new(MerkleizedBatch {
1144            inner: self.any.to_batch(),
1145            grafted,
1146            bitmap: BitmapBatch::Base(Arc::clone(&self.any.bitmap)),
1147            canonical_root: self.root,
1148        })
1149    }
1150}
1151
1152#[cfg(any(test, feature = "test-traits"))]
1153mod trait_impls {
1154    use super::*;
1155    use crate::{
1156        journal::contiguous::Mutable,
1157        qmdb::any::traits::{
1158            BatchableDb, MerkleizedBatch as MerkleizedBatchTrait,
1159            UnmerkleizedBatch as UnmerkleizedBatchTrait,
1160        },
1161    };
1162    use std::future::Future;
1163
1164    type CurrentDb<F, E, C, I, H, U, const N: usize, S> =
1165        crate::qmdb::current::db::Db<F, E, C, I, H, U, N, S>;
1166
1167    impl<F, K, V, H, E, C, I, const N: usize, S>
1168        UnmerkleizedBatchTrait<CurrentDb<F, E, C, I, H, update::Unordered<K, V>, N, S>>
1169        for UnmerkleizedBatch<F, H, update::Unordered<K, V>, N, S>
1170    where
1171        F: Graftable,
1172        K: Key,
1173        V: ValueEncoding + 'static,
1174        H: Hasher,
1175        E: Context,
1176        C: Mutable<Item = Operation<F, update::Unordered<K, V>>>,
1177        I: UnorderedIndex<Value = Location<F>> + 'static,
1178        S: Strategy,
1179        Operation<F, update::Unordered<K, V>>: Codec,
1180    {
1181        type Family = F;
1182        type K = K;
1183        type V = V::Value;
1184        type Metadata = V::Value;
1185        type Merkleized = Arc<MerkleizedBatch<F, H::Digest, update::Unordered<K, V>, N, S>>;
1186
1187        fn write(self, key: K, value: Option<V::Value>) -> Self {
1188            Self::write(self, key, value)
1189        }
1190
1191        async fn merkleize(
1192            self,
1193            db: &CurrentDb<F, E, C, I, H, update::Unordered<K, V>, N, S>,
1194            metadata: Option<V::Value>,
1195        ) -> Result<Self::Merkleized, crate::qmdb::Error<F>> {
1196            self.merkleize(db, metadata).await
1197        }
1198    }
1199
1200    impl<F, K, V, H, E, C, I, const N: usize, S>
1201        UnmerkleizedBatchTrait<CurrentDb<F, E, C, I, H, update::Ordered<K, V>, N, S>>
1202        for UnmerkleizedBatch<F, H, update::Ordered<K, V>, N, S>
1203    where
1204        F: Graftable,
1205        K: Key,
1206        V: ValueEncoding + 'static,
1207        H: Hasher,
1208        E: Context,
1209        C: Mutable<Item = Operation<F, update::Ordered<K, V>>>,
1210        I: crate::index::Ordered<Value = Location<F>> + 'static,
1211        S: Strategy,
1212        Operation<F, update::Ordered<K, V>>: Codec,
1213    {
1214        type Family = F;
1215        type K = K;
1216        type V = V::Value;
1217        type Metadata = V::Value;
1218        type Merkleized = Arc<MerkleizedBatch<F, H::Digest, update::Ordered<K, V>, N, S>>;
1219
1220        fn write(self, key: K, value: Option<V::Value>) -> Self {
1221            Self::write(self, key, value)
1222        }
1223
1224        async fn merkleize(
1225            self,
1226            db: &CurrentDb<F, E, C, I, H, update::Ordered<K, V>, N, S>,
1227            metadata: Option<V::Value>,
1228        ) -> Result<Self::Merkleized, crate::qmdb::Error<F>> {
1229            self.merkleize(db, metadata).await
1230        }
1231    }
1232
1233    impl<
1234            F: Graftable,
1235            D: Digest,
1236            U: update::Update + Send + Sync + 'static,
1237            const N: usize,
1238            S: Strategy,
1239        > MerkleizedBatchTrait for Arc<MerkleizedBatch<F, D, U, N, S>>
1240    where
1241        Operation<F, U>: Codec,
1242    {
1243        type Digest = D;
1244
1245        fn root(&self) -> D {
1246            MerkleizedBatch::root(self)
1247        }
1248    }
1249
1250    impl<F, E, K, V, C, I, H, const N: usize, S> BatchableDb
1251        for CurrentDb<F, E, C, I, H, update::Unordered<K, V>, N, S>
1252    where
1253        F: Graftable,
1254        E: Context,
1255        K: Key,
1256        V: ValueEncoding + 'static,
1257        C: Mutable<Item = Operation<F, update::Unordered<K, V>>>,
1258        I: UnorderedIndex<Value = Location<F>> + 'static,
1259        H: Hasher,
1260        S: Strategy,
1261        Operation<F, update::Unordered<K, V>>: Codec,
1262    {
1263        type Family = F;
1264        type K = K;
1265        type V = V::Value;
1266        type Merkleized = Arc<MerkleizedBatch<F, H::Digest, update::Unordered<K, V>, N, S>>;
1267        type Batch = UnmerkleizedBatch<F, H, update::Unordered<K, V>, N, S>;
1268
1269        fn new_batch(&self) -> Self::Batch {
1270            self.new_batch()
1271        }
1272
1273        fn apply_batch(
1274            &mut self,
1275            batch: Self::Merkleized,
1276        ) -> impl Future<Output = Result<core::ops::Range<Location<F>>, crate::qmdb::Error<F>>>
1277        {
1278            self.apply_batch(batch)
1279        }
1280    }
1281
1282    impl<F, E, K, V, C, I, H, const N: usize, S> BatchableDb
1283        for CurrentDb<F, E, C, I, H, update::Ordered<K, V>, N, S>
1284    where
1285        F: Graftable,
1286        E: Context,
1287        K: Key,
1288        V: ValueEncoding + 'static,
1289        C: Mutable<Item = Operation<F, update::Ordered<K, V>>>,
1290        I: crate::index::Ordered<Value = Location<F>> + 'static,
1291        H: Hasher,
1292        S: Strategy,
1293        Operation<F, update::Ordered<K, V>>: Codec,
1294    {
1295        type Family = F;
1296        type K = K;
1297        type V = V::Value;
1298        type Merkleized = Arc<MerkleizedBatch<F, H::Digest, update::Ordered<K, V>, N, S>>;
1299        type Batch = UnmerkleizedBatch<F, H, update::Ordered<K, V>, N, S>;
1300
1301        fn new_batch(&self) -> Self::Batch {
1302            self.new_batch()
1303        }
1304
1305        fn apply_batch(
1306            &mut self,
1307            batch: Self::Merkleized,
1308        ) -> impl Future<Output = Result<core::ops::Range<Location<F>>, crate::qmdb::Error<F>>>
1309        {
1310            self.apply_batch(batch)
1311        }
1312    }
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317    use super::*;
1318    use crate::mmr;
1319    use commonware_utils::bitmap::Prunable as BitMap;
1320
1321    // N=4 -> CHUNK_SIZE_BITS = 32
1322    const N: usize = 4;
1323    type Bm = BitMap<N>;
1324    type Location = mmr::Location;
1325
1326    fn make_bitmap(bits: &[bool]) -> Bm {
1327        let mut bm = Bm::new();
1328        for &b in bits {
1329            bm.push(b);
1330        }
1331        bm
1332    }
1333
1334    // ---- build_chunk_overlay tests ----
1335
1336    #[test]
1337    fn chunk_overlay_pushes() {
1338        use crate::qmdb::any::value::FixedEncoding;
1339        use commonware_utils::sequence::FixedBytes;
1340
1341        type K = FixedBytes<4>;
1342        type V = FixedEncoding<u64>;
1343        type U = crate::qmdb::any::operation::update::Unordered<K, V>;
1344
1345        let key1 = FixedBytes::from([1, 0, 0, 0]);
1346        let key2 = FixedBytes::from([2, 0, 0, 0]);
1347
1348        // Base: 4 bits, all set (previous commit at loc 3).
1349        // Segment of 4 operations starting at base_size=4.
1350        // Diff: key1 active at loc=4 (in batch), key2 active at loc=99 (not in batch,
1351        // so superseded within this batch).
1352        let base = make_bitmap(&[true; 4]);
1353        let mut diff = vec![
1354            (
1355                key1,
1356                DiffEntry::Active {
1357                    value: 100u64,
1358                    loc: Location::new(4), // offset 0 in batch
1359                    base_old_loc: None,
1360                },
1361            ),
1362            (
1363                key2,
1364                DiffEntry::Active {
1365                    value: 200u64,
1366                    loc: Location::new(99), // not in batch [4,8), so superseded
1367                    base_old_loc: None,
1368                },
1369            ),
1370        ];
1371        diff.sort_by(|a, b| a.0.cmp(&b.0));
1372
1373        let overlay = build_chunk_overlay::<mmr::Family, U, _, N>(&base, 4, 4, &diff, &[]);
1374
1375        // Chunk 0 should have: bits 0-3 from base (all set), bit 4 set (key1), bits 5-6 false
1376        // (inactive), bit 7 set (CommitFloor at loc 7). Also bit 3 cleared (previous commit).
1377        let c0 = overlay.get(0).expect("chunk 0 should be dirty");
1378        assert_ne!(c0[0] & (1 << 4), 0); // key1 active
1379        assert_eq!(c0[0] & (1 << 5), 0); // inactive
1380        assert_eq!(c0[0] & (1 << 6), 0); // inactive
1381        assert_ne!(c0[0] & (1 << 7), 0); // CommitFloor
1382        assert_eq!(c0[0] & (1 << 3), 0); // previous commit cleared
1383    }
1384
1385    #[test]
1386    fn chunk_overlay_clears() {
1387        use crate::qmdb::any::value::FixedEncoding;
1388        use commonware_utils::sequence::FixedBytes;
1389
1390        type K = FixedBytes<4>;
1391        type U = crate::qmdb::any::operation::update::Unordered<K, FixedEncoding<u64>>;
1392
1393        let key1 = FixedBytes::from([1, 0, 0, 0]);
1394        let key2 = FixedBytes::from([2, 0, 0, 0]);
1395        let key3 = FixedBytes::from([3, 0, 0, 0]);
1396
1397        // Base bitmap with 64 bits, all set.
1398        let base = make_bitmap(&[true; 64]);
1399
1400        let mut diff: Vec<(K, DiffEntry<mmr::Family, u64>)> = vec![
1401            (
1402                key1,
1403                DiffEntry::Active {
1404                    value: 100,
1405                    loc: Location::new(70),
1406                    base_old_loc: Some(Location::new(5)),
1407                },
1408            ),
1409            (
1410                key2,
1411                DiffEntry::Deleted {
1412                    base_old_loc: Some(Location::new(10)),
1413                },
1414            ),
1415            (
1416                key3,
1417                DiffEntry::Active {
1418                    value: 300,
1419                    loc: Location::new(71),
1420                    base_old_loc: None,
1421                },
1422            ),
1423        ];
1424        diff.sort_by(|a, b| a.0.cmp(&b.0));
1425
1426        // Segment of 8 ops starting at 64; previous commit at loc 63.
1427        let overlay = build_chunk_overlay::<mmr::Family, U, _, N>(&base, 8, 64, &diff, &[]);
1428
1429        // Verify bits 5 and 10 are cleared in chunk 0.
1430        let c0 = overlay.get(0).expect("chunk 0 should be dirty");
1431        assert_eq!(c0[0] & (1 << 5), 0); // bit 5 cleared
1432        assert_eq!(c0[1] & (1 << 2), 0); // bit 10 = byte 1, bit 2 cleared
1433
1434        // Other bits should still be set.
1435        assert_eq!(c0[0] & (1 << 4), 1 << 4); // bit 4 still set
1436        assert_eq!(c0[1] & (1 << 3), 1 << 3); // bit 11 still set
1437    }
1438
1439    /// Regression: when the parent bitmap has a partial last chunk that becomes complete in the
1440    /// child (without any active bits landing in that chunk), the overlay must inherit the parent's
1441    /// partial chunk data, not zero it out.
1442    #[test]
1443    fn chunk_overlay_preserves_partial_parent_chunk() {
1444        use crate::qmdb::any::value::FixedEncoding;
1445        use commonware_utils::sequence::FixedBytes;
1446
1447        type K = FixedBytes<4>;
1448        type U = crate::qmdb::any::operation::update::Unordered<K, FixedEncoding<u64>>;
1449
1450        // Base: 20 bits set (partial chunk 0, CHUNK_SIZE_BITS=32).
1451        let base = make_bitmap(&[true; 20]);
1452        assert_eq!(base.complete_chunks(), 0); // partial
1453
1454        // Segment of 20 ops starting at loc 20. This pushes total to 40 bits, completing chunk 0
1455        // (32 bits) and starting chunk 1. Diff: only one active key at loc 35 (in chunk 1), plus
1456        // CommitFloor at loc 39. No active bits land in chunk 0's new region (bits 20-31).
1457        let key1 = FixedBytes::from([1, 0, 0, 0]);
1458        let mut diff = vec![(
1459            key1,
1460            DiffEntry::Active {
1461                value: 42u64,
1462                loc: Location::new(35),
1463                base_old_loc: None,
1464            },
1465        )];
1466        diff.sort_by(|a, b| a.0.cmp(&b.0));
1467
1468        let overlay = build_chunk_overlay::<mmr::Family, U, _, N>(&base, 20, 20, &diff, &[]);
1469
1470        // Chunk 0 should be materialized and preserve the parent's first 20 bits.
1471        let c0 = overlay.get(0).expect("chunk 0 should be in overlay");
1472        // Bits 0-7 all set -> byte 0 = 0xFF
1473        assert_eq!(c0[0], 0xFF);
1474        // Bits 8-15 all set -> byte 1 = 0xFF
1475        assert_eq!(c0[1], 0xFF);
1476        // Bits 16-18 set, bit 19 cleared (previous commit), 20-23 not set -> byte 2 = 0x07
1477        assert_eq!(c0[2], 0x07);
1478    }
1479
1480    // ---- next_candidate tests ----
1481
1482    #[test]
1483    fn bitmap_scan_all_active() {
1484        let bm = make_bitmap(&[true; 8]);
1485        for i in 0..8 {
1486            assert_eq!(
1487                next_candidate(&bm, Location::new(i), 8),
1488                Some(Location::new(i))
1489            );
1490        }
1491        assert_eq!(next_candidate(&bm, Location::new(8), 8), None);
1492    }
1493
1494    #[test]
1495    fn bitmap_scan_all_inactive() {
1496        let bm = make_bitmap(&[false; 8]);
1497        assert_eq!(next_candidate(&bm, Location::new(0), 8), None);
1498    }
1499
1500    #[test]
1501    fn bitmap_scan_skips_inactive() {
1502        // Pattern: inactive, inactive, active, inactive, active
1503        let bm = make_bitmap(&[false, false, true, false, true]);
1504        assert_eq!(
1505            next_candidate(&bm, Location::new(0), 5),
1506            Some(Location::new(2))
1507        );
1508        assert_eq!(
1509            next_candidate(&bm, Location::new(3), 5),
1510            Some(Location::new(4))
1511        );
1512        assert_eq!(next_candidate(&bm, Location::new(5), 5), None);
1513    }
1514
1515    #[test]
1516    fn bitmap_scan_beyond_bitmap_len_returns_candidate() {
1517        // Bitmap has 4 bits, but tip is 8. Locations 4..8 are beyond the bitmap and should be
1518        // returned as candidates.
1519        let bm = make_bitmap(&[false; 4]);
1520        // All bitmap bits are unset, so 0..4 are skipped; loc 4 is beyond bitmap -> candidate.
1521        assert_eq!(
1522            next_candidate(&bm, Location::new(0), 8),
1523            Some(Location::new(4))
1524        );
1525        assert_eq!(
1526            next_candidate(&bm, Location::new(6), 8),
1527            Some(Location::new(6))
1528        );
1529    }
1530
1531    #[test]
1532    fn bitmap_scan_respects_tip() {
1533        let bm = make_bitmap(&[false, false, false, true]);
1534        // Active bit at 3, but tip is 3 so it's excluded.
1535        assert_eq!(next_candidate(&bm, Location::new(0), 3), None);
1536        // With tip=4, bit 3 is included.
1537        assert_eq!(
1538            next_candidate(&bm, Location::new(0), 4),
1539            Some(Location::new(3))
1540        );
1541    }
1542
1543    #[test]
1544    fn bitmap_scan_floor_at_tip() {
1545        let bm = make_bitmap(&[true; 4]);
1546        assert_eq!(next_candidate(&bm, Location::new(4), 4), None);
1547    }
1548
1549    #[test]
1550    fn bitmap_scan_empty_bitmap() {
1551        let bm = Bm::new();
1552        // Empty bitmap, but tip > 0: all locations are beyond bitmap.
1553        assert_eq!(
1554            next_candidate(&bm, Location::new(0), 5),
1555            Some(Location::new(0))
1556        );
1557        // Empty bitmap, tip = 0: no candidates.
1558        assert_eq!(next_candidate(&bm, Location::new(0), 0), None);
1559    }
1560
1561    // ---- trim_committed tests ----
1562    //
1563    // `trim_committed` is called from `MerkleizedBatch::new_batch` to strip any `Layer`s whose
1564    // overlays have already been absorbed into the shared committed bitmap by a prior apply.
1565    // The implementation is a single loop that collects uncommitted overlays top-down and
1566    // rebuilds a fresh chain rooted at `Base`. These tests cover distinct input shapes directly,
1567    // without going through the full Db/batch machinery, so the function's structural output
1568    // can be asserted.
1569
1570    /// Build a chain `Base(shared) -> Layer(len=L1) -> Layer(len=L2) -> ...` from a list of
1571    /// overlay lengths (bottom to top). Each constructed `Layer` caches `shared` per the
1572    /// struct's invariant.
1573    fn make_chain(shared: &Arc<Shared<N>>, overlay_lens: &[u64]) -> BitmapBatch<N> {
1574        let mut chain = BitmapBatch::Base(Arc::clone(shared));
1575        for &len in overlay_lens {
1576            chain = BitmapBatch::Layer(Arc::new(BitmapBatchLayer {
1577                parent: chain,
1578                overlay: Arc::new(ChunkOverlay::new(len, 0)),
1579                shared: Arc::clone(shared),
1580            }));
1581        }
1582        chain
1583    }
1584
1585    /// Walk a chain and return its overlay lengths in bottom-to-top order. Used to assert the
1586    /// structural output of `trim_committed` without touching private fields. Panics if the
1587    /// chain isn't terminated by a single `Base` at the bottom.
1588    fn chain_overlays(batch: &BitmapBatch<N>) -> Vec<u64> {
1589        let mut lens = Vec::new();
1590        let mut current = batch;
1591        while let BitmapBatch::Layer(layer) = current {
1592            lens.push(layer.overlay.len);
1593            current = &layer.parent;
1594        }
1595        assert!(matches!(current, BitmapBatch::Base(_)));
1596        lens.reverse();
1597        lens
1598    }
1599
1600    /// Input is already a bare `Base` with no speculative layers on top -- the loop body never
1601    /// runs, `kept` stays empty, and the result is a freshly constructed `Base` pointing at the
1602    /// same `Shared`. Real-world trigger: `MerkleizedBatch::new_batch` on a batch whose
1603    /// chain was previously trimmed flat (e.g., immediately after an apply collapsed everything).
1604    #[test]
1605    fn trim_committed_already_base() {
1606        let shared = Arc::new(Shared::<N>::new(make_bitmap(&[true; 64])));
1607        let base = BitmapBatch::Base(Arc::clone(&shared));
1608        let result = base.trim_committed();
1609        // Still `Base`, pointing at the same shared terminal.
1610        match result {
1611            BitmapBatch::Base(s) => assert!(Arc::ptr_eq(&s, &shared)),
1612            BitmapBatch::Layer(_) => panic!("expected Base"),
1613        }
1614    }
1615
1616    /// Every layer has been absorbed by prior applies -- the loop breaks on the first iteration
1617    /// and `kept` stays empty, so the result is a bare `Base`. This is the steady-state
1618    /// "extend a just-applied batch" flow: after `apply_batch(A)`, `A`'s own layer has
1619    /// `overlay.len == committed` and the next `new_batch` call should start from a clean
1620    /// terminal.
1621    #[test]
1622    fn trim_committed_all_committed() {
1623        // `shared.len() == 64`; the single layer's `overlay.len == 32 (<= 64)`, so it's committed.
1624        let shared = Arc::new(Shared::<N>::new(make_bitmap(&[true; 64])));
1625        let chain = make_chain(&shared, &[32]);
1626        let result = chain.trim_committed();
1627        // Collapsed to a bare Base, pointing at the original shared.
1628        match result {
1629            BitmapBatch::Base(s) => assert!(Arc::ptr_eq(&s, &shared)),
1630            BitmapBatch::Layer(_) => panic!("expected Base after full trim"),
1631        }
1632    }
1633
1634    /// Every layer is still speculative -- the loop walks all the way to `Base` without
1635    /// breaking, and `kept` holds every overlay. The rebuilt chain is structurally equivalent
1636    /// to the input (same overlay lens, same shared terminal). Real-world trigger: speculating
1637    /// multiple batches deep (A, then B off A, then C off B) without `apply_batch` in between.
1638    #[test]
1639    fn trim_committed_none_committed() {
1640        // `shared.len() == 32`; both overlays have `len > 32`, so neither is committed.
1641        let shared = Arc::new(Shared::<N>::new(make_bitmap(&[true; 32])));
1642        let chain = make_chain(&shared, &[64, 96]);
1643        let result = chain.trim_committed();
1644        // Structure must be preserved in bottom-to-top order.
1645        assert_eq!(chain_overlays(&result), vec![64, 96]);
1646    }
1647
1648    /// Exactly one layer is uncommitted (the newest) on top of a committed prefix -- the
1649    /// dominant pattern in chained growth. The loop collects the one uncommitted overlay, and
1650    /// the rebuild produces `Layer(Base, overlay_B)`. Also verifies the rebuilt layer carries
1651    /// the cached `shared` reference correctly. Real-world trigger: apply parent A, then B
1652    /// held alive off A, then `B.new_batch()` to build C.
1653    #[test]
1654    fn trim_committed_exactly_one_uncommitted() {
1655        // `shared.len() == 64`; committed layer (`overlay.len == 64`) + uncommitted (`96`).
1656        let shared = Arc::new(Shared::<N>::new(make_bitmap(&[true; 64])));
1657        let chain = make_chain(&shared, &[64, 96]);
1658        let result = chain.trim_committed();
1659        // The committed layer is gone; only the uncommitted overlay remains.
1660        assert_eq!(chain_overlays(&result), vec![96]);
1661        // And the rebuilt layer's `shared` field still points at the original terminal.
1662        assert!(Arc::ptr_eq(result.shared(), &shared));
1663    }
1664
1665    /// Two or more uncommitted layers on top of a committed prefix -- exercises the loop's
1666    /// iterated `kept.push` and the rebuild's iterated `Arc::new(BitmapBatchLayer)`, including
1667    /// the cached `shared` wire-through on every reconstructed layer. Real-world trigger:
1668    /// build A, then B off A, then C off B; apply only A; then call `C.new_batch()`.
1669    #[test]
1670    fn trim_committed_multiple_uncommitted() {
1671        // `shared.len() == 64`; committed layer (64), then two uncommitted (96, 128).
1672        let shared = Arc::new(Shared::<N>::new(make_bitmap(&[true; 64])));
1673        let chain = make_chain(&shared, &[64, 96, 128]);
1674        let result = chain.trim_committed();
1675        // Committed layer dropped; uncommitted pair preserved in order.
1676        assert_eq!(chain_overlays(&result), vec![96, 128]);
1677        // Every reconstructed layer must still cache the original shared terminal.
1678        assert!(Arc::ptr_eq(result.shared(), &shared));
1679    }
1680}