Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
35f46b0
feat(types): SharedFuzzState and FuzzWorker
yash-atreya Sep 26, 2025
431cc06
basic run_worker in FuzzExecutor
yash-atreya Sep 26, 2025
e4c6060
rename found_counterexample to found_failure in SharedFuzzState and s…
yash-atreya Sep 26, 2025
40263fd
feat: track total_rejects in SharedFuzzState
yash-atreya Sep 26, 2025
d1990ec
shared_state.increment_runs + only worker0 replays persisted failure …
yash-atreya Sep 29, 2025
ed234d8
feat: basic staggered corpus sync in run_worker
yash-atreya Sep 29, 2025
67c96a7
feat: parallelize fuzz runs + aggregate results
yash-atreya Sep 29, 2025
0cdccd9
fix: derive seeds per worker
yash-atreya Sep 30, 2025
f7310d0
fix: only increment runs for success cases + try_increment_runs atomi…
yash-atreya Sep 30, 2025
e5b00bc
fix: run only 1 worker if replaying persisted failure
yash-atreya Sep 30, 2025
1d6363b
fix: timed campaigns - introduce per worker timer
yash-atreya Sep 30, 2025
dde52cc
fix: flaky try_increment_runs
yash-atreya Oct 1, 2025
63ca2bf
fix: expect_emit_test_should_fail - identify events only using cache …
yash-atreya Oct 1, 2025
1aa39ed
fix: expect_emit_tests_should_fail
yash-atreya Oct 1, 2025
349b5ee
fix: timer should be global to fit as many runs as possible
yash-atreya Oct 1, 2025
33a294c
fix: worker_runs = config.runs/ num_workers
yash-atreya Oct 1, 2025
e8fde6f
fix: should_not_shrink_fuzz_failure
yash-atreya Oct 1, 2025
e991826
feat: propagate --jobs / threads to FuzzConfig to determine number of…
yash-atreya Oct 1, 2025
47bd12d
optimize aggregate results
yash-atreya Oct 1, 2025
e77fe5b
nit
yash-atreya Oct 1, 2025
0cca8e1
fix
yash-atreya Oct 1, 2025
aed7ee0
skip fuzzConfig.threads serialization + use placeholder for should_no…
yash-atreya Oct 2, 2025
7df4e05
fix tests
yash-atreya Oct 2, 2025
d973b79
feat: sync corpus metrics
yash-atreya Oct 2, 2025
af55f71
nit
yash-atreya Oct 2, 2025
6e190be
feat: surface failed_corpus_replays from master worker
yash-atreya Oct 2, 2025
8543ab4
feat: log worker fuzz stats
yash-atreya Oct 2, 2025
e5bd557
Merge branch 'yash/shared-corpus' into yash/parallel-fuzz
yash-atreya Oct 2, 2025
23910e8
fix corpus tests
yash-atreya Oct 2, 2025
21257dd
fix: worker corpus dir
yash-atreya Oct 3, 2025
1e529a0
Merge branch 'yash/shared-corpus' into yash/parallel-fuzz
yash-atreya Oct 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ revm-inspectors.workspace = true
semver.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml = { workspace = true, features = ["preserve_order"] }
tracing.workspace = true
walkdir.workspace = true
Expand Down
8 changes: 6 additions & 2 deletions crates/cheatcodes/src/test/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,8 +1094,12 @@ fn decode_event(
return None;
}
let t0 = topics[0]; // event sig
// Try to identify the event
let event = foundry_common::block_on(identifier.identify_event(t0))?;
// Try to identify the event - detect if we're in a Tokio runtime and use appropriate method
let event = if tokio::runtime::Handle::try_current().is_ok() {
foundry_common::block_on(identifier.identify_event(t0))?
} else {
identifier.identify_event_sync(t0)?
};

// Check if event already has indexed information from signatures
let has_indexed_info = event.inputs.iter().any(|p| p.indexed);
Expand Down
6 changes: 6 additions & 0 deletions crates/config/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub struct FuzzConfig {
pub show_logs: bool,
/// Optional timeout (in seconds) for each property test
pub timeout: Option<u32>,
/// Number of threads to use for parallel fuzz runs
///
/// This is set by passing `-j` or `--jobs`
#[serde(skip)]
pub threads: Option<usize>,
}

impl Default for FuzzConfig {
Expand All @@ -49,6 +54,7 @@ impl Default for FuzzConfig {
failure_persist_dir: None,
show_logs: false,
timeout: None,
threads: None,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/evm/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ indicatif.workspace = true
serde_json.workspace = true
serde.workspace = true
uuid.workspace = true
rayon.workspace = true
107 changes: 100 additions & 7 deletions crates/evm/evm/src/executors/corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use serde::Serialize;
use std::{
fmt,
path::PathBuf,
sync::Arc,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use uuid::Uuid;
Expand Down Expand Up @@ -101,6 +104,37 @@ impl CorpusEntry {
}

#[derive(Serialize, Default)]
pub(crate) struct GlobalCorpusMetrics {
// Number of edges seen during the invariant run
cumulative_edges_seen: Arc<AtomicUsize>,
// Number of features (new hitcount bin of previously hit edge) seen during the invariant run
cumulative_features_seen: Arc<AtomicUsize>,
// Number of corpus entries
corpus_count: Arc<AtomicUsize>,
// Number of corpus entries that are favored
favored_items: Arc<AtomicUsize>,
}

impl fmt::Display for GlobalCorpusMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
writeln!(
f,
" - cumulative edges seen: {}",
self.cumulative_edges_seen.load(Ordering::Relaxed)
)?;
writeln!(
f,
" - cumulative features seen: {}",
self.cumulative_features_seen.load(Ordering::Relaxed)
)?;
writeln!(f, " - corpus count: {}", self.corpus_count.load(Ordering::Relaxed))?;
write!(f, " - favored items: {}", self.favored_items.load(Ordering::Relaxed))?;
Ok(())
}
}

#[derive(Serialize, Default, Clone)]
pub(crate) struct CorpusMetrics {
// Number of edges seen during the invariant run.
cumulative_edges_seen: usize,
Expand Down Expand Up @@ -171,6 +205,8 @@ pub struct WorkerCorpus {
/// Worker Dir
/// corpus_dir/worker1/
worker_dir: Option<PathBuf>,
/// Metrics at last sync - used to calculate deltas while syncing with global metrics
last_sync_metrics: CorpusMetrics,
}

impl WorkerCorpus {
Expand Down Expand Up @@ -317,6 +353,7 @@ impl WorkerCorpus {
new_entry_indices: Default::default(),
last_sync_timestamp: 0,
worker_dir,
last_sync_metrics: Default::default(),
})
}

Expand Down Expand Up @@ -555,8 +592,7 @@ impl WorkerCorpus {
let corpus = &self.in_memory_corpus
[test_runner.rng().random_range(0..self.in_memory_corpus.len())];
self.current_mutated = Some(corpus.uuid);
let new_seq = corpus.tx_seq.clone();
let mut tx = new_seq.first().unwrap().clone();
let mut tx = corpus.tx_seq.first().unwrap().clone();
self.abi_mutate(&mut tx, function, test_runner, fuzz_state)?;
tx
} else {
Expand Down Expand Up @@ -769,7 +805,6 @@ impl WorkerCorpus {
};

if timestamp <= self.last_sync_timestamp {
// TODO: Delete synced file
continue;
}

Expand Down Expand Up @@ -890,7 +925,7 @@ impl WorkerCorpus {

/// To be run by the master worker (id = 0) to distribute the global corpus to sync/ directories
/// of other workers.
fn distribute(&mut self, num_workers: usize) -> eyre::Result<()> {
fn distribute(&mut self, num_workers: u32) -> eyre::Result<()> {
if self.id != 0 || self.worker_dir.is_none() {
return Ok(());
}
Expand Down Expand Up @@ -942,14 +977,70 @@ impl WorkerCorpus {
Ok(())
}

/// Syncs local metrics with global corpus metrics by calculating and applying deltas
pub(crate) fn sync_metrics(&mut self, global_corpus_metrics: &GlobalCorpusMetrics) {
// Calculate delta metrics since last sync
let edges_delta = self
.metrics
.cumulative_edges_seen
.saturating_sub(self.last_sync_metrics.cumulative_edges_seen);
let features_delta = self
.metrics
.cumulative_features_seen
.saturating_sub(self.last_sync_metrics.cumulative_features_seen);
// For corpus count and favored items, calculate deltas
let corpus_count_delta =
self.metrics.corpus_count as isize - self.last_sync_metrics.corpus_count as isize;
let favored_delta =
self.metrics.favored_items as isize - self.last_sync_metrics.favored_items as isize;

// Add delta values to global metrics

if edges_delta > 0 {
global_corpus_metrics.cumulative_edges_seen.fetch_add(edges_delta, Ordering::Relaxed);
}
if features_delta > 0 {
global_corpus_metrics
.cumulative_features_seen
.fetch_add(features_delta, Ordering::Relaxed);
}

if corpus_count_delta > 0 {
global_corpus_metrics
.corpus_count
.fetch_add(corpus_count_delta as usize, Ordering::Relaxed);
} else if corpus_count_delta < 0 {
global_corpus_metrics
.corpus_count
.fetch_sub((-corpus_count_delta) as usize, Ordering::Relaxed);
}

if favored_delta > 0 {
global_corpus_metrics
.favored_items
.fetch_add(favored_delta as usize, Ordering::Relaxed);
} else if favored_delta < 0 {
global_corpus_metrics
.favored_items
.fetch_sub((-favored_delta) as usize, Ordering::Relaxed);
}

// Store current metrics as last sync metrics for next delta calculation
self.last_sync_metrics = self.metrics.clone();
}

/// Syncs the workers in_memory_corpus and history_map with the findings from other workers.
pub fn sync(
pub(crate) fn sync(
&mut self,
num_workers: usize,
num_workers: u32,
executor: &Executor,
fuzzed_function: Option<&Function>,
fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
global_corpus_metrics: &GlobalCorpusMetrics,
) -> eyre::Result<()> {
// Sync metrics with global corpus metrics
self.sync_metrics(global_corpus_metrics);

if self.id == 0 {
// Master worker
self.calibrate(executor, fuzzed_function, fuzzed_contracts)?;
Expand Down Expand Up @@ -1040,6 +1131,7 @@ mod tests {
new_entry_indices: Default::default(),
last_sync_timestamp: 0,
worker_dir: Some(corpus_root),
last_sync_metrics: CorpusMetrics::default(),
};

(manager, seed_uuid)
Expand Down Expand Up @@ -1146,6 +1238,7 @@ mod tests {
new_entry_indices: Default::default(),
last_sync_timestamp: 0,
worker_dir: Some(corpus_root),
last_sync_metrics: CorpusMetrics::default(),
};

// First eviction should remove the non-favored one
Expand Down
Loading
Loading