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(test): Enable the test fuzzer for Wasm #6835

Merged
merged 17 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 8 additions & 26 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 @@ jsonrpsee = { version = "0.24.7", features = ["client-core"] }
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
12 changes: 8 additions & 4 deletions tooling/fuzzer/src/strategies/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use proptest::{
};
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 +29,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,16 +43,16 @@ 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
Expand All @@ -58,6 +61,7 @@ impl IntStrategy {
}
}

/// Minimum allowed negative number.
fn type_min(&self) -> i128 {
if self.bits < 128 {
-(1i128 << (self.bits - 1))
Expand All @@ -68,7 +72,7 @@ impl IntStrategy {
}

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
30 changes: 17 additions & 13 deletions tooling/fuzzer/src/strategies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,28 @@ 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)
.prop_map(|bytes| InputValue::Field(FieldElement::from_be_bytes_reduce(&bytes)))
.sboxed(),
AbiType::Integer { width, sign } if sign == &Sign::Unsigned => {
UintStrategy::new(*width as usize, dictionary)
// We've restricted the type system to only allow u64s as the maximum integer type.
let width = (*width).min(64);
UintStrategy::new(width as usize, dictionary)
.prop_map(|uint| InputValue::Field(uint.into()))
.sboxed()
}
AbiType::Integer { width, .. } => {
let shift = 2i128.pow(*width);
IntStrategy::new(*width as usize)
let width = (*width).min(64);
let shift = 2i128.pow(width);
IntStrategy::new(width as usize)
.prop_map(move |mut int| {
if int < 0 {
int += shift
Expand All @@ -38,7 +44,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 +58,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 +73,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
28 changes: 17 additions & 11 deletions tooling/fuzzer/src/strategies/uint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ use proptest::{
};
use rand::Rng;

type BinarySearch = proptest::num::u128::BinarySearch;

/// Value tree for unsigned ints (up to u128).
/// 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 0 and max possible value)
#[derive(Debug)]
pub struct UintStrategy {
/// Bit size of uint (e.g. 128)
/// Bit size of uint (e.g. 64)
bits: usize,
/// A set of fixtures to be generated
fixtures: Vec<FieldElement>,
Expand All @@ -31,25 +33,29 @@ impl UintStrategy {
/// # Arguments
/// * `bits` - Size of uint in bits
/// * `fixtures` - Set of `FieldElements` representing values which the fuzzer weight towards testing.
pub fn new(bits: usize, fixtures: HashSet<FieldElement>) -> Self {
pub fn new(bits: usize, fixtures: &HashSet<FieldElement>) -> Self {
Self {
bits,
fixtures: fixtures.into_iter().collect(),
// We can only consider the fixtures which fit into the bit width.
fixtures: fixtures.iter().filter(|f| f.num_bits() <= bits as u32).copied().collect(),
edge_weight: 10usize,
fixtures_weight: 40usize,
random_weight: 50usize,
}
}

/// Generate random numbers starting from near 0 or the maximum of the range.
fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let rng = runner.rng();
// Choose if we want values around 0 or max
let is_min = rng.gen_bool(0.5);
let offset = rng.gen_range(0..4);
let start = if is_min { offset } else { self.type_max().saturating_sub(offset) };
Ok(proptest::num::u128::BinarySearch::new(start))
Ok(BinarySearch::new(start))
}

/// Pick a random `FieldElement` from the `fixtures` as a starting point for
/// generating random numbers.
fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
// generate random cases if there's no fixtures
if self.fixtures.is_empty() {
Expand All @@ -58,21 +64,19 @@ impl UintStrategy {

// Generate value tree from fixture.
let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())];
if fixture.num_bits() <= self.bits as u32 {
return Ok(proptest::num::u128::BinarySearch::new(fixture.to_u128()));
}

// If fixture is not a valid type, generate random value.
self.generate_random_tree(runner)
Ok(BinarySearch::new(fixture.to_u128()))
}

/// Generate random values between 0 and the MAX with the given bit width.
fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let rng = runner.rng();
let start = rng.gen_range(0..=self.type_max());

Ok(proptest::num::u128::BinarySearch::new(start))
Ok(BinarySearch::new(start))
}

/// Maximum integer that fits in the given bit width.
fn type_max(&self) -> u128 {
if self.bits < 128 {
(1 << self.bits) - 1
Expand All @@ -83,8 +87,10 @@ impl UintStrategy {
}

impl Strategy for UintStrategy {
type Tree = proptest::num::u128::BinarySearch;
type Tree = BinarySearch;
type Value = u128;

/// Pick randomly from the 3 available strategies for generating unsigned integers.
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight;
let bias = runner.rng().gen_range(0..total_weight);
Expand Down
Loading
Loading