Skip to content

Commit

Permalink
chore: add support for encoding/decoding inputs from JSON (#1325)
Browse files Browse the repository at this point in the history
* chore: move `parse_str_to_field` into `mod.rs`

* chore: clean up stale comment

* feat: add support for reading/writing arguments from json

* chore: update errors to be generic over file formats

* fix: make `AbiTYpeMismatch` generic to abi type

* chore: run serialization tests on all `Format` variants

* chore: cspell

* chore: update error message

* chore: add comments explaining limits on number size
  • Loading branch information
TomAFrench committed May 11, 2023
1 parent a6de557 commit 84673ef
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 37 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion crates/noirc_abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ edition.workspace = true
acvm.workspace = true
iter-extended.workspace = true
toml.workspace = true
serde_json = "1.0"
serde.workspace = true
thiserror.workspace = true

[dev-dependencies]
serde_json = "1.0"
strum = "0.24"
strum_macros = "0.24"
16 changes: 11 additions & 5 deletions crates/noirc_abi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@ use thiserror::Error;

#[derive(Debug, Error)]
pub enum InputParserError {
#[error("input.toml file is badly formed, could not parse, {0}")]
ParseTomlMap(String),
#[error("input file is badly formed, could not parse, {0}")]
ParseInputMap(String),
#[error("Expected witness values to be integers, provided value causes `{0}` error")]
ParseStr(String),
#[error("Could not parse hex value {0}")]
ParseHexStr(String),
#[error("duplicate variable name {0}")]
DuplicateVariableName(String),
#[error("cannot parse a string toml type into {0:?}")]
#[error("cannot parse value into {0:?}")]
AbiTypeMismatch(AbiType),
#[error("Expected argument `{0}`, but none was found")]
MissingArgument(String),
}

impl From<toml::ser::Error> for InputParserError {
fn from(err: toml::ser::Error) -> Self {
Self::ParseTomlMap(err.to_string())
Self::ParseInputMap(err.to_string())
}
}

impl From<toml::de::Error> for InputParserError {
fn from(err: toml::de::Error) -> Self {
Self::ParseTomlMap(err.to_string())
Self::ParseInputMap(err.to_string())
}
}

impl From<serde_json::Error> for InputParserError {
fn from(err: serde_json::Error) -> Self {
Self::ParseInputMap(err.to_string())
}
}

Expand Down
155 changes: 155 additions & 0 deletions crates/noirc_abi/src/input_parser/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use super::{parse_str_to_field, InputValue};
use crate::{errors::InputParserError, Abi, AbiType, MAIN_RETURN_NAME};
use acvm::FieldElement;
use iter_extended::{btree_map, try_btree_map, try_vecmap, vecmap};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

pub(crate) fn parse_json(
input_string: &str,
abi: &Abi,
) -> Result<BTreeMap<String, InputValue>, InputParserError> {
// Parse input.json into a BTreeMap.
let data: BTreeMap<String, JsonTypes> = serde_json::from_str(input_string)?;

// Convert arguments to field elements.
let mut parsed_inputs = try_btree_map(abi.to_btree_map(), |(arg_name, abi_type)| {
// Check that json contains a value for each argument in the ABI.
let value = data
.get(&arg_name)
.ok_or_else(|| InputParserError::MissingArgument(arg_name.clone()))?;

InputValue::try_from_json(value.clone(), &abi_type, &arg_name)
.map(|input_value| (arg_name, input_value))
})?;

// If the json file also includes a return value then we parse it as well.
// This isn't required as the prover calculates the return value itself.
if let (Some(return_type), Some(json_return_value)) =
(&abi.return_type, data.get(MAIN_RETURN_NAME))
{
let return_value =
InputValue::try_from_json(json_return_value.clone(), return_type, MAIN_RETURN_NAME)?;
parsed_inputs.insert(MAIN_RETURN_NAME.to_owned(), return_value);
}

Ok(parsed_inputs)
}

pub(crate) fn serialize_to_json(
w_map: &BTreeMap<String, InputValue>,
) -> Result<String, InputParserError> {
let to_map: BTreeMap<_, _> =
w_map.iter().map(|(key, value)| (key, JsonTypes::from(value.clone()))).collect();

let json_string = serde_json::to_string(&to_map)?;

Ok(json_string)
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
enum JsonTypes {
// This is most likely going to be a hex string
// But it is possible to support UTF-8
String(String),
// Just a regular integer, that can fit in 64 bits.
//
// The JSON spec does not specify any limit on the size of integer number types,
// however we restrict the allowable size. Values which do not fit in a u64 should be passed
// as a string.
Integer(u64),
// Simple boolean flag
Bool(bool),
// Array of regular integers
ArrayNum(Vec<u64>),
// Array of hexadecimal integers
ArrayString(Vec<String>),
// Array of booleans
ArrayBool(Vec<bool>),
// Struct of JsonTypes
Table(BTreeMap<String, JsonTypes>),
}

impl From<InputValue> for JsonTypes {
fn from(value: InputValue) -> Self {
match value {
InputValue::Field(f) => {
let f_str = format!("0x{}", f.to_hex());
JsonTypes::String(f_str)
}
InputValue::Vec(v) => {
let array = v.iter().map(|i| format!("0x{}", i.to_hex())).collect();
JsonTypes::ArrayString(array)
}
InputValue::String(s) => JsonTypes::String(s),
InputValue::Struct(map) => {
let map_with_json_types =
btree_map(map, |(key, value)| (key, JsonTypes::from(value)));
JsonTypes::Table(map_with_json_types)
}
}
}
}

impl InputValue {
fn try_from_json(
value: JsonTypes,
param_type: &AbiType,
arg_name: &str,
) -> Result<InputValue, InputParserError> {
let input_value = match value {
JsonTypes::String(string) => match param_type {
AbiType::String { .. } => InputValue::String(string),
AbiType::Field | AbiType::Integer { .. } | AbiType::Boolean => {
InputValue::Field(parse_str_to_field(&string)?)
}

AbiType::Array { .. } | AbiType::Struct { .. } => {
return Err(InputParserError::AbiTypeMismatch(param_type.clone()))
}
},
JsonTypes::Integer(integer) => {
let new_value = FieldElement::from(i128::from(integer));

InputValue::Field(new_value)
}
JsonTypes::Bool(boolean) => InputValue::Field(boolean.into()),
JsonTypes::ArrayNum(arr_num) => {
let array_elements =
vecmap(arr_num, |elem_num| FieldElement::from(i128::from(elem_num)));

InputValue::Vec(array_elements)
}
JsonTypes::ArrayString(arr_str) => {
let array_elements = try_vecmap(arr_str, |elem_str| parse_str_to_field(&elem_str))?;

InputValue::Vec(array_elements)
}
JsonTypes::ArrayBool(arr_bool) => {
let array_elements = vecmap(arr_bool, FieldElement::from);

InputValue::Vec(array_elements)
}

JsonTypes::Table(table) => match param_type {
AbiType::Struct { fields } => {
let native_table = try_btree_map(fields, |(field_name, abi_type)| {
// Check that json contains a value for each field of the struct.
let field_id = format!("{arg_name}.{field_name}");
let value = table
.get(field_name)
.ok_or_else(|| InputParserError::MissingArgument(field_id.clone()))?;
InputValue::try_from_json(value.clone(), abi_type, &field_id)
.map(|input_value| (field_name.to_string(), input_value))
})?;

InputValue::Struct(native_table)
}
_ => return Err(InputParserError::AbiTypeMismatch(param_type.clone())),
},
};

Ok(input_value)
}
}
92 changes: 92 additions & 0 deletions crates/noirc_abi/src/input_parser/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod json;
mod toml;

use std::{collections::BTreeMap, path::Path};
Expand Down Expand Up @@ -73,13 +74,16 @@ pub trait InitialWitnessParser {

/// The different formats that are supported when parsing
/// the initial witness values
#[cfg_attr(test, derive(strum_macros::EnumIter))]
pub enum Format {
Json,
Toml,
}

impl Format {
pub fn ext(&self) -> &'static str {
match self {
Format::Json => "json",
Format::Toml => "toml",
}
}
Expand All @@ -92,6 +96,7 @@ impl Format {
abi: &Abi,
) -> Result<BTreeMap<String, InputValue>, InputParserError> {
match self {
Format::Json => json::parse_json(input_string, abi),
Format::Toml => toml::parse_toml(input_string, abi),
}
}
Expand All @@ -101,7 +106,94 @@ impl Format {
w_map: &BTreeMap<String, InputValue>,
) -> Result<String, InputParserError> {
match self {
Format::Json => json::serialize_to_json(w_map),
Format::Toml => toml::serialize_to_toml(w_map),
}
}
}

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

use acvm::FieldElement;
use strum::IntoEnumIterator;

use crate::{
input_parser::InputValue, Abi, AbiParameter, AbiType, AbiVisibility, Sign, MAIN_RETURN_NAME,
};

use super::Format;

#[test]
fn serialization_round_trip() {
let abi = Abi {
parameters: vec![
AbiParameter {
name: "foo".into(),
typ: AbiType::Field,
visibility: AbiVisibility::Private,
},
AbiParameter {
name: "bar".into(),
typ: AbiType::Struct {
fields: BTreeMap::from([
("field1".into(), AbiType::Integer { sign: Sign::Unsigned, width: 8 }),
(
"field2".into(),
AbiType::Array { length: 2, typ: Box::new(AbiType::Boolean) },
),
]),
},
visibility: AbiVisibility::Private,
},
],
return_type: Some(AbiType::String { length: 5 }),
// These two fields are unused when serializing/deserializing to file.
param_witnesses: BTreeMap::new(),
return_witnesses: Vec::new(),
};

let input_map: BTreeMap<String, InputValue> = BTreeMap::from([
("foo".into(), InputValue::Field(FieldElement::one())),
(
"bar".into(),
InputValue::Struct(BTreeMap::from([
("field1".into(), InputValue::Field(255u128.into())),
("field2".into(), InputValue::Vec(vec![true.into(), false.into()])),
])),
),
(MAIN_RETURN_NAME.into(), InputValue::String("hello".to_owned())),
]);

for format in Format::iter() {
let serialized_inputs = format.serialize(&input_map).unwrap();

let reconstructed_input_map = format.parse(&serialized_inputs, &abi).unwrap();

assert_eq!(input_map, reconstructed_input_map);
}
}
}

fn parse_str_to_field(value: &str) -> Result<FieldElement, InputParserError> {
if value.starts_with("0x") {
FieldElement::from_hex(value).ok_or_else(|| InputParserError::ParseHexStr(value.to_owned()))
} else {
value
.parse::<i128>()
.map_err(|err_msg| InputParserError::ParseStr(err_msg.to_string()))
.map(FieldElement::from)
}
}

#[cfg(test)]
mod test {
use super::parse_str_to_field;

#[test]
fn parse_empty_str_fails() {
// Check that this fails appropriately rather than being treated as 0, etc.
assert!(parse_str_to_field("").is_err());
}
}
Loading

0 comments on commit 84673ef

Please sign in to comment.