use crate::crash::CrashRecord;
use crate::executor::LiteSvmExecutor;
use crate::invariant::{InvariantRegistry, InvariantResult};
use crate::mutator::{mutate_accounts, MutationConfig};
use crate::{CoverageBitmap, FuzzAccount, FuzzAction, FuzzActionSequence};
use rand::Rng;
pub struct Fuzzer {
pub rng: Box<dyn rand::RngCore>,
pub accounts: Vec<FuzzAccount>,
pub mutation_config: MutationConfig,
pub global_coverage: CoverageBitmap,
pub invariants: InvariantRegistry,
pub round_count: u64,
pub crash_count: u64,
pub crashes: Vec<CrashRecord>,
pub seeds: Vec<FuzzActionSequence>,
pub max_sequence_length: usize,
pub available_instructions: Vec<(String, [u8; 8])>, pub program_id: String,
pub executor: Option<LiteSvmExecutor>,
pub program_bytes: Option<Vec<u8>>,
}
impl Fuzzer {
pub fn new(program_id: String) -> Self {
Self {
rng: Box::new(rand::thread_rng()),
accounts: Vec::new(),
mutation_config: MutationConfig::default(),
global_coverage: CoverageBitmap::new(),
invariants: InvariantRegistry::new(),
round_count: 0,
crash_count: 0,
crashes: Vec::new(),
seeds: Vec::new(),
max_sequence_length: 10,
available_instructions: Vec::new(),
program_id,
executor: None,
program_bytes: None,
}
}
pub fn deploy_program(&mut self, program_bytes: Vec<u8>) -> anyhow::Result<()> {
self.program_bytes = Some(program_bytes.clone());
let executor = LiteSvmExecutor::new(&self.program_id, &program_bytes)?;
self.executor = Some(executor);
Ok(())
}
pub fn deploy_program_from_file(&mut self, so_path: &str) -> anyhow::Result<()> {
let bytes = crate::executor::load_program_binary(so_path)?;
self.deploy_program(bytes)
}
pub fn reset_executor(&mut self) -> anyhow::Result<()> {
if let Some(bytes) = &self.program_bytes {
let executor = LiteSvmExecutor::new(&self.program_id, bytes)?;
self.executor = Some(executor);
}
Ok(())
}
pub fn register_instruction(&mut self, name: &str, discriminator: [u8; 8]) {
self.available_instructions
.push((name.to_string(), discriminator));
}
pub fn random_action(&mut self) -> FuzzAction {
if self.available_instructions.is_empty() {
return FuzzAction {
ix_discriminator: [0u8; 8],
ix_name: "noop".into(),
program_id: self.program_id.clone(),
accounts: vec![],
ix_data: vec![],
};
}
let idx = self.rng.gen_range(0..self.available_instructions.len());
let (name, disc) = self.available_instructions[idx].clone();
let account_count = self.rng.gen_range(1..=usize::min(self.accounts.len(), 8));
let mut used_indices = std::collections::HashSet::new();
let mut selected_accounts = Vec::new();
while selected_accounts.len() < account_count {
let i = self.rng.gen_range(0..self.accounts.len());
if used_indices.insert(i) {
selected_accounts.push(self.accounts[i].clone());
}
}
let data_len = self.rng.gen_range(8..=256);
let mut ix_data = vec![0u8; data_len];
ix_data[..8].copy_from_slice(&disc);
self.rng.fill_bytes(&mut ix_data[8..]);
FuzzAction {
ix_discriminator: disc,
ix_name: name,
program_id: self.program_id.clone(),
accounts: selected_accounts,
ix_data,
}
}
pub fn run_one_round(&mut self) -> FuzzRoundResult {
self.round_count += 1;
let seq_len = self.rng.gen_range(1..=self.max_sequence_length);
let actions: Vec<FuzzAction> = (0..seq_len).map(|_| self.random_action()).collect();
let sequence = FuzzActionSequence {
actions: actions.clone(),
initial_accounts: self.accounts.clone(),
};
let mut working_accounts = self.accounts.clone();
mutate_accounts(&mut working_accounts, &self.mutation_config, &mut self.rng);
let execution_result = match &mut self.executor {
Some(executor) => {
if let Err(e) = executor.set_accounts(&working_accounts) {
Err(anyhow::anyhow!("Failed to set accounts: {}", e))
} else {
executor.execute_sequence(&actions, &mut working_accounts)
}
}
None => {
Err(anyhow::anyhow!(
"No executor configured — call deploy_program() first"
))
}
};
let mut invariant_results = Vec::new();
let is_crash = execution_result.is_err();
if !is_crash {
invariant_results =
self.invariants
.check_all(&working_accounts, &self.accounts, self.round_count);
} else {
let _ = self.reset_executor();
}
let new_coverage: Option<CoverageBitmap> = None;
FuzzRoundResult {
round: self.round_count,
sequence,
accounts_after: working_accounts,
execution_success: execution_result.is_ok(),
execution_error: execution_result.err().map(|e| e.to_string()),
invariant_results,
is_crash,
new_coverage,
}
}
}
pub struct FuzzRoundResult {
pub round: u64,
pub sequence: FuzzActionSequence,
pub accounts_after: Vec<FuzzAccount>,
pub execution_success: bool,
pub execution_error: Option<String>,
pub invariant_results: Vec<InvariantResult>,
pub is_crash: bool,
pub new_coverage: Option<CoverageBitmap>,
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::SmallRng;
use rand::SeedableRng;
#[test]
fn test_random_action_picks_instruction() {
let mut fuzzer = Fuzzer::new("TestProg11111111111111111111111111111".into());
fuzzer.rng = Box::new(SmallRng::seed_from_u64(42));
fuzzer.accounts = vec![FuzzAccount::default(); 5];
fuzzer.register_instruction("deposit", [1, 0, 0, 0, 0, 0, 0, 0]);
fuzzer.register_instruction("withdraw", [2, 0, 0, 0, 0, 0, 0, 0]);
let action = fuzzer.random_action();
assert!(!action.ix_name.is_empty());
assert!(!action.accounts.is_empty());
}
#[test]
fn test_run_one_round_increments_counter() {
let mut fuzzer = Fuzzer::new("TestProg11111111111111111111111111111".into());
fuzzer.rng = Box::new(SmallRng::seed_from_u64(42));
fuzzer.accounts = vec![FuzzAccount::default(); 3];
fuzzer.register_instruction("transfer", [3, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(fuzzer.round_count, 0);
let _result = fuzzer.run_one_round();
assert_eq!(fuzzer.round_count, 1);
}
}