Skip to main content

commonware_storage/qmdb/immutable/
batch.rs

1//! Batch mutation API for Immutable QMDBs.
2
3use super::Immutable;
4use crate::{
5    Context,
6    journal::{authenticated, contiguous::Mutable},
7    merkle::{Family, Location, Proof},
8    qmdb::{
9        Error,
10        any::{ValueEncoding, batch::lookup_sorted},
11        batch_chain::{self, Bounds, Commitment},
12        immutable::operation::Operation,
13        operation::Key,
14    },
15    translator::Translator,
16};
17use commonware_codec::EncodeShared;
18use commonware_cryptography::{Digest, Hasher};
19use commonware_parallel::Strategy;
20use commonware_utils::iter::zip_eq;
21use std::{
22    collections::BTreeMap,
23    sync::{Arc, Weak},
24};
25
26type DiffVec<K, F, V> = Vec<(K, DiffEntry<F, V>)>;
27
28/// What happened to a key in this batch.
29#[derive(Clone)]
30pub(crate) struct DiffEntry<F: Family, V> {
31    pub(crate) value: V,
32    pub(crate) loc: Location<F>,
33}
34
35/// A speculative batch of operations whose root digest has not yet been computed, in contrast
36/// to [`MerkleizedBatch`].
37///
38/// Consuming [`UnmerkleizedBatch::merkleize`] produces an `Arc<MerkleizedBatch>`.
39/// Methods that need the committed DB (e.g. [`get`](Self::get)) accept it as a parameter.
40#[allow(clippy::type_complexity)]
41pub struct UnmerkleizedBatch<F, H, K, V, S: Strategy>
42where
43    F: Family,
44    K: Key,
45    V: ValueEncoding,
46    H: Hasher,
47{
48    /// Authenticated journal batch for computing the speculative Merkle root.
49    journal_batch: authenticated::UnmerkleizedBatch<F, H, Operation<F, K, V>, S>,
50
51    /// Pending mutations.
52    mutations: BTreeMap<K, V::Value>,
53
54    /// Parent batch in the chain. `None` for batches created directly from the DB.
55    parent: Option<Arc<MerkleizedBatch<F, H::Digest, K, V, S>>>,
56
57    /// The state immediately before this batch's operations.
58    /// This batch's i-th operation lands at location `base.size + i`.
59    base: Commitment<F, H::Digest>,
60}
61
62/// Merkleized authenticated-journal batch wrapping an [`Operation`] payload.
63type JournalBatch<F, D, K, V, S> = Arc<authenticated::MerkleizedBatch<F, D, Operation<F, K, V>, S>>;
64
65/// A speculative batch of operations whose root digest has been computed,
66/// in contrast to [`UnmerkleizedBatch`].
67///
68/// # Branch validity
69///
70/// Reads through the chain, constructing child batches, and applying the batch later are
71/// only valid while every batch applied to the DB since this batch was merkleized is an
72/// ancestor of this batch (see [`crate::qmdb::batch_chain`] for more details).
73#[derive(Clone)]
74pub struct MerkleizedBatch<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy> {
75    /// Authenticated journal batch (Merkle state + local items).
76    pub(super) journal_batch: JournalBatch<F, D, K, V, S>,
77
78    /// This batch's local key-level changes only (not accumulated from ancestors).
79    /// Sorted by key with no duplicates; queried via `lookup_sorted` (binary search).
80    pub(super) diff: Arc<DiffVec<K, F, V::Value>>,
81
82    /// The parent batch in the chain, if any.
83    pub(super) parent: Option<Weak<Self>>,
84
85    /// Arc refs to each ancestor's diff, collected during `merkleize()` while the parent
86    /// is alive. Used by `apply_batch` to apply uncommitted ancestor snapshot diffs.
87    /// 1:1 with `bounds.ancestors` (same length, same ordering).
88    pub(super) ancestor_diffs: Vec<Arc<DiffVec<K, F, V::Value>>>,
89
90    /// Position and floor bounds for this batch chain.
91    pub(super) bounds: batch_chain::Bounds<F, D>,
92}
93
94impl<F, H, K, V, S: Strategy> UnmerkleizedBatch<F, H, K, V, S>
95where
96    F: Family,
97    K: Key,
98    V: ValueEncoding,
99    H: Hasher,
100    Operation<F, K, V>: EncodeShared,
101{
102    /// Create a batch from a committed DB (no parent chain).
103    pub(super) fn new<E, C, T>(
104        immutable: &Immutable<F, E, K, V, C, H, T, S>,
105        base: Commitment<F, H::Digest>,
106    ) -> Self
107    where
108        E: Context,
109        C: Mutable<Item = Operation<F, K, V>>,
110        C::Item: EncodeShared,
111        T: Translator,
112    {
113        Self {
114            journal_batch: immutable.journal.new_batch(),
115            mutations: BTreeMap::new(),
116            parent: None,
117            base,
118        }
119    }
120
121    /// The database boundary for this batch chain.
122    ///
123    /// A batch created from the database uses its base. A child inherits its parent's `db`.
124    fn db(&self) -> Commitment<F, H::Digest> {
125        self.parent
126            .as_ref()
127            .map_or(self.base, |parent| parent.bounds.db)
128    }
129
130    /// Set a key to a value.
131    ///
132    /// If the key already exists in the database or an ancestor batch, reads
133    /// of it may return any of its written values.
134    pub fn set(mut self, key: K, value: V::Value) -> Self {
135        self.mutations.insert(key, value);
136        self
137    }
138
139    /// Read through: mutations -> ancestor diffs -> committed DB.
140    pub async fn get<E, C, T>(
141        &self,
142        key: &K,
143        db: &Immutable<F, E, K, V, C, H, T, S>,
144    ) -> Result<Option<V::Value>, Error<F>>
145    where
146        E: Context,
147        C: Mutable<Item = Operation<F, K, V>>,
148        C::Item: EncodeShared,
149        T: Translator,
150    {
151        // Check this batch's pending mutations.
152        if let Some(value) = self.mutations.get(key) {
153            return Ok(Some(value.clone()));
154        }
155        // Walk parent chain. The first parent is a strong Arc (held by UnmerkleizedBatch),
156        // subsequent parents are Weak refs.
157        if let Some(parent) = self.parent.as_ref() {
158            if let Some(entry) = lookup_sorted(parent.diff.as_slice(), key) {
159                return Ok(Some(entry.value.clone()));
160            }
161            for batch in parent.ancestors() {
162                if let Some(entry) = lookup_sorted(batch.diff.as_slice(), key) {
163                    return Ok(Some(entry.value.clone()));
164                }
165            }
166        }
167        // Fall through to base DB.
168        db.get(key).await
169    }
170
171    /// Batch read multiple keys.
172    ///
173    /// Returns results in the same order as the input keys.
174    pub async fn get_many<E, C, T>(
175        &self,
176        keys: &[&K],
177        db: &Immutable<F, E, K, V, C, H, T, S>,
178    ) -> Result<Vec<Option<V::Value>>, Error<F>>
179    where
180        E: Context,
181        C: Mutable<Item = Operation<F, K, V>>,
182        C::Item: EncodeShared,
183        T: Translator,
184    {
185        if keys.is_empty() {
186            return Ok(Vec::new());
187        }
188
189        let mut results: Vec<Option<V::Value>> = Vec::with_capacity(keys.len());
190        let mut db_indices = Vec::new();
191        let mut db_keys = Vec::new();
192
193        for (i, key) in keys.iter().enumerate() {
194            // Check local mutations.
195            if let Some(value) = self.mutations.get(*key) {
196                results.push(Some(value.clone()));
197                continue;
198            }
199
200            // Check parent diff chain.
201            let mut found = false;
202            if let Some(parent) = self.parent.as_ref() {
203                if let Some(entry) = lookup_sorted(parent.diff.as_slice(), *key) {
204                    results.push(Some(entry.value.clone()));
205                    found = true;
206                }
207                if !found {
208                    for batch in parent.ancestors() {
209                        if let Some(entry) = lookup_sorted(batch.diff.as_slice(), *key) {
210                            results.push(Some(entry.value.clone()));
211                            found = true;
212                            break;
213                        }
214                    }
215                }
216            }
217
218            if found {
219                continue;
220            }
221
222            // Need DB fallthrough.
223            db_indices.push(i);
224            db_keys.push(*key);
225            results.push(None);
226        }
227
228        if !db_keys.is_empty() {
229            let db_results = db.get_many(&db_keys).await?;
230            for (slot, value) in zip_eq(db_indices, db_results) {
231                results[slot] = value;
232            }
233        }
234
235        Ok(results)
236    }
237
238    /// Resolve mutations into operations, merkleize, and return an `Arc<MerkleizedBatch>`.
239    ///
240    /// `inactivity_floor` declares that all operations before this location are inactive.
241    /// It must be >= the database's current inactivity floor (monotonically non-decreasing).
242    #[tracing::instrument(name = "qmdb.immutable.batch.merkleize", level = "info", skip_all)]
243    pub async fn merkleize<E, C, T>(
244        self,
245        db: &Immutable<F, E, K, V, C, H, T, S>,
246        metadata: Option<V::Value>,
247        inactivity_floor: Location<F>,
248    ) -> Arc<MerkleizedBatch<F, H::Digest, K, V, S>>
249    where
250        E: Context,
251        C: Mutable<Item = Operation<F, K, V>>,
252        C::Item: EncodeShared,
253        T: Translator,
254    {
255        let base = self.base.size;
256
257        let live_ancestors: Vec<_> =
258            batch_chain::parent_and_ancestors(self.parent.as_ref(), |parent| parent.ancestors())
259                .collect();
260        let boundary = batch_chain::effective_boundary(
261            self.db(),
262            live_ancestors.last().map(|oldest| oldest.bounds.base),
263        );
264
265        // Build operations: one Set per key, then Commit. `self.mutations` is a BTreeMap, so
266        // iteration yields keys in sorted order, which `diff` relies on for binary search.
267        let mut ops: Vec<Operation<F, K, V>> = Vec::with_capacity(self.mutations.len() + 1);
268        let mut diff: DiffVec<K, F, V::Value> = Vec::with_capacity(self.mutations.len());
269
270        for (key, value) in self.mutations {
271            let loc = base + ops.len() as u64;
272            ops.push(Operation::Set(key.clone(), value.clone()));
273            diff.push((key, DiffEntry { value, loc }));
274        }
275        assert!(diff.is_sorted_by(|a, b| a.0 < b.0));
276
277        ops.push(Operation::Commit(metadata, inactivity_floor));
278
279        let total_size = base + ops.len() as u64;
280        let inactive_peaks = F::inactive_peaks(total_size, inactivity_floor);
281
282        // Leaf and node hashing dominate merkleization, so run them as one job through the
283        // strategy (see `Journal::merkleize`).
284        let (journal, root) = db
285            .journal
286            .merkleize(self.journal_batch, ops, inactive_peaks)
287            .await
288            .expect("inactive_peaks computed from batch size");
289
290        // Compute the batch chain bounds.
291        let mut ancestor_diffs = Vec::new();
292        let mut ancestors = Vec::new();
293        for batch in live_ancestors {
294            ancestor_diffs.push(Arc::clone(&batch.diff));
295            ancestors.push(batch_chain::AncestorBounds {
296                floor: batch.bounds.inactivity_floor,
297                state: batch.commitment(),
298            });
299        }
300
301        Arc::new(MerkleizedBatch {
302            journal_batch: journal,
303            diff: Arc::new(diff),
304            parent: self.parent.as_ref().map(Arc::downgrade),
305            ancestor_diffs,
306            bounds: batch_chain::Bounds {
307                base: self.base,
308                db: boundary,
309                tip: Commitment::new(total_size, root),
310                ancestors,
311                inactivity_floor,
312            },
313        })
314    }
315}
316
317impl<F: Family, D: Digest, K: Key, V: ValueEncoding, S: Strategy> MerkleizedBatch<F, D, K, V, S>
318where
319    Operation<F, K, V>: EncodeShared,
320{
321    /// Return the speculative root.
322    pub const fn root(&self) -> D {
323        self.bounds.tip.root
324    }
325
326    /// Return the [`Bounds`] of the batch.
327    pub const fn bounds(&self) -> &Bounds<F, D> {
328        &self.bounds
329    }
330
331    /// Return the operations this batch appends to the log and the location of the first.
332    #[allow(clippy::type_complexity)]
333    pub fn operations(&self) -> (Location<F>, Arc<Vec<Operation<F, K, V>>>) {
334        (
335            self.bounds.base.size,
336            Arc::clone(self.journal_batch.items()),
337        )
338    }
339
340    /// Inclusion proof for the operations returned by [`Self::operations`], anchored at
341    /// this batch's tip. The pair verifies against [`Self::root`] via
342    /// [`crate::qmdb::verify_proof`]. Together with [`Self::pinned_nodes`] they verify via
343    /// [`crate::qmdb::verify_proof_and_pinned_nodes`].
344    ///
345    /// Nodes of unapplied ancestors are read through the chain, so those ancestors must still be
346    /// alive. Nodes below the chain are read from `db`'s
347    /// [Merkle store][crate::merkle::mem::Mem], which retains them at least until
348    /// this batch's changes are flushed (by a commit or sync after apply).
349    ///
350    /// # Errors
351    ///
352    /// Returns [`crate::merkle::Error::ElementPruned`] if a required node has been pruned or
353    /// belongs to a dropped unapplied ancestor.
354    pub fn proof<E, C, H, T>(
355        &self,
356        db: &Immutable<F, E, K, V, C, H, T, S>,
357    ) -> Result<Proof<F, D>, Error<F>>
358    where
359        E: Context,
360        C: Mutable<Item = Operation<F, K, V>>,
361        H: Hasher<Digest = D>,
362        T: Translator,
363    {
364        let inactive_peaks = F::inactive_peaks(self.bounds.tip.size, self.bounds.inactivity_floor);
365        db.journal
366            .speculative_proof(&self.journal_batch, inactive_peaks)
367            .map_err(Into::into)
368    }
369
370    /// The Merkle frontier at the first operation returned by [`Self::operations`]
371    /// ([`Family::nodes_to_pin`]), which lets a consumer holding only this batch's base rebuild
372    /// compact state and replay the operations. The operations, [`Self::proof`], and pinned
373    /// nodes verify against [`Self::root`] via [`crate::qmdb::verify_proof_and_pinned_nodes`].
374    ///
375    /// Nodes of unapplied ancestors are read through the chain, so those ancestors must still be
376    /// alive. Nodes below the chain are read from `db`'s
377    /// [Merkle store][crate::merkle::mem::Mem], which retains them at least until
378    /// this batch's changes are flushed (by a commit or sync after apply).
379    ///
380    /// # Errors
381    ///
382    /// Returns [`crate::merkle::Error::ElementPruned`] if a required node has been pruned or
383    /// belongs to a dropped unapplied ancestor.
384    pub fn pinned_nodes<E, C, H, T>(
385        &self,
386        db: &Immutable<F, E, K, V, C, H, T, S>,
387    ) -> Result<Vec<D>, Error<F>>
388    where
389        E: Context,
390        C: Mutable<Item = Operation<F, K, V>>,
391        H: Hasher<Digest = D>,
392        T: Translator,
393    {
394        db.journal
395            .speculative_pinned_nodes(&self.journal_batch)
396            .map_err(Into::into)
397    }
398
399    /// Iterate over ancestor batches (parent first, then grandparent, etc.).
400    pub(super) fn ancestors(&self) -> impl Iterator<Item = Arc<Self>> + use<F, D, K, V, S> {
401        batch_chain::ancestors(self.parent.clone(), |batch| batch.parent.as_ref())
402    }
403
404    /// The [`Commitment`] this batch commits to.
405    pub(super) const fn commitment(&self) -> Commitment<F, D> {
406        self.bounds.tip
407    }
408
409    /// Read through: local diff -> ancestor diffs -> committed DB.
410    pub async fn get<E, C, H, T>(
411        &self,
412        key: &K,
413        db: &Immutable<F, E, K, V, C, H, T, S>,
414    ) -> Result<Option<V::Value>, Error<F>>
415    where
416        E: Context,
417        C: Mutable<Item = Operation<F, K, V>>,
418        C::Item: EncodeShared,
419        H: Hasher<Digest = D>,
420        T: Translator,
421    {
422        if let Some(entry) = lookup_sorted(self.diff.as_slice(), key) {
423            return Ok(Some(entry.value.clone()));
424        }
425        for batch in self.ancestors() {
426            if let Some(entry) = lookup_sorted(batch.diff.as_slice(), key) {
427                return Ok(Some(entry.value.clone()));
428            }
429        }
430        db.get(key).await
431    }
432
433    /// Batch read multiple keys.
434    ///
435    /// Returns results in the same order as the input keys.
436    pub async fn get_many<E, C, H, T>(
437        &self,
438        keys: &[&K],
439        db: &Immutable<F, E, K, V, C, H, T, S>,
440    ) -> Result<Vec<Option<V::Value>>, Error<F>>
441    where
442        E: Context,
443        C: Mutable<Item = Operation<F, K, V>>,
444        C::Item: EncodeShared,
445        H: Hasher<Digest = D>,
446        T: Translator,
447    {
448        if keys.is_empty() {
449            return Ok(Vec::new());
450        }
451
452        let mut results: Vec<Option<V::Value>> = Vec::with_capacity(keys.len());
453        let mut db_indices = Vec::new();
454        let mut db_keys = Vec::new();
455
456        for (i, key) in keys.iter().enumerate() {
457            // Check local diff.
458            if let Some(entry) = lookup_sorted(self.diff.as_slice(), *key) {
459                results.push(Some(entry.value.clone()));
460                continue;
461            }
462
463            // Walk parent chain.
464            let mut found = false;
465            for batch in self.ancestors() {
466                if let Some(entry) = lookup_sorted(batch.diff.as_slice(), *key) {
467                    results.push(Some(entry.value.clone()));
468                    found = true;
469                    break;
470                }
471            }
472
473            if found {
474                continue;
475            }
476
477            // Need DB fallthrough.
478            db_indices.push(i);
479            db_keys.push(*key);
480            results.push(None);
481        }
482
483        if !db_keys.is_empty() {
484            let db_results = db.get_many(&db_keys).await?;
485            for (slot, value) in zip_eq(db_indices, db_results) {
486                results[slot] = value;
487            }
488        }
489
490        Ok(results)
491    }
492
493    /// Create a new speculative batch of operations with this batch as its parent.
494    ///
495    /// All uncommitted ancestors in the chain must be kept alive until the child (or any
496    /// descendant) is merkleized. Dropping an uncommitted ancestor causes data
497    /// loss detected at `apply_batch` time.
498    pub fn new_batch<H>(self: &Arc<Self>) -> UnmerkleizedBatch<F, H, K, V, S>
499    where
500        H: Hasher<Digest = D>,
501    {
502        UnmerkleizedBatch {
503            journal_batch: self.journal_batch.new_batch::<H>(),
504            mutations: BTreeMap::new(),
505            parent: Some(Arc::clone(self)),
506            base: self.commitment(),
507        }
508    }
509}
510
511impl<F, E, K, V, C, H, T, S> Immutable<F, E, K, V, C, H, T, S>
512where
513    F: Family,
514    E: Context,
515    K: Key,
516    V: ValueEncoding,
517    C: Mutable<Item = Operation<F, K, V>>,
518    C::Item: EncodeShared,
519    H: Hasher,
520    T: Translator,
521    S: Strategy,
522{
523    /// Create an initial [`MerkleizedBatch`] from the committed DB state.
524    pub fn to_batch(&self) -> Arc<MerkleizedBatch<F, H::Digest, K, V, S>> {
525        Arc::new(MerkleizedBatch {
526            journal_batch: self.journal.to_merkleized_batch(),
527            diff: Arc::new(Vec::new()),
528            parent: None,
529            ancestor_diffs: Vec::new(),
530            bounds: batch_chain::Bounds::from_db(self.commitment(), self.inactivity_floor_loc),
531        })
532    }
533}