Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add support for encoding/decoding inputs from JSON #1325

Merged
merged 9 commits into from
May 11, 2023
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
Loading