Skip to main content

commonware_storage/qmdb/current/unordered/
fixed.rs

1//! An _unordered_ variant of a [crate::qmdb::current] authenticated database optimized for
2//! fixed-size values.
3//!
4//! This variant does not maintain key ordering, so it cannot generate exclusion proofs. Use
5//! [crate::qmdb::current::ordered::fixed] if exclusion proofs are required.
6//!
7//! See [Db] for the main database type.
8
9pub use super::db::KeyValueProof;
10use crate::{
11    Context,
12    index::unordered::Index,
13    journal::contiguous::fixed::Journal,
14    merkle::{Graftable, Location},
15    qmdb::{
16        Error,
17        any::{FixedValue, unordered::fixed::Operation, value::FixedEncoding},
18        current::FixedConfig as Config,
19    },
20    translator::Translator,
21};
22use commonware_cryptography::Hasher;
23use commonware_parallel::Strategy;
24use commonware_runtime::Spawner;
25use commonware_utils::Array;
26
27/// A specialization of [super::db::Db] for unordered key spaces and fixed-size values.
28pub type Db<F, E, K, V, H, T, const N: usize, S> = super::db::Db<
29    F,
30    E,
31    Journal<E, Operation<F, K, V>>,
32    K,
33    FixedEncoding<V>,
34    Index<T, Location<F>>,
35    H,
36    N,
37    S,
38>;
39
40impl<
41    F: Graftable,
42    E: Context + Spawner,
43    K: Array,
44    V: FixedValue,
45    H: Hasher,
46    T: Translator,
47    const N: usize,
48    S: Strategy,
49> Db<F, E, K, V, H, T, N, S>
50{
51    /// Initializes a [Db] authenticated database from the given `config`.
52    /// The configured [`Strategy`] is used to parallelize merkleization.
53    pub async fn init(context: E, config: Config<T, S>) -> Result<Self, Error<F>> {
54        crate::qmdb::current::init(context, config).await
55    }
56}
57
58pub mod partitioned {
59    //! A partitioned variant of [super] that uses a partitioned index for the snapshot.
60    //!
61    //! See [crate::qmdb::any::unordered::fixed::partitioned] for details on partitioned indices and
62    //! when to use them.
63
64    use super::*;
65    use crate::index::partitioned::unordered::Index;
66
67    /// A partitioned variant of [super::Db].
68    ///
69    /// The const generic `P` specifies the number of prefix bytes used for partitioning:
70    /// - `P = 1`: 256 partitions
71    /// - `P = 2`: 65,536 partitions
72    /// - `P = 3`: ~16 million partitions
73    pub type Db<F, E, K, V, H, T, const P: usize, const N: usize, S> =
74        crate::qmdb::current::unordered::db::Db<
75            F,
76            E,
77            Journal<E, Operation<F, K, V>>,
78            K,
79            FixedEncoding<V>,
80            Index<T, Location<F>, P>,
81            H,
82            N,
83            S,
84        >;
85
86    impl<
87        F: Graftable,
88        E: Context + Spawner,
89        K: Array,
90        V: FixedValue,
91        H: Hasher,
92        T: Translator,
93        const P: usize,
94        const N: usize,
95        S: Strategy,
96    > Db<F, E, K, V, H, T, P, N, S>
97    {
98        /// Initializes a [Db] authenticated database from the given `config`.
99        /// The configured [`Strategy`] is used to parallelize merkleization.
100        pub async fn init(
101            context: E,
102            config: Config<T, S, core::num::NonZeroUsize>,
103        ) -> Result<Self, Error<F>> {
104            crate::qmdb::current::init(context, config).await
105        }
106    }
107}
108
109#[cfg(test)]
110pub mod test {
111    use super::*;
112    use crate::{
113        mmr,
114        qmdb::current::{
115            tests::{fixed_config, fixed_config_partitioned},
116            unordered::tests as shared,
117        },
118        translator::{OneCap, TwoCap},
119    };
120    use commonware_cryptography::{Sha256, sha256::Digest};
121    use commonware_macros::test_traced;
122    use commonware_parallel::Sequential;
123    use commonware_runtime::{Metrics, Runner as _, Supervisor as _, deterministic};
124    use commonware_utils::TestRng;
125    use rand::Rng as _;
126    use std::collections::HashMap;
127
128    /// A type alias for the concrete [Db] type used in these unit tests.
129    type CurrentTest = Db<
130        mmr::Family,
131        deterministic::Context,
132        Digest,
133        Digest,
134        Sha256,
135        TwoCap,
136        32,
137        commonware_parallel::Sequential,
138    >;
139
140    /// Return a [Db] database initialized with a fixed config.
141    async fn open_db(context: deterministic::Context, partition_prefix: String) -> CurrentTest {
142        let cfg = fixed_config::<TwoCap>(&partition_prefix, &context);
143        CurrentTest::init(context, cfg).await.unwrap()
144    }
145
146    #[test_traced("INFO")]
147    pub fn test_current_unordered_fixed_metrics() {
148        deterministic::Runner::default().start(|ctx| async move {
149            let db = open_db(ctx.child("current"), "metrics".to_string()).await;
150            let key = Sha256::fill(1u8);
151            let value = Sha256::fill(2u8);
152            let batch = db
153                .new_batch()
154                .write(key, Some(value))
155                .merkleize(&db, None)
156                .await
157                .unwrap();
158            let (db, _) = db.apply_batch(batch).await.unwrap();
159            assert_eq!(db.get(&key).await.unwrap(), Some(value));
160            let db = db.sync().await.unwrap();
161            let boundary = db.sync_boundary();
162            let _db = db.prune(boundary).await.unwrap();
163
164            let metrics = ctx.encode();
165            for expected in [
166                "current_apply_batch_calls_total 1",
167                "current_sync_calls_total 1",
168                "current_prune_calls_total 1",
169                "current_pruned_chunks 0",
170                "current_sync_boundary 0",
171                "current_apply_batch_duration_count 1",
172                "current_sync_duration_count 1",
173                "current_prune_duration_count 1",
174                "current_any_get_calls_total 1",
175                "current_any_apply_batch_calls_total 1",
176            ] {
177                assert!(metrics.contains(expected), "missing {expected}\n{metrics}");
178            }
179            assert!(!metrics.contains("current_get_calls_total"));
180        });
181    }
182
183    /// Reads on a batch must not perturb `merkleize`: the root must match a write-only batch's
184    /// `merkleize`, both rooted at the DB (D=0) and through one pending ancestor (D=1).
185    #[test_traced("WARN")]
186    pub fn test_current_unordered_fixed_read_merkleize_parity() {
187        fn key(i: u64) -> Digest {
188            Sha256::hash(&[&i.to_be_bytes()])
189        }
190        fn val(i: u64) -> Digest {
191            Sha256::hash(&[&(i + 10000).to_be_bytes()])
192        }
193
194        deterministic::Runner::default().start(|ctx| async move {
195            let db = open_db(ctx.child("current"), "fused-parity".to_string()).await;
196
197            let mut seed = db.new_batch();
198            for i in 0..2000u64 {
199                seed = seed.write(key(i), Some(val(i)));
200            }
201            let seed = seed.merkleize(&db, None).await.unwrap();
202            let (db, _) = db.apply_batch(seed).await.unwrap();
203            let db = db.commit().await.unwrap();
204
205            let make = |salt: u64| -> Vec<(Digest, Option<Digest>)> {
206                let mut rng = TestRng::new(salt);
207                let mut out = Vec::new();
208                for _ in 0..600 {
209                    let r = rng.next_u32() % 100;
210                    if r < 60 {
211                        out.push((key(rng.next_u64() % 2000), Some(val(rng.next_u64()))));
212                    } else if r < 80 {
213                        out.push((key(rng.next_u64() % 2000), None));
214                    } else {
215                        out.push((key(2000 + rng.next_u64() % 2000), Some(val(rng.next_u64()))));
216                    }
217                }
218                let mut m: HashMap<Digest, Option<Digest>> = HashMap::new();
219                for (k, v) in out {
220                    m.insert(k, v);
221                }
222                m.into_iter().collect()
223            };
224
225            for depth in [0u8, 1u8] {
226                let parent = if depth == 1 {
227                    let mut p = db.new_batch();
228                    for (k, v) in make(900) {
229                        p = p.write(k, v);
230                    }
231                    Some(p.merkleize(&db, None).await.unwrap())
232                } else {
233                    None
234                };
235
236                let muts = make(depth as u64 + 1);
237                let new_batch = || {
238                    parent
239                        .as_ref()
240                        .map_or_else(|| db.new_batch(), |p| p.new_batch::<Sha256>())
241                };
242
243                let mut nb = new_batch();
244                for (k, v) in &muts {
245                    nb = nb.write(*k, *v);
246                }
247                let normal_root = nb.merkleize(&db, None).await.unwrap().root();
248
249                let keys: Vec<&Digest> = muts.iter().map(|(k, _)| k).collect();
250                let mut fb = new_batch();
251                let values = fb.get_many(&keys, &db).await.unwrap();
252                let plain = new_batch().get_many(&keys, &db).await.unwrap();
253                assert_eq!(values, plain, "value mismatch at depth={depth}");
254                for (k, v) in &muts {
255                    fb = fb.write(*k, *v);
256                }
257                let fused_root = fb.merkleize(&db, None).await.unwrap().root();
258                assert_eq!(normal_root, fused_root, "root mismatch at depth={depth}");
259            }
260        });
261    }
262
263    crate::qmdb::current::tests::staged_merkleize_parity_test!(
264        test_current_unordered_fixed_staged_merkleize_parity,
265        open_db
266    );
267
268    /// A staged read that resolved in a grandparent's diff must survive that grandparent
269    /// committing and being freed before `Staged::merkleize`, through the current layer:
270    /// the re-derived bases feed the grafted bitmap and `compute_current_layer`, so the
271    /// staged root must match the explicit path's and the full lifecycle must read back.
272    /// Mirrors the `any::unordered::variable` coverage of this interleaving (see
273    /// `StagedLoc`).
274    #[test_traced("WARN")]
275    pub fn test_current_unordered_fixed_staged_ancestor_commit_before_merkleize() {
276        fn key(i: u64) -> Digest {
277            Sha256::hash(&[&i.to_be_bytes()])
278        }
279        fn val(i: u64) -> Digest {
280            Sha256::hash(&[&(i + 10000).to_be_bytes()])
281        }
282
283        deterministic::Runner::default().start(|ctx| async move {
284            let db = open_db(ctx.child("current"), "staged-ancestor".to_string()).await;
285
286            // Committed base state, so the grandparent's write of key(0) supersedes a
287            // committed location. Its create of key(100) supersedes none.
288            let mut seed = db.new_batch();
289            for i in 0..8u64 {
290                seed = seed.write(key(i), Some(val(i)));
291            }
292            let seed = seed.merkleize(&db, None).await.unwrap();
293            let (db, _) = db.apply_batch(seed).await.unwrap();
294            let db = db.commit().await.unwrap();
295
296            // Grandparent -> parent chain. The parent touches neither staged key, so the
297            // staged reads resolve in the grandparent's diff.
298            let grandparent = db
299                .new_batch()
300                .write(key(0), Some(val(1_000)))
301                .write(key(100), Some(val(1_001)))
302                .merkleize(&db, None)
303                .await
304                .unwrap();
305            let parent = grandparent
306                .new_batch::<Sha256>()
307                .write(key(1), Some(val(1_002)))
308                .merkleize(&db, None)
309                .await
310                .unwrap();
311
312            let read_keys = [key(0), key(100)];
313            let keys: Vec<&Digest> = read_keys.iter().collect();
314            let (values, staged) = parent
315                .new_batch::<Sha256>()
316                .stage(&keys, &db)
317                .await
318                .unwrap();
319            assert_eq!(values, vec![Some(val(1_000)), Some(val(1_001))]);
320
321            // Commit and free the grandparent: the staged resolutions' locations migrate
322            // into the committed region, retiring their recorded bases.
323            let (db, _) = db.apply_batch(grandparent).await.unwrap();
324
325            let updates = vec![(0, Some(val(2_000))), (1, Some(val(2_001)))];
326            let staged = staged
327                .merkleize(updates, Vec::new(), None, &db)
328                .await
329                .unwrap();
330
331            // The explicit path over the same post-commit state must agree.
332            let explicit_root = parent
333                .new_batch::<Sha256>()
334                .write(key(0), Some(val(2_000)))
335                .write(key(100), Some(val(2_001)))
336                .merkleize(&db, None)
337                .await
338                .unwrap()
339                .root();
340            assert_eq!(staged.root(), explicit_root);
341
342            let (db, _) = db.apply_batch(parent).await.unwrap();
343            let (db, _) = db.apply_batch(staged).await.unwrap();
344            let db = db.commit().await.unwrap();
345
346            assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(2_000)));
347            assert_eq!(db.get(&key(100)).await.unwrap(), Some(val(2_001)));
348            assert_eq!(db.get(&key(1)).await.unwrap(), Some(val(1_002)));
349        });
350    }
351
352    /// The sync boundary recorded from a merkleized batch must match the boundary the database
353    /// reports once that batch is applied. These can diverge if the batch boundary is derived from
354    /// physical bitmap pruning rather than the batch's declared inactivity floor, because the floor
355    /// can advance past a chunk even when pruning has not run. Reopening is exercised afterward as a
356    /// persistence sanity check; it must not move the boundary.
357    #[test_traced("INFO")]
358    pub fn test_merkleized_batch_sync_boundary_matches_db() {
359        deterministic::Runner::default().start(|ctx| async move {
360            let partition = "batch-boundary-match".to_string();
361            let mut db = open_db(ctx.child("current"), partition.clone()).await;
362
363            let key = Sha256::fill(1u8);
364            let mut last_batch_boundary = mmr::Location::new(0);
365            for i in 0..300u64 {
366                let value = Sha256::hash(&[&i.to_be_bytes()]);
367                let batch = db
368                    .new_batch()
369                    .write(key, Some(value))
370                    .merkleize(&db, None)
371                    .await
372                    .unwrap();
373                last_batch_boundary = batch.sync_boundary();
374                (db, _) = db.apply_batch(batch).await.unwrap();
375            }
376            let db = db.sync().await.unwrap();
377
378            // The boundary must have advanced, otherwise the inactivity floor never crossed a chunk
379            // and the equality below would hold trivially.
380            let db_boundary = db.sync_boundary();
381            assert!(
382                *db_boundary > 0,
383                "inactivity floor never crossed a chunk; add more commits"
384            );
385
386            // The headline invariant: the boundary the batch advertised equals the boundary the DB
387            // reports after applying that batch.
388            assert_eq!(
389                last_batch_boundary, db_boundary,
390                "batch boundary diverged from applied db boundary"
391            );
392
393            // Reopening must not move the boundary.
394            drop(db);
395            let reopened = open_db(ctx.child("reopen"), partition).await;
396            assert_eq!(
397                reopened.sync_boundary(),
398                last_batch_boundary,
399                "reopened db boundary disagrees with the boundary recorded from the last merkleized batch"
400            );
401            reopened.destroy().await.unwrap();
402        });
403    }
404
405    #[test_traced("DEBUG")]
406    pub fn test_current_db_verify_proof_over_bits_in_uncommitted_chunk() {
407        shared::test_verify_proof_over_bits_in_uncommitted_chunk(open_db);
408    }
409
410    #[test_traced("DEBUG")]
411    pub fn test_current_db_range_proofs() {
412        shared::test_range_proofs(open_db);
413    }
414
415    #[test_traced("DEBUG")]
416    pub fn test_current_db_key_value_proof() {
417        shared::test_key_value_proof(open_db);
418    }
419
420    #[test_traced("WARN")]
421    pub fn test_current_db_proving_repeated_updates() {
422        shared::test_proving_repeated_updates(open_db);
423    }
424
425    /// Build a `P`-partitioned current db with churny ops across two commits (so the second commit's
426    /// updates and deletes inactivate locations from the first), prune it, then assert that
427    /// reopening it at a range of worker counts reconstructs the identical root and key-value
428    /// state. Unlike the `any` equivalence tests, the current root commits to the activity bitmap,
429    /// so this exercises the parallel build's bitmap reconstruction (`for_each_value` +
430    /// last-commit) over a pruned prefix, not just the snapshot index and MMR.
431    #[commonware_macros::boxed]
432    async fn check_current_parallel_init_equivalence<const P: usize>(
433        context: deterministic::Context,
434        partition: &'static str,
435        concurrency_sweep: &[usize],
436    ) {
437        type PartDb<const P: usize, S> = partitioned::Db<
438            mmr::Family,
439            deterministic::Context,
440            Digest,
441            Digest,
442            Sha256,
443            OneCap,
444            P,
445            32,
446            S,
447        >;
448
449        /// The value each key holds after the two commits below.
450        fn expected_value(i: u64) -> Option<Digest> {
451            if i % 7 == 1 {
452                None
453            } else if i.is_multiple_of(3) {
454                Some(Sha256::hash(&[&((i + 1) * 11).to_be_bytes()]))
455            } else {
456                Some(Sha256::hash(&[&(i * 7).to_be_bytes()]))
457            }
458        }
459
460        let cfg = fixed_config_partitioned::<OneCap>(partition, &context);
461        let db = PartDb::<P, Sequential>::init(context.child("populate"), cfg)
462            .await
463            .unwrap();
464
465        // Commit 1: insert.
466        let mut batch = db.new_batch();
467        for i in 0u64..2000 {
468            let k = Sha256::hash(&[&i.to_be_bytes()]);
469            let v = Sha256::hash(&[&(i * 7).to_be_bytes()]);
470            batch = batch.write(k, Some(v));
471        }
472        let merkleized = batch.merkleize(&db, None).await.unwrap();
473        let (db, _) = db.apply_batch(merkleized).await.unwrap();
474        let db = db.commit().await.unwrap();
475
476        // Commit 2: update a third (inactivating their commit-1 ops) and delete a seventh.
477        let mut batch = db.new_batch();
478        for i in (0u64..2000).step_by(3) {
479            let k = Sha256::hash(&[&i.to_be_bytes()]);
480            let v = Sha256::hash(&[&((i + 1) * 11).to_be_bytes()]);
481            batch = batch.write(k, Some(v));
482        }
483        for i in (1u64..2000).step_by(7) {
484            let k = Sha256::hash(&[&i.to_be_bytes()]);
485            batch = batch.write(k, None);
486        }
487        let merkleized = batch.merkleize(&db, None).await.unwrap();
488        let (db, _) = db.apply_batch(merkleized).await.unwrap();
489        let db = db.commit().await.unwrap();
490
491        // Prune so the reopens rebuild the grafted root over a bitmap with a pruned prefix.
492        let boundary = db.sync_boundary();
493        let db = db.prune(boundary).await.unwrap();
494        let db = db.sync().await.unwrap();
495        let root = db.root();
496        drop(db);
497
498        // Reopen at each concurrency. All rebuild (snapshot + bitmap) from the same log and must
499        // match the original root and serve the expected value for every key.
500        for &concurrency in concurrency_sweep {
501            let mut cfg = fixed_config_partitioned::<OneCap>(partition, &context);
502            cfg.init_concurrency = core::num::NonZeroUsize::new(concurrency).unwrap();
503            let ctx = context
504                .child("reopen")
505                .with_attribute("concurrency", concurrency);
506            let db = PartDb::<P, Sequential>::init(ctx, cfg).await.unwrap();
507            assert_eq!(
508                db.root(),
509                root,
510                "current root mismatch at P={P} concurrency={concurrency}"
511            );
512            for i in 0u64..2000 {
513                let k = Sha256::hash(&[&i.to_be_bytes()]);
514                assert_eq!(
515                    db.get(&k).await.unwrap(),
516                    expected_value(i),
517                    "value mismatch for key {i}"
518                );
519            }
520            drop(db);
521        }
522    }
523
524    /// The unordered sweep runs at P=1 only: `P=2` allocates 65,536 hash sub-indexes per index
525    /// instance, which is too memory-heavy for the default suite, and the range/offset arithmetic
526    /// is shared with the ordered variant's P=2 coverage.
527    #[test_traced("WARN")]
528    fn test_current_unordered_partitioned_p1_parallel_init_equivalence() {
529        deterministic::Runner::default().start(|context| async move {
530            check_current_parallel_init_equivalence::<1>(
531                context,
532                "current_unordered_parallel_equiv_p1",
533                &[1, 2, 3, 5],
534            )
535            .await;
536        });
537    }
538}