Skip to main content

commonware_sync/databases/
mod.rs

1//! Database-specific modules for the sync example.
2
3use 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/// Synchronization mode to use.
19#[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/// Database family to synchronize.
49#[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/// Backing storage kind used by a compact-mode server.
100#[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/// Common surface shared by all database adapters used by the sync example binaries.
130///
131/// This is intentionally the smallest shared interface: enough to create test data, mutate the
132/// database during the demo, and log the resulting root. More specific sync capabilities live in
133/// [`Syncable`] and [`CompactSyncable`].
134#[allow(clippy::type_complexity)]
135pub trait ExampleDatabase: Sized {
136    /// The merkle family used by this database.
137    type Family: merkle::Family;
138
139    /// The type of operations in the database.
140    type Operation: Encode + Send + Sync + 'static;
141
142    /// Create test operations with the given count and seed.
143    ///
144    /// `starting_loc` is the floor each commit in the returned stream should carry. Callers
145    /// applying the stream to a fresh db pass `0`; callers growing an already-running db pass
146    /// the current value of [`Self::current_floor`] so floors stay monotonic across appends.
147    /// The returned operations must end with a commit operation.
148    fn create_test_operations(count: usize, seed: u64, starting_loc: u64) -> Vec<Self::Operation>;
149
150    /// Add operations to the database, ignoring any input that doesn't end with a commit
151    /// operation.
152    fn add_operations(
153        &mut self,
154        operations: Vec<Self::Operation>,
155    ) -> impl Future<Output = Result<(), qmdb::Error<Self::Family>>> + Send;
156
157    /// Return the floor anchor a caller should pass as `starting_loc` when generating a stream
158    /// to append to this db's current state.
159    fn current_floor(&self) -> u64;
160
161    /// Get the database's root digest.
162    fn root(&self) -> Key;
163
164    /// Get the display name used in logs.
165    fn name() -> &'static str;
166}
167
168/// Capability trait for databases that support full replay-based sync.
169///
170/// These databases retain enough history to serve authenticated operations over a range, so the
171/// client can fetch and replay them into the same database family.
172#[allow(clippy::type_complexity)]
173pub trait Syncable: ExampleDatabase {
174    /// Get the total number of operations in the database (including pruned operations).
175    fn size(&self) -> impl Future<Output = Location<Self::Family>> + Send;
176
177    /// Get the most recent location from which this database can safely be synced.
178    ///
179    /// Callers constructing a sync target should use this value (or any earlier retained
180    /// location) as the `range.start`.
181    fn sync_boundary(&self) -> impl Future<Output = Location<Self::Family>> + Send;
182
183    /// Get historical proof and operations.
184    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    /// Get the pinned nodes for a lower operation boundary of `loc`.
197    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/// Capability trait for databases that can serve compact sync targets.
204///
205/// Compact sync does not replay historical operations. Instead, the server exposes the latest
206/// authenticated target for the database family, and the client reconstructs a compact-storage
207/// local database from that authenticated state.
208#[allow(clippy::type_complexity)]
209pub trait CompactSyncable: ExampleDatabase {
210    /// Return the latest compact-sync target this database can currently serve.
211    ///
212    /// Full databases implement this so they can act as compact-sync sources, and compact-storage
213    /// databases implement it so compact nodes can sync from each other. The client still
214    /// materializes into compact storage in both cases.
215    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}