Skip to content

Commit

Permalink
chore: restructure code and improve integer strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAFrench committed Jun 14, 2024
1 parent 8d9a380 commit b37f324
Show file tree
Hide file tree
Showing 18 changed files with 621 additions and 77 deletions.
13 changes: 12 additions & 1 deletion 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
7 changes: 7 additions & 0 deletions temp/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "temp"
type = "bin"
authors = [""]
compiler_version = ">=0.30.0"

[dependencies]
4 changes: 4 additions & 0 deletions temp/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main(x: i64, y: pub i64) {
assert(x != y);
}

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
56 changes: 29 additions & 27 deletions tooling/nargo/src/ops/fuzz/mod.rs → tooling/fuzzer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
use std::cell::RefCell;

use acvm::{blackbox_solver::StubbedBlackBoxSolver, FieldElement};
use noirc_abi::{arbitrary::arb_input_map, InputMap};
use noirc_abi::InputMap;
use proptest::test_runner::{TestCaseError, TestError, TestRunner};

mod strategies;
mod types;

use types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome, FuzzTestResult};

use crate::artifacts::program::ProgramArtifact;
use nargo::artifacts::program::ProgramArtifact;

use super::{execute_program, DefaultForeignCallExecutor};
use nargo::ops::{execute_program, DefaultForeignCallExecutor};

/// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`].
///
Expand All @@ -40,31 +41,32 @@ impl FuzzedExecutor {
// Stores the result and calldata of the last failed call, if any.
let counterexample: RefCell<InputMap> = RefCell::default();

let strategy = arb_input_map(&self.program.abi);

let run_result: Result<_, TestError<_>> = self.runner.clone().run(&strategy, |input_map| {
println!("fuzzing with {input_map:?}");

let fuzz_res = self.single_fuzz(input_map)?;

match fuzz_res {
FuzzOutcome::Case(_) => Ok(()),
FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason: status,
counterexample: outcome,
..
}) => {
println!("found counterexample, {outcome:?}");
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
// since that input represents the last run case, which may not correspond with
// our failure - when a fuzz case fails, proptest will try
// to run at least one more case to find a minimal failure
// case.
*counterexample.borrow_mut() = outcome;
Err(TestCaseError::fail(status))
let strategy = strategies::arb_input_map(&self.program.abi);

let run_result: Result<(), TestError<_>> =
self.runner.clone().run(&strategy, |input_map| {
println!("fuzzing with {input_map:?}");

let fuzz_res = self.single_fuzz(input_map)?;

match fuzz_res {
FuzzOutcome::Case(_) => Ok(()),
FuzzOutcome::CounterExample(CounterExampleOutcome {
exit_reason: status,
counterexample: outcome,
..
}) => {
println!("found counterexample, {outcome:?}");
// We cannot use the calldata returned by the test runner in `TestError::Fail`,
// since that input represents the last run case, which may not correspond with
// our failure - when a fuzz case fails, proptest will try
// to run at least one more case to find a minimal failure
// case.
*counterexample.borrow_mut() = outcome;
Err(TestCaseError::fail(status))
}
}
}
});
});

let mut result =
FuzzTestResult { success: run_result.is_ok(), reason: None, counterexample: None };
Expand Down
185 changes: 185 additions & 0 deletions tooling/fuzzer/src/strategies/int.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use proptest::{
strategy::{NewTree, Strategy, ValueTree},
test_runner::TestRunner,
};
use rand::Rng;

/// Value tree for signed ints (up to int256).
pub struct IntValueTree {
/// Lower base (by absolute value)
lo: i128,
/// Current value
curr: i128,

Check warning on line 12 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (curr)
/// Higher base (by absolute value)
hi: i128,
/// If true cannot be simplified or complexified

Check warning on line 15 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (complexified)
fixed: bool,
}

impl IntValueTree {
/// Create a new tree
/// # Arguments
/// * `start` - Starting value for the tree
/// * `fixed` - If `true` the tree would only contain one element and won't be simplified.
fn new(start: i128, fixed: bool) -> Self {
Self { lo: 0, curr: start, hi: start, fixed }

Check warning on line 25 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (curr)
}

fn reposition(&mut self) -> bool {
let interval = self.hi - self.lo;
let new_mid = self.lo + interval / 2i128;

if new_mid == self.curr {

Check warning on line 32 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (curr)
false
} else {
self.curr = new_mid;

Check warning on line 35 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (curr)
true
}
}

fn magnitude_greater(lhs: i128, rhs: i128) -> bool {
if lhs == 0 {
return false;
}
(lhs > rhs) ^ (lhs.is_negative())
}
}

impl ValueTree for IntValueTree {
type Value = i128;

fn current(&self) -> Self::Value {
self.curr

Check warning on line 52 in tooling/fuzzer/src/strategies/int.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (curr)
}

fn simplify(&mut self) -> bool {
if self.fixed || !Self::magnitude_greater(self.hi, self.lo) {
return false;
}
self.hi = self.curr;
self.reposition()
}

fn complicate(&mut self) -> bool {
if self.fixed || !Self::magnitude_greater(self.hi, self.lo) {
return false;
}

self.lo = if self.curr != i128::MIN && self.curr != i128::MAX {
self.curr + if self.hi.is_negative() { -1i128 } else { 1i128 }
} else {
self.curr
};

self.reposition()
}
}

/// Value tree for signed ints (up to int256).
/// The strategy combines 3 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)
/// 3. Generate a value from a predefined fixtures set
///
/// To define int fixtures:
/// - return an array of possible values for a parameter named `amount` declare a function `function
/// fixture_amount() public returns (int32[] memory)`.
/// - use `amount` named parameter in fuzzed test in order to include fixtures in fuzzed values
/// `function testFuzz_int32(int32 amount)`.
///
/// If fixture is not a valid int type then error is raised and random value generated.
#[derive(Debug)]
pub struct IntStrategy {
/// Bit size of int (e.g. 256)
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 uint in bits
/// * `fixtures` - A set of fixed values to be generated (according to fixtures weight)
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(IntValueTree::new(start, false))
}

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(IntValueTree::new(start, false))
}

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 = IntValueTree;
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),
}
}
}

#[cfg(test)]
mod tests {
use crate::strategies::int::IntValueTree;
use proptest::strategy::ValueTree;

#[test]
fn test_int_tree_complicate_should_not_overflow() {
let mut int_tree = IntValueTree::new(i128::MAX, false);
assert_eq!(int_tree.hi, i128::MAX);
assert_eq!(int_tree.curr, i128::MAX);
int_tree.complicate();
assert_eq!(int_tree.lo, i128::MAX);

let mut int_tree = IntValueTree::new(i128::MIN, false);
assert_eq!(int_tree.hi, i128::MIN);
assert_eq!(int_tree.curr, i128::MIN);
int_tree.complicate();
assert_eq!(int_tree.lo, i128::MIN);
}
}
Loading

0 comments on commit b37f324

Please sign in to comment.