diff --git a/Cargo.lock b/Cargo.lock index 6fe210ab93a..28a3f4fd2ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1922,6 +1922,8 @@ dependencies = [ "iter-extended", "serde", "serde_json", + "strum", + "strum_macros", "thiserror", "toml", ] diff --git a/crates/noirc_abi/Cargo.toml b/crates/noirc_abi/Cargo.toml index 3d12afc8293..6af0cfe78b3 100644 --- a/crates/noirc_abi/Cargo.toml +++ b/crates/noirc_abi/Cargo.toml @@ -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" \ No newline at end of file diff --git a/crates/noirc_abi/src/errors.rs b/crates/noirc_abi/src/errors.rs index 4dc8a4bdc41..80f9d665dff 100644 --- a/crates/noirc_abi/src/errors.rs +++ b/crates/noirc_abi/src/errors.rs @@ -4,15 +4,15 @@ 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), @@ -20,13 +20,19 @@ pub enum InputParserError { impl From for InputParserError { fn from(err: toml::ser::Error) -> Self { - Self::ParseTomlMap(err.to_string()) + Self::ParseInputMap(err.to_string()) } } impl From for InputParserError { fn from(err: toml::de::Error) -> Self { - Self::ParseTomlMap(err.to_string()) + Self::ParseInputMap(err.to_string()) + } +} + +impl From for InputParserError { + fn from(err: serde_json::Error) -> Self { + Self::ParseInputMap(err.to_string()) } } diff --git a/crates/noirc_abi/src/input_parser/json.rs b/crates/noirc_abi/src/input_parser/json.rs new file mode 100644 index 00000000000..d0d53ce38b2 --- /dev/null +++ b/crates/noirc_abi/src/input_parser/json.rs @@ -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, InputParserError> { + // Parse input.json into a BTreeMap. + let data: BTreeMap = 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, +) -> Result { + 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), + // Array of hexadecimal integers + ArrayString(Vec), + // Array of booleans + ArrayBool(Vec), + // Struct of JsonTypes + Table(BTreeMap), +} + +impl From 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 { + 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) + } +} diff --git a/crates/noirc_abi/src/input_parser/mod.rs b/crates/noirc_abi/src/input_parser/mod.rs index cf9e0909f57..1b54cb84df8 100644 --- a/crates/noirc_abi/src/input_parser/mod.rs +++ b/crates/noirc_abi/src/input_parser/mod.rs @@ -1,3 +1,4 @@ +mod json; mod toml; use std::{collections::BTreeMap, path::Path}; @@ -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", } } @@ -92,6 +96,7 @@ impl Format { abi: &Abi, ) -> Result, InputParserError> { match self { + Format::Json => json::parse_json(input_string, abi), Format::Toml => toml::parse_toml(input_string, abi), } } @@ -101,7 +106,94 @@ impl Format { w_map: &BTreeMap, ) -> Result { 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 = 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 { + if value.starts_with("0x") { + FieldElement::from_hex(value).ok_or_else(|| InputParserError::ParseHexStr(value.to_owned())) + } else { + value + .parse::() + .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()); + } +} diff --git a/crates/noirc_abi/src/input_parser/toml.rs b/crates/noirc_abi/src/input_parser/toml.rs index a737f784031..a5a50456e23 100644 --- a/crates/noirc_abi/src/input_parser/toml.rs +++ b/crates/noirc_abi/src/input_parser/toml.rs @@ -1,4 +1,4 @@ -use super::InputValue; +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}; @@ -12,18 +12,13 @@ pub(crate) fn parse_toml( // Parse input.toml into a BTreeMap. let data: BTreeMap = toml::from_str(input_string)?; - // The toml map is stored in an ordered BTreeMap. As the keys are strings the map is in alphanumerical order. - // When parsing the toml map we recursively go through each field to enable struct inputs. - // To match this map with the correct abi type we reorganize our abi by parameter name in a BTreeMap, while the struct fields - // in the abi are already stored in a BTreeMap. - let abi_map = abi.to_btree_map(); - // Convert arguments to field elements. - let mut parsed_inputs = try_btree_map(abi_map, |(arg_name, abi_type)| { + let mut parsed_inputs = try_btree_map(abi.to_btree_map(), |(arg_name, abi_type)| { // Check that toml 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_toml(value.clone(), &abi_type, &arg_name) .map(|input_value| (arg_name, input_value)) })?; @@ -58,7 +53,8 @@ enum TomlTypes { // 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 128 bits + // Just a regular integer, that can fit in 64 bits + // Note that the toml spec specifies that all numbers are represented as `i64`s. Integer(u64), // Simple boolean flag Bool(bool), @@ -154,25 +150,3 @@ impl InputValue { Ok(input_value) } } - -fn parse_str_to_field(value: &str) -> Result { - if value.starts_with("0x") { - FieldElement::from_hex(value).ok_or_else(|| InputParserError::ParseHexStr(value.to_owned())) - } else { - value - .parse::() - .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()); - } -}