#![no_main] use arbitrary::Arbitrary; use commonware_codec::RangeCfg; use commonware_cryptography::{sha256::Digest, Hasher, Sha256}; use commonware_runtime::{buffer::paged::CacheRef, deterministic, BufferPooler, Runner}; use commonware_storage::{ mmr::Location, qmdb::{ immutable::{Config, Immutable}, verify_proof, }, translator::TwoCap, }; use commonware_utils::{NZUsize, NZU16, NZU64}; use libfuzzer_sys::fuzz_target; use rand::{rngs::StdRng, Rng, SeedableRng}; use std::num::{NonZeroU16, NonZeroU64}; const MAX_OPERATIONS: usize = 50; const MAX_KEY_SIZE: usize = 32; const MAX_VALUE_SIZE: usize = 256; const MAX_PROOF_OPS: u64 = 100; const PAGE_SIZE: NonZeroU16 = NZU16!(77); const PAGE_CACHE_SIZE: usize = 9; const ITEMS_PER_SECTION: u64 = 5; const ITEMS_PER_BLOB: u64 = 11; #[derive(Arbitrary, Debug, Clone)] enum ImmutableOperation { Set { key_seed: u64, value_size: usize, }, Get { key_seed: u64, }, Commit { has_metadata: bool, metadata_size: usize, }, Prune { loc: u64, }, Proof { start_index: u64, max_ops: u64, }, HistoricalProof { size: u64, start_loc: u64, max_ops: u64, }, GetMetadata, OpCount, OldestRetainedLoc, Root, } #[derive(Debug)] struct FuzzInput { seed: u64, operations: Vec, } impl<'a> Arbitrary<'a> for FuzzInput { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let seed = u.arbitrary()?; let num_ops = u.int_in_range(1..=MAX_OPERATIONS)?; let mut operations = Vec::with_capacity(num_ops); for _ in 0..num_ops { operations.push(u.arbitrary()?); } Ok(FuzzInput { seed, operations }) } } fn generate_key(rng: &mut StdRng, seed: u64) -> Digest { let mut data = vec![0u8; rng.gen_range(1..=MAX_KEY_SIZE)]; for (i, byte) in data.iter_mut().enumerate() { *byte = ((seed >> (i % 8)) & 0xFF) as u8 ^ rng.gen::(); } Sha256::hash(&data) } fn generate_value(rng: &mut StdRng, size: usize) -> Vec { let actual_size = size.clamp(1, MAX_VALUE_SIZE); (0..actual_size).map(|_| rng.gen()).collect() } fn db_config(suffix: &str, pooler: &impl BufferPooler) -> Config, ())> { Config { mmr_journal_partition: format!("journal-{suffix}"), mmr_metadata_partition: format!("metadata-{suffix}"), mmr_items_per_blob: NZU64!(ITEMS_PER_BLOB), mmr_write_buffer: NZUsize!(1024), log_partition: format!("log-{suffix}"), log_items_per_section: NZU64!(ITEMS_PER_SECTION), log_compression: None, log_codec_config: ((0..=10000).into(), ()), log_write_buffer: NZUsize!(1024), translator: TwoCap, thread_pool: None, page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, NZUsize!(PAGE_CACHE_SIZE)), } } /// Assign locations to pending keys based on sorted order (matching BTreeMap /// iteration in `merkleize()`). fn assign_pending_locations( pending: &[(Digest, Vec)], base: Location, keys_set: &mut Vec<(Digest, Location)>, set_locations: &mut Vec<(Digest, Location)>, ) { let mut sorted_keys: Vec = pending.iter().map(|(k, _)| *k).collect(); sorted_keys.sort(); for (i, key) in sorted_keys.iter().enumerate() { let loc = Location::new(base.as_u64() + i as u64); keys_set.push((*key, loc)); set_locations.push((*key, loc)); } } fn fuzz(input: FuzzInput) { let runner = deterministic::Runner::seeded(input.seed); runner.start(|context| async move { let mut rng = StdRng::seed_from_u64(input.seed); let cfg = db_config("fuzz-partition", &context); let mut db = Immutable::<_, Digest, Vec, Sha256, TwoCap>::init(context, cfg) .await .unwrap(); let mut hasher = commonware_storage::mmr::StandardHasher::::new(); let mut keys_set = Vec::new(); let mut set_locations = Vec::new(); // Track locations that contain Set operations let mut last_commit_loc = None; let mut pending_sets: Vec<(Digest, Vec)> = Vec::new(); for op in input.operations { match op { ImmutableOperation::Set { key_seed, value_size, } => { let key = generate_key(&mut rng, key_seed); let value = generate_value(&mut rng, value_size); if !keys_set.iter().any(|(k, _): &(Digest, _)| k == &key) && !pending_sets.iter().any(|(k, _): &(Digest, _)| k == &key) { pending_sets.push((key, value)); } } ImmutableOperation::Get { key_seed } => { let key = generate_key(&mut rng, key_seed); let _ = db.get(&key).await; } ImmutableOperation::Commit { has_metadata, metadata_size, } => { let metadata = if has_metadata { Some(generate_value(&mut rng, metadata_size)) } else { None }; assign_pending_locations( &pending_sets, db.bounds().await.end, &mut keys_set, &mut set_locations, ); let finalized = { let mut batch = db.new_batch(); for (k, v) in pending_sets.drain(..) { batch.set(k, v); } batch.merkleize(metadata).finalize() }; db.apply_batch(finalized).await.unwrap(); last_commit_loc = Some(db.bounds().await.end - 1); } ImmutableOperation::Prune { loc } => { if let Some(commit_loc) = last_commit_loc { let safe_loc = loc % (commit_loc + 1).as_u64(); let safe_loc = Location::new(safe_loc); assign_pending_locations( &pending_sets, db.bounds().await.end, &mut keys_set, &mut set_locations, ); let finalized = { let mut batch = db.new_batch(); for (k, v) in pending_sets.drain(..) { batch.set(k, v); } batch.merkleize(None).finalize() }; db.apply_batch(finalized).await.unwrap(); last_commit_loc = Some(db.bounds().await.end - 1); db.prune(safe_loc).await.expect("prune should not fail"); let oldest = db.bounds().await.start; set_locations.retain(|(_, l): &(_, Location)| *l >= oldest); keys_set.retain(|(_, l): &(_, Location)| *l >= oldest); } } ImmutableOperation::Proof { start_index, max_ops, } => { let op_count = db.bounds().await.end; if op_count > 0 { let safe_start = start_index % op_count.as_u64(); let safe_start = Location::new(safe_start); let safe_max_ops = NonZeroU64::new((max_ops % MAX_PROOF_OPS).max(1)).unwrap(); assign_pending_locations( &pending_sets, db.bounds().await.end, &mut keys_set, &mut set_locations, ); let finalized = { let mut batch = db.new_batch(); for (k, v) in pending_sets.drain(..) { batch.set(k, v); } batch.merkleize(None).finalize() }; db.apply_batch(finalized).await.unwrap(); last_commit_loc = Some(db.bounds().await.end - 1); if let Ok((proof, ops)) = db.proof(safe_start, safe_max_ops).await { let root = db.root(); let _ = verify_proof(&mut hasher, &proof, safe_start, &ops, &root); } } } ImmutableOperation::HistoricalProof { size, start_loc, max_ops, } => { let op_count = db.bounds().await.end; if op_count > 0 && pending_sets.is_empty() { let safe_size = (size % op_count.as_u64()).max(1); let safe_size = Location::new(safe_size); let safe_start = start_loc % safe_size.as_u64(); let safe_start = Location::new(safe_start); let safe_max_ops = NonZeroU64::new((max_ops % MAX_PROOF_OPS).max(1)).unwrap(); let finalized = db.new_batch().merkleize(None).finalize(); db.apply_batch(finalized).await.unwrap(); last_commit_loc = Some(db.bounds().await.end - 1); if safe_start >= db.bounds().await.start { let _ = db .historical_proof(safe_size, safe_start, safe_max_ops) .await; } } } ImmutableOperation::GetMetadata => { let _ = db.get_metadata().await; } ImmutableOperation::OpCount => { let _ = db.bounds().await.end; } ImmutableOperation::OldestRetainedLoc => { let _ = db.bounds().await.start; } ImmutableOperation::Root => { assign_pending_locations( &pending_sets, db.bounds().await.end, &mut keys_set, &mut set_locations, ); let finalized = { let mut batch = db.new_batch(); for (k, v) in pending_sets.drain(..) { batch.set(k, v); } batch.merkleize(None).finalize() }; db.apply_batch(finalized).await.unwrap(); last_commit_loc = Some(db.bounds().await.end - 1); let _ = db.root(); } } } assign_pending_locations( &pending_sets, db.bounds().await.end, &mut keys_set, &mut set_locations, ); let finalized = { let mut batch = db.new_batch(); for (k, v) in pending_sets.drain(..) { batch.set(k, v); } batch.merkleize(None).finalize() }; db.apply_batch(finalized).await.unwrap(); db.destroy().await.unwrap(); }); } fuzz_target!(|input: FuzzInput| { fuzz(input); });