diff --git a/Cargo.lock b/Cargo.lock index 94cb163c7b7..8136f289d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2486,7 +2486,6 @@ dependencies = [ "noirc_errors", "noirc_frontend", "noirc_printable_type", - "proptest", "rand 0.8.5", "rayon", "serde", @@ -2519,6 +2518,7 @@ dependencies = [ "nargo_fmt", "nargo_toml", "noir_debugger", + "noir_fuzzer", "noir_lsp", "noirc_abi", "noirc_driver", @@ -2662,6 +2662,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "noir_fuzzer" +version = "0.30.0" +dependencies = [ + "acvm", + "nargo", + "noirc_abi", + "proptest", + "rand 0.8.5", +] + [[package]] name = "noir_grumpkin" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 639807819ab..11fec0641f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", @@ -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" } diff --git a/temp/Nargo.toml b/temp/Nargo.toml new file mode 100644 index 00000000000..6b9591f1a13 --- /dev/null +++ b/temp/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "temp" +type = "bin" +authors = [""] +compiler_version = ">=0.30.0" + +[dependencies] \ No newline at end of file diff --git a/temp/src/main.nr b/temp/src/main.nr new file mode 100644 index 00000000000..36b35d1f385 --- /dev/null +++ b/temp/src/main.nr @@ -0,0 +1,4 @@ +fn main(x: i64, y: pub i64) { + assert(x != y); +} + diff --git a/tooling/fuzzer/Cargo.toml b/tooling/fuzzer/Cargo.toml new file mode 100644 index 00000000000..5759f592634 --- /dev/null +++ b/tooling/fuzzer/Cargo.toml @@ -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 diff --git a/tooling/nargo/src/ops/fuzz/mod.rs b/tooling/fuzzer/src/lib.rs similarity index 67% rename from tooling/nargo/src/ops/fuzz/mod.rs rename to tooling/fuzzer/src/lib.rs index 58a72adcc91..d523fc371be 100644 --- a/tooling/nargo/src/ops/fuzz/mod.rs +++ b/tooling/fuzzer/src/lib.rs @@ -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`]. /// @@ -40,31 +41,32 @@ impl FuzzedExecutor { // Stores the result and calldata of the last failed call, if any. let counterexample: RefCell = 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 }; diff --git a/tooling/fuzzer/src/strategies/int.rs b/tooling/fuzzer/src/strategies/int.rs new file mode 100644 index 00000000000..c529d2f9501 --- /dev/null +++ b/tooling/fuzzer/src/strategies/int.rs @@ -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, + /// Higher base (by absolute value) + hi: i128, + /// If true cannot be simplified or 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 } + } + + fn reposition(&mut self) -> bool { + let interval = self.hi - self.lo; + let new_mid = self.lo + interval / 2i128; + + if new_mid == self.curr { + false + } else { + self.curr = new_mid; + 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 + } + + 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 { + 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 { + 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 { + 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); + } +} diff --git a/tooling/fuzzer/src/strategies/mod.rs b/tooling/fuzzer/src/strategies/mod.rs new file mode 100644 index 00000000000..f5b03953ba8 --- /dev/null +++ b/tooling/fuzzer/src/strategies/mod.rs @@ -0,0 +1,97 @@ +use int::IntStrategy; +use prop::collection::vec; +use proptest::prelude::*; + +use acvm::{AcirField, FieldElement}; + +use noirc_abi::{input_parser::InputValue, Abi, AbiType, InputMap, Sign}; +use std::collections::BTreeMap; +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 { + 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) + .prop_map(|uint| InputValue::Field(uint.into())) + .sboxed() + } + AbiType::Integer { width, .. } => { + let shift = 2i128.pow(*width); + IntStrategy::new(*width as usize) + .prop_map(move |mut int| { + if int < 0 { + int += shift + } + InputValue::Field(int.into()) + }) + .sboxed() + } + 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}}}"); + proptest::string::string_regex(&string_regex) + .expect("parsing of regex should always succeed") + .prop_map(InputValue::String) + .sboxed() + } + AbiType::Array { length, typ } => { + let length = *length as usize; + let elements = vec(arb_value_from_abi_type(typ), length..=length); + + 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)).sboxed()) + .collect(); + + fields + .prop_map(|fields| { + let fields: BTreeMap<_, _> = fields.into_iter().collect(); + InputValue::Struct(fields) + }) + .sboxed() + } + + AbiType::Tuple { fields } => { + let fields: Vec<_> = fields.iter().map(arb_value_from_abi_type).collect(); + fields.prop_map(InputValue::Vec).sboxed() + } + } +} + +pub(super) fn arb_input_map(abi: &Abi) -> BoxedStrategy { + let values: Vec<_> = abi + .parameters + .iter() + .map(|param| (Just(param.name.clone()), arb_value_from_abi_type(¶m.typ))) + .collect(); + + values + .prop_map(|values| { + let input_map: InputMap = values.into_iter().collect(); + input_map + }) + .boxed() +} diff --git a/tooling/fuzzer/src/strategies/uint.rs b/tooling/fuzzer/src/strategies/uint.rs new file mode 100644 index 00000000000..a7ecf89e5e1 --- /dev/null +++ b/tooling/fuzzer/src/strategies/uint.rs @@ -0,0 +1,152 @@ +use proptest::{ + strategy::{NewTree, Strategy, ValueTree}, + test_runner::TestRunner, +}; +use rand::Rng; + +/// Value tree for unsigned ints (up to uint256). +pub struct UintValueTree { + /// Lower base + lo: u128, + /// Current value + curr: u128, + /// Higher base + hi: u128, + /// If true cannot be simplified or complexified + fixed: bool, +} + +impl UintValueTree { + /// 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: u128, fixed: bool) -> Self { + Self { lo: 0, curr: start, hi: start, fixed } + } + + fn reposition(&mut self) -> bool { + let interval = self.hi - self.lo; + let new_mid = self.lo + interval / 2; + + if new_mid == self.curr { + false + } else { + self.curr = new_mid; + true + } + } +} + +impl ValueTree for UintValueTree { + type Value = u128; + + fn current(&self) -> Self::Value { + self.curr + } + + fn simplify(&mut self) -> bool { + if self.fixed || (self.hi <= self.lo) { + return false; + } + self.hi = self.curr; + self.reposition() + } + + fn complicate(&mut self) -> bool { + if self.fixed || (self.hi <= self.lo) { + return false; + } + + self.lo = self.curr.wrapping_add(1); + self.reposition() + } +} + +/// Value tree for unsigned ints (up to uint256). +/// 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 0 and max possible value) +/// 3. Generate a value from a predefined fixtures set +/// +/// To define uint fixtures: +/// - return an array of possible values for a parameter named `amount` declare a function `function +/// fixture_amount() public returns (uint32[] memory)`. +/// - use `amount` named parameter in fuzzed test in order to include fixtures in fuzzed values +/// `function testFuzz_uint32(uint32 amount)`. +/// +/// If fixture is not a valid uint type then error is raised and random value generated. +#[derive(Debug)] +pub struct UintStrategy { + /// Bit size of uint (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 UintStrategy { + /// 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 { + 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(UintValueTree::new(start, false)) + } + + fn generate_random_tree(&self, runner: &mut TestRunner) -> NewTree { + let rng = runner.rng(); + let start: u128 = rng.gen_range(0..=self.type_max()); + + Ok(UintValueTree::new(start, false)) + } + + fn type_max(&self) -> u128 { + if self.bits < 128 { + (1 << self.bits) - 1 + } else { + u128::MAX + } + } +} + +impl Strategy for UintStrategy { + type Tree = UintValueTree; + type Value = u128; + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let total_weight = self.random_weight + self.edge_weight; + let bias = runner.rng().gen_range(0..total_weight); + // randomly select one of 3 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::uint::UintValueTree; + use proptest::strategy::ValueTree; + + #[test] + fn test_uint_tree_complicate_max() { + let mut uint_tree = UintValueTree::new(u128::MAX, false); + assert_eq!(uint_tree.hi, u128::MAX); + assert_eq!(uint_tree.curr, u128::MAX); + uint_tree.complicate(); + assert_eq!(uint_tree.lo, u128::MIN); + } +} diff --git a/tooling/nargo/src/ops/fuzz/types.rs b/tooling/fuzzer/src/types.rs similarity index 100% rename from tooling/nargo/src/ops/fuzz/types.rs rename to tooling/fuzzer/src/types.rs diff --git a/tooling/nargo/Cargo.toml b/tooling/nargo/Cargo.toml index 07333c6ef86..8abec267d20 100644 --- a/tooling/nargo/Cargo.toml +++ b/tooling/nargo/Cargo.toml @@ -25,7 +25,6 @@ tracing.workspace = true rayon = "1.8.0" jsonrpc.workspace = true rand.workspace = true -proptest.workspace = true [dev-dependencies] # TODO: This dependency is used to generate unit tests for `get_all_paths_in_dir` diff --git a/tooling/nargo/src/ops/mod.rs b/tooling/nargo/src/ops/mod.rs index 53d55b5d750..cada2f0e915 100644 --- a/tooling/nargo/src/ops/mod.rs +++ b/tooling/nargo/src/ops/mod.rs @@ -12,7 +12,6 @@ pub use self::test::{run_test, TestStatus}; mod compile; mod execute; mod foreign_calls; -pub mod fuzz; mod optimize; mod test; mod transform; diff --git a/tooling/nargo_cli/Cargo.toml b/tooling/nargo_cli/Cargo.toml index 0d934322c75..aad7f93ff75 100644 --- a/tooling/nargo_cli/Cargo.toml +++ b/tooling/nargo_cli/Cargo.toml @@ -32,6 +32,7 @@ noirc_driver.workspace = true noirc_frontend = { workspace = true, features = ["bn254"] } noirc_abi.workspace = true noirc_errors.workspace = true +noir_fuzzer.workspace = true acvm = { workspace = true, features = ["bn254"] } bn254_blackbox_solver.workspace = true toml.workspace = true diff --git a/tooling/nargo_cli/src/cli/execute_cmd.rs b/tooling/nargo_cli/src/cli/execute_cmd.rs index de4f54bd605..b548336275b 100644 --- a/tooling/nargo_cli/src/cli/execute_cmd.rs +++ b/tooling/nargo_cli/src/cli/execute_cmd.rs @@ -6,7 +6,6 @@ use clap::Args; use nargo::artifacts::debug::DebugArtifact; use nargo::constants::PROVER_INPUT_FILE; use nargo::errors::try_to_diagnose_runtime_error; -use nargo::ops::fuzz::FuzzedExecutor; use nargo::ops::DefaultForeignCallExecutor; use nargo::package::Package; use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; @@ -14,7 +13,6 @@ use noirc_abi::input_parser::{Format, InputValue}; use noirc_abi::InputMap; use noirc_driver::{CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING}; use noirc_frontend::graph::CrateName; -use proptest::test_runner::TestRunner; use super::compile_cmd::compile_workspace_full; use super::fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir}; @@ -114,13 +112,6 @@ pub(crate) fn execute_program( ) -> Result, CliError> { let initial_witness = compiled_program.abi.encode(inputs_map, None)?; - let runner = TestRunner::default(); - let fuzzer = FuzzedExecutor::new(compiled_program.clone().into(), runner); - - let result = fuzzer.fuzz(); - - println!("{result:?}"); - let solved_witness_stack_err = nargo::ops::execute_program( &compiled_program.program, initial_witness, diff --git a/tooling/nargo_cli/src/cli/fuzz_cmd.rs b/tooling/nargo_cli/src/cli/fuzz_cmd.rs new file mode 100644 index 00000000000..430e7b2d79b --- /dev/null +++ b/tooling/nargo_cli/src/cli/fuzz_cmd.rs @@ -0,0 +1,71 @@ +use clap::Args; + +use nargo::artifacts::program::ProgramArtifact; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use noir_fuzzer::FuzzedExecutor; +use noirc_driver::{CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING}; +use noirc_frontend::graph::CrateName; +use proptest::test_runner::TestRunner; + +use super::compile_cmd::compile_workspace_full; +use super::NargoConfig; +use crate::cli::fs::program::read_program_from_file; +use crate::errors::CliError; + +/// Executes a circuit to calculate its return value +#[derive(Debug, Clone, Args)] +#[clap(visible_alias = "e")] +pub(crate) struct FuzzCommand { + /// The name of the package to execute + #[clap(long, conflicts_with = "workspace")] + package: Option, + + /// Execute all packages in the workspace + #[clap(long, conflicts_with = "package")] + workspace: bool, + + #[clap(flatten)] + compile_options: CompileOptions, + + /// JSON RPC url to solve oracle calls + #[clap(long)] + oracle_resolver: Option, +} + +pub(crate) fn run(args: FuzzCommand, config: NargoConfig) -> Result<(), CliError> { + let toml_path = get_package_manifest(&config.program_dir)?; + let default_selection = + if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; + let selection = args.package.map_or(default_selection, PackageSelection::Selected); + let workspace = resolve_workspace_from_toml( + &toml_path, + selection, + Some(NOIR_ARTIFACT_VERSION_STRING.to_string()), + )?; + + // Compile the full workspace in order to generate any build artifacts. + compile_workspace_full(&workspace, &args.compile_options)?; + + let binary_packages = workspace.into_iter().filter(|package| package.is_binary()); + for package in binary_packages { + let program_artifact_path = workspace.package_build_path(package); + let program: CompiledProgram = read_program_from_file(program_artifact_path)?.into(); + + fuzz_program(program.into(), args.oracle_resolver.as_deref())?; + } + Ok(()) +} + +fn fuzz_program( + compiled_program: ProgramArtifact, + _foreign_call_resolver_url: Option<&str>, +) -> Result<(), CliError> { + let runner = TestRunner::default(); + let fuzzer = FuzzedExecutor::new(compiled_program, runner); + + let result = fuzzer.fuzz(); + + println!("{result:?}"); + + Ok(()) +} diff --git a/tooling/nargo_cli/src/cli/mod.rs b/tooling/nargo_cli/src/cli/mod.rs index 485ccc7abaf..53a09f61736 100644 --- a/tooling/nargo_cli/src/cli/mod.rs +++ b/tooling/nargo_cli/src/cli/mod.rs @@ -15,6 +15,7 @@ mod debug_cmd; mod execute_cmd; mod export_cmd; mod fmt_cmd; +mod fuzz_cmd; mod info_cmd; mod init_cmd; mod lsp_cmd; @@ -66,6 +67,7 @@ enum NargoCommand { #[command(hide = true)] // Hidden while the feature is being built out Debug(debug_cmd::DebugCommand), Test(test_cmd::TestCommand), + Fuzz(fuzz_cmd::FuzzCommand), Info(info_cmd::InfoCommand), Lsp(lsp_cmd::LspCommand), #[command(hide = true)] @@ -98,6 +100,7 @@ pub(crate) fn start_cli() -> eyre::Result<()> { NargoCommand::Execute(args) => execute_cmd::run(args, config), NargoCommand::Export(args) => export_cmd::run(args, config), NargoCommand::Test(args) => test_cmd::run(args, config), + NargoCommand::Fuzz(args) => fuzz_cmd::run(args, config), NargoCommand::Info(args) => info_cmd::run(args, config), NargoCommand::Lsp(args) => lsp_cmd::run(args, config), NargoCommand::Dap(args) => dap_cmd::run(args, config), diff --git a/tooling/noirc_abi/src/arbitrary.rs b/tooling/noirc_abi/src/arbitrary.rs index ef3630c2550..c68d36d1576 100644 --- a/tooling/noirc_abi/src/arbitrary.rs +++ b/tooling/noirc_abi/src/arbitrary.rs @@ -1,19 +1,15 @@ -use iter_extended::{btree_map, vecmap}; use prop::collection::vec; use proptest::prelude::*; use acvm::{AcirField, FieldElement}; -use crate::{ - input_parser::InputValue, Abi, AbiParameter, AbiReturnType, AbiType, AbiVisibility, InputMap, - Sign, -}; +use crate::{input_parser::InputValue, Abi, AbiType, InputMap, Sign}; use std::collections::{BTreeMap, HashSet}; pub(super) use proptest_derive::Arbitrary; /// Mutates an iterator of mutable references to [`String`]s to ensure that all values are unique. -fn ensure_unique_strings<'a>(iter: impl Iterator) { +pub(super) fn ensure_unique_strings<'a>(iter: impl Iterator) { let mut seen_values: HashSet = HashSet::default(); for value in iter { while seen_values.contains(value.as_str()) { @@ -32,7 +28,7 @@ proptest::prop_compose! { } } -fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy { +pub(super) fn arb_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy { match abi_type { AbiType::Field => vec(any::(), 32) .prop_map(|bytes| InputValue::Field(FieldElement::from_be_bytes_reduce(&bytes))) @@ -124,22 +120,6 @@ pub(super) fn arb_abi_type() -> BoxedStrategy { .boxed() } -fn arb_abi_param_and_value() -> BoxedStrategy<(AbiParameter, InputValue)> { - arb_abi_type() - .prop_flat_map(|typ| { - let value = arb_value_from_abi_type(&typ); - let param = arb_abi_param(typ); - (param, value) - }) - .boxed() -} - -fn arb_abi_param(typ: AbiType) -> SBoxedStrategy { - (".+", any::()) - .prop_map(move |(name, visibility)| AbiParameter { name, typ: typ.clone(), visibility }) - .sboxed() -} - pub fn arb_input_map(abi: &Abi) -> BoxedStrategy { let values: Vec<_> = abi .parameters @@ -154,17 +134,3 @@ pub fn arb_input_map(abi: &Abi) -> BoxedStrategy { }) .boxed() } - -prop_compose! { - pub(super) fn arb_abi_and_input_map() - (mut parameters_with_values in proptest::collection::vec(arb_abi_param_and_value(), 0..100), return_type: Option) - -> (Abi, InputMap) { - // Require that all parameter names are unique. - ensure_unique_strings(parameters_with_values.iter_mut().map(|(param_name,_)| &mut param_name.name)); - - let parameters = vecmap(¶meters_with_values, |(param, _)| param.clone()); - let input_map = btree_map(parameters_with_values, |(param, value)| (param.name, value)); - - (Abi { parameters, return_type, error_types: BTreeMap::default() }, input_map) - } -} diff --git a/tooling/noirc_abi/src/lib.rs b/tooling/noirc_abi/src/lib.rs index bc9f9d6d947..ee2329111ac 100644 --- a/tooling/noirc_abi/src/lib.rs +++ b/tooling/noirc_abi/src/lib.rs @@ -496,9 +496,46 @@ pub fn display_abi_error( #[cfg(test)] mod test { + use std::collections::BTreeMap; + + use iter_extended::{btree_map, vecmap}; use proptest::prelude::*; - use crate::arbitrary::arb_abi_and_input_map; + use crate::{ + arbitrary::{arb_abi_type, arb_value_from_abi_type, ensure_unique_strings}, + input_parser::InputValue, + Abi, AbiParameter, AbiType, AbiVisibility, InputMap, + }; + + fn arb_abi_param_and_value() -> BoxedStrategy<(AbiParameter, InputValue)> { + arb_abi_type() + .prop_flat_map(|typ| { + let value = arb_value_from_abi_type(&typ); + let param = arb_abi_param(typ); + (param, value) + }) + .boxed() + } + + fn arb_abi_param(typ: AbiType) -> SBoxedStrategy { + (".+", any::()) + .prop_map(move |(name, visibility)| AbiParameter { name, typ: typ.clone(), visibility }) + .sboxed() + } + + prop_compose! { + pub(super) fn arb_abi_and_input_map() + (mut parameters_with_values in proptest::collection::vec(arb_abi_param_and_value(), 0..100), return_type: Option) + -> (Abi, InputMap) { + // Require that all parameter names are unique. + ensure_unique_strings(parameters_with_values.iter_mut().map(|(param_name,_)| &mut param_name.name)); + + let parameters = vecmap(¶meters_with_values, |(param, _)| param.clone()); + let input_map = btree_map(parameters_with_values, |(param, value)| (param.name, value)); + + (Abi { parameters, return_type, error_types: BTreeMap::default() }, input_map) + } + } proptest! { #[test]