Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
36 changes: 9 additions & 27 deletions Cargo.lock

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

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,12 @@ jsonrpc = { version = "0.16.0", features = ["minreq_http"] }
flate2 = "1.0.24"
color-eyre = "0.6.2"
rand = "0.8.5"
proptest = "1.2.0"
proptest-derive = "0.4.0"
# The `fork` and `timeout` feature doesn't compile with Wasm (wait-timeout doesn't find the `imp` module).
proptest = { version = "1.6.0", default-features = false, features = [
"std",
"bit-set",
] }
proptest-derive = "0.5.0"
rayon = "1.8.0"
sha2 = { version = "0.10.6", features = ["compress"] }
sha3 = "0.10.6"
Expand Down
3 changes: 3 additions & 0 deletions tooling/fuzzer/src/dictionary/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ pub(super) fn build_dictionary_from_program<F: AcirField>(program: &Program<F>)
constants
}

/// Collect `Field` values used in the opcodes of an ACIR circuit.
fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<F> {
let mut constants: HashSet<F> = HashSet::new();

/// Pull out all the fields from an expression.
fn insert_expr<F: AcirField>(dictionary: &mut HashSet<F>, expr: &Expression<F>) {
let quad_coefficients = expr.mul_terms.iter().map(|(k, _, _)| *k);
let linear_coefficients = expr.linear_combinations.iter().map(|(k, _)| *k);
Expand Down Expand Up @@ -104,6 +106,7 @@ fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<
constants
}

/// Collect `Field` values used in the opcodes of a Brillig function.
fn build_dictionary_from_unconstrained_function<F: AcirField>(
function: &BrilligBytecode<F>,
) -> HashSet<F> {
Expand Down
18 changes: 9 additions & 9 deletions tooling/fuzzer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzTestResult};

use noirc_artifacts::program::ProgramArtifact;

/// An executor for Noir programs which which provides fuzzing support using [`proptest`].
/// An executor for Noir programs 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
Expand All @@ -38,22 +38,22 @@ pub struct FuzzedExecutor<E> {
runner: TestRunner,
}

impl<
E: Fn(
&Program<FieldElement>,
WitnessMap<FieldElement>,
) -> Result<WitnessStack<FieldElement>, String>,
> FuzzedExecutor<E>
impl<E> FuzzedExecutor<E>
where
E: Fn(
&Program<FieldElement>,
WitnessMap<FieldElement>,
) -> Result<WitnessStack<FieldElement>, String>,
{
/// Instantiates a fuzzed executor given a testrunner
/// Instantiates a fuzzed executor given a [TestRunner].
pub fn new(program: ProgramArtifact, executor: E, runner: TestRunner) -> Self {
Self { program, executor, runner }
}

/// Fuzzes the provided program.
pub fn fuzz(&self) -> FuzzTestResult {
let dictionary = build_dictionary_from_program(&self.program.bytecode);
let strategy = strategies::arb_input_map(&self.program.abi, dictionary);
let strategy = strategies::arb_input_map(&self.program.abi, &dictionary);

let run_result: Result<(), TestError<InputMap>> =
self.runner.clone().run(&strategy, |input_map| {
Expand Down
31 changes: 23 additions & 8 deletions tooling/fuzzer/src/strategies/int.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use std::cmp;

use proptest::{
strategy::{NewTree, Strategy},
test_runner::TestRunner,
};
use rand::Rng;

type BinarySearch = proptest::num::i128::BinarySearch;

/// 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`
Expand All @@ -27,6 +31,7 @@ impl IntStrategy {
Self { bits, edge_weight: 10usize, random_weight: 50usize }
}

/// Generate random values near MIN or the MAX value.
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let rng = runner.rng();

Expand All @@ -40,35 +45,45 @@ impl IntStrategy {
3 => self.type_max() - offset,
_ => unreachable!(),
};
Ok(proptest::num::i128::BinarySearch::new(start))
Ok(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))
Ok(BinarySearch::new(start))
}

/// Maximum allowed positive number.
fn type_max(&self) -> i128 {
if self.bits < 128 {
(1i128 << (self.bits - 1)) - 1
let bits = self.type_max_bits();
if bits < 128 {
(1i128 << (bits - 1)) - 1
} else {
i128::MAX
}
}

/// Minimum allowed negative number.
fn type_min(&self) -> i128 {
if self.bits < 128 {
-(1i128 << (self.bits - 1))
let bits = self.type_max_bits();
if bits < 128 {
-(1i128 << (bits - 1))
} else {
i128::MIN
}
}

/// Maximum number of bits for which we generate random numbers.
///
/// We've restricted the type system to only allow u64s as the maximum integer type.
fn type_max_bits(&self) -> usize {
cmp::min(self.bits, 64)
}
}

impl Strategy for IntStrategy {
type Tree = proptest::num::i128::BinarySearch;
type Tree = BinarySearch;
type Value = i128;

fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
Expand Down
21 changes: 11 additions & 10 deletions tooling/fuzzer/src/strategies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ use uint::UintStrategy;
mod int;
mod uint;

/// Create a strategy for generating random values for an [AbiType].
///
/// Uses the `dictionary` for unsigned integer types.
pub(super) fn arb_value_from_abi_type(
abi_type: &AbiType,
dictionary: HashSet<FieldElement>,
dictionary: &HashSet<FieldElement>,
) -> SBoxedStrategy<InputValue> {
match abi_type {
AbiType::Field => vec(any::<u8>(), 32)
Expand All @@ -38,7 +41,6 @@ pub(super) fn arb_value_from_abi_type(
AbiType::Boolean => {
any::<bool>().prop_map(|val| InputValue::Field(FieldElement::from(val))).sboxed()
}

AbiType::String { length } => {
// Strings only allow ASCII characters as each character must be able to be represented by a single byte.
let string_regex = format!("[[:ascii:]]{{{length}}}");
Expand All @@ -53,12 +55,11 @@ pub(super) fn arb_value_from_abi_type(

elements.prop_map(InputValue::Vec).sboxed()
}

AbiType::Struct { fields, .. } => {
let fields: Vec<SBoxedStrategy<(String, InputValue)>> = fields
.iter()
.map(|(name, typ)| {
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary.clone())).sboxed()
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary)).sboxed()
})
.collect();

Expand All @@ -69,25 +70,25 @@ pub(super) fn arb_value_from_abi_type(
})
.sboxed()
}

AbiType::Tuple { fields } => {
let fields: Vec<_> =
fields.iter().map(|typ| arb_value_from_abi_type(typ, dictionary.clone())).collect();
fields.iter().map(|typ| arb_value_from_abi_type(typ, dictionary)).collect();
fields.prop_map(InputValue::Vec).sboxed()
}
}
}

/// Given the [Abi] description of a [ProgramArtifact], generate random [InputValue]s for each circuit parameter.
///
/// Use the `dictionary` to draw values from for numeric types.
pub(super) fn arb_input_map(
abi: &Abi,
dictionary: HashSet<FieldElement>,
dictionary: &HashSet<FieldElement>,
) -> BoxedStrategy<InputMap> {
let values: Vec<_> = abi
.parameters
.iter()
.map(|param| {
(Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary.clone()))
})
.map(|param| (Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary)))
.collect();

values
Expand Down
Loading