use crate::Key;
use commonware_codec::Encode;
use commonware_storage::{
merkle::{self, Location, Proof},
qmdb::{self, sync::compact},
};
use std::{future::Future, num::NonZeroU64};
pub mod any;
pub mod current;
pub mod immutable;
pub mod immutable_compact;
pub mod keyless;
pub mod keyless_compact;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncMode {
Full,
Compact,
}
impl std::str::FromStr for SyncMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"full" => Ok(Self::Full),
"compact" => Ok(Self::Compact),
_ => Err(format!(
"Invalid sync mode: '{s}'. Must be 'full' or 'compact'",
)),
}
}
}
impl SyncMode {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Full => "full",
Self::Compact => "compact",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DatabaseType {
Any,
Current,
Immutable,
Keyless,
}
impl std::str::FromStr for DatabaseType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"any" => Ok(Self::Any),
"current" => Ok(Self::Current),
"immutable" => Ok(Self::Immutable),
"keyless" => Ok(Self::Keyless),
_ => Err(format!(
"Invalid database family: '{s}'. Must be 'any', 'current', 'immutable', or \
'keyless'",
)),
}
}
}
impl DatabaseType {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Any => "any",
Self::Current => "current",
Self::Immutable => "immutable",
Self::Keyless => "keyless",
}
}
pub const fn supports_client_mode(self, mode: SyncMode) -> bool {
match mode {
SyncMode::Full => matches!(
self,
Self::Any | Self::Current | Self::Immutable | Self::Keyless
),
SyncMode::Compact => matches!(self, Self::Immutable | Self::Keyless),
}
}
pub const fn supports_compact_storage(self) -> bool {
matches!(self, Self::Immutable | Self::Keyless)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageKind {
Full,
Compact,
}
impl std::str::FromStr for StorageKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"full" => Ok(Self::Full),
"compact" => Ok(Self::Compact),
_ => Err(format!(
"Invalid storage kind: '{s}'. Must be 'full' or 'compact'",
)),
}
}
}
impl StorageKind {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Full => "full",
Self::Compact => "compact",
}
}
}
#[allow(clippy::type_complexity)]
pub trait ExampleDatabase: Sized {
type Family: merkle::Family;
type Operation: Encode + Send + Sync + 'static;
fn create_test_operations(count: usize, seed: u64, starting_loc: u64) -> Vec<Self::Operation>;
fn add_operations(
&mut self,
operations: Vec<Self::Operation>,
) -> impl Future<Output = Result<(), qmdb::Error<Self::Family>>> + Send;
fn current_floor(&self) -> u64;
fn root(&self) -> Key;
fn name() -> &'static str;
}
#[allow(clippy::type_complexity)]
pub trait Syncable: ExampleDatabase {
fn size(&self) -> impl Future<Output = Location<Self::Family>> + Send;
fn sync_boundary(&self) -> impl Future<Output = Location<Self::Family>> + Send;
fn historical_proof(
&self,
op_count: Location<Self::Family>,
start_loc: Location<Self::Family>,
max_ops: NonZeroU64,
) -> impl Future<
Output = Result<
(Proof<Self::Family, Key>, Vec<Self::Operation>),
qmdb::Error<Self::Family>,
>,
> + Send;
fn pinned_nodes_at(
&self,
loc: Location<Self::Family>,
) -> impl Future<Output = Result<Vec<Key>, qmdb::Error<Self::Family>>> + Send;
}
#[allow(clippy::type_complexity)]
pub trait CompactSyncable: ExampleDatabase {
fn current_target(&self) -> impl Future<Output = compact::Target<Self::Family, Key>> + Send;
}
#[cfg(test)]
mod tests {
use super::{
immutable, immutable_compact, keyless, keyless_compact, DatabaseType, ExampleDatabase,
SyncMode,
};
use commonware_runtime::{deterministic, Runner as _, Supervisor as _};
#[test]
fn test_supported_client_mode_matrix() {
assert!(DatabaseType::Any.supports_client_mode(SyncMode::Full));
assert!(!DatabaseType::Any.supports_client_mode(SyncMode::Compact));
assert!(DatabaseType::Current.supports_client_mode(SyncMode::Full));
assert!(!DatabaseType::Current.supports_client_mode(SyncMode::Compact));
assert!(DatabaseType::Immutable.supports_client_mode(SyncMode::Full));
assert!(DatabaseType::Immutable.supports_client_mode(SyncMode::Compact));
assert!(DatabaseType::Keyless.supports_client_mode(SyncMode::Full));
assert!(DatabaseType::Keyless.supports_client_mode(SyncMode::Compact));
}
#[test]
fn test_compact_storage_support() {
assert!(!DatabaseType::Any.supports_compact_storage());
assert!(!DatabaseType::Current.supports_compact_storage());
assert!(DatabaseType::Immutable.supports_compact_storage());
assert!(DatabaseType::Keyless.supports_compact_storage());
}
#[test]
fn test_immutable_full_compact_root_floor_equivalence() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let mut full = immutable::Database::init(
context.child("full"),
immutable::create_config(&context),
)
.await
.unwrap();
let mut compact = immutable_compact::Database::init(
context.child("compact"),
immutable_compact::create_config(&context),
)
.await
.unwrap();
for (count, seed) in [(12usize, 42u64), (15, 99)] {
let starting_loc = full.current_floor();
assert_eq!(starting_loc, compact.current_floor());
let ops = immutable::create_test_operations(count, seed, starting_loc);
full.add_operations(ops.clone()).await.unwrap();
compact.add_operations(ops).await.unwrap();
assert_eq!(full.root(), compact.root());
assert_eq!(full.current_floor(), compact.current_floor());
assert_eq!(compact.current_target().root, full.root());
}
full.destroy().await.unwrap();
compact.destroy().await.unwrap();
});
}
#[test]
fn test_keyless_full_compact_root_floor_equivalence() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let mut full =
keyless::Database::init(context.child("full"), keyless::create_config(&context))
.await
.unwrap();
let mut compact = keyless_compact::Database::init(
context.child("compact"),
keyless_compact::create_config(&context),
)
.await
.unwrap();
for (count, seed) in [(12usize, 42u64), (15, 99)] {
let starting_loc = full.current_floor();
assert_eq!(starting_loc, compact.current_floor());
let ops = keyless::create_test_operations(count, seed, starting_loc);
full.add_operations(ops.clone()).await.unwrap();
compact.add_operations(ops).await.unwrap();
assert_eq!(full.root(), compact.root());
assert_eq!(full.current_floor(), compact.current_floor());
assert_eq!(compact.current_target().root, full.root());
}
full.destroy().await.unwrap();
compact.destroy().await.unwrap();
});
}
}