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: build simple dictionary from inspecting ACIR program #5264

Merged
merged 19 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
5 changes: 5 additions & 0 deletions test_programs/noir_test_success/fuzzer_checks/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "fuzzer_checks"
type = "bin"
authors = [""]
[dependencies]
7 changes: 7 additions & 0 deletions test_programs/noir_test_success/fuzzer_checks/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use dep::std::field::bn254::{TWO_POW_128, assert_gt};
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved

#[test(should_fail_with = "42 is not allowed")]
fn finds_magic_value(x: u32) {
let x = x as u64;
assert(2 * x != 42, "42 is not allowed");
}
124 changes: 124 additions & 0 deletions tooling/fuzzer/src/dictionary/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! This module defines how to build a dictionary of values which are likely to be correspond
//! to significant inputs during fuzzing by inspecting the [Program] being fuzzed.
//!
//! This dictionary can be fed into the fuzzer's [strategy][proptest::strategy::Strategy] in order to bias it towards
//! generating these values to ensure they get proper coverage.
use std::collections::HashSet;

use acvm::{
acir::{
circuit::{
brillig::{BrilligBytecode, BrilligInputs},
directives::Directive,
opcodes::{BlackBoxFuncCall, FunctionInput},
Circuit, Opcode, Program,
},
native_types::Expression,
},
brillig_vm::brillig::Opcode as BrilligOpcode,
AcirField,
};

/// Constructs a [HashSet<F>] of values pulled from a [Program<F>] which are likely to be correspond
/// to significant inputs during fuzzing.
pub(super) fn build_dictionary_from_program<F: AcirField>(program: &Program<F>) -> HashSet<F> {
let constrained_dictionaries = program.functions.iter().map(build_dictionary_from_circuit);
let unconstrained_dictionaries =
program.unconstrained_functions.iter().map(build_dictionary_from_unconstrained_function);
let dictionaries = constrained_dictionaries.chain(unconstrained_dictionaries);

let mut constants: HashSet<F> = HashSet::new();
for dictionary in dictionaries {
constants.extend(dictionary);
}
constants
}

fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<F> {
let mut constants: HashSet<F> = HashSet::new();

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);
let coefficients = linear_coefficients.chain(quad_coefficients);

dictionary.extend(coefficients.clone());
dictionary.insert(expr.q_c);

// We divide the constant term by any coefficients in the expression to aid solving constraints such as `2 * x - 4 == 0`.
let scaled_constants = coefficients.map(|coefficient| expr.q_c / coefficient);
dictionary.extend(scaled_constants);
}

fn insert_array_len<F: AcirField, T>(dictionary: &mut HashSet<F>, array: &[T]) {
let array_length = array.len() as u128;
dictionary.insert(F::from(array_length));
dictionary.insert(F::from(array_length - 1));
}

for opcode in &circuit.opcodes {
match opcode {
Opcode::AssertZero(expr)
| Opcode::Call { predicate: Some(expr), .. }
| Opcode::MemoryOp { predicate: Some(expr), .. }
| Opcode::Directive(Directive::ToLeRadix { a: expr, .. }) => {
insert_expr(&mut constants, expr)
}

Opcode::MemoryInit { init, .. } => insert_array_len(&mut constants, init),

Opcode::BrilligCall { inputs, predicate, .. } => {
for input in inputs {
match input {
BrilligInputs::Single(expr) => insert_expr(&mut constants, expr),
BrilligInputs::Array(exprs) => {
exprs.iter().for_each(|expr| insert_expr(&mut constants, expr));
insert_array_len(&mut constants, exprs);
}
BrilligInputs::MemoryArray(_) => (),
}
}
if let Some(predicate) = predicate {
insert_expr(&mut constants, predicate)
}
}

Opcode::BlackBoxFuncCall(BlackBoxFuncCall::RANGE {
input: FunctionInput { num_bits, .. },
}) => {
let field = 1u128.wrapping_shl(*num_bits);
constants.insert(F::from(field));
constants.insert(F::from(field - 1));
}
_ => (),
}
}

constants
}

fn build_dictionary_from_unconstrained_function<F: AcirField>(
function: &BrilligBytecode<F>,
) -> HashSet<F> {
let mut constants: HashSet<F> = HashSet::new();

for opcode in &function.bytecode {
match opcode {
BrilligOpcode::Cast { bit_size, .. } => {
let field = 1u128.wrapping_shl(*bit_size);
constants.insert(F::from(field));
constants.insert(F::from(field - 1));
}
BrilligOpcode::Const { bit_size, value, .. } => {
constants.insert(*value);

let field = 1u128.wrapping_shl(*bit_size);
constants.insert(F::from(field));
constants.insert(F::from(field - 1));
}
_ => (),
}
}

constants
}
5 changes: 4 additions & 1 deletion tooling/fuzzer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
//! Code is used under the MIT license.

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

mod dictionary;
mod strategies;
mod types;

Expand Down Expand Up @@ -37,7 +39,8 @@ impl FuzzedExecutor {

/// Fuzzes the provided program.
pub fn fuzz(&self) -> FuzzTestResult {
let strategy = strategies::arb_input_map(&self.program.abi);
let dictionary = build_dictionary_from_program(&self.program.bytecode);
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
36 changes: 19 additions & 17 deletions tooling/fuzzer/src/strategies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@ use proptest::prelude::*;
use acvm::{AcirField, FieldElement};

use noirc_abi::{input_parser::InputValue, Abi, AbiType, InputMap, Sign};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashSet};
use uint::UintStrategy;

mod int;
mod uint;

proptest::prop_compose! {
pub(super) fn arb_field_from_integer(bit_size: u32)(value: u128)-> FieldElement {
let width = (bit_size % 128).clamp(1, 127);
let max_value = 2u128.pow(width) - 1;
let value = value % max_value;
FieldElement::from(value)
}
}

pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<InputValue> {
pub(super) fn arb_value_from_abi_type(
abi_type: &AbiType,
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)
UintStrategy::new(*width as usize, dictionary)
.prop_map(|uint| InputValue::Field(uint.into()))
.sboxed()
}
Expand Down Expand Up @@ -55,15 +49,17 @@ pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<Inpu
}
AbiType::Array { length, typ } => {
let length = *length as usize;
let elements = vec(arb_value_from_abi_type(typ), length..=length);
let elements = vec(arb_value_from_abi_type(typ, dictionary), length..=length);

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)).sboxed())
.map(|(name, typ)| {
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary.clone())).sboxed()
})
.collect();

fields
Expand All @@ -75,17 +71,23 @@ pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<Inpu
}

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

pub(super) fn arb_input_map(abi: &Abi) -> BoxedStrategy<InputMap> {
pub(super) fn arb_input_map(
abi: &Abi,
dictionary: HashSet<FieldElement>,
) -> BoxedStrategy<InputMap> {
let values: Vec<_> = abi
.parameters
.iter()
.map(|param| (Just(param.name.clone()), arb_value_from_abi_type(&param.typ)))
.map(|param| {
(Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary.clone()))
})
.collect();

values
Expand Down
40 changes: 35 additions & 5 deletions tooling/fuzzer/src/strategies/uint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::collections::HashSet;

use acvm::{AcirField, FieldElement};
use proptest::{
strategy::{NewTree, Strategy},
test_runner::TestRunner,
Expand All @@ -13,9 +16,12 @@ use rand::Rng;
pub struct UintStrategy {
/// Bit size of uint (e.g. 128)
bits: usize,

/// A set of fixtures to be generated
fixtures: Vec<FieldElement>,
/// The weight for edge cases (+/- 3 around 0 and max possible value)
edge_weight: usize,
/// The weight for fixtures
fixtures_weight: usize,
/// The weight for purely random values
random_weight: usize,
}
Expand All @@ -24,8 +30,15 @@ impl UintStrategy {
/// Create a new strategy.
/// # Arguments
/// * `bits` - Size of uint in bits
TomAFrench marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(bits: usize) -> Self {
Self { bits, edge_weight: 10usize, random_weight: 50usize }
/// * `fixtures` - Set of `FieldElements` representing values which the fuzzer weight towards testing.
pub fn new(bits: usize, fixtures: HashSet<FieldElement>) -> Self {
Self {
bits,
fixtures: fixtures.into_iter().collect(),
edge_weight: 10usize,
fixtures_weight: 40usize,
random_weight: 50usize,
}
}

fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
Expand All @@ -37,6 +50,22 @@ impl UintStrategy {
Ok(proptest::num::u128::BinarySearch::new(start))
}

fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
// generate random cases if there's no fixtures
if self.fixtures.is_empty() {
return self.generate_random_tree(runner);
}

// 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)
}

fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let rng = runner.rng();
let start = rng.gen_range(0..=self.type_max());
Expand All @@ -57,11 +86,12 @@ impl Strategy for UintStrategy {
type Tree = proptest::num::u128::BinarySearch;
type Value = u128;
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
let total_weight = self.random_weight + self.edge_weight;
let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight;
let bias = runner.rng().gen_range(0..total_weight);
// randomly select one of 2 strategies
// randomly select one of 3 strategies
match bias {
x if x < self.edge_weight => self.generate_edge_tree(runner),
x if x < self.edge_weight + self.fixtures_weight => self.generate_fixtures_tree(runner),
_ => self.generate_random_tree(runner),
}
}
Expand Down
Loading