use crate::{
merkle::Graftable,
qmdb::{
any::{ordered::Update, ValueEncoding},
current::proof::OperationProof,
operation::Key,
},
};
use bytes::{Buf, BufMut};
use commonware_codec::{EncodeSize, Read, ReadExt as _, Write};
use commonware_cryptography::Digest;
pub mod db;
pub mod fixed;
#[cfg(any(test, feature = "test-traits"))]
mod test_trait_impls;
pub mod variable;
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum ExclusionProof<F: Graftable, K: Key, V: ValueEncoding, D: Digest, const N: usize> {
KeyValue(OperationProof<F, D, N>, Update<K, V>),
Commit(OperationProof<F, D, N>, Option<V::Value>),
}
const KEY_VALUE_CONTEXT: u8 = 0;
const COMMIT_CONTEXT: u8 = 1;
impl<F, K, V, D, const N: usize> Write for ExclusionProof<F, K, V, D, N>
where
F: Graftable,
K: Key,
V: ValueEncoding,
D: Digest,
Update<K, V>: Write,
{
fn write(&self, buf: &mut impl BufMut) {
match self {
Self::KeyValue(op_proof, update) => {
KEY_VALUE_CONTEXT.write(buf);
op_proof.write(buf);
update.write(buf);
}
Self::Commit(op_proof, value) => {
COMMIT_CONTEXT.write(buf);
op_proof.write(buf);
value.write(buf);
}
}
}
}
impl<F, K, V, D, const N: usize> EncodeSize for ExclusionProof<F, K, V, D, N>
where
F: Graftable,
K: Key,
V: ValueEncoding,
D: Digest,
Update<K, V>: EncodeSize,
{
fn encode_size(&self) -> usize {
1 + match self {
Self::KeyValue(op_proof, update) => op_proof.encode_size() + update.encode_size(),
Self::Commit(op_proof, value) => op_proof.encode_size() + value.encode_size(),
}
}
}
impl<F, K, V, D, const N: usize> Read for ExclusionProof<F, K, V, D, N>
where
F: Graftable,
K: Key,
V: ValueEncoding,
D: Digest,
Update<K, V>: Read,
{
type Cfg = (usize, <Update<K, V> as Read>::Cfg, <V::Value as Read>::Cfg);
fn read_cfg(
buf: &mut impl Buf,
(max_digests, update_cfg, value_cfg): &Self::Cfg,
) -> Result<Self, commonware_codec::Error> {
match u8::read(buf)? {
KEY_VALUE_CONTEXT => {
let op_proof = OperationProof::<F, D, N>::read_cfg(buf, max_digests)?;
let update = Update::<K, V>::read_cfg(buf, update_cfg)?;
Ok(Self::KeyValue(op_proof, update))
}
COMMIT_CONTEXT => {
let op_proof = OperationProof::<F, D, N>::read_cfg(buf, max_digests)?;
let value = Option::<V::Value>::read_cfg(buf, value_cfg)?;
Ok(Self::Commit(op_proof, value))
}
tag => Err(commonware_codec::Error::InvalidEnum(tag)),
}
}
}
#[cfg(feature = "arbitrary")]
impl<F, K, V, D, const N: usize> arbitrary::Arbitrary<'_> for ExclusionProof<F, K, V, D, N>
where
F: Graftable,
K: Key,
V: ValueEncoding,
D: Digest,
K: for<'a> arbitrary::Arbitrary<'a>,
V::Value: for<'a> arbitrary::Arbitrary<'a>,
D: for<'a> arbitrary::Arbitrary<'a>,
F::PendingChunk<D>: for<'a> arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let op_proof = u.arbitrary()?;
if u.arbitrary()? {
Ok(Self::KeyValue(op_proof, u.arbitrary()?))
} else {
Ok(Self::Commit(op_proof, u.arbitrary()?))
}
}
}
#[cfg(test)]
pub mod tests {
use super::{db, ExclusionProof};
use crate::{
index::ordered::Index,
journal::{contiguous::Mutable, Error as JournalError},
merkle::{Graftable, Location, Proof},
mmb,
qmdb::{
any::{
ordered::{Operation, Update},
traits::{DbAny, UnmerkleizedBatch as _},
value::FixedEncoding,
ValueEncoding,
},
current::{
proof::{OperationProof, RangeProof},
tests::apply_random_ops,
BitmapPrunedBits,
},
store::tests::{TestKey, TestValue},
Error,
},
translator::OneCap,
Persistable,
};
use commonware_codec::{Codec, Decode as _, Encode as _, EncodeSize as _};
use commonware_cryptography::{sha256::Digest, Digest as _, Hasher as _, Sha256};
use commonware_runtime::{
deterministic::{self, Context},
Runner as _, Supervisor as _,
};
use commonware_utils::{
bitmap::{Prunable as BitMap, Readable as _},
NZU64,
};
use core::future::Future;
use rand::RngCore;
type TestDb<F, C, V> = db::Db<
F,
deterministic::Context,
C,
Digest,
V,
Index<OneCap, Location<F>>,
Sha256,
32,
commonware_parallel::Sequential,
>;
pub fn test_build_small_close_reopen<F, C, Fn, Fut>(mut open_db: Fn)
where
F: Graftable,
C: DbAny<F> + BitmapPrunedBits,
C::Key: TestKey,
<C as DbAny<F>>::Value: TestValue,
Fn: FnMut(Context, String) -> Fut,
Fut: Future<Output = C>,
{
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let partition = "build-small".to_string();
let db: C = open_db(context.child("first"), partition.clone()).await;
assert_eq!(db.inactivity_floor_loc().await, Location::<F>::new(0));
assert_eq!(db.oldest_retained().await, 0);
let root0 = db.root();
drop(db);
let mut db: C = open_db(context.child("second"), partition.clone()).await;
assert!(db.get_metadata().await.unwrap().is_none());
assert_eq!(db.root(), root0);
let k1: C::Key = TestKey::from_seed(0);
let v1: <C as DbAny<F>>::Value = TestValue::from_seed(10);
assert!(db.get(&k1).await.unwrap().is_none());
let merkleized = db
.new_batch()
.write(k1, Some(v1.clone()))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
db.commit().await.unwrap();
assert_eq!(db.get(&k1).await.unwrap().unwrap(), v1);
assert!(db.get_metadata().await.unwrap().is_none());
let root1 = db.root();
assert_ne!(root1, root0);
drop(db);
let mut db: C = open_db(context.child("third"), partition.clone()).await;
assert_eq!(db.root(), root1);
assert!(db.get(&k1).await.unwrap().is_some());
assert!(db.get(&k1).await.unwrap().is_some());
let metadata: <C as DbAny<F>>::Value = TestValue::from_seed(1);
let merkleized = db
.new_batch()
.write(k1, None)
.merkleize(&db, Some(metadata.clone()))
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
db.commit().await.unwrap();
assert_eq!(db.get_metadata().await.unwrap().unwrap(), metadata);
let root2 = db.root();
drop(db);
let mut db: C = open_db(context.child("fourth"), partition.clone()).await;
assert_eq!(db.get_metadata().await.unwrap().unwrap(), metadata);
assert_eq!(db.root(), root2);
assert!(db.get(&k1).await.unwrap().is_none());
let merkleized = db.new_batch().merkleize(&db, None).await.unwrap();
db.apply_batch(merkleized).await.unwrap();
db.commit().await.unwrap();
let root3 = db.root();
assert_ne!(root3, root2);
let bounds = db.bounds().await;
for i in 0..*bounds.end - 1 {
assert!(!db.get_bit(i));
}
assert!(db.get_bit(*bounds.end - 1));
let merkleized = db
.new_batch()
.write(k1, Some(v1))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
assert_ne!(db.root(), root3);
db.destroy().await.unwrap();
});
}
pub(super) fn test_verify_proof_over_bits_in_uncommitted_chunk<F, C, V, Fn, Fut>(
mut open_db: Fn,
) where
F: Graftable,
C: Mutable<Item = Operation<F, Digest, V>> + Persistable<Error = JournalError> + 'static,
V: ValueEncoding<Value = Digest> + 'static,
Operation<F, Digest, V>: Codec,
TestDb<F, C, V>: DbAny<F, Key = Digest, Value = Digest, Digest = Digest> + 'static,
Fn: FnMut(Context, String) -> Fut + 'static,
Fut: Future<Output = TestDb<F, C, V>>,
{
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let hasher = crate::qmdb::hasher::<Sha256>();
let partition = "build-small".to_string();
let mut db = open_db(context.child("db"), partition.clone()).await;
let k = Sha256::fill(0x01);
let v1 = Sha256::fill(0xA1);
let merkleized = db
.new_batch()
.write(k, Some(v1))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
let (_, op_loc) = db.any.get_with_loc(&k).await.unwrap().unwrap();
let proof = db.key_value_proof(&hasher, k).await.unwrap();
let root = db.root();
assert!(TestDb::<F, C, V>::verify_key_value_proof(
&hasher, k, v1, &proof, &root,
));
let v2 = Sha256::fill(0xA2);
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, k, v2, &proof, &root,
));
let mut mangled_proof = proof.clone();
mangled_proof.next_key = Sha256::fill(0xFF);
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher,
k,
v1,
&mangled_proof,
&root,
));
let merkleized = db
.new_batch()
.write(k, Some(v2))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
let root = db.root();
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, k, v2, &proof, &root,
));
let proof = db.key_value_proof(&hasher, k).await.unwrap();
assert!(TestDb::<F, C, V>::verify_key_value_proof(
&hasher, k, v2, &proof, &root,
));
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, k, v1, &proof, &root,
));
let (p, _, chunks) = db.range_proof(&hasher, op_loc, NZU64!(1)).await.unwrap();
let proof_inactive = db::KeyValueProof {
proof: crate::qmdb::current::proof::OperationProof {
loc: op_loc,
chunk: chunks[0],
range_proof: p,
},
next_key: k,
};
let op = Operation::Update(Update {
key: k,
value: v1,
next_key: k,
});
assert!(TestDb::<F, C, V>::verify_range_proof(
&hasher,
&proof_inactive.proof.range_proof,
proof_inactive.proof.loc,
&[op],
&[proof_inactive.proof.chunk],
&root,
));
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher,
k,
v1,
&proof_inactive,
&root,
));
let (_, active_loc) = db.any.get_with_loc(&k).await.unwrap().unwrap();
assert_ne!(active_loc, proof_inactive.proof.loc);
assert_eq!(
BitMap::<32>::to_chunk_index(*active_loc),
BitMap::<32>::to_chunk_index(*proof_inactive.proof.loc)
);
let mut fake_proof = proof_inactive.clone();
fake_proof.proof.loc = active_loc;
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher,
k,
v1,
&fake_proof,
&root,
));
let mut modified_chunk = proof_inactive.proof.chunk;
let bit_pos = *proof_inactive.proof.loc;
let byte_idx = bit_pos / 8;
let bit_idx = bit_pos % 8;
modified_chunk[byte_idx as usize] |= 1 << bit_idx;
let mut fake_proof = proof_inactive.clone();
fake_proof.proof.chunk = modified_chunk;
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher,
k,
v1,
&fake_proof,
&root,
));
db.destroy().await.unwrap();
});
}
pub(super) fn test_range_proofs<F, C, V, Fn, Fut>(mut open_db: Fn)
where
F: Graftable,
C: Mutable<Item = Operation<F, Digest, V>> + Persistable<Error = JournalError> + 'static,
V: ValueEncoding<Value = Digest> + 'static,
Operation<F, Digest, V>: Codec,
TestDb<F, C, V>: DbAny<F, Key = Digest, Value = Digest, Digest = Digest> + 'static,
Fn: FnMut(Context, String) -> Fut + 'static,
Fut: Future<Output = TestDb<F, C, V>>,
{
let executor = deterministic::Runner::default();
executor.start(|mut context| async move {
let partition = "range-proofs".to_string();
let hasher = crate::qmdb::hasher::<Sha256>();
let db = open_db(context.child("db"), partition.clone()).await;
let root = db.root();
let proof = RangeProof {
proof: Proof::default(),
pending_chunk_digest: None.try_into().unwrap(),
partial_chunk_digest: None,
ops_root: Digest::EMPTY,
};
assert!(!TestDb::<F, C, V>::verify_range_proof(
&hasher,
&proof,
Location::<F>::new(0),
&[],
&[],
&root,
));
let mut db = apply_random_ops::<F, TestDb<F, C, V>>(200, true, context.next_u64(), db)
.await
.unwrap();
let merkleized = db.new_batch().merkleize(&db, None).await.unwrap();
db.apply_batch(merkleized).await.unwrap();
let root = db.root();
let max_ops = 4;
let end_loc = db.bounds().await.end;
let start_loc = db.any.inactivity_floor_loc();
for loc in *start_loc..*end_loc {
let loc = Location::<F>::new(loc);
let (proof, ops, chunks) =
db.range_proof(&hasher, loc, NZU64!(max_ops)).await.unwrap();
assert!(
TestDb::<F, C, V>::verify_range_proof(
&hasher, &proof, loc, &ops, &chunks, &root
),
"failed to verify range at start_loc {start_loc}",
);
let mut chunks_with_extra = chunks.clone();
chunks_with_extra.push(chunks[chunks.len() - 1]);
assert!(!TestDb::<F, C, V>::verify_range_proof(
&hasher,
&proof,
loc,
&ops,
&chunks_with_extra,
&root,
));
}
db.destroy().await.unwrap();
});
}
pub(super) fn test_key_value_proof<F, C, V, Fn, Fut>(mut open_db: Fn)
where
F: Graftable,
C: Mutable<Item = Operation<F, Digest, V>> + Persistable<Error = JournalError> + 'static,
V: ValueEncoding<Value = Digest> + 'static,
Operation<F, Digest, V>: Codec,
TestDb<F, C, V>: DbAny<F, Key = Digest, Value = Digest, Digest = Digest> + 'static,
Fn: FnMut(Context, String) -> Fut + 'static,
Fut: Future<Output = TestDb<F, C, V>>,
{
let executor = deterministic::Runner::default();
executor.start(|mut context| async move {
let partition = "range-proofs".to_string();
let hasher = crate::qmdb::hasher::<Sha256>();
let db = open_db(context.child("db"), partition.clone()).await;
let mut db = apply_random_ops::<F, TestDb<F, C, V>>(500, true, context.next_u64(), db)
.await
.unwrap();
let merkleized = db.new_batch().merkleize(&db, None).await.unwrap();
db.apply_batch(merkleized).await.unwrap();
let root = db.root();
let bad_key = Sha256::fill(0xAA);
let res = db.key_value_proof(&hasher, bad_key).await;
assert!(matches!(res, Err(Error::KeyNotFound)));
let start = *db.inactivity_floor_loc();
for i in start..db.any.bitmap.len() {
if !db.any.bitmap.get_bit(i) {
continue;
}
let op = db.any.log.read(Location::<F>::new(i)).await.unwrap();
let (key, value) = match op {
Operation::Update(key_data) => (key_data.key, key_data.value),
Operation::CommitFloor(_, _) => continue,
_ => unreachable!("expected update or commit floor operation"),
};
let proof = db.key_value_proof(&hasher, key).await.unwrap();
assert!(TestDb::<F, C, V>::verify_key_value_proof(
&hasher, key, value, &proof, &root
));
let wrong_val = Sha256::hash(&[0xFF]);
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, key, wrong_val, &proof, &root
));
let wrong_key = Sha256::hash(&[0xEE]);
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, wrong_key, value, &proof, &root
));
let wrong_root = Sha256::hash(&[0xDD]);
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher,
key,
value,
&proof,
&wrong_root,
));
let mut bad_proof = proof.clone();
bad_proof.next_key = wrong_key;
assert!(!TestDb::<F, C, V>::verify_key_value_proof(
&hasher, key, value, &bad_proof, &root,
));
}
db.destroy().await.unwrap();
});
}
pub(super) fn test_proving_repeated_updates<F, C, V, Fn, Fut>(mut open_db: Fn)
where
F: Graftable,
C: Mutable<Item = Operation<F, Digest, V>> + Persistable<Error = JournalError> + 'static,
V: ValueEncoding<Value = Digest> + 'static,
Operation<F, Digest, V>: Codec,
TestDb<F, C, V>: DbAny<F, Key = Digest, Value = Digest, Digest = Digest> + 'static,
Fn: FnMut(Context, String) -> Fut + 'static,
Fut: Future<Output = TestDb<F, C, V>>,
{
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let hasher = crate::qmdb::hasher::<Sha256>();
let partition = "build-small".to_string();
let mut db = open_db(context.child("db"), partition.clone()).await;
let k = Sha256::fill(0x00);
let mut old_val = Sha256::fill(0x00);
for i in 1u8..=255 {
let v = Sha256::fill(i);
let merkleized = db
.new_batch()
.write(k, Some(v))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
assert_eq!(db.get(&k).await.unwrap().unwrap(), v);
let root = db.root();
let proof = db.key_value_proof(&hasher, k).await.unwrap();
assert!(
TestDb::<F, C, V>::verify_key_value_proof(&hasher, k, v, &proof, &root),
"proof of update {i} failed to verify"
);
assert!(
!TestDb::<F, C, V>::verify_key_value_proof(&hasher, k, old_val, &proof, &root,),
"proof of update {i} verified when it should not have"
);
old_val = v;
}
db.destroy().await.unwrap();
});
}
pub(super) fn test_exclusion_proofs<F, C, V, Fn, Fut>(mut open_db: Fn)
where
F: Graftable + PartialEq,
C: Mutable<Item = Operation<F, Digest, V>> + Persistable<Error = JournalError> + 'static,
V: ValueEncoding<Value = Digest> + PartialEq + core::fmt::Debug + 'static,
Operation<F, Digest, V>: Codec,
TestDb<F, C, V>: DbAny<F, Key = Digest, Value = Digest, Digest = Digest> + 'static,
Fn: FnMut(Context, String) -> Fut + 'static,
Fut: Future<Output = TestDb<F, C, V>>,
{
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let hasher = crate::qmdb::hasher::<Sha256>();
let partition = "exclusion-proofs".to_string();
let mut db = open_db(context.child("db"), partition.clone()).await;
let key_exists_1 = Sha256::fill(0x10);
let empty_root = db.root();
let empty_proof = db.exclusion_proof(&hasher, &key_exists_1).await.unwrap();
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&empty_proof,
&empty_root,
));
let v1 = Sha256::fill(0xA1);
let merkleized = db
.new_batch()
.write(key_exists_1, Some(v1))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
let root = db.root();
let result = db.exclusion_proof(&hasher, &key_exists_1).await;
assert!(matches!(result, Err(Error::KeyExists)));
let greater_key = Sha256::fill(0xFF);
let lesser_key = Sha256::fill(0x00);
let proof = db.exclusion_proof(&hasher, &greater_key).await.unwrap();
let proof2 = db.exclusion_proof(&hasher, &lesser_key).await.unwrap();
assert_eq!(proof, proof2);
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&greater_key,
&proof,
&root,
));
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&lesser_key,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&proof,
&root,
));
let key_exists_2 = Sha256::fill(0x30);
let v2 = Sha256::fill(0xB2);
let merkleized = db
.new_batch()
.write(key_exists_2, Some(v2))
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
let root = db.root();
let lesser_key = Sha256::fill(0x0F); let greater_key = Sha256::fill(0x31); let middle_key = Sha256::fill(0x20); let proof = db.exclusion_proof(&hasher, &greater_key).await.unwrap();
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&greater_key,
&proof,
&root,
));
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&lesser_key,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&middle_key,
&proof,
&root,
));
let new_proof = db.exclusion_proof(&hasher, &lesser_key).await.unwrap();
assert_eq!(proof, new_proof);
let proof = db.exclusion_proof(&hasher, &middle_key).await.unwrap();
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&proof,
&root,
));
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&middle_key,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_2,
&proof,
&root,
));
let conflicting_middle_key = Sha256::fill(0x11); assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&conflicting_middle_key,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&greater_key,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&lesser_key,
&proof,
&root,
));
let merkleized = db
.new_batch()
.write(key_exists_1, None)
.write(key_exists_2, None)
.merkleize(&db, None)
.await
.unwrap();
db.apply_batch(merkleized).await.unwrap();
db.sync().await.unwrap();
let root = db.root();
assert!(db.is_empty());
assert_ne!(db.bounds().await.end, 0);
assert_ne!(root, empty_root);
let proof = db.exclusion_proof(&hasher, &key_exists_1).await.unwrap();
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&proof,
&root,
));
assert!(TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_2,
&proof,
&root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&empty_proof, &root,
));
assert!(!TestDb::<F, C, V>::verify_exclusion_proof(
&hasher,
&key_exists_1,
&proof,
&empty_root, ));
});
}
fn sample_op_proof() -> OperationProof<mmb::Family, Digest, 32> {
let range_proof = RangeProof {
proof: Proof::<mmb::Family, Digest> {
leaves: mmb::Location::new(7),
inactive_peaks: 0,
digests: vec![Sha256::hash(b"sib")],
},
pending_chunk_digest: None,
partial_chunk_digest: None,
ops_root: Sha256::hash(b"ops"),
};
let chunk: [u8; 32] = core::array::from_fn(|i| i as u8);
OperationProof {
loc: mmb::Location::new(5),
chunk,
range_proof,
}
}
fn op_proof_digest_count(proof: &OperationProof<mmb::Family, Digest, 32>) -> usize {
proof.range_proof.proof.digests.len()
}
type CodecExclusionProof =
ExclusionProof<mmb::Family, Digest, FixedEncoding<Digest>, Digest, 32>;
type CodecKeyValueProof = db::KeyValueProof<mmb::Family, Digest, Digest, 32>;
const MAX_DIGESTS: usize = 64;
#[test]
fn test_key_value_proof_codec_roundtrip() {
let proof = CodecKeyValueProof {
proof: sample_op_proof(),
next_key: Sha256::hash(b"next-key"),
};
let encoded = proof.encode();
assert_eq!(encoded.len(), proof.encode_size());
let decoded = CodecKeyValueProof::decode_cfg(encoded, &(MAX_DIGESTS, ())).unwrap();
assert_eq!(decoded, proof);
}
#[test]
fn test_key_value_proof_codec_enforces_merkle_digest_budget() {
let proof = CodecKeyValueProof {
proof: sample_op_proof(),
next_key: Sha256::hash(b"next-key"),
};
let total_digests = op_proof_digest_count(&proof.proof);
let encoded = proof.encode();
let decoded =
CodecKeyValueProof::decode_cfg(encoded.clone(), &(total_digests, ())).unwrap();
assert_eq!(decoded, proof);
assert!(CodecKeyValueProof::decode_cfg(encoded, &(total_digests - 1, ())).is_err());
}
#[test]
fn test_exclusion_proof_codec_roundtrip() {
let cases = [
CodecExclusionProof::KeyValue(
sample_op_proof(),
Update {
key: Sha256::hash(b"key"),
value: Sha256::hash(b"value"),
next_key: Sha256::hash(b"next-key"),
},
),
CodecExclusionProof::Commit(sample_op_proof(), Some(Sha256::hash(b"metadata"))),
CodecExclusionProof::Commit(sample_op_proof(), None),
];
for proof in cases {
let encoded = proof.encode();
assert_eq!(encoded.len(), proof.encode_size());
let decoded = CodecExclusionProof::decode_cfg(encoded, &(MAX_DIGESTS, (), ())).unwrap();
assert_eq!(decoded, proof);
}
}
#[test]
fn test_exclusion_proof_codec_enforces_merkle_digest_budget() {
let cases = [
CodecExclusionProof::KeyValue(
sample_op_proof(),
Update {
key: Sha256::hash(b"key"),
value: Sha256::hash(b"value"),
next_key: Sha256::hash(b"next-key"),
},
),
CodecExclusionProof::Commit(sample_op_proof(), Some(Sha256::hash(b"metadata"))),
CodecExclusionProof::Commit(sample_op_proof(), None),
];
for proof in cases {
let total_digests = match &proof {
CodecExclusionProof::KeyValue(op_proof, _) => op_proof_digest_count(op_proof),
CodecExclusionProof::Commit(op_proof, _) => op_proof_digest_count(op_proof),
};
let encoded = proof.encode();
let decoded =
CodecExclusionProof::decode_cfg(encoded.clone(), &(total_digests, (), ())).unwrap();
assert_eq!(decoded, proof);
assert!(
CodecExclusionProof::decode_cfg(encoded, &(total_digests - 1, (), ())).is_err()
);
}
}
#[test]
fn test_exclusion_proof_rejects_unknown_tag() {
let mut bytes = vec![42u8]; bytes.extend_from_slice(&[0u8; 32]); let result = CodecExclusionProof::decode_cfg(bytes.as_slice(), &(MAX_DIGESTS, (), ()));
assert!(result.is_err());
}
#[cfg(feature = "arbitrary")]
mod conformance {
use crate::{
merkle::{mmb, mmr},
qmdb::{
any::value::{FixedEncoding, VariableEncoding},
current::ordered::{db::KeyValueProof, ExclusionProof},
},
};
use commonware_codec::conformance::CodecConformance;
use commonware_cryptography::sha256::Digest as Sha256Digest;
use commonware_utils::sequence::U64;
commonware_conformance::conformance_tests! {
CodecConformance<KeyValueProof<mmr::Family, U64, Sha256Digest, 32>>,
CodecConformance<KeyValueProof<mmb::Family, U64, Sha256Digest, 32>>,
CodecConformance<ExclusionProof<mmr::Family, U64, FixedEncoding<U64>, Sha256Digest, 32>>,
CodecConformance<ExclusionProof<mmr::Family, U64, VariableEncoding<Vec<u8>>, Sha256Digest, 32>>,
CodecConformance<ExclusionProof<mmb::Family, U64, FixedEncoding<U64>, Sha256Digest, 32>>,
CodecConformance<ExclusionProof<mmb::Family, U64, VariableEncoding<Vec<u8>>, Sha256Digest, 32>>,
}
}
}