Skip to main content

commonware_storage/qmdb/current/ordered/
fixed.rs

1//! An _ordered_ variant of a [crate::qmdb::current] authenticated database optimized for fixed-size
2//! values.
3//!
4//! This variant maintains the lexicographic-next active key for each active key, enabling exclusion
5//! proofs (proving a key is currently inactive). Use [crate::qmdb::current::unordered::fixed] if
6//! exclusion proofs are not needed.
7//!
8//! See [Db] for the main database type and [super::ExclusionProof] for proving key inactivity.
9
10pub use super::db::KeyValueProof;
11use crate::{
12    Context,
13    index::ordered::Index,
14    journal::contiguous::fixed::Journal,
15    merkle::{Graftable, Location},
16    qmdb::{
17        Error,
18        any::{FixedValue, ordered::fixed::Operation, value::FixedEncoding},
19        current::FixedConfig as Config,
20    },
21    translator::Translator,
22};
23use commonware_cryptography::Hasher;
24use commonware_parallel::Strategy;
25use commonware_runtime::Spawner;
26use commonware_utils::Array;
27
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] 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 variant of [super] that uses a partitioned index for the snapshot.
60
61    use super::*;
62    use crate::index::partitioned::ordered::Index;
63
64    /// A partitioned variant of [super::Db].
65    ///
66    /// The const generic `P` specifies the number of prefix bytes used for partitioning:
67    /// - `P = 1`: 256 partitions
68    /// - `P = 2`: 65,536 partitions
69    /// - `P = 3`: ~16 million partitions
70    pub type Db<F, E, K, V, H, T, const P: usize, const N: usize, S> =
71        crate::qmdb::current::ordered::db::Db<
72            F,
73            E,
74            Journal<E, Operation<F, K, V>>,
75            K,
76            FixedEncoding<V>,
77            Index<T, Location<F>, P>,
78            H,
79            N,
80            S,
81        >;
82
83    impl<
84        F: Graftable,
85        E: Context + Spawner,
86        K: Array,
87        V: FixedValue,
88        H: Hasher,
89        T: Translator,
90        const P: usize,
91        const N: usize,
92        S: Strategy,
93    > Db<F, E, K, V, H, T, P, N, S>
94    {
95        /// Initializes a [Db] authenticated database from the given `config`.
96        pub async fn init(
97            context: E,
98            config: Config<T, S, core::num::NonZeroUsize>,
99        ) -> 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::{
111            Error,
112            current::{
113                ordered::tests as shared,
114                tests::{fixed_config, fixed_config_partitioned},
115            },
116        },
117        translator::OneCap,
118    };
119    use commonware_cryptography::{Sha256, sha256::Digest};
120    use commonware_macros::{boxed, test_traced};
121    use commonware_parallel::Sequential;
122    use commonware_runtime::{Runner as _, Supervisor as _, deterministic};
123    use commonware_utils::{
124        NZU64,
125        bitmap::{Prunable as BitMap, Readable as _},
126    };
127
128    /// A type alias for the concrete [Db] type used in these unit tests.
129    type CurrentTest =
130        Db<mmr::Family, deterministic::Context, Digest, Digest, Sha256, OneCap, 32, Sequential>;
131
132    /// Return an [Db] database initialized with a fixed config.
133    async fn open_db(context: deterministic::Context, partition_prefix: String) -> CurrentTest {
134        let cfg = fixed_config::<OneCap>(&partition_prefix, &context);
135        CurrentTest::init(context, cfg).await.unwrap()
136    }
137
138    #[test_traced("DEBUG")]
139    pub fn test_current_db_verify_proof_over_bits_in_uncommitted_chunk() {
140        shared::test_verify_proof_over_bits_in_uncommitted_chunk(open_db);
141    }
142
143    #[test_traced("DEBUG")]
144    pub fn test_current_db_range_proofs() {
145        shared::test_range_proofs(open_db);
146    }
147
148    /// Regression test: requesting a range proof for a location in a pruned bitmap chunk
149    /// must return `Error::OperationPruned`, not panic in the bitmap accessor.
150    #[test_traced("DEBUG")]
151    pub fn test_range_proof_returns_error_on_pruned_chunks() {
152        let executor = deterministic::Runner::default();
153        executor.start(|context| async move {
154            let partition = "range-proofs-pruned".to_string();
155            let mut db = open_db(context.child("db"), partition).await;
156
157            let chunk_bits = BitMap::<32>::CHUNK_SIZE_BITS;
158
159            // Repeatedly update the same key to generate many inactive operations,
160            // pushing the inactivity floor past at least one full bitmap chunk.
161            let key = Sha256::fill(0x11);
162            for i in 0..chunk_bits + 10 {
163                let value = Sha256::hash(&[&i.to_be_bytes()]);
164                let merkleized = db
165                    .new_batch()
166                    .write(key, Some(value))
167                    .merkleize(&db, None)
168                    .await
169                    .unwrap();
170                (db, _) = db.apply_batch(merkleized).await.unwrap();
171            }
172
173            // Prune the database
174            let boundary = db.sync_boundary();
175            let db = db.prune(boundary).await.unwrap();
176
177            assert!(
178                db.any.bitmap.pruned_chunks() > 0,
179                "expected at least one pruned chunk"
180            );
181
182            // Requesting a range proof at location 0 (in the pruned range) should return
183            // OperationPruned, not panic.
184            let result = db.range_proof(Location::new(0), NZU64!(1)).await;
185            assert!(
186                matches!(result, Err(Error::OperationPruned(_))),
187                "expected OperationPruned, got {result:?}"
188            );
189
190            db.destroy().await.unwrap();
191        });
192    }
193
194    #[test_traced("DEBUG")]
195    pub fn test_current_db_key_value_proof() {
196        shared::test_key_value_proof(open_db);
197    }
198
199    #[test_traced("WARN")]
200    pub fn test_current_db_proving_repeated_updates() {
201        shared::test_proving_repeated_updates(open_db);
202    }
203
204    #[test_traced("DEBUG")]
205    pub fn test_current_db_exclusion_proofs() {
206        shared::test_exclusion_proofs(open_db);
207    }
208
209    crate::qmdb::current::tests::staged_merkleize_parity_test!(
210        test_current_ordered_fixed_staged_merkleize_parity,
211        open_db
212    );
213
214    /// Build a `P`-partitioned current db with churny ops across two commits (so the second commit's
215    /// updates and deletes inactivate locations from the first), prune it, then assert that
216    /// reopening it at a range of worker counts reconstructs the identical root and key-value
217    /// state. Unlike the `any` equivalence tests, the current root commits to the activity bitmap,
218    /// so this exercises the parallel build's bitmap reconstruction (`for_each_value` +
219    /// last-commit) over a pruned prefix, not just the snapshot index and MMR.
220    #[boxed]
221    async fn check_current_parallel_init_equivalence<const P: usize>(
222        context: deterministic::Context,
223        partition: &'static str,
224        concurrency_sweep: &[usize],
225    ) {
226        type PartDb<const P: usize, S> = partitioned::Db<
227            mmr::Family,
228            deterministic::Context,
229            Digest,
230            Digest,
231            Sha256,
232            OneCap,
233            P,
234            32,
235            S,
236        >;
237
238        /// The value each key holds after the two commits below.
239        fn expected_value(i: u64) -> Option<Digest> {
240            if i % 7 == 1 {
241                None
242            } else if i.is_multiple_of(3) {
243                Some(Sha256::hash(&[&((i + 1) * 11).to_be_bytes()]))
244            } else {
245                Some(Sha256::hash(&[&(i * 7).to_be_bytes()]))
246            }
247        }
248
249        let cfg = fixed_config_partitioned::<OneCap>(partition, &context);
250        let db = PartDb::<P, Sequential>::init(context.child("populate"), cfg)
251            .await
252            .unwrap();
253
254        // Commit 1: insert.
255        let mut batch = db.new_batch();
256        for i in 0u64..2000 {
257            let k = Sha256::hash(&[&i.to_be_bytes()]);
258            let v = Sha256::hash(&[&(i * 7).to_be_bytes()]);
259            batch = batch.write(k, Some(v));
260        }
261        let merkleized = batch.merkleize(&db, None).await.unwrap();
262        let (db, _) = db.apply_batch(merkleized).await.unwrap();
263        let db = db.commit().await.unwrap();
264
265        // Commit 2: update a third (inactivating their commit-1 ops) and delete a seventh.
266        let mut batch = db.new_batch();
267        for i in (0u64..2000).step_by(3) {
268            let k = Sha256::hash(&[&i.to_be_bytes()]);
269            let v = Sha256::hash(&[&((i + 1) * 11).to_be_bytes()]);
270            batch = batch.write(k, Some(v));
271        }
272        for i in (1u64..2000).step_by(7) {
273            let k = Sha256::hash(&[&i.to_be_bytes()]);
274            batch = batch.write(k, None);
275        }
276        let merkleized = batch.merkleize(&db, None).await.unwrap();
277        let (db, _) = db.apply_batch(merkleized).await.unwrap();
278        let db = db.commit().await.unwrap();
279
280        // Prune so the reopens rebuild the grafted root over a bitmap with a pruned prefix.
281        let boundary = db.sync_boundary();
282        let db = db.prune(boundary).await.unwrap();
283        let db = db.sync().await.unwrap();
284        let root = db.root();
285        drop(db);
286
287        // Reopen at each concurrency. All rebuild (snapshot + bitmap) from the same log and must
288        // match the original root and serve the expected value for every key.
289        for &concurrency in concurrency_sweep {
290            let mut cfg = fixed_config_partitioned::<OneCap>(partition, &context);
291            cfg.init_concurrency = core::num::NonZeroUsize::new(concurrency).unwrap();
292            let ctx = context
293                .child("reopen")
294                .with_attribute("concurrency", concurrency);
295            let db = PartDb::<P, Sequential>::init(ctx, cfg).await.unwrap();
296            assert_eq!(
297                db.root(),
298                root,
299                "current root mismatch at P={P} concurrency={concurrency}"
300            );
301            for i in 0u64..2000 {
302                let k = Sha256::hash(&[&i.to_be_bytes()]);
303                assert_eq!(
304                    db.get(&k).await.unwrap(),
305                    expected_value(i),
306                    "value mismatch for key {i}"
307                );
308            }
309            drop(db);
310        }
311    }
312
313    #[test_traced("WARN")]
314    fn test_current_ordered_partitioned_p1_parallel_init_equivalence() {
315        deterministic::Runner::default().start(|context| async move {
316            check_current_parallel_init_equivalence::<1>(
317                context,
318                "current_parallel_equiv_p1",
319                &[1, 2, 3, 5],
320            )
321            .await;
322        });
323    }
324
325    #[test_traced("WARN")]
326    fn test_current_ordered_partitioned_p2_parallel_init_equivalence() {
327        deterministic::Runner::default().start(|context| async move {
328            check_current_parallel_init_equivalence::<2>(
329                context,
330                "current_parallel_equiv_p2",
331                &[1, 2, 3, 5],
332            )
333            .await;
334        });
335    }
336
337    /// P=3 allocates `2^24` partition slots per index, so it is too memory-heavy for the default
338    /// suite. Run it explicitly with `--ignored` (and ideally `--release`). Only serial and one
339    /// offset-parallel reopen are checked.
340    #[test_traced("WARN")]
341    #[ignore]
342    fn test_current_ordered_partitioned_p3_parallel_init_equivalence() {
343        deterministic::Runner::default().start(|context| async move {
344            check_current_parallel_init_equivalence::<3>(
345                context,
346                "current_parallel_equiv_p3",
347                &[1, 3],
348            )
349            .await;
350        });
351    }
352}