diff --git a/Cargo.lock b/Cargo.lock index 3480a785d70..7c772cc29a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2730,6 +2730,8 @@ dependencies = [ "noirc_printable_type", "num-bigint", "num-traits", + "proptest", + "proptest-derive", "serde", "serde_json", "strum", @@ -3329,9 +3331,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", @@ -3341,12 +3343,23 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.7.4", + "regex-syntax 0.8.2", "rusty-fork", "tempfile", "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 = "quick-error" version = "1.2.3" @@ -3578,12 +3591,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "regex-syntax" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - [[package]] name = "regex-syntax" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 66a19bf7766..012590e2cf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,6 +133,8 @@ jsonrpc = { version = "0.16.0", features = ["minreq_http"] } flate2 = "1.0.24" rand = "0.8.5" proptest = "1.2.0" +proptest-derive = "0.4.0" + im = { version = "15.1", features = ["serde"] } tracing = "0.1.40" diff --git a/tooling/noirc_abi/Cargo.toml b/tooling/noirc_abi/Cargo.toml index baae2dfa35e..40e05af7ab1 100644 --- a/tooling/noirc_abi/Cargo.toml +++ b/tooling/noirc_abi/Cargo.toml @@ -23,6 +23,9 @@ num-traits = "0.2" [dev-dependencies] strum = "0.24" strum_macros = "0.24" +proptest.workspace = true +proptest-derive.workspace = true + [features] bn254 = [ @@ -32,4 +35,4 @@ bn254 = [ bls12_381 = [ "acvm/bls12_381", "noirc_frontend/bls12_381", -] \ No newline at end of file +] diff --git a/tooling/noirc_abi/src/arbitrary.rs b/tooling/noirc_abi/src/arbitrary.rs new file mode 100644 index 00000000000..d21ae21d45c --- /dev/null +++ b/tooling/noirc_abi/src/arbitrary.rs @@ -0,0 +1,117 @@ +use iter_extended::{btree_map, vecmap}; +use proptest::prelude::*; + +use acvm::FieldElement; + +use crate::{ + input_parser::InputValue, Abi, AbiParameter, AbiReturnType, AbiType, AbiVisibility, 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) { + let mut seen_values: HashSet = HashSet::default(); + for value in iter { + while seen_values.contains(value.as_str()) { + value.push('1'); + } + seen_values.insert(value.clone()); + } +} + +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; + FieldElement::from(value.clamp(0, max_value)) + } +} + +fn arb_primitive_abi_type_and_value( +) -> impl proptest::strategy::Strategy { + proptest::prop_oneof![ + any::().prop_map(|val| (AbiType::Field, InputValue::Field(FieldElement::from(val)))), + any::<(Sign, u32)>().prop_flat_map(|(sign, width)| { + let width = (width % 128).clamp(1, 127); + ( + Just(AbiType::Integer { sign, width }), + arb_field_from_integer(width).prop_map(InputValue::Field), + ) + }), + any::() + .prop_map(|val| (AbiType::Boolean, InputValue::Field(FieldElement::from(val)))), + ".+".prop_map(|str| ( + AbiType::String { length: str.len() as u32 }, + InputValue::String(str) + )) + ] +} + +fn arb_abi_type_and_value() -> impl proptest::strategy::Strategy { + let leaf = arb_primitive_abi_type_and_value(); + + leaf.prop_recursive( + 8, // 8 levels deep + 256, // Shoot for maximum size of 256 nodes + 10, // We put up to 10 items per collection + |inner| { + prop_oneof![ + // TODO: support `AbiType::Array`. + // This is non-trivial due to the need to get N `InputValue`s which are all compatible with + // the element's `AbiType`.` + prop::collection::vec(inner.clone(), 1..10).prop_map(|typ| { + let (fields, values): (Vec<_>, Vec<_>) = typ.into_iter().unzip(); + let tuple_type = AbiType::Tuple { fields }; + (tuple_type, InputValue::Vec(values)) + }), + (".*", prop::collection::vec((inner.clone(), ".*"), 1..10)).prop_map( + |(path, mut typ)| { + // Require that all field names are unique. + ensure_unique_strings(typ.iter_mut().map(|(_, field_name)| field_name)); + + let (types_and_values, names): (Vec<_>, Vec<_>) = typ.into_iter().unzip(); + let (types, values): (Vec<_>, Vec<_>) = + types_and_values.into_iter().unzip(); + + let fields = names.clone().into_iter().zip(types).collect(); + let struct_values = names.into_iter().zip(values).collect(); + let struct_type = AbiType::Struct { path, fields }; + + (struct_type, InputValue::Struct(struct_values)) + } + ), + ] + }, + ) +} + +proptest::prop_compose! { + pub(super) fn arb_abi_type()((typ, _) in arb_abi_type_and_value())-> AbiType { + typ + } +} + +prop_compose! { + fn arb_abi_param_and_value() + ((typ, value) in arb_abi_type_and_value(), name: String, visibility: AbiVisibility) + -> (AbiParameter, InputValue) { + (AbiParameter{ name, typ, visibility }, value) + } +} + +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 2abb28e2538..547acf65169 100644 --- a/tooling/noirc_abi/src/lib.rs +++ b/tooling/noirc_abi/src/lib.rs @@ -27,6 +27,9 @@ use std::{collections::BTreeMap, str}; // // This ABI has nothing to do with ACVM or ACIR. Although they implicitly have a relationship +#[cfg(test)] +mod arbitrary; + pub mod errors; pub mod input_parser; mod serialization; @@ -77,6 +80,7 @@ pub enum AbiType { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] #[serde(rename_all = "lowercase")] /// Represents whether the parameter is public or known only to the prover. pub enum AbiVisibility { @@ -123,6 +127,7 @@ pub enum AbiDistinctness { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] #[serde(rename_all = "lowercase")] pub enum Sign { Unsigned, @@ -243,10 +248,12 @@ impl From<&AbiType> for PrintableType { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] /// An argument or return value of the circuit's `main` function. pub struct AbiParameter { pub name: String, #[serde(rename = "type")] + #[cfg_attr(test, proptest(strategy = "arbitrary::arb_abi_type()"))] pub typ: AbiType, pub visibility: AbiVisibility, } @@ -258,16 +265,21 @@ impl AbiParameter { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] + pub struct AbiReturnType { + #[cfg_attr(test, proptest(strategy = "arbitrary::arb_abi_type()"))] pub abi_type: AbiType, pub visibility: AbiVisibility, } #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(arbitrary::Arbitrary))] pub struct Abi { /// An ordered list of the arguments to the program's `main` function, specifying their types and visibility. pub parameters: Vec, pub return_type: Option, + #[cfg_attr(test, proptest(strategy = "proptest::prelude::Just(BTreeMap::from([]))"))] pub error_types: BTreeMap, } @@ -605,56 +617,18 @@ pub fn display_abi_error( #[cfg(test)] mod test { - use std::collections::BTreeMap; + use proptest::prelude::*; - use acvm::{AcirField, FieldElement}; + use crate::arbitrary::arb_abi_and_input_map; - use crate::{ - input_parser::InputValue, Abi, AbiParameter, AbiReturnType, AbiType, AbiVisibility, - InputMap, - }; + proptest! { + #[test] + fn encoding_and_decoding_returns_original_witness_map((abi, input_map) in arb_abi_and_input_map()) { + let witness_map = abi.encode(&input_map, None).unwrap(); + let (decoded_inputs, return_value) = abi.decode(&witness_map).unwrap(); - #[test] - fn witness_encoding_roundtrip() { - let abi = Abi { - parameters: vec![ - AbiParameter { - name: "thing1".to_string(), - typ: AbiType::Array { length: 2, typ: Box::new(AbiType::Field) }, - visibility: AbiVisibility::Public, - }, - AbiParameter { - name: "thing2".to_string(), - typ: AbiType::Field, - visibility: AbiVisibility::Public, - }, - ], - return_type: Some(AbiReturnType { - abi_type: AbiType::Field, - visibility: AbiVisibility::Public, - }), - error_types: BTreeMap::default(), - }; - - // Note we omit return value from inputs - let inputs: InputMap = BTreeMap::from([ - ( - "thing1".to_string(), - InputValue::Vec(vec![ - InputValue::Field(FieldElement::one()), - InputValue::Field(FieldElement::one()), - ]), - ), - ("thing2".to_string(), InputValue::Field(FieldElement::zero())), - ]); - - let witness_map = abi.encode(&inputs, None).unwrap(); - let (reconstructed_inputs, return_value) = abi.decode(&witness_map).unwrap(); - - for (key, expected_value) in inputs { - assert_eq!(reconstructed_inputs[&key], expected_value); + prop_assert_eq!(decoded_inputs, input_map); + prop_assert_eq!(return_value, None); } - - assert!(return_value.is_none()); } }