-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add fuzzer for Noir programs (#5251)
# Description ## Problem\* Step towards #5249 ## Summary\* This PR adds a very simple fuzzer which can be used to find inputs to Noir programs which fail to execute. The motivation for this is to eventually allow `nargo test` to run on noir functions with arguments and automatically fuzz them in a similar fashion to `forge test`. It's currently very hit and miss on how quickly it can zero in on failing cases. For example, the program below is near-unfuzzable currently. ```rust fn main(x: u32, y: u32) { assert(x != y); } ``` ## Additional Context ## Documentation\* Check one: - [ ] No documentation needed. - [ ] Documentation included in this PR. - [x] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings.
- Loading branch information
1 parent
6cbe6a0
commit e100017
Showing
16 changed files
with
524 additions
and
45 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "noir_fuzzer" | ||
description = "A fuzzer for Noir programs" | ||
version.workspace = true | ||
authors.workspace = true | ||
edition.workspace = true | ||
rust-version.workspace = true | ||
license.workspace = true | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
acvm.workspace = true | ||
nargo.workspace = true | ||
noirc_artifacts.workspace = true | ||
noirc_abi.workspace = true | ||
proptest.workspace = true | ||
rand.workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
//! This module has been adapted from Foundry's fuzzing implementation for the EVM. | ||
//! https://github.com/foundry-rs/foundry/blob/6a85dbaa62f1c305f31cab37781232913055ae28/crates/evm/evm/src/executors/fuzz/mod.rs#L40 | ||
//! | ||
//! Code is used under the MIT license. | ||
use acvm::{blackbox_solver::StubbedBlackBoxSolver, FieldElement}; | ||
use noirc_abi::InputMap; | ||
use proptest::test_runner::{TestCaseError, TestError, TestRunner}; | ||
|
||
mod strategies; | ||
mod types; | ||
|
||
use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzTestResult}; | ||
|
||
use noirc_artifacts::program::ProgramArtifact; | ||
|
||
use nargo::ops::{execute_program, DefaultForeignCallExecutor}; | ||
|
||
/// An executor for Noir programs which which provides fuzzing support using [`proptest`]. | ||
/// | ||
/// After instantiation, calling `fuzz` will proceed to hammer the program with | ||
/// inputs, until it finds a counterexample. The provided [`TestRunner`] contains all the | ||
/// configuration which can be overridden via [environment variables](proptest::test_runner::Config) | ||
pub struct FuzzedExecutor { | ||
/// The program to be fuzzed | ||
program: ProgramArtifact, | ||
|
||
/// The fuzzer | ||
runner: TestRunner, | ||
} | ||
|
||
impl FuzzedExecutor { | ||
/// Instantiates a fuzzed executor given a testrunner | ||
pub fn new(program: ProgramArtifact, runner: TestRunner) -> Self { | ||
Self { program, runner } | ||
} | ||
|
||
/// Fuzzes the provided program. | ||
pub fn fuzz(&self) -> FuzzTestResult { | ||
let strategy = strategies::arb_input_map(&self.program.abi); | ||
|
||
let run_result: Result<(), TestError<InputMap>> = | ||
self.runner.clone().run(&strategy, |input_map| { | ||
let fuzz_res = self.single_fuzz(input_map)?; | ||
|
||
match fuzz_res { | ||
FuzzOutcome::Case(_) => Ok(()), | ||
FuzzOutcome::CounterExample(CounterExampleOutcome { | ||
exit_reason: status, | ||
.. | ||
}) => Err(TestCaseError::fail(status)), | ||
} | ||
}); | ||
|
||
match run_result { | ||
Ok(()) => FuzzTestResult { success: true, reason: None, counterexample: None }, | ||
|
||
Err(TestError::Abort(reason)) => FuzzTestResult { | ||
success: false, | ||
reason: Some(reason.to_string()), | ||
counterexample: None, | ||
}, | ||
Err(TestError::Fail(reason, counterexample)) => { | ||
let reason = reason.to_string(); | ||
let reason = if reason.is_empty() { None } else { Some(reason) }; | ||
|
||
FuzzTestResult { success: false, reason, counterexample: Some(counterexample) } | ||
} | ||
} | ||
} | ||
|
||
/// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome` | ||
/// or a `CounterExampleOutcome` | ||
pub fn single_fuzz(&self, input_map: InputMap) -> Result<FuzzOutcome, TestCaseError> { | ||
let initial_witness = self.program.abi.encode(&input_map, None).unwrap(); | ||
let result = execute_program( | ||
&self.program.bytecode, | ||
initial_witness, | ||
&StubbedBlackBoxSolver, | ||
&mut DefaultForeignCallExecutor::<FieldElement>::new(false, None), | ||
); | ||
|
||
// TODO: Add handling for `vm.assume` equivalent | ||
|
||
match result { | ||
Ok(_) => Ok(FuzzOutcome::Case(CaseOutcome { case: input_map })), | ||
Err(err) => Ok(FuzzOutcome::CounterExample(CounterExampleOutcome { | ||
exit_reason: err.to_string(), | ||
counterexample: input_map, | ||
})), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
use proptest::{ | ||
strategy::{NewTree, Strategy}, | ||
test_runner::TestRunner, | ||
}; | ||
use rand::Rng; | ||
|
||
/// Strategy for signed ints (up to i128). | ||
/// The strategy combines 2 different strategies, each assigned a specific weight: | ||
/// 1. Generate purely random value in a range. This will first choose bit size uniformly (up `bits` | ||
/// param). Then generate a value for this bit size. | ||
/// 2. Generate a random value around the edges (+/- 3 around min, 0 and max possible value) | ||
#[derive(Debug)] | ||
pub struct IntStrategy { | ||
/// Bit size of int (e.g. 128) | ||
bits: usize, | ||
/// The weight for edge cases (+/- 3 around 0 and max possible value) | ||
edge_weight: usize, | ||
/// The weight for purely random values | ||
random_weight: usize, | ||
} | ||
|
||
impl IntStrategy { | ||
/// Create a new strategy. | ||
/// # Arguments | ||
/// * `bits` - Size of int in bits | ||
pub fn new(bits: usize) -> Self { | ||
Self { bits, edge_weight: 10usize, random_weight: 50usize } | ||
} | ||
|
||
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> { | ||
let rng = runner.rng(); | ||
|
||
let offset = rng.gen_range(0..4); | ||
// Choose if we want values around min, -0, +0, or max | ||
let kind = rng.gen_range(0..4); | ||
let start = match kind { | ||
0 => self.type_min() + offset, | ||
1 => -offset - 1i128, | ||
2 => offset, | ||
3 => self.type_max() - offset, | ||
_ => unreachable!(), | ||
}; | ||
Ok(proptest::num::i128::BinarySearch::new(start)) | ||
} | ||
|
||
fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> { | ||
let rng = runner.rng(); | ||
|
||
let start: i128 = rng.gen_range(self.type_min()..=self.type_max()); | ||
Ok(proptest::num::i128::BinarySearch::new(start)) | ||
} | ||
|
||
fn type_max(&self) -> i128 { | ||
if self.bits < 128 { | ||
(1i128 << (self.bits - 1)) - 1 | ||
} else { | ||
i128::MAX | ||
} | ||
} | ||
|
||
fn type_min(&self) -> i128 { | ||
if self.bits < 128 { | ||
-(1i128 << (self.bits - 1)) | ||
} else { | ||
i128::MIN | ||
} | ||
} | ||
} | ||
|
||
impl Strategy for IntStrategy { | ||
type Tree = proptest::num::i128::BinarySearch; | ||
type Value = i128; | ||
|
||
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> { | ||
let total_weight = self.random_weight + self.edge_weight; | ||
let bias = runner.rng().gen_range(0..total_weight); | ||
// randomly select one of 2 strategies | ||
match bias { | ||
x if x < self.edge_weight => self.generate_edge_tree(runner), | ||
_ => self.generate_random_tree(runner), | ||
} | ||
} | ||
} |
Oops, something went wrong.