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