Skip to main content

commonware_storage/qmdb/
mod.rs

1//! A collection of authenticated databases inspired by QMDB (Quick Merkle Database).
2//!
3//! # Terminology
4//!
5//! A database's state is derived from an append-only log of state-changing _operations_.
6//!
7//! In a _keyed_ database, a _key_ either has a _value_ or it doesn't, and different types of
8//! operations modify the state of a specific key. A key that has a value can change to one without
9//! a value through the _delete_ operation. The _update_ operation gives a key a specific value. We
10//! sometimes call an update for a key that doesn't already have a value a _create_ operation, but
11//! its representation in the log is the same.
12//!
13//! Keys with values are called _active_. An operation is called _active_ if (1) its key is active,
14//! (2) it is an update operation, and (3) it is the most recent operation for that key.
15//!
16//! # Database Lifecycle
17//!
18//! All variants are modified through a batch API that follows a common pattern:
19//! 1. Create a batch from the database.
20//! 2. Stage mutations on the batch.
21//! 3. Merkleize the batch -- this resolves mutations against the current state and computes
22//!    the Merkle root that would result from applying them.
23//! 4. Inspect the root or create child batches.
24//! 5. Apply the batch to the database (uncommitted ancestors are applied automatically).
25//!
26//! The specific mutation methods vary by variant.
27//! See each variant's module documentation for the concrete API and usage examples.
28//!
29//! Persistence and cleanup are managed directly on the database: `sync()`, `prune()`,
30//! and `destroy()`.
31//!
32//! # Traits
33//!
34//! Keyed mutable variants ([any] and [current]) implement `any::traits::DbAny`.
35//!
36//! # Acknowledgments
37//!
38//! The following resources were used as references when implementing this crate:
39//!
40//! * [QMDB: Quick Merkle Database](https://arxiv.org/abs/2501.05262)
41//! * [Merkle Mountain
42//!   Ranges](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/merkle-mountain-range.md)
43
44use crate::{
45    index::{Cursor, Unordered as Index},
46    journal::{
47        contiguous::{Contiguous, Mutable},
48        Error as JournalError,
49    },
50    merkle::{
51        hasher::{Hasher as MerkleHasher, Standard as StandardHasher},
52        Bagging, Family, Location,
53    },
54    qmdb::operation::Operation,
55};
56use commonware_codec::Encode;
57use commonware_cryptography::Hasher;
58use commonware_utils::{cache::Clock, NZUsize};
59use core::num::NonZeroUsize;
60use futures::{pin_mut, StreamExt as _};
61use thiserror::Error;
62
63pub mod any;
64pub mod batch_chain;
65pub(crate) mod bitmap;
66pub(crate) mod compact;
67#[cfg(test)]
68mod conformance;
69pub mod current;
70pub mod immutable;
71pub mod keyless;
72mod metrics;
73pub mod operation;
74pub mod store;
75pub mod sync;
76pub mod verify;
77
78pub use verify::{
79    create_multi_proof, create_proof_store, verify_multi_proof, verify_proof,
80    verify_proof_and_extract_digests, verify_proof_and_pinned_nodes,
81};
82
83/// Merkle peak bagging policy used by QMDB operation roots.
84pub(crate) const ROOT_BAGGING: Bagging = Bagging::BackwardFold;
85
86/// Return the Merkle hasher configuration used by QMDB operation roots and proofs.
87pub const fn hasher<H: Hasher>() -> StandardHasher<H> {
88    StandardHasher::new(ROOT_BAGGING)
89}
90
91/// Return the root of an operation log containing only `operation`.
92///
93/// This lets database variants derive their initial root from the bootstrap commit without
94/// opening a database.
95fn single_operation_root<F: Family, H: Hasher>(operation: &impl Encode) -> H::Digest {
96    let hasher = hasher::<H>();
97    let leaf = MerkleHasher::<F>::leaf_digest(
98        &hasher,
99        F::location_to_position(Location::new(0)),
100        &operation.encode(),
101    );
102    MerkleHasher::<F>::root(&hasher, Location::new(1), 0, [&leaf])
103        .expect("a single-leaf Merkle root is always valid")
104}
105
106/// Look up the inactivity floor declared at the commit immediately preceding `op_count`.
107///
108/// `op_count` must be a non-zero commit-boundary historical size: the operation at `op_count - 1`
109/// must itself be a commit op (one for which `floor_of` returns `Some`).
110///
111/// # Errors
112///
113/// - [`Error::HistoricalFloorPruned`] if `op_count` is zero (no preceding commit exists), or if
114///   `op_count - 1` is retained but is not a commit op (either because the caller passed a
115///   non-commit-boundary size, or because pruning removed the commit that would have governed this
116///   size).
117/// - [`JournalError::ItemPruned`] if `op_count - 1` precedes the oldest retained location.
118pub(crate) async fn find_inactivity_floor_at<F, R>(
119    reader: &R,
120    op_count: Location<F>,
121    floor_of: impl Fn(&R::Item) -> Option<Location<F>>,
122) -> Result<Location<F>, Error<F>>
123where
124    F: Family,
125    R: Contiguous,
126{
127    let Some(last_op) = op_count.checked_sub(1) else {
128        return Err(Error::HistoricalFloorPruned(op_count));
129    };
130    let last_op = *last_op;
131    let bounds = reader.bounds();
132    if last_op < bounds.start {
133        return Err(JournalError::ItemPruned(last_op).into());
134    }
135
136    let op = reader.read(last_op).await?;
137    let floor = floor_of(&op).ok_or(Error::HistoricalFloorPruned(op_count))?;
138    if floor > Location::new(last_op) {
139        return Err(Error::DataCorrupted(
140            "inactivity floor exceeds commit location",
141        ));
142    }
143    Ok(floor)
144}
145
146/// Compute the inactive peak count for a historical operation count.
147pub(crate) async fn inactive_peaks_at<F, R>(
148    reader: &R,
149    op_count: Location<F>,
150    floor_of: impl Fn(&R::Item) -> Option<Location<F>>,
151) -> Result<usize, Error<F>>
152where
153    F: Family,
154    R: Contiguous,
155{
156    if op_count == Location::new(0) {
157        return Ok(0);
158    }
159
160    let floor = find_inactivity_floor_at::<F, _>(reader, op_count, floor_of).await?;
161    Ok(F::inactive_peaks(F::location_to_position(op_count), floor))
162}
163
164/// Errors that can occur when interacting with an authenticated database.
165#[derive(Error, Debug)]
166pub enum Error<F: Family> {
167    #[error("data corrupted: {0}")]
168    DataCorrupted(&'static str),
169
170    #[error("merkle error: {0}")]
171    Merkle(#[from] crate::merkle::Error<F>),
172
173    #[error("metadata error: {0}")]
174    Metadata(#[from] crate::metadata::Error),
175
176    #[error("journal error: {0}")]
177    Journal(#[from] crate::journal::Error),
178
179    #[error("runtime error: {0}")]
180    Runtime(#[from] commonware_runtime::Error),
181
182    #[error("operation pruned: {0}")]
183    OperationPruned(Location<F>),
184
185    /// The requested key was not found in the snapshot.
186    #[error("key not found")]
187    KeyNotFound,
188
189    /// The key exists in the db, so we cannot prove its exclusion.
190    #[error("key exists")]
191    KeyExists,
192
193    #[error("unexpected data at location: {0}")]
194    UnexpectedData(Location<F>),
195
196    #[error("location out of bounds: {0} >= {1}")]
197    LocationOutOfBounds(Location<F>, Location<F>),
198
199    #[error("prune location {0} beyond minimum required location {1}")]
200    PruneBeyondMinRequired(Location<F>, Location<F>),
201
202    /// The batch was created from a different database state than the current one.
203    ///
204    /// See [`batch_chain`] for more details on staleness detection.
205    #[error(
206        "stale batch: db has {db_size} ops, batch requires {batch_db_size}, {batch_base_size}, or an ancestor boundary"
207    )]
208    StaleBatch {
209        db_size: u64,
210        batch_db_size: u64,
211        batch_base_size: u64,
212    },
213
214    /// The batch's inactivity floor is lower than the database's current floor.
215    #[error("floor regressed: batch floor {0} < current floor {1}")]
216    FloorRegressed(Location<F>, Location<F>),
217
218    /// The batch's inactivity floor exceeds its own commit operation's location. The floor
219    /// must not sit past the commit, since a subsequent `prune(floor)` would then remove the
220    /// last readable commit from the journal.
221    #[error("floor beyond commit location: floor {0} > commit loc {1}")]
222    FloorBeyondSize(Location<F>, Location<F>),
223
224    /// The inactivity floor that governed the requested `historical_size` is not retrievable from
225    /// the journal, so the wrapper cannot derive the `inactive_peaks` count needed to construct a
226    /// proof matching the historical root.
227    ///
228    /// Historical proofs require `historical_size` to be a commit-boundary: the operation at
229    /// `historical_size - 1` must itself be a commit op declaring the governing floor. This error
230    /// fires when the caller passes a non-commit-boundary size, or when pruning has removed the
231    /// commit that would have governed the size.
232    #[error("historical floor pruned for size: {0}")]
233    HistoricalFloorPruned(Location<F>),
234}
235
236impl<F: Family> From<crate::journal::authenticated::Error<F>> for Error<F> {
237    fn from(e: crate::journal::authenticated::Error<F>) -> Self {
238        match e {
239            crate::journal::authenticated::Error::Journal(j) => Self::Journal(j),
240            crate::journal::authenticated::Error::Merkle(m) => Self::Merkle(m),
241        }
242    }
243}
244
245/// The size of the read buffer to use for replaying the operations log when rebuilding the
246/// snapshot.
247const SNAPSHOT_READ_BUFFER_SIZE: NonZeroUsize = NZUsize!(1 << 16);
248
249/// Builds the database's snapshot by replaying the log starting at the inactivity floor. Assumes
250/// the log is not pruned beyond the inactivity floor. The callback is invoked for each replayed
251/// operation, indicating activity status updates. The first argument of the callback is the
252/// activity status of the operation, and the second argument is the location of the operation it
253/// inactivates (if any). Returns the number of active keys in the db.
254///
255/// `cache_size` bounds a `(location -> key)` cache that lets collision resolution resolve
256/// candidates from memory instead of re-reading the log; `None` disables it.
257pub(super) async fn build_snapshot_from_log<F, C, I, Fn>(
258    inactivity_floor_loc: crate::merkle::Location<F>,
259    reader: &C,
260    snapshot: &mut I,
261    cache_size: Option<NonZeroUsize>,
262    mut callback: Fn,
263) -> Result<usize, Error<F>>
264where
265    F: crate::merkle::Family,
266    C: Contiguous<Item: Operation<F>>,
267    I: Index<Value = crate::merkle::Location<F>>,
268    Fn: FnMut(bool, Option<crate::merkle::Location<F>>),
269{
270    let bounds = reader.bounds();
271    let stream = reader
272        .replay(*inactivity_floor_loc, SNAPSHOT_READ_BUFFER_SIZE)
273        .await?;
274    pin_mut!(stream);
275    let last_commit_loc = bounds.end.saturating_sub(1);
276
277    // Memoize `(location -> key)` for replayed update ops so collision resolution in
278    // `find_update_op` resolves candidates from memory instead of re-reading (and re-decoding) the
279    // log.
280    let mut cache = cache_size.map(Clock::<u64, <C::Item as Operation<F>>::Key>::new);
281
282    let mut active_keys: usize = 0;
283    while let Some(result) = stream.next().await {
284        let (loc, op) = result?;
285        if let Some(key) = op.key() {
286            if op.is_delete() {
287                let old_loc = delete_key(snapshot, reader, key, cache.as_mut()).await?;
288                callback(false, old_loc);
289                if old_loc.is_some() {
290                    active_keys -= 1;
291                }
292            } else if op.is_update() {
293                let new_loc = crate::merkle::Location::new(loc);
294                let old_loc = update_key(snapshot, reader, key, new_loc, cache.as_mut()).await?;
295                callback(true, old_loc);
296                if old_loc.is_none() {
297                    active_keys += 1;
298                }
299
300                // This update op is now a `find_update_op` candidate for later ops of its key.
301                if let Some(cache) = cache.as_mut() {
302                    cache.put(loc, key.clone());
303                }
304            }
305        } else if op.has_floor().is_some() {
306            callback(loc == last_commit_loc, None);
307        }
308    }
309
310    Ok(active_keys)
311}
312
313/// Delete `key` from the snapshot if it exists, using a stable log reader, and return the
314/// previously associated location.
315async fn delete_key<F, I, R>(
316    snapshot: &mut I,
317    reader: &R,
318    key: &<R::Item as Operation<F>>::Key,
319    cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
320) -> Result<Option<Location<F>>, Error<F>>
321where
322    F: Family,
323    I: Index<Value = Location<F>>,
324    R: Contiguous,
325    R::Item: Operation<F>,
326{
327    // If the translated key is in the snapshot, get a cursor to look for the key.
328    let Some(mut cursor) = snapshot.get_mut(key) else {
329        return Ok(None);
330    };
331
332    // Find the matching key among all conflicts, then delete it.
333    let Some(loc) = find_update_op::<F, _>(reader, &mut cursor, key, cache).await? else {
334        return Ok(None);
335    };
336    cursor.delete();
337
338    Ok(Some(loc))
339}
340
341/// Update `key` in the snapshot using a stable log reader, returning its old location if present.
342async fn update_key<F, I, R>(
343    snapshot: &mut I,
344    reader: &R,
345    key: &<R::Item as Operation<F>>::Key,
346    new_loc: Location<F>,
347    cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
348) -> Result<Option<Location<F>>, Error<F>>
349where
350    F: Family,
351    I: Index<Value = Location<F>>,
352    R: Contiguous,
353    R::Item: Operation<F>,
354{
355    // If the translated key is not in the snapshot, insert the new location. Otherwise, get a
356    // cursor to look for the key.
357    let Some(mut cursor) = snapshot.get_mut_or_insert(key, new_loc) else {
358        return Ok(None);
359    };
360
361    // Find the matching key among all conflicts, then update its location.
362    if let Some(loc) = find_update_op::<F, _>(reader, &mut cursor, key, cache).await? {
363        assert!(new_loc > loc);
364        cursor.update(new_loc);
365        return Ok(Some(loc));
366    }
367
368    // The key wasn't in the snapshot, so add it to the cursor.
369    cursor.insert(new_loc);
370
371    Ok(None)
372}
373
374/// Find and return the location of the update operation for `key`, if it exists. The cursor is
375/// positioned at the matching location, and can be used to update or delete the key.
376async fn find_update_op<F, R>(
377    reader: &R,
378    cursor: &mut impl Cursor<Value = Location<F>>,
379    key: &<R::Item as Operation<F>>::Key,
380    mut cache: Option<&mut Clock<u64, <R::Item as Operation<F>>::Key>>,
381) -> Result<Option<Location<F>>, Error<F>>
382where
383    F: Family,
384    R: Contiguous,
385    R::Item: Operation<F>,
386{
387    while let Some(&loc) = cursor.next() {
388        // Consult the cache first; on a miss, read the log and populate.
389        let matches = if let Some(k) = cache.as_deref().and_then(|c| c.get(&*loc)) {
390            *k == *key
391        } else {
392            let op = reader.read(*loc).await?;
393            let k = op.key().expect("operation without key");
394            let matches = *k == *key;
395            if let Some(cache) = cache.as_deref_mut() {
396                cache.put(*loc, k.clone());
397            }
398            matches
399        };
400        if matches {
401            return Ok(Some(loc));
402        }
403    }
404
405    Ok(None)
406}
407
408/// For the given `key` which is known to exist in the snapshot with location `old_loc`, update
409/// its location to `new_loc`.
410///
411/// # Panics
412///
413/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
414fn update_known_loc<F: Family, I: Index<Value = Location<F>>>(
415    snapshot: &mut I,
416    key: &[u8],
417    old_loc: Location<F>,
418    new_loc: Location<F>,
419) {
420    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
421    assert!(
422        cursor.find(|&loc| *loc == old_loc),
423        "known key with given old_loc should have been found"
424    );
425    cursor.update(new_loc);
426}
427
428/// For the given `key` which is known to exist in the snapshot with location `old_loc`, delete
429/// it from the snapshot.
430///
431/// # Panics
432///
433/// Panics if `key` is not found in the snapshot or if `old_loc` is not found in the cursor.
434fn delete_known_loc<F: Family, I: Index<Value = Location<F>>>(
435    snapshot: &mut I,
436    key: &[u8],
437    old_loc: Location<F>,
438) {
439    let mut cursor = snapshot.get_mut(key).expect("key should be known to exist");
440    assert!(
441        cursor.find(|&loc| *loc == old_loc),
442        "known key with given old_loc should have been found"
443    );
444    cursor.delete();
445}
446
447/// A wrapper of DB state required for implementing inactivity floor management.
448pub(crate) struct FloorHelper<
449    'a,
450    F: Family,
451    I: Index<Value = Location<F>>,
452    C: Mutable<Item: Operation<F>>,
453> {
454    pub snapshot: &'a mut I,
455    pub log: &'a mut C,
456}
457
458impl<F, I, C> FloorHelper<'_, F, I, C>
459where
460    F: Family,
461    I: Index<Value = Location<F>>,
462    C: Mutable<Item: Operation<F>>,
463{
464    /// Moves the given operation to the tip of the log if it is active, rendering its old location
465    /// inactive. If the operation was not active, then this is a no-op. Returns whether the
466    /// operation was moved.
467    async fn move_op_if_active(
468        &mut self,
469        op: C::Item,
470        old_loc: Location<F>,
471    ) -> Result<bool, Error<F>> {
472        let Some(key) = op.key() else {
473            return Ok(false); // operations without keys cannot be active
474        };
475
476        // If we find a snapshot entry corresponding to the operation, we know it's active.
477        {
478            let Some(mut cursor) = self.snapshot.get_mut(key) else {
479                return Ok(false);
480            };
481            if !cursor.find(|&loc| loc == old_loc) {
482                return Ok(false);
483            }
484
485            // Update the operation's snapshot location to point to tip.
486            cursor.update(Location::<F>::new(self.log.bounds().end));
487        }
488
489        // Apply the operation at tip.
490        self.log.append(&op).await?;
491
492        Ok(true)
493    }
494
495    /// Raise the inactivity floor by taking one _step_, which involves searching for the first
496    /// active operation above the inactivity floor, moving it to tip, and then setting the
497    /// inactivity floor to the location following the moved operation. This method is therefore
498    /// guaranteed to raise the floor by at least one. Returns the new inactivity floor location.
499    ///
500    /// # Panics
501    ///
502    /// Expects there is at least one active operation above the inactivity floor, and panics
503    /// otherwise.
504    async fn raise_floor(
505        &mut self,
506        mut inactivity_floor_loc: Location<F>,
507    ) -> Result<Location<F>, Error<F>> {
508        let tip_loc: Location<F> = Location::new(self.log.bounds().end);
509        loop {
510            assert!(
511                *inactivity_floor_loc < tip_loc,
512                "no active operations above the inactivity floor"
513            );
514            let old_loc = inactivity_floor_loc;
515            inactivity_floor_loc += 1;
516            let op = self.log.read(*old_loc).await?;
517            if self.move_op_if_active(op, old_loc).await? {
518                return Ok(inactivity_floor_loc);
519            }
520        }
521    }
522}