Skip to main content

commonware_storage/qmdb/current/unordered/
variable.rs

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