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

feat: add json encoding to noir wasm #1059

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 215 additions & 146 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions crates/noirc_abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,4 @@ iter-extended.workspace = true
toml.workspace = true
serde.workspace = true
thiserror.workspace = true

[dev-dependencies]
serde_json = "1.0"
10 changes: 8 additions & 2 deletions crates/noirc_abi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ use thiserror::Error;

#[derive(Debug, Error)]
pub enum InputParserError {
#[error("input.toml file is badly formed, could not parse, {0}")]
#[error("input file is badly formed, could not parse, {0}")]
ParseTomlMap(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 a string into type {0:?}")]
AbiTypeMismatch(AbiType),
#[error("Expected argument `{0}`, but none was found")]
MissingArgument(String),
Expand All @@ -30,6 +30,12 @@ impl From<toml::de::Error> for InputParserError {
}
}

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

#[derive(Debug, Error)]
pub enum AbiError {
#[error("{0}")]
Expand Down
166 changes: 166 additions & 0 deletions crates/noirc_abi/src/input_parser/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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)?;

// The json map is stored in an ordered BTreeMap. As the keys are strings the map is in alphanumerical order.
// When parsing the json 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)| {
// 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 128 bits
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) => {
let new_value = if boolean { FieldElement::one() } else { FieldElement::zero() };

InputValue::Field(new_value)
}
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, |elem_bool| {
if elem_bool {
FieldElement::one()
} else {
FieldElement::zero()
}
});

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)
}
}
27 changes: 27 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 @@ -74,12 +75,14 @@ pub trait InitialWitnessParser {
/// The different formats that are supported when parsing
/// the initial witness values
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 +95,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 +105,30 @@ 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),
}
}
}

pub(self) 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());
}
}
3 changes: 3 additions & 0 deletions crates/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ crate-type = ["cdylib"]
[dependencies]

acvm.workspace = true
noirc_abi.workspace = true
noirc_driver.workspace = true
noirc_frontend.workspace = true
wasm-bindgen.workspace = true
Expand All @@ -21,6 +22,8 @@ log = "0.4.17"
wasm-logger = "0.2.0"
console_error_panic_hook = "0.1.7"
gloo-utils = { version = "0.1", features = ["serde"] }
serde_json = "1.0"
js-sys = { version = "0.3.61" }

[build-dependencies]
build-data = "0.1.3"
15 changes: 15 additions & 0 deletions crates/wasm/src/js_sys_util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::collections::BTreeMap;

use acvm::{acir::native_types::Witness, FieldElement};

pub(crate) fn witness_map_to_js_map(witness_map: BTreeMap<Witness, FieldElement>) -> js_sys::Map {
let js_map = js_sys::Map::new();
for (witness, field_value) in witness_map.iter() {
let js_idx = js_sys::Number::from(witness.0);
let mut hex_str = "0x".to_owned();
hex_str.push_str(&field_value.to_hex());
let js_hex_str = js_sys::JsString::from(hex_str);
js_map.set(&js_idx, &js_hex_str);
}
js_map
}
Loading