Skip to main content

commonware_sync/databases/
keyless.rs

1//! Keyless database types and helpers for the sync example.
2//!
3//! A `keyless` database is append-only: operations are stored by location rather than by key.
4//! It supports `Append(value)` and `Commit(metadata, floor)` operations. For sync, the engine
5//! targets the Merkle root over all operations, and the client reconstructs the same state by
6//! replaying the fetched operations.
7
8use crate::{Hasher, Key, Value};
9use commonware_cryptography::{Hasher as CryptoHasher, Sha256};
10use commonware_parallel::Sequential;
11use commonware_runtime::{buffer, BufferPooler};
12use commonware_storage::{
13    journal::contiguous::fixed::Config as FConfig,
14    merkle::{
15        full::Config as MmrConfig,
16        mmr::{self, Location, Proof},
17    },
18    qmdb::{
19        self,
20        keyless::{self, fixed},
21        operation::Committable,
22        sync::compact,
23    },
24    Context,
25};
26use commonware_utils::{NZUsize, NZU16, NZU64};
27use std::num::NonZeroU64;
28use tracing::error;
29
30/// Database type alias.
31pub type Database<E> = fixed::Db<mmr::Family, E, Value, Hasher, Sequential>;
32
33/// Operation type alias.
34pub type Operation = fixed::Operation<mmr::Family, Value>;
35
36/// Create a database configuration for the keyless variant.
37pub fn create_config(context: &impl BufferPooler) -> fixed::Config<Sequential> {
38    let page_cache = buffer::paged::CacheRef::from_pooler(context, NZU16!(2048), NZUsize!(10));
39    keyless::Config {
40        merkle: MmrConfig {
41            journal_partition: "mmr-journal".into(),
42            metadata_partition: "mmr-metadata".into(),
43            items_per_blob: NZU64!(4096),
44            write_buffer: NZUsize!(4096),
45            strategy: Sequential,
46            page_cache: page_cache.clone(),
47        },
48        log: FConfig {
49            partition: "log-journal".into(),
50            items_per_blob: NZU64!(4096),
51            write_buffer: NZUsize!(4096),
52            page_cache,
53        },
54    }
55}
56
57/// Create deterministic test operations for demonstration purposes.
58///
59/// Generates Append operations and periodic Commit operations. Every commit in the stream
60/// carries `starting_loc` as its inactivity floor. Pass `0` for a fresh db; for growth, pass
61/// the live db's [`super::ExampleDatabase::current_floor`] so floors stay monotonic.
62pub fn create_test_operations(count: usize, seed: u64, starting_loc: u64) -> Vec<Operation> {
63    let mut operations = Vec::new();
64    let mut hasher = <Hasher as CryptoHasher>::new();
65    let floor = Location::new(starting_loc);
66
67    for i in 0..count {
68        let value = {
69            hasher.update(&i.to_be_bytes());
70            hasher.update(&seed.to_be_bytes());
71            hasher.finalize()
72        };
73
74        operations.push(Operation::Append(value));
75
76        if (i + 1) % 10 == 0 {
77            operations.push(Operation::Commit(None, floor));
78        }
79    }
80
81    // Always end with a commit.
82    operations.push(Operation::Commit(Some(Sha256::fill(1)), floor));
83    operations
84}
85
86impl<E> super::ExampleDatabase for Database<E>
87where
88    E: Context,
89{
90    type Family = mmr::Family;
91    type Operation = Operation;
92
93    fn create_test_operations(count: usize, seed: u64, starting_loc: u64) -> Vec<Self::Operation> {
94        create_test_operations(count, seed, starting_loc)
95    }
96
97    async fn add_operations(
98        &mut self,
99        operations: Vec<Self::Operation>,
100    ) -> Result<(), qmdb::Error<mmr::Family>> {
101        if operations.last().is_none() || !operations.last().unwrap().is_commit() {
102            // Ignore bad inputs rather than return errors.
103            error!("operations must end with a commit");
104            return Ok(());
105        }
106
107        let mut batch = self.new_batch();
108        for operation in operations {
109            match operation {
110                Operation::Append(value) => {
111                    batch = batch.append(value);
112                }
113                Operation::Commit(metadata, floor) => {
114                    let merkleized = batch.merkleize(self, metadata, floor).await;
115                    self.apply_batch(merkleized).await?;
116                    self.commit().await?;
117                    batch = self.new_batch();
118                }
119            }
120        }
121        Ok(())
122    }
123
124    fn current_floor(&self) -> u64 {
125        *self.last_commit_loc()
126    }
127
128    fn root(&self) -> Key {
129        self.root()
130    }
131
132    fn name() -> &'static str {
133        "keyless"
134    }
135}
136
137impl<E> super::Syncable for Database<E>
138where
139    E: Context,
140{
141    fn size(&self) -> Location {
142        self.bounds().end
143    }
144
145    fn sync_boundary(&self) -> Location {
146        self.sync_boundary()
147    }
148
149    async fn historical_proof(
150        &self,
151        op_count: Location,
152        start_loc: Location,
153        max_ops: NonZeroU64,
154    ) -> Result<(Proof<Key>, Vec<Self::Operation>), qmdb::Error<mmr::Family>> {
155        self.historical_proof(op_count, start_loc, max_ops).await
156    }
157
158    async fn pinned_nodes_at(&self, loc: Location) -> Result<Vec<Key>, qmdb::Error<mmr::Family>> {
159        self.pinned_nodes_at(loc).await
160    }
161}
162
163impl<E> super::CompactSyncable for Database<E>
164where
165    E: Context,
166{
167    fn target(&self) -> compact::Target<Self::Family, Key> {
168        compact::Target::new(self.root(), self.bounds().end)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::databases::ExampleDatabase;
176    use commonware_runtime::deterministic;
177
178    type KeylessDb = Database<deterministic::Context>;
179
180    #[test]
181    fn test_create_test_operations() {
182        let ops = <KeylessDb as ExampleDatabase>::create_test_operations(5, 12345, 0);
183        assert_eq!(ops.len(), 6); // 5 operations + 1 commit
184
185        if let Operation::Commit(Some(_), _) = &ops[5] {
186            // ok
187        } else {
188            panic!("last operation should be a commit with metadata");
189        }
190    }
191
192    #[test]
193    fn test_deterministic_operations() {
194        // Operations should be deterministic based on seed
195        let ops1 = <KeylessDb as ExampleDatabase>::create_test_operations(3, 12345, 0);
196        let ops2 = <KeylessDb as ExampleDatabase>::create_test_operations(3, 12345, 0);
197        assert_eq!(ops1, ops2);
198
199        // Different seeds should produce different operations
200        let ops3 = <KeylessDb as ExampleDatabase>::create_test_operations(3, 54321, 0);
201        assert_ne!(ops1, ops3);
202    }
203}