Skip to main content

commonware_sync/databases/
any.rs

1//! Any database types and helpers for the sync example.
2
3use crate::{Hasher, Key, Translator, Value};
4use commonware_cryptography::Hasher as CryptoHasher;
5use commonware_runtime::{buffer, Clock, Metrics, Storage};
6use commonware_storage::{
7    mmr::{Location, Proof},
8    qmdb::{
9        self,
10        any::{
11            unordered::{
12                fixed::{Db, Operation as FixedOperation},
13                Update,
14            },
15            FixedConfig as Config,
16        },
17        operation::Committable,
18        store::LogStore,
19    },
20};
21use commonware_utils::{NZUsize, NZU16, NZU64};
22use std::{future::Future, num::NonZeroU64};
23use tracing::error;
24
25/// Database type alias for the Clean state.
26pub type Database<E> = Db<E, Key, Value, Hasher, Translator>;
27
28/// Operation type alias.
29pub type Operation = FixedOperation<Key, Value>;
30
31/// Create a database configuration for use in tests.
32pub fn create_config() -> Config<Translator> {
33    Config {
34        mmr_journal_partition: "mmr_journal".into(),
35        mmr_metadata_partition: "mmr_metadata".into(),
36        mmr_items_per_blob: NZU64!(4096),
37        mmr_write_buffer: NZUsize!(1024),
38        log_journal_partition: "log_journal".into(),
39        log_items_per_blob: NZU64!(4096),
40        log_write_buffer: NZUsize!(1024),
41        translator: Translator::default(),
42        thread_pool: None,
43        page_cache: buffer::paged::CacheRef::new(NZU16!(1024), NZUsize!(10)),
44    }
45}
46
47impl<E> crate::databases::Syncable for Database<E>
48where
49    E: Storage + Clock + Metrics,
50{
51    type Operation = Operation;
52
53    fn create_test_operations(count: usize, seed: u64) -> Vec<Self::Operation> {
54        let mut hasher = <Hasher as CryptoHasher>::new();
55        let mut operations = Vec::new();
56        for i in 0..count {
57            let key = {
58                hasher.update(&i.to_be_bytes());
59                hasher.update(&seed.to_be_bytes());
60                hasher.finalize()
61            };
62
63            let value = {
64                hasher.update(&key);
65                hasher.update(b"value");
66                hasher.finalize()
67            };
68
69            operations.push(Operation::Update(Update(key, value)));
70
71            if (i + 1) % 10 == 0 {
72                operations.push(Operation::CommitFloor(None, Location::from(i + 1)));
73            }
74        }
75
76        // Always end with a commit
77        operations.push(Operation::CommitFloor(None, Location::from(count)));
78        operations
79    }
80
81    async fn add_operations(
82        self,
83        operations: Vec<Self::Operation>,
84    ) -> Result<Self, commonware_storage::qmdb::Error> {
85        if operations.last().is_none() || !operations.last().unwrap().is_commit() {
86            // Ignore bad inputs rather than return errors.
87            error!("operations must end with a commit");
88            return Ok(self);
89        }
90        let mut db = self.into_mutable();
91        let num_ops = operations.len();
92
93        for (i, operation) in operations.into_iter().enumerate() {
94            match operation {
95                Operation::Update(Update(key, value)) => {
96                    db.update(key, value).await?;
97                }
98                Operation::Delete(key) => {
99                    db.delete(key).await?;
100                }
101                Operation::CommitFloor(metadata, _) => {
102                    let (durable_db, _) = db.commit(metadata).await?;
103                    if i == num_ops - 1 {
104                        // Last operation - return the clean database
105                        return Ok(durable_db.into_merkleized());
106                    }
107                    // Not the last operation - continue in mutable state
108                    db = durable_db.into_mutable();
109                }
110            }
111        }
112        panic!("operations should end with a commit");
113    }
114
115    fn root(&self) -> Key {
116        self.root()
117    }
118
119    fn size(&self) -> Location {
120        LogStore::bounds(self).end
121    }
122
123    fn inactivity_floor(&self) -> Location {
124        self.inactivity_floor_loc()
125    }
126
127    fn historical_proof(
128        &self,
129        op_count: Location,
130        start_loc: Location,
131        max_ops: NonZeroU64,
132    ) -> impl Future<Output = Result<(Proof<Key>, Vec<Self::Operation>), qmdb::Error>> + Send {
133        self.historical_proof(op_count, start_loc, max_ops)
134    }
135
136    fn name() -> &'static str {
137        "any"
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::databases::Syncable;
145    use commonware_runtime::deterministic;
146
147    type AnyDb = Database<deterministic::Context>;
148
149    #[test]
150    fn test_create_test_operations() {
151        let ops = <AnyDb as Syncable>::create_test_operations(5, 12345);
152        assert_eq!(ops.len(), 6); // 5 operations + 1 commit
153
154        if let Operation::CommitFloor(_, loc) = &ops[5] {
155            assert_eq!(*loc, 5);
156        } else {
157            panic!("Last operation should be a commit");
158        }
159    }
160
161    #[test]
162    fn test_deterministic_operations() {
163        // Operations should be deterministic based on seed
164        let ops1 = <AnyDb as Syncable>::create_test_operations(3, 12345);
165        let ops2 = <AnyDb as Syncable>::create_test_operations(3, 12345);
166        assert_eq!(ops1, ops2);
167
168        // Different seeds should produce different operations
169        let ops3 = <AnyDb as Syncable>::create_test_operations(3, 54321);
170        assert_ne!(ops1, ops3);
171    }
172}