Skip to content

Commit

Permalink
feat: add JS types for ABI and input maps (#3023)
Browse files Browse the repository at this point in the history
Co-authored-by: kevaundray <kevtheappdev@gmail.com>
  • Loading branch information
TomAFrench and kevaundray authored Oct 9, 2023
1 parent 9fb7183 commit 599e7a1
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 69 deletions.
7 changes: 4 additions & 3 deletions compiler/integration-tests/test/browser/recursion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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);

Expand All @@ -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'],
};
Expand Down
6 changes: 3 additions & 3 deletions tooling/noir_js/src/witness_generation.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> {
export async function generateWitness(compiledProgram: CompiledCircuit, inputs: InputMap): Promise<Uint8Array> {
// 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.
Expand Down
25 changes: 14 additions & 11 deletions tooling/noir_js/test/node/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ 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
const inputs = {
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
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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.',
Expand All @@ -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);
Expand Down
17 changes: 10 additions & 7 deletions tooling/noir_js/test/node/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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
Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions tooling/noir_js_types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"types": "./lib/esm/types.d.ts"
}
},
"dependencies": {
"@noir-lang/noirc_abi": "workspace:*"
},
"devDependencies": {
"@types/prettier": "^3",
"eslint": "^8.50.0",
Expand Down
4 changes: 3 additions & 1 deletion tooling/noir_js_types/src/types.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,5 +21,5 @@ export type ProofData = {

export type CompiledCircuit = {
bytecode: string;
abi: object;
abi: Abi;
};
94 changes: 75 additions & 19 deletions tooling/noirc_abi_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number[]>,
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<JsInputValue>,
) -> Result<JsWitnessMap, JsAbiError> {
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<String, JsonTypes> =
JsValueSerdeExt::into_serde(&inputs).map_err(|err| err.to_string())?;
let return_value: Option<InputValue> = 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<InputValue> = 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<String, InputValue> =
Expand All @@ -62,9 +117,10 @@ pub fn abi_encode(
}

#[wasm_bindgen(js_name = abiDecode)]
pub fn abi_decode(abi: JsValue, witness_map: JsWitnessMap) -> Result<JsValue, JsAbiError> {
pub fn abi_decode(abi: JsAbi, witness_map: JsWitnessMap) -> Result<JsValue, JsAbiError> {
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);

Expand Down
12 changes: 7 additions & 5 deletions tooling/noirc_abi_wasm/test/browser/abi_encode.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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;
});
6 changes: 3 additions & 3 deletions tooling/noirc_abi_wasm/test/browser/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ 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³⁸)',
);
});

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');
});
12 changes: 7 additions & 5 deletions tooling/noirc_abi_wasm/test/node/abi_encode.test.ts
Original file line number Diff line number Diff line change
@@ -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;
});
Loading

0 comments on commit 599e7a1

Please sign in to comment.