Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fuzzer for Noir programs #5251

Merged
merged 17 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
13 changes: 13 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
# Crates related to tooling built on top of the Noir compiler
"tooling/lsp",
"tooling/debugger",
"tooling/fuzzer",
"tooling/nargo",
"tooling/nargo_fmt",
"tooling/nargo_cli",
Expand Down Expand Up @@ -68,6 +69,7 @@ noirc_frontend = { path = "compiler/noirc_frontend" }
noirc_printable_type = { path = "compiler/noirc_printable_type" }

# Noir tooling workspace dependencies
noir_fuzzer = { path = "tooling/fuzzer" }
nargo = { path = "tooling/nargo" }
nargo_fmt = { path = "tooling/nargo_fmt" }
nargo_toml = { path = "tooling/nargo_toml" }
Expand Down
9 changes: 0 additions & 9 deletions compiler/noirc_frontend/src/elaborator/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,6 @@ pub(super) fn recursive_non_entrypoint_function(
}
}

/// Test functions cannot have arguments in order to be executable.
pub(super) fn test_function_with_args(func: &NoirFunction) -> Option<ResolverError> {
if func.attributes().is_test_function() && !func.parameters().is_empty() {
Some(ResolverError::TestFunctionHasParameters { span: func.name_ident().span() })
} else {
None
}
}

/// Check that we are not passing a mutable reference from a constrained runtime to an unconstrained runtime.
pub(super) fn unconstrained_function_args(
function_args: &[(Type, ExprId, Span)],
Expand Down
1 change: 0 additions & 1 deletion compiler/noirc_frontend/src/elaborator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,6 @@ impl<'context> Elaborator<'context> {
self.run_lint(|elaborator| {
lints::low_level_function_outside_stdlib(func, elaborator.crate_id).map(Into::into)
});
self.run_lint(|_| lints::test_function_with_args(func).map(Into::into));
self.run_lint(|_| {
lints::recursive_non_entrypoint_function(func, is_entry_point).map(Into::into)
});
Expand Down
10 changes: 1 addition & 9 deletions compiler/noirc_frontend/src/hir/resolution/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::hir_def::expr::{
use crate::hir_def::function::FunctionBody;
use crate::hir_def::traits::{Trait, TraitConstraint};
use crate::macros_api::SecondaryAttribute;
use crate::token::{Attributes, FunctionAttribute};
use crate::token::Attributes;
use regex::Regex;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::rc::Rc;
Expand Down Expand Up @@ -1042,14 +1042,6 @@ impl<'a> Resolver<'a> {
});
}

if matches!(attributes.function, Some(FunctionAttribute::Test { .. }))
&& !parameters.is_empty()
{
self.push_err(ResolverError::TestFunctionHasParameters {
span: func.name_ident().span(),
});
}

let mut typ = Type::Function(parameter_types, return_type, Box::new(Type::Unit));

if !generics.is_empty() {
Expand Down
18 changes: 13 additions & 5 deletions noir_stdlib/src/uint128.nr
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,12 @@ mod tests {
use crate::uint128::{U128, pow64, pow63};

#[test]
fn test_not() {
let num = U128::from_u64s_le(0, 0);
fn test_not(lo: u64, hi: u64) {
let num = U128::from_u64s_le(lo, hi);
let not_num = num.not();

let max_u64: Field = pow64 - 1;
assert_eq(not_num.hi, max_u64);
assert_eq(not_num.lo, max_u64);
assert_eq(not_num.hi, (hi.not() as Field));
assert_eq(not_num.lo, (lo.not() as Field));

let not_not_num = not_num.not();
assert_eq(num, not_not_num);
Expand Down Expand Up @@ -493,6 +492,15 @@ mod tests {
let end = a.to_integer();
assert_eq(start, end);
}

#[test]
fn integer_conversions_fuzz(lo: u64, hi: u64) {
let start: Field = (lo as Field) + pow64 * (hi as Field);
let a = U128::from_integer(start);
let end = a.to_integer();
assert_eq(start, end);
}

#[test]
fn test_wrapping_mul() {
// 1*0==0
Expand Down
17 changes: 17 additions & 0 deletions tooling/fuzzer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[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_abi.workspace = true
proptest.workspace = true
rand.workspace = true
93 changes: 93 additions & 0 deletions tooling/fuzzer/src/lib.rs
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 nargo::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
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved

match result {
Ok(_) => Ok(FuzzOutcome::Case(CaseOutcome { case: input_map })),
Err(err) => Ok(FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason: err.to_string(),
counterexample: input_map,
})),
}
}
}
83 changes: 83 additions & 0 deletions tooling/fuzzer/src/strategies/int.rs
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),
}
}
}
Loading
Loading