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    index::unordered::Index,
12    journal::contiguous::fixed::Journal,
13    merkle::{Graftable, Location},
14    qmdb::{
15        any::{unordered::fixed::Operation, value::FixedEncoding, FixedValue},
16        current::FixedConfig as Config,
17        Error,
18    },
19    translator::Translator,
20    Context,
21};
22use commonware_cryptography::Hasher;
23use commonware_parallel::Strategy;
24use commonware_utils::Array;
25
26/// A specialization of [super::db::Db] for unordered key spaces and fixed-size values.
27pub type Db<F, E, K, V, H, T, const N: usize, S> = super::db::Db<
28    F,
29    E,
30    Journal<E, Operation<F, K, V>>,
31    K,
32    FixedEncoding<V>,
33    Index<T, Location<F>>,
34    H,
35    N,
36    S,
37>;
38
39impl<
40        F: Graftable,
41        E: Context,
42        K: Array,
43        V: FixedValue,
44        H: Hasher,
45        T: Translator,
46        const N: usize,
47        S: Strategy,
48    > Db<F, E, K, V, H, T, N, S>
49{
50    /// Initializes a [Db] authenticated database from the given `config`.
51    /// The configured [`Strategy`] is used to parallelize merkleization.
52    pub async fn init(context: E, config: Config<T, S>) -> Result<Self, Error<F>> {
53        crate::qmdb::current::init(context, config).await
54    }
55}
56
57pub mod partitioned {
58    //! A partitioned variant of [super] that uses a partitioned index for the snapshot.
59    //!
60    //! See [crate::qmdb::any::unordered::fixed::partitioned] for details on partitioned indices and
61    //! when to use them.
62
63    use super::*;
64    use crate::index::partitioned::unordered::Index;
65
66    /// A partitioned variant of [super::Db].
67    ///
68    /// The const generic `P` specifies the number of prefix bytes used for partitioning:
69    /// - `P = 1`: 256 partitions
70    /// - `P = 2`: 65,536 partitions
71    /// - `P = 3`: ~16 million partitions
72    pub type Db<F, E, K, V, H, T, const P: usize, const N: usize, S> =
73        crate::qmdb::current::unordered::db::Db<
74            F,
75            E,
76            Journal<E, Operation<F, K, V>>,
77            K,
78            FixedEncoding<V>,
79            Index<T, Location<F>, P>,
80            H,
81            N,
82            S,
83        >;
84
85    impl<
86            F: Graftable,
87            E: Context,
88            K: Array,
89            V: FixedValue,
90            H: Hasher,
91            T: Translator,
92            const P: usize,
93            const N: usize,
94            S: Strategy,
95        > Db<F, E, K, V, H, T, P, N, S>
96    {
97        /// Initializes a [Db] authenticated database from the given `config`.
98        /// The configured [`Strategy`] is used to parallelize merkleization.
99        pub async fn init(context: E, config: Config<T, S>) -> Result<Self, Error<F>> {
100            crate::qmdb::current::init(context, config).await
101        }
102    }
103}
104
105#[cfg(test)]
106pub mod test {
107    use super::*;
108    use crate::{
109        mmr,
110        qmdb::current::{tests::fixed_config, unordered::tests as shared},
111        translator::TwoCap,
112    };
113    use commonware_cryptography::{sha256::Digest, Sha256};
114    use commonware_macros::test_traced;
115    use commonware_runtime::{deterministic, Metrics, Runner as _, Supervisor as _};
116    use commonware_utils::TestRng;
117    use rand::Rng as _;
118    use std::collections::HashMap;
119
120    /// A type alias for the concrete [Db] type used in these unit tests.
121    type CurrentTest = Db<
122        mmr::Family,
123        deterministic::Context,
124        Digest,
125        Digest,
126        Sha256,
127        TwoCap,
128        32,
129        commonware_parallel::Sequential,
130    >;
131
132    /// Return a [Db] database initialized with a fixed config.
133    async fn open_db(context: deterministic::Context, partition_prefix: String) -> CurrentTest {
134        let cfg = fixed_config::<TwoCap>(&partition_prefix, &context);
135        CurrentTest::init(context, cfg).await.unwrap()
136    }
137
138    #[test_traced("INFO")]
139    pub fn test_current_unordered_fixed_metrics() {
140        deterministic::Runner::default().start(|ctx| async move {
141            let mut db = open_db(ctx.child("current"), "metrics".to_string()).await;
142            let key = Sha256::fill(1u8);
143            let value = Sha256::fill(2u8);
144            let batch = db
145                .new_batch()
146                .write(key, Some(value))
147                .merkleize(&db, None)
148                .await
149                .unwrap();
150            db.apply_batch(batch).await.unwrap();
151            assert_eq!(db.get(&key).await.unwrap(), Some(value));
152            db.sync().await.unwrap();
153            db.prune(db.sync_boundary()).await.unwrap();
154
155            let metrics = ctx.encode();
156            for expected in [
157                "current_apply_batch_calls_total 1",
158                "current_sync_calls_total 1",
159                "current_prune_calls_total 1",
160                "current_pruned_chunks 0",
161                "current_sync_boundary 0",
162                "current_apply_batch_duration_count 1",
163                "current_sync_duration_count 1",
164                "current_prune_duration_count 1",
165                "current_any_get_calls_total 1",
166                "current_any_apply_batch_calls_total 1",
167            ] {
168                assert!(metrics.contains(expected), "missing {expected}\n{metrics}");
169            }
170            assert!(!metrics.contains("current_get_calls_total"));
171        });
172    }
173
174    /// Reads on a batch must not perturb `merkleize`: the root must match a write-only batch's
175    /// `merkleize`, both rooted at the DB (D=0) and through one pending ancestor (D=1).
176    #[test_traced("WARN")]
177    pub fn test_current_unordered_fixed_read_merkleize_parity() {
178        fn key(i: u64) -> Digest {
179            Sha256::hash(&i.to_be_bytes())
180        }
181        fn val(i: u64) -> Digest {
182            Sha256::hash(&(i + 10000).to_be_bytes())
183        }
184
185        deterministic::Runner::default().start(|ctx| async move {
186            let mut db = open_db(ctx.child("current"), "fused-parity".to_string()).await;
187
188            let mut seed = db.new_batch();
189            for i in 0..2000u64 {
190                seed = seed.write(key(i), Some(val(i)));
191            }
192            let seed = seed.merkleize(&db, None).await.unwrap();
193            db.apply_batch(seed).await.unwrap();
194            db.commit().await.unwrap();
195
196            let make = |salt: u64| -> Vec<(Digest, Option<Digest>)> {
197                let mut rng = TestRng::new(salt);
198                let mut out = Vec::new();
199                for _ in 0..600 {
200                    let r = rng.next_u32() % 100;
201                    if r < 60 {
202                        out.push((key(rng.next_u64() % 2000), Some(val(rng.next_u64()))));
203                    } else if r < 80 {
204                        out.push((key(rng.next_u64() % 2000), None));
205                    } else {
206                        out.push((key(2000 + rng.next_u64() % 2000), Some(val(rng.next_u64()))));
207                    }
208                }
209                let mut m: HashMap<Digest, Option<Digest>> = HashMap::new();
210                for (k, v) in out {
211                    m.insert(k, v);
212                }
213                m.into_iter().collect()
214            };
215
216            for depth in [0u8, 1u8] {
217                let parent = if depth == 1 {
218                    let mut p = db.new_batch();
219                    for (k, v) in make(900) {
220                        p = p.write(k, v);
221                    }
222                    Some(p.merkleize(&db, None).await.unwrap())
223                } else {
224                    None
225                };
226
227                let muts = make(depth as u64 + 1);
228                let new_batch = || {
229                    parent
230                        .as_ref()
231                        .map_or_else(|| db.new_batch(), |p| p.new_batch::<Sha256>())
232                };
233
234                let mut nb = new_batch();
235                for (k, v) in &muts {
236                    nb = nb.write(*k, *v);
237                }
238                let normal_root = nb.merkleize(&db, None).await.unwrap().root();
239
240                let keys: Vec<&Digest> = muts.iter().map(|(k, _)| k).collect();
241                let mut fb = new_batch();
242                let values = fb.get_many(&keys, &db).await.unwrap();
243                let plain = new_batch().get_many(&keys, &db).await.unwrap();
244                assert_eq!(values, plain, "value mismatch at depth={depth}");
245                for (k, v) in &muts {
246                    fb = fb.write(*k, *v);
247                }
248                let fused_root = fb.merkleize(&db, None).await.unwrap().root();
249                assert_eq!(normal_root, fused_root, "root mismatch at depth={depth}");
250            }
251        });
252    }
253
254    crate::qmdb::current::tests::staged_merkleize_parity_test!(
255        test_current_unordered_fixed_staged_merkleize_parity,
256        open_db
257    );
258
259    /// A staged read that resolved in a grandparent's diff must survive that grandparent
260    /// committing and being freed before `Staged::merkleize`, through the current layer:
261    /// the re-derived bases feed the grafted bitmap and `compute_current_layer`, so the
262    /// staged root must match the explicit path's and the full lifecycle must read back.
263    /// Mirrors the `any::unordered::variable` coverage of this interleaving (see
264    /// `StagedLoc`).
265    #[test_traced("WARN")]
266    pub fn test_current_unordered_fixed_staged_ancestor_commit_before_merkleize() {
267        fn key(i: u64) -> Digest {
268            Sha256::hash(&i.to_be_bytes())
269        }
270        fn val(i: u64) -> Digest {
271            Sha256::hash(&(i + 10000).to_be_bytes())
272        }
273
274        deterministic::Runner::default().start(|ctx| async move {
275            let mut db = open_db(ctx.child("current"), "staged-ancestor".to_string()).await;
276
277            // Committed base state, so the grandparent's write of key(0) supersedes a
278            // committed location. Its create of key(100) supersedes none.
279            let mut seed = db.new_batch();
280            for i in 0..8u64 {
281                seed = seed.write(key(i), Some(val(i)));
282            }
283            let seed = seed.merkleize(&db, None).await.unwrap();
284            db.apply_batch(seed).await.unwrap();
285            db.commit().await.unwrap();
286
287            // Grandparent -> parent chain. The parent touches neither staged key, so the
288            // staged reads resolve in the grandparent's diff.
289            let grandparent = db
290                .new_batch()
291                .write(key(0), Some(val(1_000)))
292                .write(key(100), Some(val(1_001)))
293                .merkleize(&db, None)
294                .await
295                .unwrap();
296            let parent = grandparent
297                .new_batch::<Sha256>()
298                .write(key(1), Some(val(1_002)))
299                .merkleize(&db, None)
300                .await
301                .unwrap();
302
303            let read_keys = [key(0), key(100)];
304            let keys: Vec<&Digest> = read_keys.iter().collect();
305            let (values, staged) = parent
306                .new_batch::<Sha256>()
307                .stage(&keys, &db)
308                .await
309                .unwrap();
310            assert_eq!(values, vec![Some(val(1_000)), Some(val(1_001))]);
311
312            // Commit and free the grandparent: the staged resolutions' locations migrate
313            // into the committed region, retiring their recorded bases.
314            db.apply_batch(grandparent).await.unwrap();
315
316            let updates = vec![(0, Some(val(2_000))), (1, Some(val(2_001)))];
317            let staged = staged
318                .merkleize(updates, Vec::new(), None, &db)
319                .await
320                .unwrap();
321
322            // The explicit path over the same post-commit state must agree.
323            let explicit_root = parent
324                .new_batch::<Sha256>()
325                .write(key(0), Some(val(2_000)))
326                .write(key(100), Some(val(2_001)))
327                .merkleize(&db, None)
328                .await
329                .unwrap()
330                .root();
331            assert_eq!(staged.root(), explicit_root);
332
333            db.apply_batch(parent).await.unwrap();
334            db.apply_batch(staged).await.unwrap();
335            db.commit().await.unwrap();
336
337            assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(2_000)));
338            assert_eq!(db.get(&key(100)).await.unwrap(), Some(val(2_001)));
339            assert_eq!(db.get(&key(1)).await.unwrap(), Some(val(1_002)));
340        });
341    }
342
343    /// The sync boundary recorded from a merkleized batch must match the boundary the database
344    /// reports once that batch is applied. These can diverge if the batch boundary is derived from
345    /// physical bitmap pruning rather than the batch's declared inactivity floor, because the floor
346    /// can advance past a chunk even when pruning has not run. Reopening is exercised afterward as a
347    /// persistence sanity check; it must not move the boundary.
348    #[test_traced("INFO")]
349    pub fn test_merkleized_batch_sync_boundary_matches_db() {
350        deterministic::Runner::default().start(|ctx| async move {
351            let partition = "batch-boundary-match".to_string();
352            let mut db = open_db(ctx.child("current"), partition.clone()).await;
353
354            let key = Sha256::fill(1u8);
355            let mut last_batch_boundary = mmr::Location::new(0);
356            for i in 0..300u64 {
357                let value = Sha256::hash(&i.to_be_bytes());
358                let batch = db
359                    .new_batch()
360                    .write(key, Some(value))
361                    .merkleize(&db, None)
362                    .await
363                    .unwrap();
364                last_batch_boundary = batch.sync_boundary();
365                db.apply_batch(batch).await.unwrap();
366            }
367            db.sync().await.unwrap();
368
369            // The boundary must have advanced, otherwise the inactivity floor never crossed a chunk
370            // and the equality below would hold trivially.
371            let db_boundary = db.sync_boundary();
372            assert!(
373                *db_boundary > 0,
374                "inactivity floor never crossed a chunk; add more commits"
375            );
376
377            // The headline invariant: the boundary the batch advertised equals the boundary the DB
378            // reports after applying that batch.
379            assert_eq!(
380                last_batch_boundary, db_boundary,
381                "batch boundary diverged from applied db boundary"
382            );
383
384            // Reopening must not move the boundary.
385            drop(db);
386            let reopened = open_db(ctx.child("reopen"), partition).await;
387            assert_eq!(
388                reopened.sync_boundary(),
389                last_batch_boundary,
390                "reopened db boundary disagrees with the boundary recorded from the last merkleized batch"
391            );
392            reopened.destroy().await.unwrap();
393        });
394    }
395
396    #[test_traced("DEBUG")]
397    pub fn test_current_db_verify_proof_over_bits_in_uncommitted_chunk() {
398        shared::test_verify_proof_over_bits_in_uncommitted_chunk(open_db);
399    }
400
401    #[test_traced("DEBUG")]
402    pub fn test_current_db_range_proofs() {
403        shared::test_range_proofs(open_db);
404    }
405
406    #[test_traced("DEBUG")]
407    pub fn test_current_db_key_value_proof() {
408        shared::test_key_value_proof(open_db);
409    }
410
411    #[test_traced("WARN")]
412    pub fn test_current_db_proving_repeated_updates() {
413        shared::test_proving_repeated_updates(open_db);
414    }
415}