Skip to content

Commit 508e677

Browse files
authored
feat: build simple dictionary from inspecting ACIR program (#5264)
# Description ## Problem\* Resolves #5262 ## Summary\* This PR pulls out a bunch of relevant constants from the ACIR/brillig bytecode so we can feed these into the fuzzer's strategy to ensure proper coverage of these values. ## Additional Context ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings.
1 parent 318314d commit 508e677

6 files changed

Lines changed: 193 additions & 23 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[package]
2+
name = "fuzzer_checks"
3+
type = "bin"
4+
authors = [""]
5+
[dependencies]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
#[test(should_fail_with = "42 is not allowed")]
3+
fn finds_magic_value(x: u32) {
4+
let x = x as u64;
5+
assert(2 * x != 42, "42 is not allowed");
6+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! This module defines how to build a dictionary of values which are likely to be correspond
2+
//! to significant inputs during fuzzing by inspecting the [Program] being fuzzed.
3+
//!
4+
//! This dictionary can be fed into the fuzzer's [strategy][proptest::strategy::Strategy] in order to bias it towards
5+
//! generating these values to ensure they get proper coverage.
6+
use std::collections::HashSet;
7+
8+
use acvm::{
9+
acir::{
10+
circuit::{
11+
brillig::{BrilligBytecode, BrilligInputs},
12+
directives::Directive,
13+
opcodes::{BlackBoxFuncCall, FunctionInput},
14+
Circuit, Opcode, Program,
15+
},
16+
native_types::Expression,
17+
},
18+
brillig_vm::brillig::Opcode as BrilligOpcode,
19+
AcirField,
20+
};
21+
22+
/// Constructs a [HashSet<F>] of values pulled from a [Program<F>] which are likely to be correspond
23+
/// to significant inputs during fuzzing.
24+
pub(super) fn build_dictionary_from_program<F: AcirField>(program: &Program<F>) -> HashSet<F> {
25+
let constrained_dictionaries = program.functions.iter().map(build_dictionary_from_circuit);
26+
let unconstrained_dictionaries =
27+
program.unconstrained_functions.iter().map(build_dictionary_from_unconstrained_function);
28+
let dictionaries = constrained_dictionaries.chain(unconstrained_dictionaries);
29+
30+
let mut constants: HashSet<F> = HashSet::new();
31+
for dictionary in dictionaries {
32+
constants.extend(dictionary);
33+
}
34+
constants
35+
}
36+
37+
fn build_dictionary_from_circuit<F: AcirField>(circuit: &Circuit<F>) -> HashSet<F> {
38+
let mut constants: HashSet<F> = HashSet::new();
39+
40+
fn insert_expr<F: AcirField>(dictionary: &mut HashSet<F>, expr: &Expression<F>) {
41+
let quad_coefficients = expr.mul_terms.iter().map(|(k, _, _)| *k);
42+
let linear_coefficients = expr.linear_combinations.iter().map(|(k, _)| *k);
43+
let coefficients = linear_coefficients.chain(quad_coefficients);
44+
45+
dictionary.extend(coefficients.clone());
46+
dictionary.insert(expr.q_c);
47+
48+
// We divide the constant term by any coefficients in the expression to aid solving constraints such as `2 * x - 4 == 0`.
49+
let scaled_constants = coefficients.map(|coefficient| expr.q_c / coefficient);
50+
dictionary.extend(scaled_constants);
51+
}
52+
53+
fn insert_array_len<F: AcirField, T>(dictionary: &mut HashSet<F>, array: &[T]) {
54+
let array_length = array.len() as u128;
55+
dictionary.insert(F::from(array_length));
56+
dictionary.insert(F::from(array_length - 1));
57+
}
58+
59+
for opcode in &circuit.opcodes {
60+
match opcode {
61+
Opcode::AssertZero(expr)
62+
| Opcode::Call { predicate: Some(expr), .. }
63+
| Opcode::MemoryOp { predicate: Some(expr), .. }
64+
| Opcode::Directive(Directive::ToLeRadix { a: expr, .. }) => {
65+
insert_expr(&mut constants, expr)
66+
}
67+
68+
Opcode::MemoryInit { init, .. } => insert_array_len(&mut constants, init),
69+
70+
Opcode::BrilligCall { inputs, predicate, .. } => {
71+
for input in inputs {
72+
match input {
73+
BrilligInputs::Single(expr) => insert_expr(&mut constants, expr),
74+
BrilligInputs::Array(exprs) => {
75+
exprs.iter().for_each(|expr| insert_expr(&mut constants, expr));
76+
insert_array_len(&mut constants, exprs);
77+
}
78+
BrilligInputs::MemoryArray(_) => (),
79+
}
80+
}
81+
if let Some(predicate) = predicate {
82+
insert_expr(&mut constants, predicate)
83+
}
84+
}
85+
86+
Opcode::BlackBoxFuncCall(BlackBoxFuncCall::RANGE {
87+
input: FunctionInput { num_bits, .. },
88+
}) => {
89+
let field = 1u128.wrapping_shl(*num_bits);
90+
constants.insert(F::from(field));
91+
constants.insert(F::from(field - 1));
92+
}
93+
_ => (),
94+
}
95+
}
96+
97+
constants
98+
}
99+
100+
fn build_dictionary_from_unconstrained_function<F: AcirField>(
101+
function: &BrilligBytecode<F>,
102+
) -> HashSet<F> {
103+
let mut constants: HashSet<F> = HashSet::new();
104+
105+
for opcode in &function.bytecode {
106+
match opcode {
107+
BrilligOpcode::Cast { bit_size, .. } => {
108+
let field = 1u128.wrapping_shl(*bit_size);
109+
constants.insert(F::from(field));
110+
constants.insert(F::from(field - 1));
111+
}
112+
BrilligOpcode::Const { bit_size, value, .. } => {
113+
constants.insert(*value);
114+
115+
let field = 1u128.wrapping_shl(*bit_size);
116+
constants.insert(F::from(field));
117+
constants.insert(F::from(field - 1));
118+
}
119+
_ => (),
120+
}
121+
}
122+
123+
constants
124+
}

tooling/fuzzer/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
//! Code is used under the MIT license.
55
66
use acvm::{blackbox_solver::StubbedBlackBoxSolver, FieldElement};
7+
use dictionary::build_dictionary_from_program;
78
use noirc_abi::InputMap;
89
use proptest::test_runner::{TestCaseError, TestError, TestRunner};
910

11+
mod dictionary;
1012
mod strategies;
1113
mod types;
1214

@@ -37,7 +39,8 @@ impl FuzzedExecutor {
3739

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

4245
let run_result: Result<(), TestError<InputMap>> =
4346
self.runner.clone().run(&strategy, |input_map| {

tooling/fuzzer/src/strategies/mod.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,22 @@ use proptest::prelude::*;
55
use acvm::{AcirField, FieldElement};
66

77
use noirc_abi::{input_parser::InputValue, Abi, AbiType, InputMap, Sign};
8-
use std::collections::BTreeMap;
8+
use std::collections::{BTreeMap, HashSet};
99
use uint::UintStrategy;
1010

1111
mod int;
1212
mod uint;
1313

14-
proptest::prop_compose! {
15-
pub(super) fn arb_field_from_integer(bit_size: u32)(value: u128)-> FieldElement {
16-
let width = (bit_size % 128).clamp(1, 127);
17-
let max_value = 2u128.pow(width) - 1;
18-
let value = value % max_value;
19-
FieldElement::from(value)
20-
}
21-
}
22-
23-
pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<InputValue> {
14+
pub(super) fn arb_value_from_abi_type(
15+
abi_type: &AbiType,
16+
dictionary: HashSet<FieldElement>,
17+
) -> SBoxedStrategy<InputValue> {
2418
match abi_type {
2519
AbiType::Field => vec(any::<u8>(), 32)
2620
.prop_map(|bytes| InputValue::Field(FieldElement::from_be_bytes_reduce(&bytes)))
2721
.sboxed(),
2822
AbiType::Integer { width, sign } if sign == &Sign::Unsigned => {
29-
UintStrategy::new(*width as usize)
23+
UintStrategy::new(*width as usize, dictionary)
3024
.prop_map(|uint| InputValue::Field(uint.into()))
3125
.sboxed()
3226
}
@@ -55,15 +49,17 @@ pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<Inpu
5549
}
5650
AbiType::Array { length, typ } => {
5751
let length = *length as usize;
58-
let elements = vec(arb_value_from_abi_type(typ), length..=length);
52+
let elements = vec(arb_value_from_abi_type(typ, dictionary), length..=length);
5953

6054
elements.prop_map(InputValue::Vec).sboxed()
6155
}
6256

6357
AbiType::Struct { fields, .. } => {
6458
let fields: Vec<SBoxedStrategy<(String, InputValue)>> = fields
6559
.iter()
66-
.map(|(name, typ)| (Just(name.clone()), arb_value_from_abi_type(typ)).sboxed())
60+
.map(|(name, typ)| {
61+
(Just(name.clone()), arb_value_from_abi_type(typ, dictionary.clone())).sboxed()
62+
})
6763
.collect();
6864

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

7773
AbiType::Tuple { fields } => {
78-
let fields: Vec<_> = fields.iter().map(arb_value_from_abi_type).collect();
74+
let fields: Vec<_> =
75+
fields.iter().map(|typ| arb_value_from_abi_type(typ, dictionary.clone())).collect();
7976
fields.prop_map(InputValue::Vec).sboxed()
8077
}
8178
}
8279
}
8380

84-
pub(super) fn arb_input_map(abi: &Abi) -> BoxedStrategy<InputMap> {
81+
pub(super) fn arb_input_map(
82+
abi: &Abi,
83+
dictionary: HashSet<FieldElement>,
84+
) -> BoxedStrategy<InputMap> {
8585
let values: Vec<_> = abi
8686
.parameters
8787
.iter()
88-
.map(|param| (Just(param.name.clone()), arb_value_from_abi_type(&param.typ)))
88+
.map(|param| {
89+
(Just(param.name.clone()), arb_value_from_abi_type(&param.typ, dictionary.clone()))
90+
})
8991
.collect();
9092

9193
values

tooling/fuzzer/src/strategies/uint.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::collections::HashSet;
2+
3+
use acvm::{AcirField, FieldElement};
14
use proptest::{
25
strategy::{NewTree, Strategy},
36
test_runner::TestRunner,
@@ -13,9 +16,12 @@ use rand::Rng;
1316
pub struct UintStrategy {
1417
/// Bit size of uint (e.g. 128)
1518
bits: usize,
16-
19+
/// A set of fixtures to be generated
20+
fixtures: Vec<FieldElement>,
1721
/// The weight for edge cases (+/- 3 around 0 and max possible value)
1822
edge_weight: usize,
23+
/// The weight for fixtures
24+
fixtures_weight: usize,
1925
/// The weight for purely random values
2026
random_weight: usize,
2127
}
@@ -24,8 +30,15 @@ impl UintStrategy {
2430
/// Create a new strategy.
2531
/// # Arguments
2632
/// * `bits` - Size of uint in bits
27-
pub fn new(bits: usize) -> Self {
28-
Self { bits, edge_weight: 10usize, random_weight: 50usize }
33+
/// * `fixtures` - Set of `FieldElements` representing values which the fuzzer weight towards testing.
34+
pub fn new(bits: usize, fixtures: HashSet<FieldElement>) -> Self {
35+
Self {
36+
bits,
37+
fixtures: fixtures.into_iter().collect(),
38+
edge_weight: 10usize,
39+
fixtures_weight: 40usize,
40+
random_weight: 50usize,
41+
}
2942
}
3043

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

53+
fn generate_fixtures_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
54+
// generate random cases if there's no fixtures
55+
if self.fixtures.is_empty() {
56+
return self.generate_random_tree(runner);
57+
}
58+
59+
// Generate value tree from fixture.
60+
let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())];
61+
if fixture.num_bits() <= self.bits as u32 {
62+
return Ok(proptest::num::u128::BinarySearch::new(fixture.to_u128()));
63+
}
64+
65+
// If fixture is not a valid type, generate random value.
66+
self.generate_random_tree(runner)
67+
}
68+
4069
fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
4170
let rng = runner.rng();
4271
let start = rng.gen_range(0..=self.type_max());
@@ -57,11 +86,12 @@ impl Strategy for UintStrategy {
5786
type Tree = proptest::num::u128::BinarySearch;
5887
type Value = u128;
5988
fn new_tree(&self, runner: &mut TestRunner) -> NewTree<Self> {
60-
let total_weight = self.random_weight + self.edge_weight;
89+
let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight;
6190
let bias = runner.rng().gen_range(0..total_weight);
62-
// randomly select one of 2 strategies
91+
// randomly select one of 3 strategies
6392
match bias {
6493
x if x < self.edge_weight => self.generate_edge_tree(runner),
94+
x if x < self.edge_weight + self.fixtures_weight => self.generate_fixtures_tree(runner),
6595
_ => self.generate_random_tree(runner),
6696
}
6797
}

0 commit comments

Comments
 (0)