#![no_main] use arbitrary::Arbitrary; use commonware_cryptography::Sha256; use commonware_runtime::{buffer::paged::CacheRef, deterministic, BufferPooler, Runner}; use commonware_storage::{ journal::contiguous::fixed::Config as FConfig, mmr::{self, journaled::Config as MmrConfig}, qmdb::current::{unordered::fixed::Db as CurrentDb, FixedConfig as Config}, translator::OneCap, }; use commonware_utils::{sequence::FixedBytes, NZUsize, NZU16, NZU64}; use libfuzzer_sys::fuzz_target; use std::num::NonZeroU16; type Key = FixedBytes<32>; type Value = FixedBytes<32>; type Db = CurrentDb; const PAGE_SIZE: NonZeroU16 = NZU16!(137); const COLLISION_GROUPS: u8 = 4; const KEY_SPACE: u64 = 32; const MAX_INITIAL_WRITES: usize = 16; const MAX_PARENT_MUTATIONS: usize = 16; const MAX_CHILD_MUTATIONS: usize = 16; #[derive(Arbitrary, Debug, Clone, Copy)] struct KeySeed { prefix: u8, suffix: u64, } #[derive(Arbitrary, Debug, Clone)] struct SeededWrite { key: KeySeed, value: [u8; 32], } #[derive(Arbitrary, Debug, Clone)] enum Mutation { Write { key: KeySeed, value: [u8; 32] }, Delete { key: KeySeed }, } #[derive(Debug)] struct FuzzInput { initial: Vec, parent: Vec, child: Vec, } impl<'a> Arbitrary<'a> for FuzzInput { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let initial_len = u.int_in_range(0..=MAX_INITIAL_WRITES)?; let parent_len = u.int_in_range(1..=MAX_PARENT_MUTATIONS)?; let child_len = u.int_in_range(1..=MAX_CHILD_MUTATIONS)?; let initial = (0..initial_len) .map(|_| SeededWrite::arbitrary(u)) .collect::, _>>()?; let parent = (0..parent_len) .map(|_| Mutation::arbitrary(u)) .collect::, _>>()?; let child = (0..child_len) .map(|_| Mutation::arbitrary(u)) .collect::, _>>()?; Ok(Self { initial, parent, child, }) } } fn test_config(name: &str, pooler: &impl BufferPooler) -> Config { let page_cache = CacheRef::from_pooler(pooler, PAGE_SIZE, NZUsize!(2)); Config { merkle_config: MmrConfig { journal_partition: format!("{name}-mmr"), metadata_partition: format!("{name}-meta"), items_per_blob: NZU64!(17), write_buffer: NZUsize!(1024), thread_pool: None, page_cache: page_cache.clone(), }, journal_config: FConfig { partition: format!("{name}-log"), items_per_blob: NZU64!(13), write_buffer: NZUsize!(1024), page_cache, }, grafted_metadata_partition: format!("{name}-grafted"), translator: OneCap, } } fn key_from_seed(seed: KeySeed) -> Key { let mut bytes = [0u8; 32]; bytes[0] = seed.prefix % COLLISION_GROUPS; let suffix = seed.suffix % KEY_SPACE; bytes[24..].copy_from_slice(&suffix.to_be_bytes()); Key::new(bytes) } fn value_from_bytes(bytes: [u8; 32]) -> Value { Value::new(bytes) } fn fuzz(input: FuzzInput) { let runner = deterministic::Runner::default(); runner.start(|context| async move { let cfg = test_config("fuzz-current-unordered-pending-vs-committed-root", &context); let mut db = Db::init(context.clone(), cfg) .await .expect("init current unordered db"); // Seed the committed base state so the wrapper sees collision-heavy // inner batching before parent/child comparisons. let mut batch = db.new_batch(); for write in &input.initial { batch = batch.write( key_from_seed(write.key), Some(value_from_bytes(write.value)), ); } let initial = batch.merkleize(&db, None).await.unwrap(); db.apply_batch(initial).await.unwrap(); db.commit().await.unwrap(); // Build the child while the parent is still pending. let mut batch = db.new_batch(); for mutation in &input.parent { batch = match mutation { Mutation::Write { key, value } => { batch.write(key_from_seed(*key), Some(value_from_bytes(*value))) } Mutation::Delete { key } => batch.write(key_from_seed(*key), None), }; } let parent = batch.merkleize(&db, None).await.unwrap(); let mut batch = parent.new_batch::(); for mutation in &input.child { batch = match mutation { Mutation::Write { key, value } => { batch.write(key_from_seed(*key), Some(value_from_bytes(*value))) } Mutation::Delete { key } => batch.write(key_from_seed(*key), None), }; } let pending_child = batch.merkleize(&db, None).await.unwrap(); // Commit the parent, then rebuild the same logical child from the // committed wrapper state. Both canonical and ops roots must match. db.apply_batch(parent).await.unwrap(); db.commit().await.unwrap(); let mut batch = db.new_batch(); for mutation in &input.child { batch = match mutation { Mutation::Write { key, value } => { batch.write(key_from_seed(*key), Some(value_from_bytes(*value))) } Mutation::Delete { key } => batch.write(key_from_seed(*key), None), }; } let committed_child = batch.merkleize(&db, None).await.unwrap(); assert_eq!( pending_child.root(), committed_child.root(), "current root depended on pending-vs-committed parent path" ); assert_eq!( pending_child.ops_root(), committed_child.ops_root(), "current ops root depended on pending-vs-committed parent path" ); // Apply the pending child and verify the DB state matches. db.apply_batch(pending_child).await.unwrap(); assert_eq!( db.root(), committed_child.root(), "pending child canonical root diverged" ); assert_eq!( db.ops_root(), committed_child.ops_root(), "pending child ops root diverged" ); db.destroy().await.unwrap(); }); } fuzz_target!(|input: FuzzInput| { fuzz(input); });