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    mmr::Location,
14    qmdb::{
15        any::{unordered::fixed::Operation, value::FixedEncoding, FixedValue},
16        current::FixedConfig as Config,
17        Error,
18    },
19    translator::Translator,
20};
21use commonware_cryptography::Hasher;
22use commonware_runtime::{Clock, Metrics, Storage as RStorage};
23use commonware_utils::Array;
24
25/// A specialization of [super::db::Db] for unordered key spaces and fixed-size values.
26pub type Db<E, K, V, H, T, const N: usize> =
27    super::db::Db<E, Journal<E, Operation<K, V>>, K, FixedEncoding<V>, Index<T, Location>, H, N>;
28
29impl<
30        E: RStorage + Clock + Metrics,
31        K: Array,
32        V: FixedValue,
33        H: Hasher,
34        T: Translator,
35        const N: usize,
36    > Db<E, K, V, H, T, N>
37{
38    /// Initializes a [Db] authenticated database from the given `config`. Leverages parallel
39    /// Merkleization to initialize the bitmap MMR if a thread pool is provided.
40    pub async fn init(context: E, config: Config<T>) -> Result<Self, Error> {
41        crate::qmdb::current::init_fixed(context, config, |ctx, t| Index::new(ctx, t)).await
42    }
43}
44
45pub mod partitioned {
46    //! A partitioned variant of [super] that uses a partitioned index for the snapshot.
47    //!
48    //! See [crate::qmdb::any::unordered::fixed::partitioned] for details on partitioned indices and
49    //! when to use them.
50
51    pub use super::KeyValueProof;
52    use crate::{
53        index::partitioned::unordered::Index,
54        journal::contiguous::fixed::Journal,
55        mmr::Location,
56        qmdb::{
57            any::{unordered::fixed::partitioned::Operation, value::FixedEncoding, FixedValue},
58            current::FixedConfig as Config,
59            Error,
60        },
61        translator::Translator,
62    };
63    use commonware_cryptography::Hasher;
64    use commonware_runtime::{Clock, Metrics, Storage as RStorage};
65    use commonware_utils::Array;
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<E, K, V, H, T, const P: usize, const N: usize> =
74        crate::qmdb::current::unordered::db::Db<
75            E,
76            Journal<E, Operation<K, V>>,
77            K,
78            FixedEncoding<V>,
79            Index<T, Location, P>,
80            H,
81            N,
82        >;
83
84    impl<
85            E: RStorage + Clock + Metrics,
86            K: Array,
87            V: FixedValue,
88            H: Hasher,
89            T: Translator,
90            const P: usize,
91            const N: usize,
92        > Db<E, K, V, H, T, P, N>
93    {
94        /// Initializes a [Db] authenticated database from the given `config`. Leverages parallel
95        /// Merkleization to initialize the bitmap MMR if a thread pool is provided.
96        pub async fn init(context: E, config: Config<T>) -> Result<Self, Error> {
97            crate::qmdb::current::init_fixed(context, config, |ctx, t| Index::new(ctx, t)).await
98        }
99    }
100}
101
102#[cfg(test)]
103pub mod test {
104    use super::*;
105    use crate::{
106        kv::tests::{assert_gettable, assert_send},
107        mmr::{hasher::Hasher as _, Proof, StandardHasher},
108        qmdb::{
109            any::operation::update::Unordered as UnorderedUpdate,
110            current::{
111                proof::RangeProof,
112                tests::{apply_random_ops, fixed_config},
113            },
114            store::{
115                tests::{assert_log_store, assert_merkleized_store, assert_prunable_store},
116                LogStore as _,
117            },
118        },
119        translator::TwoCap,
120    };
121    use commonware_cryptography::{sha256::Digest, Digest as _, Sha256};
122    use commonware_macros::test_traced;
123    use commonware_runtime::{deterministic, Runner as _};
124    use commonware_utils::{bitmap::Prunable as BitMap, NZU64};
125    use rand::RngCore;
126
127    /// A type alias for the concrete [Db] type used in these unit tests.
128    type CurrentTest = Db<deterministic::Context, Digest, Digest, Sha256, TwoCap, 32>;
129
130    /// Return an [Db] database initialized with a fixed config.
131    async fn open_db(context: deterministic::Context, partition_prefix: String) -> CurrentTest {
132        let cfg = fixed_config::<TwoCap>(&partition_prefix, &context);
133        CurrentTest::init(context, cfg).await.unwrap()
134    }
135
136    /// Build a tiny database and make sure we can't convince the verifier that some old value of a
137    /// key is active. We specifically test over the partial chunk case, since these bits are yet to
138    /// be committed to the underlying MMR.
139    #[test_traced("DEBUG")]
140    pub fn test_current_db_verify_proof_over_bits_in_uncommitted_chunk() {
141        let executor = deterministic::Runner::default();
142        executor.start(|context| async move {
143            let mut hasher = StandardHasher::<Sha256>::new();
144            let partition = "build-small".to_string();
145            let mut db = open_db(context.with_label("db"), partition.clone()).await;
146
147            // Add one key.
148            let k = Sha256::fill(0x01);
149            let v1 = Sha256::fill(0xA1);
150            let finalized = {
151                let mut batch = db.new_batch();
152                batch.write(k, Some(v1));
153                batch.merkleize(None).await.unwrap().finalize()
154            };
155            db.apply_batch(finalized).await.unwrap();
156
157            let (_, op_loc) = db.any.get_with_loc(&k).await.unwrap().unwrap();
158            let proof = db.key_value_proof(hasher.inner(), k).await.unwrap();
159
160            // Proof should be verifiable against current root.
161            let root = db.root();
162            assert!(CurrentTest::verify_key_value_proof(
163                hasher.inner(),
164                k,
165                v1,
166                &proof,
167                &root
168            ));
169
170            let v2 = Sha256::fill(0xA2);
171            // Proof should not verify against a different value.
172            assert!(!CurrentTest::verify_key_value_proof(
173                hasher.inner(),
174                k,
175                v2,
176                &proof,
177                &root,
178            ));
179
180            // Update the key to a new value (v2), which inactivates the previous operation.
181            let finalized = {
182                let mut batch = db.new_batch();
183                batch.write(k, Some(v2));
184                batch.merkleize(None).await.unwrap().finalize()
185            };
186            db.apply_batch(finalized).await.unwrap();
187            let root = db.root();
188
189            // New value should not be verifiable against the old proof.
190            assert!(!CurrentTest::verify_key_value_proof(
191                hasher.inner(),
192                k,
193                v2,
194                &proof,
195                &root,
196            ));
197
198            // But the new value should verify against a new proof.
199            let proof = db.key_value_proof(hasher.inner(), k).await.unwrap();
200            assert!(CurrentTest::verify_key_value_proof(
201                hasher.inner(),
202                k,
203                v2,
204                &proof,
205                &root,
206            ));
207
208            // Old value will not verify against new proof.
209            assert!(!CurrentTest::verify_key_value_proof(
210                hasher.inner(),
211                k,
212                v1,
213                &proof,
214                &root,
215            ));
216
217            // Create a proof of the now-inactive update operation assigining v1 to k against the
218            // current root.
219            let (range_proof, _, chunks) = db
220                .range_proof(hasher.inner(), op_loc, NZU64!(1))
221                .await
222                .unwrap();
223            let proof_inactive = KeyValueProof {
224                loc: op_loc,
225                chunk: chunks[0],
226                range_proof,
227            };
228            // This proof should verify using verify_range_proof which does not check activity
229            // status.
230            let op = Operation::Update(UnorderedUpdate(k, v1));
231            assert!(CurrentTest::verify_range_proof(
232                hasher.inner(),
233                &proof_inactive.range_proof,
234                proof_inactive.loc,
235                &[op],
236                &[proof_inactive.chunk],
237                &root,
238            ));
239
240            // But this proof should *not* verify as a key value proof, since verification will see
241            // that the operation is inactive.
242            assert!(!CurrentTest::verify_key_value_proof(
243                hasher.inner(),
244                k,
245                v1,
246                &proof_inactive,
247                &root,
248            ));
249
250            // Attempt #1 to "fool" the verifier:  change the location to that of an active
251            // operation. This should not fool the verifier if we're properly validating the
252            // inclusion of the operation itself, and not just the chunk.
253            let (_, active_loc) = db.any.get_with_loc(&k).await.unwrap().unwrap();
254            // The new location should differ but still be in the same chunk.
255            assert_ne!(active_loc, proof_inactive.loc);
256            assert_eq!(
257                BitMap::<32>::to_chunk_index(*active_loc),
258                BitMap::<32>::to_chunk_index(*proof_inactive.loc)
259            );
260            let mut fake_proof = proof_inactive.clone();
261            fake_proof.loc = active_loc;
262            assert!(!CurrentTest::verify_key_value_proof(
263                hasher.inner(),
264                k,
265                v1,
266                &fake_proof,
267                &root,
268            ));
269
270            // Attempt #2 to "fool" the verifier: Modify the chunk in the proof info to make it look
271            // like the operation is active by flipping its corresponding bit to 1. This should not
272            // fool the verifier if we are correctly incorporating the partial chunk information
273            // into the root computation.
274            let mut modified_chunk = proof_inactive.chunk;
275            let bit_pos = *proof_inactive.loc;
276            let byte_idx = bit_pos / 8;
277            let bit_idx = bit_pos % 8;
278            modified_chunk[byte_idx as usize] |= 1 << bit_idx;
279
280            let mut fake_proof = proof_inactive.clone();
281            fake_proof.chunk = modified_chunk;
282            assert!(!CurrentTest::verify_key_value_proof(
283                hasher.inner(),
284                k,
285                v1,
286                &fake_proof,
287                &root,
288            ));
289
290            db.destroy().await.unwrap();
291        });
292    }
293
294    #[test_traced("DEBUG")]
295    pub fn test_current_db_range_proofs() {
296        let executor = deterministic::Runner::default();
297        executor.start(|mut context| async move {
298            let partition = "range-proofs".to_string();
299            let mut hasher = StandardHasher::<Sha256>::new();
300            let db = open_db(context.with_label("db"), partition.clone()).await;
301            let root = db.root();
302
303            // Empty range proof should not crash or verify, since even an empty db has a single
304            // commit op.
305            let proof = RangeProof {
306                proof: Proof::default(),
307                partial_chunk_digest: None,
308                ops_root: Digest::EMPTY,
309            };
310            assert!(!CurrentTest::verify_range_proof(
311                hasher.inner(),
312                &proof,
313                Location::new(0),
314                &[],
315                &[],
316                &root,
317            ));
318
319            let mut db = apply_random_ops::<CurrentTest>(200, true, context.next_u64(), db)
320                .await
321                .unwrap();
322            let finalized = db.new_batch().merkleize(None).await.unwrap().finalize();
323            db.apply_batch(finalized).await.unwrap();
324            let root = db.root();
325
326            // Make sure size-constrained batches of operations are provable from the oldest
327            // retained op to tip.
328            let max_ops = 4;
329            let end_loc = db.size().await;
330            let start_loc = db.any.inactivity_floor_loc();
331
332            for loc in *start_loc..*end_loc {
333                let loc = Location::new(loc);
334                let (proof, ops, chunks) = db
335                    .range_proof(hasher.inner(), loc, NZU64!(max_ops))
336                    .await
337                    .unwrap();
338                assert!(
339                    CurrentTest::verify_range_proof(
340                        hasher.inner(),
341                        &proof,
342                        loc,
343                        &ops,
344                        &chunks,
345                        &root
346                    ),
347                    "failed to verify range at start_loc {start_loc}",
348                );
349                // Proof should not verify if we include extra chunks.
350                let mut chunks_with_extra = chunks.clone();
351                chunks_with_extra.push(chunks[chunks.len() - 1]);
352                assert!(!CurrentTest::verify_range_proof(
353                    hasher.inner(),
354                    &proof,
355                    loc,
356                    &ops,
357                    &chunks_with_extra,
358                    &root,
359                ));
360            }
361
362            db.destroy().await.unwrap();
363        });
364    }
365
366    #[test_traced("DEBUG")]
367    pub fn test_current_db_key_value_proof() {
368        let executor = deterministic::Runner::default();
369        executor.start(|mut context| async move {
370            let partition = "range-proofs".to_string();
371            let mut hasher = StandardHasher::<Sha256>::new();
372            let db = open_db(context.with_label("db"), partition.clone()).await;
373            let mut db = apply_random_ops::<CurrentTest>(500, true, context.next_u64(), db)
374                .await
375                .unwrap();
376            let finalized = db.new_batch().merkleize(None).await.unwrap().finalize();
377            db.apply_batch(finalized).await.unwrap();
378            let root = db.root();
379
380            // Confirm bad keys produce the expected error.
381            let bad_key = Sha256::fill(0xAA);
382            let res = db.key_value_proof(hasher.inner(), bad_key).await;
383            assert!(matches!(res, Err(Error::KeyNotFound)));
384
385            let start = *db.inactivity_floor_loc();
386            for i in start..db.status.len() {
387                if !db.status.get_bit(i) {
388                    continue;
389                }
390                // Found an active operation! Create a proof for its active current key/value if
391                // it's a key-updating operation.
392                let (key, value) = match db.any.log.read(Location::new(i)).await.unwrap() {
393                    Operation::Update(UnorderedUpdate(key, value)) => (key, value),
394                    Operation::CommitFloor(_, _) => continue,
395                    Operation::Delete(_) => {
396                        unreachable!("location does not reference update/commit operation")
397                    }
398                };
399
400                let proof = db.key_value_proof(hasher.inner(), key).await.unwrap();
401                // Proof should validate against the current value and correct root.
402                assert!(CurrentTest::verify_key_value_proof(
403                    hasher.inner(),
404                    key,
405                    value,
406                    &proof,
407                    &root
408                ));
409                // Proof should fail against the wrong value. Use hash instead of fill to ensure
410                // the value differs from any key/value created by TestKey::from_seed (which uses
411                // fill patterns).
412                let wrong_val = Sha256::hash(&[0xFF]);
413                assert!(!CurrentTest::verify_key_value_proof(
414                    hasher.inner(),
415                    key,
416                    wrong_val,
417                    &proof,
418                    &root
419                ));
420                // Proof should fail against the wrong key.
421                let wrong_key = Sha256::hash(&[0xEE]);
422                assert!(!CurrentTest::verify_key_value_proof(
423                    hasher.inner(),
424                    wrong_key,
425                    value,
426                    &proof,
427                    &root
428                ));
429                // Proof should fail against the wrong root.
430                let wrong_root = Sha256::hash(&[0xDD]);
431                assert!(!CurrentTest::verify_key_value_proof(
432                    hasher.inner(),
433                    key,
434                    value,
435                    &proof,
436                    &wrong_root,
437                ));
438            }
439
440            db.destroy().await.unwrap();
441        });
442    }
443
444    /// Repeatedly update the same key to a new value and ensure we can prove its current value
445    /// after each update.
446    #[test_traced("WARN")]
447    pub fn test_current_db_proving_repeated_updates() {
448        let executor = deterministic::Runner::default();
449        executor.start(|context| async move {
450            let mut hasher = StandardHasher::<Sha256>::new();
451            let partition = "build-small".to_string();
452            let mut db = open_db(context.with_label("db"), partition.clone()).await;
453
454            // Add one key.
455            let k = Sha256::fill(0x00);
456            let mut old_val = Sha256::fill(0x00);
457            for i in 1u8..=255 {
458                let v = Sha256::fill(i);
459                let finalized = {
460                    let mut batch = db.new_batch();
461                    batch.write(k, Some(v));
462                    batch.merkleize(None).await.unwrap().finalize()
463                };
464                db.apply_batch(finalized).await.unwrap();
465                assert_eq!(db.get(&k).await.unwrap().unwrap(), v);
466                let root = db.root();
467
468                // Create a proof for the current value of k.
469                let proof = db.key_value_proof(hasher.inner(), k).await.unwrap();
470                assert!(
471                    CurrentTest::verify_key_value_proof(hasher.inner(), k, v, &proof, &root),
472                    "proof of update {i} failed to verify"
473                );
474                // Ensure the proof does NOT verify if we use the previous value.
475                assert!(
476                    !CurrentTest::verify_key_value_proof(hasher.inner(), k, old_val, &proof, &root),
477                    "proof of update {i} verified when it should not have"
478                );
479                old_val = v;
480            }
481
482            db.destroy().await.unwrap();
483        });
484    }
485
486    #[allow(dead_code)]
487    fn assert_db_futures_are_send(db: &mut CurrentTest, key: Digest, loc: Location) {
488        assert_gettable(db, &key);
489        assert_log_store(db);
490        assert_prunable_store(db, loc);
491        assert_merkleized_store(db, loc);
492        assert_send(db.sync());
493    }
494}