diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts index c773e80ea43..bdc44d8db5a 100644 --- a/compiler/integration-tests/test/browser/recursion.test.ts +++ b/compiler/integration-tests/test/browser/recursion.test.ts @@ -9,6 +9,7 @@ import { acvm, abi, generateWitness } from '@noir-lang/noir_js'; import * as TOML from 'smol-toml'; import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'; import { getFile } from './utils.js'; +import { Field, InputMap } from '@noir-lang/noirc_abi'; const logger = new Logger({ name: 'test', minLevel: TEST_LOG_LEVEL }); @@ -50,7 +51,7 @@ describe('It compiles noir program code, receiving circuit bytes and abi object. it('Should generate valid inner proof for correct input, then verify proof within a proof', async () => { const main_program = await getCircuit(circuit_main_source); - const main_inputs = TOML.parse(circuit_main_toml); + const main_inputs: InputMap = TOML.parse(circuit_main_toml) as InputMap; const main_backend = new BarretenbergBackend(main_program); @@ -69,10 +70,10 @@ describe('It compiles noir program code, receiving circuit bytes and abi object. numPublicInputs, ); - const recursion_inputs = { + const recursion_inputs: InputMap = { verification_key: vkAsFields, proof: proofAsFields, - public_inputs: [main_inputs.y], + public_inputs: [main_inputs.y as Field], key_hash: vkHash, input_aggregation_object: ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'], }; diff --git a/tooling/noir_js/src/witness_generation.ts b/tooling/noir_js/src/witness_generation.ts index 3b1dfd90109..f3307837736 100644 --- a/tooling/noir_js/src/witness_generation.ts +++ b/tooling/noir_js/src/witness_generation.ts @@ -1,13 +1,13 @@ -import { abiEncode } from '@noir-lang/noirc_abi'; +import { abiEncode, InputMap } from '@noir-lang/noirc_abi'; import { base64Decode } from './base64_decode.js'; import { executeCircuit } from '@noir-lang/acvm_js'; import { witnessMapToUint8Array } from './serialize.js'; import { CompiledCircuit } from '@noir-lang/types'; // Generates the witnesses needed to feed into the chosen proving system -export async function generateWitness(compiledProgram: CompiledCircuit, inputs: unknown): Promise { +export async function generateWitness(compiledProgram: CompiledCircuit, inputs: InputMap): Promise { // Throws on ABI encoding error - const witnessMap = abiEncode(compiledProgram.abi, inputs, null); + const witnessMap = abiEncode(compiledProgram.abi, inputs); // Execute the circuit to generate the rest of the witnesses and serialize // them into a Uint8Array. diff --git a/tooling/noir_js/test/node/e2e.test.ts b/tooling/noir_js/test/node/e2e.test.ts index fe0d26c7e3b..5e6b566121e 100644 --- a/tooling/noir_js/test/node/e2e.test.ts +++ b/tooling/noir_js/test/node/e2e.test.ts @@ -3,6 +3,9 @@ import assert_lt_json from '../noir_compiled_examples/assert_lt/target/assert_lt import { generateWitness } from '../../src/index.js'; import { Noir } from '../../src/program.js'; import { BarretenbergBackend as Backend } from '@noir-lang/backend_barretenberg'; +import { CompiledCircuit } from '@noir-lang/types'; + +const assert_lt_program = assert_lt_json as CompiledCircuit; it('end-to-end proof creation and verification (outer)', async () => { // Noir.Js part @@ -10,12 +13,12 @@ it('end-to-end proof creation and verification (outer)', async () => { x: '2', y: '3', }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await generateWitness(assert_lt_program, inputs); // bb.js part // // Proof creation - const prover = new Backend(assert_lt_json); + const prover = new Backend(assert_lt_program); const proof = await prover.generateFinalProof(serializedWitness); // Proof verification @@ -31,9 +34,9 @@ it('end-to-end proof creation and verification (outer) -- Program API', async () }; // Initialize backend - const backend = new Backend(assert_lt_json); + const backend = new Backend(assert_lt_program); // Initialize program - const program = new Noir(assert_lt_json, backend); + const program = new Noir(assert_lt_program, backend); // Generate proof const proof = await program.generateFinalProof(inputs); @@ -48,12 +51,12 @@ it('end-to-end proof creation and verification (inner)', async () => { x: '2', y: '3', }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await generateWitness(assert_lt_program, inputs); // bb.js part // // Proof creation - const prover = new Backend(assert_lt_json); + const prover = new Backend(assert_lt_program); const proof = await prover.generateIntermediateProof(serializedWitness); // Proof verification @@ -79,15 +82,15 @@ it('[BUG] -- bb.js null function or function signature mismatch (different insta x: '2', y: '3', }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await generateWitness(assert_lt_program, inputs); // bb.js part - const prover = new Backend(assert_lt_json); + const prover = new Backend(assert_lt_program); const proof = await prover.generateFinalProof(serializedWitness); try { - const verifier = new Backend(assert_lt_json); + const verifier = new Backend(assert_lt_program); await verifier.verifyFinalProof(proof); expect.fail( 'bb.js currently returns a bug when we try to verify a proof with a different Barretenberg instance that created it.', @@ -111,13 +114,13 @@ it('[BUG] -- bb.js null function or function signature mismatch (outer-inner) ', x: '2', y: '3', }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await generateWitness(assert_lt_program, inputs); // bb.js part // // Proof creation // - const prover = new Backend(assert_lt_json); + const prover = new Backend(assert_lt_program); // Create a proof using both proving systems, the majority of the time // one would only use outer proofs. const proofOuter = await prover.generateFinalProof(serializedWitness); diff --git a/tooling/noir_js/test/node/smoke.test.ts b/tooling/noir_js/test/node/smoke.test.ts index 4b0291c0f41..931416b2e26 100644 --- a/tooling/noir_js/test/node/smoke.test.ts +++ b/tooling/noir_js/test/node/smoke.test.ts @@ -1,13 +1,16 @@ import { expect } from 'chai'; import assert_lt_json from '../noir_compiled_examples/assert_lt/target/assert_lt.json' assert { type: 'json' }; import { generateWitness } from '../../src/index.js'; +import { CompiledCircuit } from '@noir-lang/types'; + +const assert_lt_program = assert_lt_json as CompiledCircuit; it('generates witnesses successfully', async () => { const inputs = { x: '2', y: '3', }; - expect(() => generateWitness(assert_lt_json, inputs)).to.not.throw; + expect(() => generateWitness(assert_lt_program, inputs)).to.not.throw; }); it('string input and number input are the same', async () => { @@ -19,8 +22,8 @@ it('string input and number input are the same', async () => { x: 2, y: 3, }; - const solvedWitnessString = await generateWitness(assert_lt_json, inputsString); - const solvedWitnessNumber = await generateWitness(assert_lt_json, inputsNumber); + const solvedWitnessString = await generateWitness(assert_lt_program, inputsString); + const solvedWitnessNumber = await generateWitness(assert_lt_program, inputsNumber); expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -34,8 +37,8 @@ it('string input and number input are the same', async () => { y: 3, }; - const solvedWitnessString = await generateWitness(assert_lt_json, inputsString); - const solvedWitnessNumber = await generateWitness(assert_lt_json, inputsNumber); + const solvedWitnessString = await generateWitness(assert_lt_program, inputsString); + const solvedWitnessNumber = await generateWitness(assert_lt_program, inputsNumber); expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -46,7 +49,7 @@ it('0x prefixed string input for inputs will throw', async () => { }; try { - await generateWitness(assert_lt_json, inputsHexPrefix); + await generateWitness(assert_lt_program, inputsHexPrefix); expect.fail('Expected generatedWitness to throw, due to inputs being prefixed with 0x. Currently not supported'); } catch (error) { // Successfully errored due to 0x not being supported. Update this test once/if we choose @@ -62,7 +65,7 @@ describe('input validation', () => { }; try { - await generateWitness(assert_lt_json, inputs); + await generateWitness(assert_lt_program, inputs); expect.fail('Expected generatedWitness to throw, due to x not being convertible to a uint64'); } catch (error) { const knownError = error as Error; diff --git a/tooling/noir_js_types/package.json b/tooling/noir_js_types/package.json index eb913f16ca3..f4801a546c7 100644 --- a/tooling/noir_js_types/package.json +++ b/tooling/noir_js_types/package.json @@ -28,6 +28,9 @@ "types": "./lib/esm/types.d.ts" } }, + "dependencies": { + "@noir-lang/noirc_abi": "workspace:*" + }, "devDependencies": { "@types/prettier": "^3", "eslint": "^8.50.0", diff --git a/tooling/noir_js_types/src/types.ts b/tooling/noir_js_types/src/types.ts index 357e440f155..6285972d1e9 100644 --- a/tooling/noir_js_types/src/types.ts +++ b/tooling/noir_js_types/src/types.ts @@ -1,3 +1,5 @@ +import { Abi } from '@noir-lang/noirc_abi'; + export interface Backend { // Generate an outer proof. This is the proof for the circuit which will verify // inner proofs and or can be seen as the proof created for regular circuits. @@ -19,5 +21,5 @@ export type ProofData = { export type CompiledCircuit = { bytecode: string; - abi: object; + abi: Abi; }; diff --git a/tooling/noirc_abi_wasm/src/lib.rs b/tooling/noirc_abi_wasm/src/lib.rs index 2b1fc672fc4..24e0e6506fb 100644 --- a/tooling/noirc_abi_wasm/src/lib.rs +++ b/tooling/noirc_abi_wasm/src/lib.rs @@ -22,28 +22,83 @@ use errors::JsAbiError; use js_witness_map::JsWitnessMap; use temp::{input_value_from_json_type, JsonTypes}; +#[wasm_bindgen(typescript_custom_section)] +const INPUT_MAP: &'static str = r#" +export type Field = string | number | boolean; +export type InputValue = Field | Field[] | InputMap; +export type InputMap = { [key: string]: InputValue }; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Object, js_name = "InputMap", typescript_type = "InputMap")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type JsInputMap; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Object, js_name = "InputValue", typescript_type = "InputValue")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type JsInputValue; +} + +#[wasm_bindgen(typescript_custom_section)] +const ABI: &'static str = r#" +export type Visibility = "public" | "private"; +export type Sign = "unsigned" | "signed"; +export type AbiType = + { kind: "field" } | + { kind: "boolean" } | + { kind: "string", length: number } | + { kind: "integer", sign: Sign, width: number } | + { kind: "array", length: number, type: AbiType } | + { kind: "tuple", fields: AbiType[] } | + { kind: "struct", path: string, fields: [string, AbiType][] }; + +export type AbiParameter = { + name: string, + type: AbiType, + visibility: Visibility, +}; + +export type Abi = { + parameters: AbiParameter[], + param_witnesses: Record, + return_type: AbiType | null, + return_witnesses: number[], +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends = js_sys::Object, js_name = "Abi", typescript_type = "Abi")] + #[derive(Clone, Debug, PartialEq, Eq)] + pub type JsAbi; +} + #[wasm_bindgen(js_name = abiEncode)] pub fn abi_encode( - abi: JsValue, - inputs: JsValue, - return_value: JsValue, + abi: JsAbi, + inputs: JsInputMap, + return_value: Option, ) -> Result { console_error_panic_hook::set_once(); - let abi: Abi = JsValueSerdeExt::into_serde(&abi).map_err(|err| err.to_string())?; + let abi: Abi = + JsValueSerdeExt::into_serde(&JsValue::from(abi)).map_err(|err| err.to_string())?; let inputs: BTreeMap = - JsValueSerdeExt::into_serde(&inputs).map_err(|err| err.to_string())?; - let return_value: Option = if return_value.is_undefined() || return_value.is_null() - { - None - } else { - let toml_return_value = - JsValueSerdeExt::into_serde(&return_value).expect("could not decode return value"); - Some(input_value_from_json_type( - toml_return_value, - abi.return_type.as_ref().unwrap(), - MAIN_RETURN_NAME, - )?) - }; + JsValueSerdeExt::into_serde(&JsValue::from(inputs)).map_err(|err| err.to_string())?; + let return_value: Option = return_value + .map(|return_value| { + let toml_return_value = JsValueSerdeExt::into_serde(&JsValue::from(return_value)) + .expect("could not decode return value"); + input_value_from_json_type( + toml_return_value, + abi.return_type.as_ref().unwrap(), + MAIN_RETURN_NAME, + ) + }) + .transpose()?; let abi_map = abi.to_btree_map(); let parsed_inputs: BTreeMap = @@ -62,9 +117,10 @@ pub fn abi_encode( } #[wasm_bindgen(js_name = abiDecode)] -pub fn abi_decode(abi: JsValue, witness_map: JsWitnessMap) -> Result { +pub fn abi_decode(abi: JsAbi, witness_map: JsWitnessMap) -> Result { console_error_panic_hook::set_once(); - let abi: Abi = JsValueSerdeExt::into_serde(&abi).map_err(|err| err.to_string())?; + let abi: Abi = + JsValueSerdeExt::into_serde(&JsValue::from(abi)).map_err(|err| err.to_string())?; let witness_map = WitnessMap::from(witness_map); diff --git a/tooling/noirc_abi_wasm/test/browser/abi_encode.test.ts b/tooling/noirc_abi_wasm/test/browser/abi_encode.test.ts index 5e604aa5b66..e1aaf0dc2c0 100644 --- a/tooling/noirc_abi_wasm/test/browser/abi_encode.test.ts +++ b/tooling/noirc_abi_wasm/test/browser/abi_encode.test.ts @@ -1,5 +1,5 @@ import { expect } from '@esm-bundle/chai'; -import initNoirAbi, { abiEncode, abiDecode, WitnessMap } from '@noir-lang/noirc_abi'; +import initNoirAbi, { abiEncode, abiDecode, WitnessMap, Field } from '@noir-lang/noirc_abi'; import { DecodedInputs } from '../types'; beforeEach(async () => { @@ -9,11 +9,13 @@ beforeEach(async () => { it('recovers original inputs when abi encoding and decoding', async () => { const { abi, inputs } = await import('../shared/abi_encode'); - const initial_witness: WitnessMap = abiEncode(abi, inputs, null); + const initial_witness: WitnessMap = abiEncode(abi, inputs); const decoded_inputs: DecodedInputs = abiDecode(abi, initial_witness); - expect(BigInt(decoded_inputs.inputs.foo)).to.be.equal(BigInt(inputs.foo)); - expect(BigInt(decoded_inputs.inputs.bar[0])).to.be.equal(BigInt(inputs.bar[0])); - expect(BigInt(decoded_inputs.inputs.bar[1])).to.be.equal(BigInt(inputs.bar[1])); + const foo: Field = inputs.foo as Field; + const bar: Field[] = inputs.bar as Field[]; + expect(BigInt(decoded_inputs.inputs.foo)).to.be.equal(BigInt(foo)); + expect(BigInt(decoded_inputs.inputs.bar[0])).to.be.equal(BigInt(bar[0])); + expect(BigInt(decoded_inputs.inputs.bar[1])).to.be.equal(BigInt(bar[1])); expect(decoded_inputs.return_value).to.be.null; }); diff --git a/tooling/noirc_abi_wasm/test/browser/errors.test.ts b/tooling/noirc_abi_wasm/test/browser/errors.test.ts index 5f9b40a195c..429a2d446a3 100644 --- a/tooling/noirc_abi_wasm/test/browser/errors.test.ts +++ b/tooling/noirc_abi_wasm/test/browser/errors.test.ts @@ -8,7 +8,7 @@ beforeEach(async () => { it('errors when an integer input overflows', async () => { const { abi, inputs } = await import('../shared/uint_overflow'); - expect(() => abiEncode(abi, inputs, null)).to.throw( + expect(() => abiEncode(abi, inputs)).to.throw( 'The parameter foo is expected to be a Integer { sign: Unsigned, width: 32 } but found incompatible value Field(2³⁸)', ); }); @@ -16,11 +16,11 @@ it('errors when an integer input overflows', async () => { it('errors when passing a field in place of an array', async () => { const { abi, inputs } = await import('../shared/field_as_array'); - expect(() => abiEncode(abi, inputs, null)).to.throw('cannot parse value into Array { length: 2, typ: Field }'); + expect(() => abiEncode(abi, inputs)).to.throw('cannot parse value into Array { length: 2, typ: Field }'); }); it('errors when passing an array in place of a field', async () => { const { abi, inputs } = await import('../shared/array_as_field'); - expect(() => abiEncode(abi, inputs, null)).to.throw('cannot parse value into Field'); + expect(() => abiEncode(abi, inputs)).to.throw('cannot parse value into Field'); }); diff --git a/tooling/noirc_abi_wasm/test/node/abi_encode.test.ts b/tooling/noirc_abi_wasm/test/node/abi_encode.test.ts index 830e45cf10b..a49c10b6ea6 100644 --- a/tooling/noirc_abi_wasm/test/node/abi_encode.test.ts +++ b/tooling/noirc_abi_wasm/test/node/abi_encode.test.ts @@ -1,15 +1,17 @@ import { expect } from 'chai'; -import { abiEncode, abiDecode, WitnessMap } from '@noir-lang/noirc_abi'; +import { abiEncode, abiDecode, WitnessMap, Field } from '@noir-lang/noirc_abi'; import { DecodedInputs } from '../types'; it('recovers original inputs when abi encoding and decoding', async () => { const { abi, inputs } = await import('../shared/abi_encode'); - const initial_witness: WitnessMap = abiEncode(abi, inputs, null); + const initial_witness: WitnessMap = abiEncode(abi, inputs); const decoded_inputs: DecodedInputs = abiDecode(abi, initial_witness); - expect(BigInt(decoded_inputs.inputs.foo)).to.be.equal(BigInt(inputs.foo)); - expect(BigInt(decoded_inputs.inputs.bar[0])).to.be.equal(BigInt(inputs.bar[0])); - expect(BigInt(decoded_inputs.inputs.bar[1])).to.be.equal(BigInt(inputs.bar[1])); + const foo: Field = inputs.foo as Field; + const bar: Field[] = inputs.bar as Field[]; + expect(BigInt(decoded_inputs.inputs.foo)).to.be.equal(BigInt(foo)); + expect(BigInt(decoded_inputs.inputs.bar[0])).to.be.equal(BigInt(bar[0])); + expect(BigInt(decoded_inputs.inputs.bar[1])).to.be.equal(BigInt(bar[1])); expect(decoded_inputs.return_value).to.be.null; }); diff --git a/tooling/noirc_abi_wasm/test/node/errors.test.ts b/tooling/noirc_abi_wasm/test/node/errors.test.ts index ee0670ab5be..0d007e64803 100644 --- a/tooling/noirc_abi_wasm/test/node/errors.test.ts +++ b/tooling/noirc_abi_wasm/test/node/errors.test.ts @@ -4,7 +4,7 @@ import { abiEncode } from '@noir-lang/noirc_abi'; it('errors when an integer input overflows', async () => { const { abi, inputs } = await import('../shared/uint_overflow'); - expect(() => abiEncode(abi, inputs, null)).to.throw( + expect(() => abiEncode(abi, inputs)).to.throw( 'The parameter foo is expected to be a Integer { sign: Unsigned, width: 32 } but found incompatible value Field(2³⁸)', ); }); @@ -12,11 +12,11 @@ it('errors when an integer input overflows', async () => { it('errors when passing a field in place of an array', async () => { const { abi, inputs } = await import('../shared/field_as_array'); - expect(() => abiEncode(abi, inputs, null)).to.throw('cannot parse value into Array { length: 2, typ: Field }'); + expect(() => abiEncode(abi, inputs)).to.throw('cannot parse value into Array { length: 2, typ: Field }'); }); it('errors when passing an array in place of a field', async () => { const { abi, inputs } = await import('../shared/array_as_field'); - expect(() => abiEncode(abi, inputs, null)).to.throw('cannot parse value into Field'); + expect(() => abiEncode(abi, inputs)).to.throw('cannot parse value into Field'); }); diff --git a/tooling/noirc_abi_wasm/test/shared/abi_encode.ts b/tooling/noirc_abi_wasm/test/shared/abi_encode.ts index 0f9c93a4f76..28379745dec 100644 --- a/tooling/noirc_abi_wasm/test/shared/abi_encode.ts +++ b/tooling/noirc_abi_wasm/test/shared/abi_encode.ts @@ -1,6 +1,6 @@ -// TODO: Add type definitions for these +import { Abi, InputMap } from '@noir-lang/noirc_abi'; -export const abi = { +export const abi: Abi = { parameters: [ { name: 'foo', type: { kind: 'field' }, visibility: 'private' }, { @@ -14,7 +14,7 @@ export const abi = { return_witnesses: [], }; -export const inputs = { +export const inputs: InputMap = { foo: '1', bar: ['1', '2'], }; diff --git a/tooling/noirc_abi_wasm/test/shared/array_as_field.ts b/tooling/noirc_abi_wasm/test/shared/array_as_field.ts index 06e6a7beebf..ba58f075702 100644 --- a/tooling/noirc_abi_wasm/test/shared/array_as_field.ts +++ b/tooling/noirc_abi_wasm/test/shared/array_as_field.ts @@ -1,4 +1,6 @@ -export const abi = { +import { Abi, InputMap } from '@noir-lang/noirc_abi'; + +export const abi: Abi = { parameters: [ { name: 'foo', @@ -11,6 +13,6 @@ export const abi = { return_witnesses: [], }; -export const inputs = { +export const inputs: InputMap = { foo: ['1', '2'], }; diff --git a/tooling/noirc_abi_wasm/test/shared/field_as_array.ts b/tooling/noirc_abi_wasm/test/shared/field_as_array.ts index 89ae529d6b1..931720d5e1b 100644 --- a/tooling/noirc_abi_wasm/test/shared/field_as_array.ts +++ b/tooling/noirc_abi_wasm/test/shared/field_as_array.ts @@ -1,4 +1,6 @@ -export const abi = { +import { Abi, InputMap } from '@noir-lang/noirc_abi'; + +export const abi: Abi = { parameters: [ { name: 'foo', @@ -11,6 +13,6 @@ export const abi = { return_witnesses: [], }; -export const inputs = { +export const inputs: InputMap = { foo: '1', }; diff --git a/tooling/noirc_abi_wasm/test/shared/uint_overflow.ts b/tooling/noirc_abi_wasm/test/shared/uint_overflow.ts index 87f59b1440e..ee87e050b23 100644 --- a/tooling/noirc_abi_wasm/test/shared/uint_overflow.ts +++ b/tooling/noirc_abi_wasm/test/shared/uint_overflow.ts @@ -1,4 +1,6 @@ -export const abi = { +import { Abi, InputMap } from '@noir-lang/noirc_abi'; + +export const abi: Abi = { parameters: [ { name: 'foo', @@ -11,6 +13,6 @@ export const abi = { return_witnesses: [], }; -export const inputs = { +export const inputs: InputMap = { foo: `0x${(1n << 38n).toString(16)}`, };