diff --git a/Cargo.lock b/Cargo.lock index eb6d4327faf..6235fbf4f8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,18 +654,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" @@ -2775,12 +2775,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" @@ -3247,7 +3241,7 @@ dependencies = [ "num-bigint", "num-traits", "proptest", - "proptest-derive 0.4.0", + "proptest-derive", "serde", "serde_json", "strum", @@ -3374,7 +3368,7 @@ dependencies = [ "num-traits", "petgraph", "proptest", - "proptest-derive 0.5.0", + "proptest-derive", "rangemap", "rustc-hash 1.1.0", "serde", @@ -3486,7 +3480,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3868,9 +3861,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", @@ -3886,17 +3879,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" diff --git a/Cargo.toml b/Cargo.toml index 73937667c14..8d48a846457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" 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..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 @@ -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 } } @@ -53,7 +53,7 @@ impl< /// 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/int.rs b/tooling/fuzzer/src/strategies/int.rs index d11cafcfae5..22dded7c7b7 100644 --- a/tooling/fuzzer/src/strategies/int.rs +++ b/tooling/fuzzer/src/strategies/int.rs @@ -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` @@ -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 { let rng = runner.rng(); @@ -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 { 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 @@ -58,6 +61,7 @@ impl IntStrategy { } } + /// Minimum allowed negative number. fn type_min(&self) -> i128 { if self.bits < 128 { -(1i128 << (self.bits - 1)) @@ -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 { diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs index 46187a28d5b..99c7ca56f2e 100644 --- a/tooling/fuzzer/src/strategies/mod.rs +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -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, + 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) + // 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 @@ -38,7 +44,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,12 +58,11 @@ pub(super) fn arb_value_from_abi_type( elements.prop_map(InputValue::Vec).sboxed() } - AbiType::Struct { fields, .. } => { 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(); @@ -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, + 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 94610dbc829..402e6559752 100644 --- a/tooling/fuzzer/src/strategies/uint.rs +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -7,6 +7,8 @@ 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` @@ -14,7 +16,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, @@ -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) -> 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, } } + /// 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(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() { @@ -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 { 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 @@ -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 { let total_weight = self.random_weight + self.fixtures_weight + self.edge_weight; let bias = runner.rng().gen_range(0..total_weight); diff --git a/tooling/nargo/Cargo.toml b/tooling/nargo/Cargo.toml index 9fb46b78bc9..4587638d693 100644 --- a/tooling/nargo/Cargo.toml +++ b/tooling/nargo/Cargo.toml @@ -28,15 +28,13 @@ tracing.workspace = true serde.workspace = true serde_json.workspace = true walkdir = "2.5.0" +noir_fuzzer = { workspace = true } +proptest = { workspace = true } # Some dependencies are optional so we can compile to Wasm. tokio = { workspace = true, optional = true } rand = { workspace = true, optional = true } -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -noir_fuzzer = { workspace = true } -proptest = { workspace = true } - [dev-dependencies] jsonrpsee = { workspace = true, features = ["server"] } diff --git a/tooling/nargo/src/ops/test.rs b/tooling/nargo/src/ops/test.rs index bfd5cd3713f..a2f94cd61eb 100644 --- a/tooling/nargo/src/ops/test.rs +++ b/tooling/nargo/src/ops/test.rs @@ -96,70 +96,58 @@ where 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; - #[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 runner = - TestRunner::new(Config { failure_persistence: None, ..Config::default() }); + let abi = compiled_program.abi.clone(); + let debug = compiled_program.debug.clone(); - let abi = compiled_program.abi.clone(); - let debug = compiled_program.debug.clone(); + let executor = |program: &Program, + initial_witness: WitnessMap| + -> Result, String> { + // Use a base layer that doesn't handle anything, which we handle in the `execute` below. + let inner_executor = + build_foreign_call_executor(PrintOutput::None, layers::Unhandled); - let executor = - |program: &Program, - initial_witness: WitnessMap| - -> Result, String> { - // Use a base layer that doesn't handle anything, which we handle in the `execute` below. - let inner_executor = - build_foreign_call_executor(PrintOutput::None, layers::Unhandled); - let mut foreign_call_executor = - TestForeignCallExecutor::new(inner_executor); + let mut foreign_call_executor = TestForeignCallExecutor::new(inner_executor); - let circuit_execution = execute_program( - program, - initial_witness, - blackbox_solver, - &mut foreign_call_executor, - ); + let circuit_execution = execute_program( + program, + initial_witness, + blackbox_solver, + &mut foreign_call_executor, + ); - let status = test_status_program_compile_pass( - test_function, - &abi, - &debug, - &circuit_execution, - ); + // Check if a failure was actually expected. + 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()) - } - }; + 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 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 result = fuzzer.fuzz(); + if result.success { + TestStatus::Pass + } else { + TestStatus::Fail { + message: result.reason.unwrap_or_default(), + error_diagnostic: None, } } }