pub use super::db::KeyValueProof;
use crate::{
index::unordered::Index,
journal::contiguous::fixed::Journal,
merkle::{Graftable, Location},
qmdb::{
any::{unordered::fixed::Operation, value::FixedEncoding, FixedValue},
current::FixedConfig as Config,
Error,
},
translator::Translator,
Context,
};
use commonware_cryptography::Hasher;
use commonware_parallel::Strategy;
use commonware_utils::Array;
pub type Db<F, E, K, V, H, T, const N: usize, S> = super::db::Db<
F,
E,
Journal<E, Operation<F, K, V>>,
K,
FixedEncoding<V>,
Index<T, Location<F>>,
H,
N,
S,
>;
impl<
F: Graftable,
E: Context,
K: Array,
V: FixedValue,
H: Hasher,
T: Translator,
const N: usize,
S: Strategy,
> Db<F, E, K, V, H, T, N, S>
{
pub async fn init(context: E, config: Config<T, S>) -> Result<Self, Error<F>> {
crate::qmdb::current::init(context, config).await
}
}
pub mod partitioned {
use super::*;
use crate::index::partitioned::unordered::Index;
pub type Db<F, E, K, V, H, T, const P: usize, const N: usize, S> =
crate::qmdb::current::unordered::db::Db<
F,
E,
Journal<E, Operation<F, K, V>>,
K,
FixedEncoding<V>,
Index<T, Location<F>, P>,
H,
N,
S,
>;
impl<
F: Graftable,
E: Context,
K: Array,
V: FixedValue,
H: Hasher,
T: Translator,
const P: usize,
const N: usize,
S: Strategy,
> Db<F, E, K, V, H, T, P, N, S>
{
pub async fn init(context: E, config: Config<T, S>) -> Result<Self, Error<F>> {
crate::qmdb::current::init(context, config).await
}
}
}
#[cfg(test)]
pub mod test {
use super::*;
use crate::{
mmr,
qmdb::current::{tests::fixed_config, unordered::tests as shared},
translator::TwoCap,
};
use commonware_cryptography::{sha256::Digest, Sha256};
use commonware_macros::test_traced;
use commonware_runtime::{deterministic, Metrics, Runner as _, Supervisor as _};
use commonware_utils::TestRng;
use rand::Rng as _;
use std::collections::HashMap;
type CurrentTest = Db<
mmr::Family,
deterministic::Context,
Digest,
Digest,
Sha256,
TwoCap,
32,
commonware_parallel::Sequential,
>;
async fn open_db(context: deterministic::Context, partition_prefix: String) -> CurrentTest {
let cfg = fixed_config::<TwoCap>(&partition_prefix, &context);
CurrentTest::init(context, cfg).await.unwrap()
}
#[test_traced("INFO")]
pub fn test_current_unordered_fixed_metrics() {
deterministic::Runner::default().start(|ctx| async move {
let mut db = open_db(ctx.child("current"), "metrics".to_string()).await;
let key = Sha256::fill(1u8);
let value = Sha256::fill(2u8);
let batch = db
.new_batch()
.write(key, Some(value))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(batch).await.unwrap();
assert_eq!(db.get(&key).await.unwrap(), Some(value));
db.sync().await.unwrap();
db.prune(db.sync_boundary()).await.unwrap();
let metrics = ctx.encode();
for expected in [
"current_apply_batch_calls_total 1",
"current_sync_calls_total 1",
"current_prune_calls_total 1",
"current_pruned_chunks 0",
"current_sync_boundary 0",
"current_apply_batch_duration_count 1",
"current_sync_duration_count 1",
"current_prune_duration_count 1",
"current_any_get_calls_total 1",
"current_any_apply_batch_calls_total 1",
] {
assert!(metrics.contains(expected), "missing {expected}\n{metrics}");
}
assert!(!metrics.contains("current_get_calls_total"));
});
}
#[test_traced("WARN")]
pub fn test_current_unordered_fixed_read_merkleize_parity() {
fn key(i: u64) -> Digest {
Sha256::hash(&i.to_be_bytes())
}
fn val(i: u64) -> Digest {
Sha256::hash(&(i + 10000).to_be_bytes())
}
deterministic::Runner::default().start(|ctx| async move {
let mut db = open_db(ctx.child("current"), "fused-parity".to_string()).await;
let mut seed = db.new_batch();
for i in 0..2000u64 {
seed = seed.write(key(i), Some(val(i)));
}
let seed = seed.merkleize(&db, None).await.unwrap();
db.apply_batch(seed).await.unwrap();
db.commit().await.unwrap();
let make = |salt: u64| -> Vec<(Digest, Option<Digest>)> {
let mut rng = TestRng::new(salt);
let mut out = Vec::new();
for _ in 0..600 {
let r = rng.next_u32() % 100;
if r < 60 {
out.push((key(rng.next_u64() % 2000), Some(val(rng.next_u64()))));
} else if r < 80 {
out.push((key(rng.next_u64() % 2000), None));
} else {
out.push((key(2000 + rng.next_u64() % 2000), Some(val(rng.next_u64()))));
}
}
let mut m: HashMap<Digest, Option<Digest>> = HashMap::new();
for (k, v) in out {
m.insert(k, v);
}
m.into_iter().collect()
};
for depth in [0u8, 1u8] {
let parent = if depth == 1 {
let mut p = db.new_batch();
for (k, v) in make(900) {
p = p.write(k, v);
}
Some(p.merkleize(&db, None).await.unwrap())
} else {
None
};
let muts = make(depth as u64 + 1);
let new_batch = || {
parent
.as_ref()
.map_or_else(|| db.new_batch(), |p| p.new_batch::<Sha256>())
};
let mut nb = new_batch();
for (k, v) in &muts {
nb = nb.write(*k, *v);
}
let normal_root = nb.merkleize(&db, None).await.unwrap().root();
let keys: Vec<&Digest> = muts.iter().map(|(k, _)| k).collect();
let mut fb = new_batch();
let values = fb.get_many(&keys, &db).await.unwrap();
let plain = new_batch().get_many(&keys, &db).await.unwrap();
assert_eq!(values, plain, "value mismatch at depth={depth}");
for (k, v) in &muts {
fb = fb.write(*k, *v);
}
let fused_root = fb.merkleize(&db, None).await.unwrap().root();
assert_eq!(normal_root, fused_root, "root mismatch at depth={depth}");
}
});
}
crate::qmdb::current::tests::staged_merkleize_parity_test!(
test_current_unordered_fixed_staged_merkleize_parity,
open_db
);
#[test_traced("WARN")]
pub fn test_current_unordered_fixed_staged_ancestor_commit_before_merkleize() {
fn key(i: u64) -> Digest {
Sha256::hash(&i.to_be_bytes())
}
fn val(i: u64) -> Digest {
Sha256::hash(&(i + 10000).to_be_bytes())
}
deterministic::Runner::default().start(|ctx| async move {
let mut db = open_db(ctx.child("current"), "staged-ancestor".to_string()).await;
let mut seed = db.new_batch();
for i in 0..8u64 {
seed = seed.write(key(i), Some(val(i)));
}
let seed = seed.merkleize(&db, None).await.unwrap();
db.apply_batch(seed).await.unwrap();
db.commit().await.unwrap();
let grandparent = db
.new_batch()
.write(key(0), Some(val(1_000)))
.write(key(100), Some(val(1_001)))
.merkleize(&db, None)
.await
.unwrap();
let parent = grandparent
.new_batch::<Sha256>()
.write(key(1), Some(val(1_002)))
.merkleize(&db, None)
.await
.unwrap();
let read_keys = [key(0), key(100)];
let keys: Vec<&Digest> = read_keys.iter().collect();
let (values, staged) = parent
.new_batch::<Sha256>()
.stage(&keys, &db)
.await
.unwrap();
assert_eq!(values, vec![Some(val(1_000)), Some(val(1_001))]);
db.apply_batch(grandparent).await.unwrap();
let updates = vec![(0, Some(val(2_000))), (1, Some(val(2_001)))];
let staged = staged
.merkleize(updates, Vec::new(), None, &db)
.await
.unwrap();
let explicit_root = parent
.new_batch::<Sha256>()
.write(key(0), Some(val(2_000)))
.write(key(100), Some(val(2_001)))
.merkleize(&db, None)
.await
.unwrap()
.root();
assert_eq!(staged.root(), explicit_root);
db.apply_batch(parent).await.unwrap();
db.apply_batch(staged).await.unwrap();
db.commit().await.unwrap();
assert_eq!(db.get(&key(0)).await.unwrap(), Some(val(2_000)));
assert_eq!(db.get(&key(100)).await.unwrap(), Some(val(2_001)));
assert_eq!(db.get(&key(1)).await.unwrap(), Some(val(1_002)));
});
}
#[test_traced("INFO")]
pub fn test_merkleized_batch_sync_boundary_matches_db() {
deterministic::Runner::default().start(|ctx| async move {
let partition = "batch-boundary-match".to_string();
let mut db = open_db(ctx.child("current"), partition.clone()).await;
let key = Sha256::fill(1u8);
let mut last_batch_boundary = mmr::Location::new(0);
for i in 0..300u64 {
let value = Sha256::hash(&i.to_be_bytes());
let batch = db
.new_batch()
.write(key, Some(value))
.merkleize(&db, None)
.await
.unwrap();
last_batch_boundary = batch.sync_boundary();
db.apply_batch(batch).await.unwrap();
}
db.sync().await.unwrap();
let db_boundary = db.sync_boundary();
assert!(
*db_boundary > 0,
"inactivity floor never crossed a chunk; add more commits"
);
assert_eq!(
last_batch_boundary, db_boundary,
"batch boundary diverged from applied db boundary"
);
drop(db);
let reopened = open_db(ctx.child("reopen"), partition).await;
assert_eq!(
reopened.sync_boundary(),
last_batch_boundary,
"reopened db boundary disagrees with the boundary recorded from the last merkleized batch"
);
reopened.destroy().await.unwrap();
});
}
#[test_traced("DEBUG")]
pub fn test_current_db_verify_proof_over_bits_in_uncommitted_chunk() {
shared::test_verify_proof_over_bits_in_uncommitted_chunk(open_db);
}
#[test_traced("DEBUG")]
pub fn test_current_db_range_proofs() {
shared::test_range_proofs(open_db);
}
#[test_traced("DEBUG")]
pub fn test_current_db_key_value_proof() {
shared::test_key_value_proof(open_db);
}
#[test_traced("WARN")]
pub fn test_current_db_proving_repeated_updates() {
shared::test_proving_repeated_updates(open_db);
}
}