commonware_sync/databases/
mod.rs1use crate::Key;
4use commonware_codec::Encode;
5use commonware_storage::{
6 merkle::{self, Location, Proof},
7 qmdb::{self, sync::compact},
8};
9use std::{future::Future, num::NonZeroU64};
10
11pub mod any;
12pub mod current;
13pub mod immutable;
14pub mod immutable_compact;
15pub mod keyless;
16pub mod keyless_compact;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SyncMode {
21 Full,
22 Compact,
23}
24
25impl std::str::FromStr for SyncMode {
26 type Err = String;
27
28 fn from_str(s: &str) -> Result<Self, Self::Err> {
29 match s.to_lowercase().as_str() {
30 "full" => Ok(Self::Full),
31 "compact" => Ok(Self::Compact),
32 _ => Err(format!(
33 "Invalid sync mode: '{s}'. Must be 'full' or 'compact'",
34 )),
35 }
36 }
37}
38
39impl SyncMode {
40 pub const fn as_str(&self) -> &'static str {
41 match self {
42 Self::Full => "full",
43 Self::Compact => "compact",
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum DatabaseType {
51 Any,
52 Current,
53 Immutable,
54 Keyless,
55}
56
57impl std::str::FromStr for DatabaseType {
58 type Err = String;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 match s.to_lowercase().as_str() {
62 "any" => Ok(Self::Any),
63 "current" => Ok(Self::Current),
64 "immutable" => Ok(Self::Immutable),
65 "keyless" => Ok(Self::Keyless),
66 _ => Err(format!(
67 "Invalid database family: '{s}'. Must be 'any', 'current', 'immutable', or \
68 'keyless'",
69 )),
70 }
71 }
72}
73
74impl DatabaseType {
75 pub const fn as_str(&self) -> &'static str {
76 match self {
77 Self::Any => "any",
78 Self::Current => "current",
79 Self::Immutable => "immutable",
80 Self::Keyless => "keyless",
81 }
82 }
83
84 pub const fn supports_client_mode(self, mode: SyncMode) -> bool {
85 match mode {
86 SyncMode::Full => matches!(
87 self,
88 Self::Any | Self::Current | Self::Immutable | Self::Keyless
89 ),
90 SyncMode::Compact => matches!(self, Self::Immutable | Self::Keyless),
91 }
92 }
93
94 pub const fn supports_compact_storage(self) -> bool {
95 matches!(self, Self::Immutable | Self::Keyless)
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum StorageKind {
102 Full,
103 Compact,
104}
105
106impl std::str::FromStr for StorageKind {
107 type Err = String;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 match s.to_lowercase().as_str() {
111 "full" => Ok(Self::Full),
112 "compact" => Ok(Self::Compact),
113 _ => Err(format!(
114 "Invalid storage kind: '{s}'. Must be 'full' or 'compact'",
115 )),
116 }
117 }
118}
119
120impl StorageKind {
121 pub const fn as_str(&self) -> &'static str {
122 match self {
123 Self::Full => "full",
124 Self::Compact => "compact",
125 }
126 }
127}
128
129#[allow(clippy::type_complexity)]
135pub trait ExampleDatabase: Sized {
136 type Family: merkle::Family;
138
139 type Operation: Encode + Send + Sync + 'static;
141
142 fn create_test_operations(count: usize, seed: u64, starting_loc: u64) -> Vec<Self::Operation>;
149
150 fn add_operations(
153 &mut self,
154 operations: Vec<Self::Operation>,
155 ) -> impl Future<Output = Result<(), qmdb::Error<Self::Family>>> + Send;
156
157 fn current_floor(&self) -> u64;
160
161 fn root(&self) -> Key;
163
164 fn name() -> &'static str;
166}
167
168#[allow(clippy::type_complexity)]
173pub trait Syncable: ExampleDatabase {
174 fn size(&self) -> impl Future<Output = Location<Self::Family>> + Send;
176
177 fn sync_boundary(&self) -> impl Future<Output = Location<Self::Family>> + Send;
182
183 fn historical_proof(
185 &self,
186 op_count: Location<Self::Family>,
187 start_loc: Location<Self::Family>,
188 max_ops: NonZeroU64,
189 ) -> impl Future<
190 Output = Result<
191 (Proof<Self::Family, Key>, Vec<Self::Operation>),
192 qmdb::Error<Self::Family>,
193 >,
194 > + Send;
195
196 fn pinned_nodes_at(
198 &self,
199 loc: Location<Self::Family>,
200 ) -> impl Future<Output = Result<Vec<Key>, qmdb::Error<Self::Family>>> + Send;
201}
202
203#[allow(clippy::type_complexity)]
209pub trait CompactSyncable: ExampleDatabase {
210 fn current_target(&self) -> impl Future<Output = compact::Target<Self::Family, Key>> + Send;
216}
217
218#[cfg(test)]
219mod tests {
220 use super::{
221 immutable, immutable_compact, keyless, keyless_compact, DatabaseType, ExampleDatabase,
222 SyncMode,
223 };
224 use commonware_runtime::{deterministic, Runner as _, Supervisor as _};
225
226 #[test]
227 fn test_supported_client_mode_matrix() {
228 assert!(DatabaseType::Any.supports_client_mode(SyncMode::Full));
229 assert!(!DatabaseType::Any.supports_client_mode(SyncMode::Compact));
230
231 assert!(DatabaseType::Current.supports_client_mode(SyncMode::Full));
232 assert!(!DatabaseType::Current.supports_client_mode(SyncMode::Compact));
233
234 assert!(DatabaseType::Immutable.supports_client_mode(SyncMode::Full));
235 assert!(DatabaseType::Immutable.supports_client_mode(SyncMode::Compact));
236
237 assert!(DatabaseType::Keyless.supports_client_mode(SyncMode::Full));
238 assert!(DatabaseType::Keyless.supports_client_mode(SyncMode::Compact));
239 }
240
241 #[test]
242 fn test_compact_storage_support() {
243 assert!(!DatabaseType::Any.supports_compact_storage());
244 assert!(!DatabaseType::Current.supports_compact_storage());
245 assert!(DatabaseType::Immutable.supports_compact_storage());
246 assert!(DatabaseType::Keyless.supports_compact_storage());
247 }
248
249 #[test]
250 fn test_immutable_full_compact_root_floor_equivalence() {
251 let executor = deterministic::Runner::default();
252 executor.start(|context| async move {
253 let mut full = immutable::Database::init(
254 context.child("full"),
255 immutable::create_config(&context),
256 )
257 .await
258 .unwrap();
259 let mut compact = immutable_compact::Database::init(
260 context.child("compact"),
261 immutable_compact::create_config(&context),
262 )
263 .await
264 .unwrap();
265
266 for (count, seed) in [(12usize, 42u64), (15, 99)] {
267 let starting_loc = full.current_floor();
268 assert_eq!(starting_loc, compact.current_floor());
269
270 let ops = immutable::create_test_operations(count, seed, starting_loc);
271
272 full.add_operations(ops.clone()).await.unwrap();
273 compact.add_operations(ops).await.unwrap();
274
275 assert_eq!(full.root(), compact.root());
276 assert_eq!(full.current_floor(), compact.current_floor());
277 assert_eq!(compact.current_target().root, full.root());
278 }
279
280 full.destroy().await.unwrap();
281 compact.destroy().await.unwrap();
282 });
283 }
284
285 #[test]
286 fn test_keyless_full_compact_root_floor_equivalence() {
287 let executor = deterministic::Runner::default();
288 executor.start(|context| async move {
289 let mut full =
290 keyless::Database::init(context.child("full"), keyless::create_config(&context))
291 .await
292 .unwrap();
293 let mut compact = keyless_compact::Database::init(
294 context.child("compact"),
295 keyless_compact::create_config(&context),
296 )
297 .await
298 .unwrap();
299
300 for (count, seed) in [(12usize, 42u64), (15, 99)] {
301 let starting_loc = full.current_floor();
302 assert_eq!(starting_loc, compact.current_floor());
303
304 let ops = keyless::create_test_operations(count, seed, starting_loc);
305
306 full.add_operations(ops.clone()).await.unwrap();
307 compact.add_operations(ops).await.unwrap();
308
309 assert_eq!(full.root(), compact.root());
310 assert_eq!(full.current_floor(), compact.current_floor());
311 assert_eq!(compact.current_target().root, full.root());
312 }
313
314 full.destroy().await.unwrap();
315 compact.destroy().await.unwrap();
316 });
317 }
318}