From 8e205496170aafbbda51924aac3566598710ffb1 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 10:15:33 +0000 Subject: [PATCH 01/14] Change UIntStrategy to use u64 --- tooling/fuzzer/src/dictionary/mod.rs | 3 +++ tooling/fuzzer/src/lib.rs | 14 +++++++------- tooling/fuzzer/src/strategies/mod.rs | 10 +++++++--- tooling/fuzzer/src/strategies/uint.rs | 27 ++++++++++++++++++--------- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/tooling/fuzzer/src/dictionary/mod.rs b/tooling/fuzzer/src/dictionary/mod.rs index 172edfa54c2..7fe1c4cd602 100644 --- a/tooling/fuzzer/src/dictionary/mod.rs +++ b/tooling/fuzzer/src/dictionary/mod.rs @@ -33,9 +33,11 @@ pub(super) fn build_dictionary_from_program(program: &Program) constants } +/// Collect `Field` values used in the opcodes of an ACIR circuit. fn build_dictionary_from_circuit(circuit: &Circuit) -> HashSet { let mut constants: HashSet = HashSet::new(); + /// Pull out all the fields from an expression. fn insert_expr(dictionary: &mut HashSet, expr: &Expression) { let quad_coefficients = expr.mul_terms.iter().map(|(k, _, _)| *k); let linear_coefficients = expr.linear_combinations.iter().map(|(k, _)| *k); @@ -104,6 +106,7 @@ fn build_dictionary_from_circuit(circuit: &Circuit) -> HashSet< constants } +/// Collect `Field` values used in the opcodes of a Brillig function. fn build_dictionary_from_unconstrained_function( function: &BrilligBytecode, ) -> HashSet { diff --git a/tooling/fuzzer/src/lib.rs b/tooling/fuzzer/src/lib.rs index 28a43279c95..72dc3f80d46 100644 --- a/tooling/fuzzer/src/lib.rs +++ b/tooling/fuzzer/src/lib.rs @@ -38,14 +38,14 @@ pub struct FuzzedExecutor { runner: TestRunner, } -impl< - E: Fn( - &Program, - WitnessMap, - ) -> Result, String>, - > FuzzedExecutor +impl FuzzedExecutor +where + E: Fn( + &Program, + WitnessMap, + ) -> Result, 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 } } diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 46187a28d5b..09a5feebc13 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -11,6 +11,9 @@ 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, @@ -25,6 +28,7 @@ pub(super) fn arb_value_from_abi_type( .sboxed() } AbiType::Integer { width, .. } => { + // TODO: i128 in wasm? let shift = 2i128.pow(*width); IntStrategy::new(*width as usize) .prop_map(move |mut int| { @@ -38,7 +42,6 @@ pub(super) fn arb_value_from_abi_type( AbiType::Boolean => { any::().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}}}"); @@ -53,7 +56,6 @@ pub(super) fn arb_value_from_abi_type( elements.prop_map(InputValue::Vec).sboxed() } - AbiType::Struct { fields, .. } => { let fields: Vec> = fields .iter() @@ -69,7 +71,6 @@ 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(); @@ -78,6 +79,9 @@ pub(super) fn arb_value_from_abi_type( } } +/// 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, diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index 94610dbc829..5d01b3fc910 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -14,7 +14,7 @@ use rand::Rng; /// 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, @@ -41,15 +41,18 @@ impl UintStrategy { } } + /// Generate random numbers starting from near 0 or the maximum of the range. fn generate_edge_tree(&self, runner: &mut TestRunner) -> NewTree { 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(proptest::num::u64::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 { // generate random cases if there's no fixtures if self.fixtures.is_empty() { @@ -59,32 +62,38 @@ 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 let Some(start) = fixture.try_to_u64() { + return Ok(proptest::num::u64::BinarySearch::new(start)); + } } // If fixture is not a valid type, generate random value. self.generate_random_tree(runner) } + /// Generate random values between 0 and the MAX with the given bit width. fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { let rng = runner.rng(); let start = rng.gen_range(0..=self.type_max()); - Ok(proptest::num::u128::BinarySearch::new(start)) + Ok(proptest::num::u64::BinarySearch::new(start)) } - fn type_max(&self) -> u128 { - if self.bits < 128 { + /// Maximum integer that fits in the given bit width. + fn type_max(&self) -> u64 { + if self.bits < 64 { (1 << self.bits) - 1 } else { - u128::MAX + u64::MAX } } } impl Strategy for UintStrategy { - type Tree = proptest::num::u128::BinarySearch; - type Value = u128; + type Tree = proptest::num::u64::BinarySearch; + type Value = u64; + + /// Pick randomly from the 3 available strategies for generating unsigned integers. fn new_tree(&self, runner: &mut TestRunner) -> NewTree { let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight; let bias = runner.rng().gen_range(0..total_weight); From 0097468e08d960d55412a506e937c6dea84d637e Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 10:19:56 +0000 Subject: [PATCH 02/14] Filter fixtures which wouldn't fit into the bits --- tooling/fuzzer/src/lib.rs | 4 ++-- tooling/fuzzer/src/strategies/mod.rs | 14 ++++++-------- tooling/fuzzer/src/strategies/uint.rs | 11 +++++------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/tooling/fuzzer/src/lib.rs b/tooling/fuzzer/src/lib.rs index 72dc3f80d46..324be323fc2 100644 --- a/tooling/fuzzer/src/lib.rs +++ b/tooling/fuzzer/src/lib.rs @@ -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 @@ -53,7 +53,7 @@ where /// 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> = self.runner.clone().run(&strategy, |input_map| { diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 09a5feebc13..8823e56264e 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -16,14 +16,14 @@ mod uint; /// Uses the `dictionary` for unsigned integer types. pub(super) fn arb_value_from_abi_type( abi_type: &AbiType, - dictionary: HashSet, + dictionary: &HashSet, ) -> SBoxedStrategy { match abi_type { AbiType::Field => vec(any::(), 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) + UintStrategy::new(*width as usize, &dictionary) .prop_map(|uint| InputValue::Field(uint.into())) .sboxed() } @@ -60,7 +60,7 @@ pub(super) fn arb_value_from_abi_type( let fields: Vec> = 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(); @@ -73,7 +73,7 @@ pub(super) fn arb_value_from_abi_type( } 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() } } @@ -84,14 +84,12 @@ pub(super) fn arb_value_from_abi_type( /// Use the `dictionary` to draw values from for numeric types. pub(super) fn arb_input_map( abi: &Abi, - dictionary: HashSet, + dictionary: &HashSet, ) -> BoxedStrategy { let values: Vec<_> = abi .parameters .iter() - .map(|param| { - (Just(param.name.clone()), arb_value_from_abi_type(¶m.typ, dictionary.clone())) - }) + .map(|param| (Just(param.name.clone()), arb_value_from_abi_type(¶m.typ, dictionary))) .collect(); values diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index 5d01b3fc910..abb2743ce9d 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -31,10 +31,11 @@ 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) -> Self { + pub fn new(bits: usize, fixtures: &HashSet) -> 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, @@ -61,10 +62,8 @@ 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 { - if let Some(start) = fixture.try_to_u64() { - return Ok(proptest::num::u64::BinarySearch::new(start)); - } + if let Some(start) = fixture.try_to_u64() { + return Ok(proptest::num::u64::BinarySearch::new(start)); } // If fixture is not a valid type, generate random value. From 73328ce551f75d6d51e380bba8abcac6a5e11589 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 10:27:08 +0000 Subject: [PATCH 03/14] Enable fuzzer in wasm --- tooling/nargo/src/ops/test.rs | 109 +++++++++++++++------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/tooling/nargo/src/ops/test.rs b/tooling/nargo/src/ops/test.rs index 1306150518d..4d495116da5 100644 --- a/tooling/nargo/src/ops/test.rs +++ b/tooling/nargo/src/ops/test.rs @@ -107,69 +107,56 @@ pub fn run_test>( status } } else { - #[cfg(target_arch = "wasm32")] - { - // We currently don't support fuzz testing on wasm32 as the u128 strategies do not exist on this platform. - TestStatus::Fail { - message: "Fuzz tests are not supported on wasm32".to_string(), - error_diagnostic: None, + use acvm::acir::circuit::Program; + use noir_fuzzer::FuzzedExecutor; + use proptest::test_runner::Config; + use proptest::test_runner::TestRunner; + + let runner = + TestRunner::new(Config { failure_persistence: None, ..Config::default() }); + + let abi = compiled_program.abi.clone(); + let debug = compiled_program.debug.clone(); + + let executor = |program: &Program, + initial_witness: WitnessMap| + -> Result, String> { + let circuit_execution = execute_program( + program, + initial_witness, + blackbox_solver, + &mut TestForeignCallExecutor::::new( + PrintOutput::None, + foreign_call_resolver_url, + root_path.clone(), + package_name.clone(), + ), + ); + + let status = test_status_program_compile_pass( + test_function, + &abi, + &debug, + &circuit_execution, + ); + + if let TestStatus::Fail { message, error_diagnostic: _ } = status { + Err(message) + } else { + // The fuzzer doesn't care about the actual result. + Ok(WitnessStack::default()) } - } + }; - #[cfg(not(target_arch = "wasm32"))] - { - use acvm::acir::circuit::Program; - use noir_fuzzer::FuzzedExecutor; - use proptest::test_runner::Config; - use proptest::test_runner::TestRunner; - - let runner = - TestRunner::new(Config { failure_persistence: None, ..Config::default() }); - - let abi = compiled_program.abi.clone(); - let debug = compiled_program.debug.clone(); - - let executor = - |program: &Program, - initial_witness: WitnessMap| - -> Result, String> { - let circuit_execution = execute_program( - program, - initial_witness, - blackbox_solver, - &mut TestForeignCallExecutor::::new( - PrintOutput::None, - foreign_call_resolver_url, - root_path.clone(), - package_name.clone(), - ), - ); - - let status = test_status_program_compile_pass( - test_function, - &abi, - &debug, - &circuit_execution, - ); - - if let TestStatus::Fail { message, error_diagnostic: _ } = status { - Err(message) - } else { - // The fuzzer doesn't care about the actual result. - Ok(WitnessStack::default()) - } - }; - - let fuzzer = FuzzedExecutor::new(compiled_program.into(), executor, runner); - - let result = fuzzer.fuzz(); - if result.success { - TestStatus::Pass - } else { - TestStatus::Fail { - message: result.reason.unwrap_or_default(), - error_diagnostic: None, - } + let fuzzer = FuzzedExecutor::new(compiled_program.into(), executor, runner); + + let result = fuzzer.fuzz(); + if result.success { + TestStatus::Pass + } else { + TestStatus::Fail { + message: result.reason.unwrap_or_default(), + error_diagnostic: None, } } } From d932107590b5fd739e4cbf371a7925cb342a5625 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 11:21:43 +0000 Subject: [PATCH 04/14] Use i64 strategy for i128 --- tooling/fuzzer/src/strategies/int.rs | 46 ++++++++++++++++----------- tooling/fuzzer/src/strategies/mod.rs | 4 +-- tooling/fuzzer/src/strategies/uint.rs | 30 +++++++++++------ 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index d11cafcfae5..3e720cb4db3 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -1,9 +1,19 @@ +use std::cmp; + use proptest::{ strategy::{NewTree, Strategy}, test_runner::TestRunner, }; use rand::Rng; +/// Using `i64` instead of `i128` because the latter was not available for Wasm. +/// Once https://github.com/proptest-rs/proptest/pull/519 is released we can switch +/// back, although since we've restricted the type system to only allow u64s +/// as the maximum integer type. We could come up with strategy that covers +/// double the range of i64 by using u64 and mapping it to i128, but I'm not +/// sure it's worth the effort. +type BinarySearch = proptest::num::i64::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` @@ -27,6 +37,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 { let rng = runner.rng(); @@ -35,41 +46,40 @@ impl IntStrategy { let kind = rng.gen_range(0..4); let start = match kind { 0 => self.type_min() + offset, - 1 => -offset - 1i128, + 1 => -offset - 1i64, 2 => offset, 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 { let rng = runner.rng(); - let start: i128 = rng.gen_range(self.type_min()..=self.type_max()); - Ok(proptest::num::i128::BinarySearch::new(start)) + let start: i64 = rng.gen_range(self.type_min()..=self.type_max()); + Ok(BinarySearch::new(start)) } - fn type_max(&self) -> i128 { - if self.bits < 128 { - (1i128 << (self.bits - 1)) - 1 - } else { - i128::MAX - } + /// We've restricted the type system to only allow u64s as the maximum integer type. + fn type_max(&self) -> i64 { + (1i64 << (self.type_max_bits() - 1)) - 1 } - fn type_min(&self) -> i128 { - if self.bits < 128 { - -(1i128 << (self.bits - 1)) - } else { - i128::MIN - } + /// We've restricted the type system to only allow u64s as the maximum integer type. + fn type_min(&self) -> i64 { + -(1i64 << (self.type_max_bits() - 1)) + } + + /// Maximum number of bits for which we generate random numbers. + fn type_max_bits(&self) -> usize { + cmp::max(self.bits, 64) } } impl Strategy for IntStrategy { - type Tree = proptest::num::i128::BinarySearch; - type Value = i128; + type Tree = BinarySearch; + type Value = i64; fn new_tree(&self, runner: &mut TestRunner) -> NewTree { let total_weight = self.random_weight + self.edge_weight; diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 8823e56264e..3025be0b669 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -28,13 +28,13 @@ pub(super) fn arb_value_from_abi_type( .sboxed() } AbiType::Integer { width, .. } => { - // TODO: i128 in wasm? - let shift = 2i128.pow(*width); + let shift = 2i64.pow(*width); IntStrategy::new(*width as usize) .prop_map(move |mut int| { if int < 0 { int += shift } + let int: i128 = int.into(); InputValue::Field(int.into()) }) .sboxed() diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index abb2743ce9d..a33c7edb3bc 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{cmp, collections::HashSet}; use acvm::{AcirField, FieldElement}; use proptest::{ @@ -7,6 +7,15 @@ use proptest::{ }; use rand::Rng; +/// Using `u64` instead of `u128` because the latter was not available for Wasm. +/// Once https://github.com/proptest-rs/proptest/pull/519 is released we can switch +/// back, although since we've restricted the type system to only allow u64s +/// as the maximum integer type. +type BinarySearch = proptest::num::u64::BinarySearch; + +/// We have a `IntegerBitSize::U128` numeric type, but we will only generate values up to 64 bits here. +const MAX_BIT_SIZE: usize = 64; + /// 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` @@ -49,7 +58,7 @@ impl UintStrategy { 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::u64::BinarySearch::new(start)) + Ok(BinarySearch::new(start)) } /// Pick a random `FieldElement` from the `fixtures` as a starting point for @@ -63,7 +72,7 @@ impl UintStrategy { // Generate value tree from fixture. let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())]; if let Some(start) = fixture.try_to_u64() { - return Ok(proptest::num::u64::BinarySearch::new(start)); + return Ok(BinarySearch::new(start)); } // If fixture is not a valid type, generate random value. @@ -75,21 +84,22 @@ impl UintStrategy { let rng = runner.rng(); let start = rng.gen_range(0..=self.type_max()); - Ok(proptest::num::u64::BinarySearch::new(start)) + Ok(BinarySearch::new(start)) } /// Maximum integer that fits in the given bit width. fn type_max(&self) -> u64 { - if self.bits < 64 { - (1 << self.bits) - 1 - } else { - u64::MAX - } + (1 << self.type_max_bits()) - 1 + } + + /// Maximum bits that we generate values for. + fn type_max_bits(&self) -> usize { + cmp::max(self.bits, MAX_BIT_SIZE) } } impl Strategy for UintStrategy { - type Tree = proptest::num::u64::BinarySearch; + type Tree = BinarySearch; type Value = u64; /// Pick randomly from the 3 available strategies for generating unsigned integers. From e48b66b76a6f296ced5a346f095b6945cbb845c7 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 11:36:35 +0000 Subject: [PATCH 05/14] Fix clippy --- tooling/fuzzer/src/strategies/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 3025be0b669..04bf161b070 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -23,7 +23,7 @@ pub(super) fn arb_value_from_abi_type( .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) + UintStrategy::new(*width as usize, dictionary) .prop_map(|uint| InputValue::Field(uint.into())) .sboxed() } From 71ab8a7fc94b88d77d5e7f023e8c4bd708bec782 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 11:57:03 +0000 Subject: [PATCH 06/14] Enable fuzzer and proptest deps for Wasm --- tooling/nargo/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tooling/nargo/Cargo.toml b/tooling/nargo/Cargo.toml index 1dbb9978b0b..d8258e3a367 100644 --- a/tooling/nargo/Cargo.toml +++ b/tooling/nargo/Cargo.toml @@ -28,8 +28,6 @@ jsonrpc.workspace = true rand.workspace = true serde.workspace = true walkdir = "2.5.0" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] noir_fuzzer.workspace = true proptest.workspace = true From 53445a0e83740034be1908ddc9fbd7f86582cf9b Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:12:25 +0000 Subject: [PATCH 07/14] Update to the latest proptest --- Cargo.lock | 36 +++++++++--------------------------- Cargo.toml | 4 ++-- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4907de7ae62..e0491d85ea2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,18 +498,18 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -2570,12 +2570,6 @@ version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - [[package]] name = "libredox" version = "0.0.2" @@ -3082,7 +3076,7 @@ dependencies = [ "num-bigint", "num-traits", "proptest", - "proptest-derive 0.4.0", + "proptest-derive", "serde", "serde_json", "strum", @@ -3208,7 +3202,7 @@ dependencies = [ "num-traits", "petgraph", "proptest", - "proptest-derive 0.5.0", + "proptest-derive", "rangemap", "rustc-hash", "serde", @@ -3324,7 +3318,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3721,9 +3714,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", @@ -3739,17 +3732,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "proptest-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "proptest-derive" version = "0.5.0" @@ -5421,7 +5403,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0acee2a040b..abb774eaf68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,8 +152,8 @@ 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" +proptest = "1.6.0" +proptest-derive = "0.5.0" rayon = "1.8.0" sha2 = { version = "0.10.6", features = ["compress"] } sha3 = "0.10.6" From ee07eb3f8cf2bc2dda6a71bfb268ea347a3e0326 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:18:30 +0000 Subject: [PATCH 08/14] Disable fork and timeout --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index abb774eaf68..cfcf559b7fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,7 +152,11 @@ jsonrpc = { version = "0.16.0", features = ["minreq_http"] } flate2 = "1.0.24" color-eyre = "0.6.2" rand = "0.8.5" -proptest = "1.6.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"] } From 4e901a3765505d754811a78cd9890e3d88423786 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:22:53 +0000 Subject: [PATCH 09/14] Use u128 again --- tooling/fuzzer/src/strategies/uint.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index a33c7edb3bc..439b56cb6f4 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -7,14 +7,7 @@ use proptest::{ }; use rand::Rng; -/// Using `u64` instead of `u128` because the latter was not available for Wasm. -/// Once https://github.com/proptest-rs/proptest/pull/519 is released we can switch -/// back, although since we've restricted the type system to only allow u64s -/// as the maximum integer type. -type BinarySearch = proptest::num::u64::BinarySearch; - -/// We have a `IntegerBitSize::U128` numeric type, but we will only generate values up to 64 bits here. -const MAX_BIT_SIZE: usize = 64; +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: @@ -71,8 +64,8 @@ impl UintStrategy { // Generate value tree from fixture. let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())]; - if let Some(start) = fixture.try_to_u64() { - return Ok(BinarySearch::new(start)); + if fixture.num_bits() <= self.type_max_bits() as u32 { + return Ok(BinarySearch::new(fixture.to_u128())); } // If fixture is not a valid type, generate random value. @@ -88,19 +81,19 @@ impl UintStrategy { } /// Maximum integer that fits in the given bit width. - fn type_max(&self) -> u64 { + fn type_max(&self) -> u128 { (1 << self.type_max_bits()) - 1 } /// Maximum bits that we generate values for. fn type_max_bits(&self) -> usize { - cmp::max(self.bits, MAX_BIT_SIZE) + cmp::max(self.bits, 128) } } impl Strategy for UintStrategy { type Tree = BinarySearch; - type Value = u64; + type Value = u128; /// Pick randomly from the 3 available strategies for generating unsigned integers. fn new_tree(&self, runner: &mut TestRunner) -> NewTree { From 33ac44172de14f5d74e79f78058f6b2067819670 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:24:57 +0000 Subject: [PATCH 10/14] Fix type_max_bits --- tooling/fuzzer/src/strategies/int.rs | 2 +- tooling/fuzzer/src/strategies/uint.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index 3e720cb4db3..c34e034b83a 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -73,7 +73,7 @@ impl IntStrategy { /// Maximum number of bits for which we generate random numbers. fn type_max_bits(&self) -> usize { - cmp::max(self.bits, 64) + cmp::min(self.bits, 64) } } diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index 439b56cb6f4..6862fd59b9a 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -87,7 +87,7 @@ impl UintStrategy { /// Maximum bits that we generate values for. fn type_max_bits(&self) -> usize { - cmp::max(self.bits, 128) + cmp::min(self.bits, 128) } } From 2157dea0ccc6468f881c09751c9c2c7c737eb15a Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:26:22 +0000 Subject: [PATCH 11/14] Use i128 again --- tooling/fuzzer/src/strategies/int.rs | 24 +++++++++--------------- tooling/fuzzer/src/strategies/mod.rs | 3 +-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index c34e034b83a..9f1a1f5fd49 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -6,13 +6,7 @@ use proptest::{ }; use rand::Rng; -/// Using `i64` instead of `i128` because the latter was not available for Wasm. -/// Once https://github.com/proptest-rs/proptest/pull/519 is released we can switch -/// back, although since we've restricted the type system to only allow u64s -/// as the maximum integer type. We could come up with strategy that covers -/// double the range of i64 by using u64 and mapping it to i128, but I'm not -/// sure it's worth the effort. -type BinarySearch = proptest::num::i64::BinarySearch; +type BinarySearch = proptest::num::i128::BinarySearch; /// Strategy for signed ints (up to i128). /// The strategy combines 2 different strategies, each assigned a specific weight: @@ -46,7 +40,7 @@ impl IntStrategy { let kind = rng.gen_range(0..4); let start = match kind { 0 => self.type_min() + offset, - 1 => -offset - 1i64, + 1 => -offset - 1i128, 2 => offset, 3 => self.type_max() - offset, _ => unreachable!(), @@ -57,29 +51,29 @@ impl IntStrategy { fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { let rng = runner.rng(); - let start: i64 = rng.gen_range(self.type_min()..=self.type_max()); + let start: i128 = rng.gen_range(self.type_min()..=self.type_max()); Ok(BinarySearch::new(start)) } /// We've restricted the type system to only allow u64s as the maximum integer type. - fn type_max(&self) -> i64 { - (1i64 << (self.type_max_bits() - 1)) - 1 + fn type_max(&self) -> i128 { + (1i128 << (self.type_max_bits() - 1)) - 1 } /// We've restricted the type system to only allow u64s as the maximum integer type. - fn type_min(&self) -> i64 { - -(1i64 << (self.type_max_bits() - 1)) + fn type_min(&self) -> i128 { + -(1i128 << (self.type_max_bits() - 1)) } /// Maximum number of bits for which we generate random numbers. fn type_max_bits(&self) -> usize { - cmp::min(self.bits, 64) + cmp::min(self.bits, 128) } } impl Strategy for IntStrategy { type Tree = BinarySearch; - type Value = i64; + type Value = i128; fn new_tree(&self, runner: &mut TestRunner) -> NewTree { let total_weight = self.random_weight + self.edge_weight; diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 04bf161b070..af7dcd20a2c 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -28,13 +28,12 @@ pub(super) fn arb_value_from_abi_type( .sboxed() } AbiType::Integer { width, .. } => { - let shift = 2i64.pow(*width); + let shift = 2i128.pow(*width); IntStrategy::new(*width as usize) .prop_map(move |mut int| { if int < 0 { int += shift } - let int: i128 = int.into(); InputValue::Field(int.into()) }) .sboxed() From 9b77c356fb53f76b74efd515819fe3c7f1c214ce Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:33:13 +0000 Subject: [PATCH 12/14] Limit the actual maximum values to 64 bits --- tooling/fuzzer/src/strategies/int.rs | 4 +++- tooling/fuzzer/src/strategies/uint.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index 9f1a1f5fd49..502b7d1fb77 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -66,8 +66,10 @@ impl IntStrategy { } /// 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, 128) + cmp::min(self.bits, 64) } } diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index 6862fd59b9a..223b13ea08e 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -86,8 +86,10 @@ impl UintStrategy { } /// Maximum bits that we generate values for. + /// + /// 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, 128) + cmp::min(self.bits, 64) } } From bd37ad1d7421eea6beaf6de71d2119e541b0672e Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:49:02 +0000 Subject: [PATCH 13/14] Fix max formulas to work for 128 bits as well --- tooling/fuzzer/src/strategies/int.rs | 19 ++++++++++++++----- tooling/fuzzer/src/strategies/uint.rs | 7 ++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index 502b7d1fb77..708865f2b7f 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -50,19 +50,28 @@ impl IntStrategy { fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { let rng = runner.rng(); - let start: i128 = rng.gen_range(self.type_min()..=self.type_max()); Ok(BinarySearch::new(start)) } - /// We've restricted the type system to only allow u64s as the maximum integer type. + /// Maximum allowed positive number. fn type_max(&self) -> i128 { - (1i128 << (self.type_max_bits() - 1)) - 1 + let bits = self.type_max_bits(); + if bits < 128 { + (1i128 << (bits - 1)) - 1 + } else { + i128::MAX + } } - /// We've restricted the type system to only allow u64s as the maximum integer type. + /// Minimum allowed negative number. fn type_min(&self) -> i128 { - -(1i128 << (self.type_max_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. diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index 223b13ea08e..bc205fe8766 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -82,7 +82,12 @@ impl UintStrategy { /// Maximum integer that fits in the given bit width. fn type_max(&self) -> u128 { - (1 << self.type_max_bits()) - 1 + let bits = self.type_max_bits(); + if bits < 128 { + (1 << bits) - 1 + } else { + u128::MAX + } } /// Maximum bits that we generate values for. From 8fc244e63d27f9d5e7138187e5144a4a35e98c2c Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 17 Dec 2024 12:59:40 +0000 Subject: [PATCH 14/14] Move the restriction to 64 bits outside the strategy --- tooling/fuzzer/src/strategies/int.rs | 19 ++++--------------- tooling/fuzzer/src/strategies/mod.rs | 9 ++++++--- tooling/fuzzer/src/strategies/uint.rs | 20 ++++---------------- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs index 708865f2b7f..22dded7c7b7 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -1,5 +1,3 @@ -use std::cmp; - use proptest::{ strategy::{NewTree, Strategy}, test_runner::TestRunner, @@ -56,9 +54,8 @@ impl IntStrategy { /// Maximum allowed positive number. fn type_max(&self) -> i128 { - let bits = self.type_max_bits(); - if bits < 128 { - (1i128 << (bits - 1)) - 1 + if self.bits < 128 { + (1i128 << (self.bits - 1)) - 1 } else { i128::MAX } @@ -66,20 +63,12 @@ impl IntStrategy { /// Minimum allowed negative number. fn type_min(&self) -> i128 { - let bits = self.type_max_bits(); - if bits < 128 { - -(1i128 << (bits - 1)) + if self.bits < 128 { + -(1i128 << (self.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 { diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index af7dcd20a2c..99c7ca56f2e 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -23,13 +23,16 @@ pub(super) fn arb_value_from_abi_type( .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 diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs index bc205fe8766..402e6559752 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -1,4 +1,4 @@ -use std::{cmp, collections::HashSet}; +use std::collections::HashSet; use acvm::{AcirField, FieldElement}; use proptest::{ @@ -64,12 +64,8 @@ impl UintStrategy { // Generate value tree from fixture. let fixture = &self.fixtures[runner.rng().gen_range(0..self.fixtures.len())]; - if fixture.num_bits() <= self.type_max_bits() as u32 { - return Ok(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. @@ -82,20 +78,12 @@ impl UintStrategy { /// Maximum integer that fits in the given bit width. fn type_max(&self) -> u128 { - let bits = self.type_max_bits(); - if bits < 128 { - (1 << bits) - 1 + if self.bits < 128 { + (1 << self.bits) - 1 } else { u128::MAX } } - - /// Maximum bits that we generate values for. - /// - /// 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 UintStrategy {