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