Skip to content

Commit

Permalink
chore: add property tests for ABI encoding (#5216)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves #5226

## Summary\*

This PR adds property testing to ABI encoding so that we can show that
any valid `Abi` + `InputMap` combination can be abi encoded and decoded
to return the original inputs.

## Additional Context



## Documentation\*

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
TomAFrench authored Jun 13, 2024
1 parent 85f4093 commit 8bfaa93
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 57 deletions.
25 changes: 16 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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"
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"rfind",
"rustc",
"rustup",
"sboxed",
"schnorr",
"sdiv",
"secp256k1",
Expand Down
5 changes: 4 additions & 1 deletion tooling/noirc_abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ num-traits = "0.2"
[dev-dependencies]
strum = "0.24"
strum_macros = "0.24"
proptest.workspace = true
proptest-derive.workspace = true


[features]
bn254 = ["acvm/bn254"]
bls12_381 = ["acvm/bls12_381"]
bls12_381 = ["acvm/bls12_381"]
154 changes: 154 additions & 0 deletions tooling/noirc_abi/src/arbitrary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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 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<Item = &'a mut String>) {
let mut seen_values: HashSet<String> = 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_value_from_abi_type(abi_type: &AbiType) -> SBoxedStrategy<InputValue> {
match abi_type {
AbiType::Field => vec(any::<u8>(), 32)
.prop_map(|bytes| InputValue::Field(FieldElement::from_be_bytes_reduce(&bytes)))
.sboxed(),
AbiType::Integer { width, .. } => {
arb_field_from_integer(*width).prop_map(InputValue::Field).sboxed()
}

AbiType::Boolean => {
any::<bool>().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<SBoxedStrategy<(String, InputValue)>> = 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()
}
}
}

fn arb_primitive_abi_type() -> SBoxedStrategy<AbiType> {
const MAX_STRING_LEN: u32 = 1000;
proptest::prop_oneof![
Just(AbiType::Field),
Just(AbiType::Boolean),
any::<(Sign, u32)>().prop_map(|(sign, width)| {
let width = (width % 128).clamp(1, 127);
AbiType::Integer { sign, width }
}),
// restrict length of strings to avoid running out of memory
(1..MAX_STRING_LEN).prop_map(|length| AbiType::String { length }),
]
.sboxed()
}

pub(super) fn arb_abi_type() -> BoxedStrategy<AbiType> {
let leaf = arb_primitive_abi_type();

leaf.prop_recursive(
8, // up to 8 levels deep
256, // Shoot for maximum size of 256 nodes
10, // We put up to 10 items per collection
|inner| {
prop_oneof![
(1..10u32, inner.clone())
.prop_map(|(length, typ)| { AbiType::Array { length, typ: Box::new(typ) } })
.boxed(),
prop::collection::vec(inner.clone(), 1..10)
.prop_map(|fields| { AbiType::Tuple { fields } })
.boxed(),
(".*", prop::collection::vec((".+", inner), 1..10))
.prop_map(|(path, mut fields)| {
// Require that all field names are unique.
ensure_unique_strings(fields.iter_mut().map(|(field_name, _)| field_name));
AbiType::Struct { path, fields }
})
.boxed(),
]
},
)
.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<AbiParameter> {
(".+", any::<AbiVisibility>())
.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<AbiReturnType>)
-> (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(&parameters_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)
}
}
67 changes: 20 additions & 47 deletions tooling/noirc_abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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;
Expand Down Expand Up @@ -75,6 +78,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 {
Expand All @@ -86,6 +90,7 @@ pub enum AbiVisibility {
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(test, derive(arbitrary::Arbitrary))]
#[serde(rename_all = "lowercase")]
pub enum Sign {
Unsigned,
Expand Down Expand Up @@ -142,10 +147,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,
}
Expand All @@ -157,16 +164,20 @@ 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<AbiParameter>,
pub return_type: Option<AbiReturnType>,
#[cfg_attr(test, proptest(strategy = "proptest::prelude::Just(BTreeMap::from([]))"))]
pub error_types: BTreeMap<ErrorSelector, AbiErrorType>,
}

Expand Down Expand Up @@ -488,56 +499,18 @@ pub fn display_abi_error<F: AcirField>(

#[cfg(test)]
mod test {
use std::collections::BTreeMap;

use acvm::{AcirField, FieldElement};
use proptest::prelude::*;

use crate::{
input_parser::InputValue, Abi, AbiParameter, AbiReturnType, AbiType, AbiVisibility,
InputMap,
};
use crate::arbitrary::arb_abi_and_input_map;

#[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(),
};
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();

// 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());
}
}

0 comments on commit 8bfaa93

Please sign in to comment.