From f1d91bdc58cc96b9c05271b7d09a0e0ae6d9fee4 Mon Sep 17 00:00:00 2001 From: "abel.chen@megaeth.technology" Date: Tue, 13 Jan 2026 03:36:00 +0000 Subject: [PATCH] feat: apply cargo fuzz --- Cargo.lock | 38 +++ Cargo.toml | 2 +- fuzz/.gitignore | 4 + fuzz/Cargo.toml | 27 ++ .../fuzz.rs => fuzz/fuzz_targets/salt_fuzz.rs | 312 ++++++++---------- salt/src/lib.rs | 2 - 6 files changed, 208 insertions(+), 177 deletions(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml rename salt/src/fuzz.rs => fuzz/fuzz_targets/salt_fuzz.rs (66%) diff --git a/Cargo.lock b/Cargo.lock index b5eabcd4..23d685f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -297,6 +303,8 @@ version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -700,6 +708,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -722,6 +740,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "log" version = "0.4.27" @@ -993,6 +1021,16 @@ dependencies = [ "zerocopy 0.7.35", ] +[[package]] +name = "salt-fuzz" +version = "0.0.0" +dependencies = [ + "bincode", + "libfuzzer-sys", + "rand 0.9.2", + "salt", +] + [[package]] name = "same-file" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 3babfaec..e89630e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["ipa-multipoint", "banderwagon", "salt"] +members = ["ipa-multipoint", "banderwagon", "salt", "fuzz"] resolver = "2" [profile.release] diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..41f46841 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "salt-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +rand = "0.9" +bincode = { version = "2.0", features = ["serde"] } + +[dependencies.salt] +path = "../salt" + +[features] +test-bucket-resize = ["salt/test-bucket-resize"] +enable-hugepages = ["salt/enable-hugepages"] + +[[bin]] +name = "salt_fuzz" +path = "fuzz_targets/salt_fuzz.rs" +test = false +doc = false +bench = false diff --git a/salt/src/fuzz.rs b/fuzz/fuzz_targets/salt_fuzz.rs similarity index 66% rename from salt/src/fuzz.rs rename to fuzz/fuzz_targets/salt_fuzz.rs index 1a5da224..dc5cc443 100644 --- a/salt/src/fuzz.rs +++ b/fuzz/fuzz_targets/salt_fuzz.rs @@ -1,19 +1,22 @@ -//! End-to-end fuzz testing for SALT blockchain state management. - -use crate::constant::{NUM_BUCKETS, NUM_META_BUCKETS}; -use crate::traits::StateReader; -use crate::types::{BucketId, SaltKey}; -use crate::{ - EphemeralSaltState, MemStore, SaltValue, SaltWitness, StateRoot, StateUpdates, Witness, +#![no_main] + +use libfuzzer_sys::fuzz_target; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use salt::{ + constant::{NUM_BUCKETS, NUM_META_BUCKETS}, + traits::StateReader, + BucketId, EphemeralSaltState, MemStore, SaltKey, SaltValue, SaltWitness, StateRoot, + StateUpdates, Witness, }; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::sync::OnceLock; /// A state modification resulting from transaction execution. /// /// Operations reference keys via indices into a pre-generated KV pool, /// allowing the fuzzer to focus on operation sequences rather than key generation. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub enum Operation { /// Inserts or updates the key at pool index with a new single-byte value. /// @@ -33,7 +36,7 @@ pub enum Operation { /// After transaction execution produces state modifications, these changes are /// applied to the state trie in small batches to enable pipelining of operations /// like state trie updates and block propagation. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Block { /// Small batches of state modifications to apply incrementally. /// @@ -50,6 +53,94 @@ pub struct Block { pub lookups: Vec, } +static STORE: OnceLock = OnceLock::new(); + +fuzz_target!(|data: &[u8]| { + // fuzzed code goes here + if data.len() < 1024 { + return; + } + + let seed: u64 = u64::from_le_bytes(data[0..8].try_into().unwrap()); + let blocks = generate_blocks(seed, data); + e2e_test(&blocks); +}); + +/// Reads an environment variable and parses it, falling back to default if missing or invalid. +fn env(key: &str, default: T) -> T { + std::env::var(key) + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) +} + +/// Generates a sequence of test blocks from fuzzer input data. +/// +/// Converts raw fuzzer bytes into structured blocks containing mini-blocks and lookups. +/// Each byte is interpreted as an operation: values < 180 become Insert operations, +/// others become Delete operations. The function divides the input data into blocks, +/// then further subdivides each block into mini-blocks to simulate pipelined execution. +/// +/// # Arguments +/// * `seed` - Random seed for deterministic key/value generation +/// * `data` - Raw fuzzer input bytes to convert into operations +/// +/// # Returns +/// A vector of blocks, each containing mini-blocks of operations and lookup keys +fn generate_blocks(seed: u64, data: &[u8]) -> Vec { + let total_size = data.len(); + let blocks_per_run = total_size.div_ceil(env("RANDOM_BLOCKS", 3)); + let mini_blocks_per_block = blocks_per_run.div_ceil(env("RANDOM_MINI_BLOCKS", 10)); + let lookups_per_block = env("RANDOM_LOOKUPS", 50); + let mut rng = StdRng::seed_from_u64(seed); + + let blocks: Vec = data + .chunks(blocks_per_run) + .map(|chunk| Block { + mini_blocks: chunk + .chunks(mini_blocks_per_block) + .map(|mini_chucks| { + mini_chucks + .iter() + .map(|op| { + if *op < 180 { + Operation::Insert(rng.random(), rng.random()) + } else { + Operation::Delete(rng.random()) + } + }) + .collect() + }) + .collect(), + lookups: (0..lookups_per_block).map(|_| rng.random()).collect(), + }) + .collect(); + blocks +} + +/// Converts test operations into plain key-value updates. +/// +/// Maps operation indices to actual keys via the KV pool, producing +/// the key-value pairs that will be applied to the state. +fn get_plain_kv_updates( + operations: &[Operation], + kv_pool: &[(Vec, Vec)], +) -> Vec<(Vec, Option>)> { + operations + .iter() + .map(|op| match op { + Operation::Insert(idx, new_value) => { + let (key, _) = &kv_pool[*idx as usize % kv_pool.len()]; + (key.clone(), Some(vec![*new_value])) + } + Operation::Delete(idx) => { + let (key, _) = &kv_pool[*idx as usize % kv_pool.len()]; + (key.clone(), None) + } + }) + .collect() +} + /// End-to-end test validating SALT blockchain state management correctness. /// /// Simulates the complete block processing lifecycle from both block producer @@ -74,7 +165,6 @@ pub struct Block { /// /// # Panics /// Panics if any consistency check fails, indicating a bug in SALT's implementation. -#[cfg(test)] fn e2e_test(blocks: &Vec) { // Generate the plain key-value pairs to be used in testing beforehand let kv_pool_size = env("RANDOM_KV_POOL_SIZE", 4096); @@ -87,15 +177,26 @@ fn e2e_test(blocks: &Vec) { .collect(); // Create the mock database and BTreeMap-based reference state implementation. - let db = MemStore::new(); - let mut ref_state = BTreeMap::new(); - let mut pre_state_root = StateRoot::rebuild(&db) + let db = STORE.get_or_init(MemStore::default); + let mut ref_state: BTreeMap, Vec> = db + .entries(SaltKey::bucket_range( + NUM_META_BUCKETS as BucketId, + (NUM_BUCKETS - 1) as BucketId, + )) + .expect("Failed to enumerate entries") + .iter() + .map(|(_, v)| (v.key().to_vec(), v.value().to_vec())) + .collect(); + let mut pre_state_root = StateRoot::rebuild(db) .expect("Failed to get initial state root") .0; + let mut revert_state_updates = StateUpdates::default(); + let expected_revert_state_root = pre_state_root; + for block in blocks { - let mut state = EphemeralSaltState::new(&db); - let mut trie = StateRoot::new(&db); + let mut state = EphemeralSaltState::new(db); + let mut trie = StateRoot::new(db); let mut state_updates = StateUpdates::default(); // Block producer: Process read-only lookups that occur during transaction execution. @@ -153,9 +254,14 @@ fn e2e_test(blocks: &Vec) { state_updates.merge(canon_updates); // Block producer: Generate witness containing all data needed for stateless validation - let witness = Witness::create([], &lookups, &all_modifications, &db) + let witness = Witness::create([], &lookups, &all_modifications, db) .expect("Failed to create witness"); + // Save revert information for post-test verification: stores the + // inverse updates to enable rolling back all blocks to initial state later. + // This validates that state updates are correctly invertible. + revert_state_updates.merge(state_updates.clone()); + // Block producer: persist to database db.update_state(state_updates); db.update_trie(trie_updates); @@ -257,162 +363,20 @@ fn e2e_test(blocks: &Vec) { )) .expect("Failed to enumerate entries"); assert_eq!(entries.len(), ref_state.len(), "Entry count mismatch"); -} -/// Converts test operations into plain key-value updates. -/// -/// Maps operation indices to actual keys via the KV pool, producing -/// the key-value pairs that will be applied to the state. -fn get_plain_kv_updates( - operations: &[Operation], - kv_pool: &[(Vec, Vec)], -) -> Vec<(Vec, Option>)> { - operations - .iter() - .map(|op| match op { - Operation::Insert(idx, new_value) => { - let (key, _) = &kv_pool[*idx as usize % kv_pool.len()]; - (key.clone(), Some(vec![*new_value])) - } - Operation::Delete(idx) => { - let (key, _) = &kv_pool[*idx as usize % kv_pool.len()]; - (key.clone(), None) - } - }) - .collect() -} - -/// Reads an environment variable and parses it, falling back to default if missing or invalid. -#[cfg(test)] -fn env(key: &str, default: T) -> T { - std::env::var(key) - .ok() - .and_then(|v| v.parse().ok()) - .unwrap_or(default) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Stress test validating SALT correctness with randomly generated operation sequences. - /// - /// Performs property-based stress testing by running multiple iterations with different - /// random seeds, validating that state consistency and trie consistency properties hold - /// across diverse operation sequences. Each iteration: - /// - Generates random blocks with operations (70% Insert, 30% Delete) - /// - Validates via [`e2e_test`] (state oracle matching + trie root consistency) - /// - Uses iteration number as RNG seed for deterministic reproduction - /// - On failure, saves input to `random_stress_failure_{timestamp}.json` for replay via - /// [`replay_test_failure`] - /// - /// Configuration via environment variables (with defaults): - /// - `RANDOM_KV_POOL_SIZE=4096` - Size of the key-value pool - /// - `RANDOM_ITERATIONS=100` - Number of test iterations - /// - `RANDOM_BLOCKS=3` - Blocks per iteration - /// - `RANDOM_MINI_BLOCKS=10` - Mini-blocks per block - /// - `RANDOM_OPS=100` - Operations per mini-block - /// - `RANDOM_LOOKUPS=50` - Lookups per block - #[test] - #[ignore] - fn test_e2e_random_stress() { - use rand::rngs::StdRng; - use rand::{Rng, SeedableRng}; - use std::panic; - - let ( - iterations, - blocks_per_iter, - mini_blocks_per_block, - ops_per_mini_block, - lookups_per_block, - ) = ( - env("RANDOM_ITERATIONS", 100), - env("RANDOM_BLOCKS", 3), - env("RANDOM_MINI_BLOCKS", 10), - env("RANDOM_OPS", 100), - env("RANDOM_LOOKUPS", 50), - ); - - println!("\nStarting deterministic random loop test:"); - println!( - " {} iterations x {} blocks x {} mini-blocks x {} ops", - iterations, blocks_per_iter, mini_blocks_per_block, ops_per_mini_block - ); - println!( - " Total operations: {}\n", - iterations * blocks_per_iter * mini_blocks_per_block * ops_per_mini_block - ); - - for iteration in 0..iterations { - println!("Iteration {}/{}...", iteration + 1, iterations); - - let mut rng = StdRng::seed_from_u64(iteration as u64); - let blocks: Vec = (0..blocks_per_iter) - .map(|_| Block { - mini_blocks: (0..mini_blocks_per_block) - .map(|_| { - (0..ops_per_mini_block) - .map(|_| { - if rng.random_bool(0.7) { - Operation::Insert(rng.random(), rng.random()) - } else { - Operation::Delete(rng.random()) - } - }) - .collect() - }) - .collect(), - lookups: (0..lookups_per_block).map(|_| rng.random()).collect(), - }) - .collect(); - - if let Err(err) = panic::catch_unwind(panic::AssertUnwindSafe(|| e2e_test(&blocks))) { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - let filename = format!("random_stress_failure_{}.json", timestamp); - if let Ok(json) = serde_json::to_string_pretty(&blocks) { - let _ = std::fs::write(&filename, json); - eprintln!( - "\nTest failed at iteration {} (seed: {})", - iteration, iteration - ); - eprintln!("Failing input saved to: {}", filename); - eprintln!( - "Replay with: TEST_FAILURE={} cargo test replay_test_failure -- --ignored", - filename - ); - } - panic::resume_unwind(err); - } - } - - println!("\nAll {} tests passed!", iterations); - } - - /// Debugging utility to replay a saved test failure from a JSON file. - /// - /// This is not a standalone test - it reproduces failures by re-running - /// the exact operation sequence that caused a previous test failure. - /// - /// Usage: - /// ```bash - /// TEST_FAILURE=random_stress_failure_1234567890.json cargo test replay_test_failure -- --nocapture --ignored - /// ``` - #[test] - #[ignore] - fn replay_test_failure() { - let filename = - std::env::var("TEST_FAILURE").expect("TEST_FAILURE environment variable must be set"); - let json = std::fs::read_to_string(&filename) - .unwrap_or_else(|e| panic!("Failed to read {}: {}", filename, e)); - let blocks: Vec = serde_json::from_str(&json) - .unwrap_or_else(|e| panic!("Failed to parse {}: {}", filename, e)); - - println!("Replaying from {} ({} blocks)", filename, blocks.len()); - e2e_test(&blocks); - println!("Replay passed!"); - } + // Post-test verification: Revert blocks back to initial state to validate state reconstruction. + // + // This section tests the system's ability to correctly undo state changes by applying + // inverse state updates in reverse order. This validates that: + // 1. State updates are correctly invertible (can be undone) + // 2. The trie correctly computes intermediate state roots during reversion + // + // When bucket expansion occurs, this also validates the correctness of bucket contraction + let (revert_state_root, _) = StateRoot::new(&db) + .update_fin(&revert_state_updates.inverse()) + .expect("Failed to compute state root during reversion"); + assert_eq!( + revert_state_root, expected_revert_state_root, + "State root mismatch during reversion" + ); } diff --git a/salt/src/lib.rs b/salt/src/lib.rs index aaf09c63..aaf4efa3 100644 --- a/salt/src/lib.rs +++ b/salt/src/lib.rs @@ -19,8 +19,6 @@ pub mod types; pub use types::*; pub mod mem_store; pub use mem_store::MemStore; -#[cfg(test)] -pub mod fuzz; #[cfg(test)] mod tests {