Skip to content

Commit

Permalink
feat: Refine Noir.js API (#2732)
Browse files Browse the repository at this point in the history
Co-authored-by: TomAFrench <tom@tomfren.ch>
  • Loading branch information
kevaundray and TomAFrench authored Sep 21, 2023
1 parent eeb5381 commit e79f1ed
Show file tree
Hide file tree
Showing 28 changed files with 909 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
pull_request:
paths:
- ./compiler/integration-tests
- ./compiler/integration-tests/**
schedule:
- cron: "0 2 * * *" # Run nightly at 2 AM UTC

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ test_cases.forEach((testInfo) => {

const noir_source_url = new URL(
`${base_relative_path}/${test_case}/src/main.nr`,
import.meta.url
import.meta.url,
);
const prover_toml_url = new URL(
`${base_relative_path}/${test_case}/Prover.toml`,
import.meta.url
import.meta.url,
);

const noir_source = await getFile(noir_source_url);
Expand Down Expand Up @@ -101,15 +101,15 @@ test_cases.forEach((testInfo) => {
try {
compressedByteCode = Uint8Array.from(
atob(compile_output.circuit),
(c) => c.charCodeAt(0)
(c) => c.charCodeAt(0),
);

solvedWitness = await executeCircuit(
compressedByteCode,
witnessMap,
() => {
throw Error("unexpected oracle");
}
},
);
} catch (e) {
expect(e, "Abi Encoding Step").to.not.be.an("error");
Expand All @@ -130,7 +130,7 @@ test_cases.forEach((testInfo) => {
await api.srsInitSrs(
new RawBuffer(crs.getG1Data()),
crs.numPoints,
new RawBuffer(crs.getG2Data())
new RawBuffer(crs.getG2Data()),
);

const acirComposer = await api.acirNewAcirComposer(CIRCUIT_SIZE);
Expand All @@ -140,22 +140,22 @@ test_cases.forEach((testInfo) => {
acirComposer,
acirUint8Array,
witnessUint8Array,
isRecursive
isRecursive,
);

// And this took ~5 minutes!
const verified = await api.acirVerifyProof(
acirComposer,
proof,
isRecursive
isRecursive,
);

expect(verified).to.be.true;
} catch (e) {
expect(e, "Proving and Verifying").to.not.be.an("error");
throw e;
}
}
},
);

suite.addTest(mochaTest);
Expand Down
10 changes: 5 additions & 5 deletions compiler/source-resolver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@ export const read_file = function (source_id: string): string {
return result;
} else {
throw new Error(
"Noir source resolver function MUST return String synchronously. Are you trying to return anything else, eg. `Promise`?"
"Noir source resolver function MUST return String synchronously. Are you trying to return anything else, eg. `Promise`?",
);
}
} else {
throw new Error(
"Not yet initialized. Use initializeResolver(() => string)"
"Not yet initialized. Use initializeResolver(() => string)",
);
}
};

function initialize(
noir_resolver: (source_id: string) => string
noir_resolver: (source_id: string) => string,
): (source_id: string) => string {
if (typeof noir_resolver === "function") {
return noir_resolver;
} else {
throw new Error(
"Provided Noir Resolver is not a function, hint: use function(module_id) => NoirSource as second parameter"
"Provided Noir Resolver is not a function, hint: use function(module_id) => NoirSource as second parameter",
);
}
}

export function initializeResolver(
resolver: (source_id: string) => string
resolver: (source_id: string) => string,
): void {
resolveFunction = initialize(resolver);
}
2 changes: 1 addition & 1 deletion compiler/source-resolver/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export declare const read_file: (source_id: string) => string;
export declare function initializeResolver(
resolver: (source_id: string) => string
resolver: (source_id: string) => string,
): void;
2 changes: 1 addition & 1 deletion compiler/wasm/test/node/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("noir wasm compilation", () => {

console.log(
"Compilation is a match? ",
wasmCircuitBase64 === cliCircuitBase64
wasmCircuitBase64 === cliCircuitBase64,
);

expect(wasmCircuitBase64).to.equal(cliCircuitBase64);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
"@typescript-eslint/parser": "^5.59.5",
"chai": "^4.3.7",
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^5.0.0",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"prettier": "3.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
Expand Down
1 change: 1 addition & 0 deletions tooling/noir_js/.eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
test/backend/barretenberg.ts
File renamed without changes.
1 change: 1 addition & 0 deletions tooling/noir_js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!test/noir_compiled_examples/*/target
8 changes: 8 additions & 0 deletions tooling/noir_js/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"require": "ts-node/register",
"loader": "ts-node/esm",
"extensions": ["ts"],
"spec": [
"test/node/**/*.test.ts*"
]
}
6 changes: 6 additions & 0 deletions tooling/noir_js/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"parser": "typescript",
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all"
}
20 changes: 19 additions & 1 deletion tooling/noir_js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"version": "0.12.0",
"packageManager": "yarn@3.5.1",
"license": "(MIT OR Apache-2.0)",
"type": "module",
"dependencies": {
"@noir-lang/acvm_js": "0.27.0",
"@noir-lang/noirc_abi": "workspace:*"
"@noir-lang/noirc_abi": "workspace:*",
"fflate": "^0.8.0"
},
"files": [
"lib",
Expand All @@ -17,10 +19,26 @@
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"dev": "tsc --watch",
"build": "tsc",
"test": "yarn test:node",
"test:node": "mocha --timeout 25000",
"prettier": "prettier 'src/**/*.ts'",
"prettier:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
"lint": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ./.eslintignore --max-warnings 0"
},
"devDependencies": {
"@aztec/bb.js": "0.7.2",
"@types/chai": "^4",
"@types/mocha": "^10.0.1",
"@types/node": "^20.6.2",
"@types/prettier": "^3",
"chai": "^4.3.8",
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^5.0.0",
"mocha": "^10.2.0",
"prettier": "3.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
13 changes: 13 additions & 0 deletions tooling/noir_js/src/base64_decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Since this is a simple function, we can use feature detection to
// see if we are in the nodeJs environment or the browser environment.
export function base64Decode(input: string): Uint8Array {
if (typeof Buffer !== 'undefined') {
// Node.js environment
return Buffer.from(input, 'base64');
} else if (typeof atob === 'function') {
// Browser environment
return Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
} else {
throw new Error('No implementation found for base64 decoding.');
}
}
11 changes: 8 additions & 3 deletions tooling/noir_js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import * as acvm from "@noir-lang/acvm_js";
import * as noirc from "@noir-lang/noirc_abi";
export { acvm, noirc };
import * as acvm from '@noir-lang/acvm_js';
import * as abi from '@noir-lang/noirc_abi';

export { acvm, abi };

import { generateWitness } from './witness_generation.js';
import { acirToUint8Array, witnessMapToUint8Array } from './serialize.js';
export { acirToUint8Array, witnessMapToUint8Array, generateWitness };
55 changes: 55 additions & 0 deletions tooling/noir_js/src/input_validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Check if all of the input values are correct according to the ABI
export function validateInputs(inputs, abi) {
for (const param of abi.parameters) {
const inputValue = inputs[param.name];
if (inputValue === undefined) {
// This is checked by noirc_abi, so we could likely remove this check
return { isValid: false, error: `Input for ${param.name} is missing` };
}
if (!checkType(inputValue, param.type)) {
return {
isValid: false,
error: `Input for ${param.name} is the wrong type, expected ${type_to_string(param.type)}, got "${inputValue}"`,
};
}
}
return { isValid: true, error: null };
}

// Checks that value is of type "type"
// Where type is taken from the abi
function checkType(value, type) {
switch (type.kind) {
case 'integer':
if (type.sign === 'unsigned') {
return isUnsignedInteger(value, type.width);
}
// Other integer sign checks can be added here
break;
// Other type.kind checks can be added here
}
return false;
}

function type_to_string(type): string {
switch (type.kind) {
case 'integer':
if (type.sign === 'unsigned') {
return `uint${type.width}`;
}
break;
case 'array':
return `${type_to_string(type.element)}[${type.length}]`;
}
return 'unknown type';
}

// Returns true if `value` is an unsigned integer that is less than 2^{width}
function isUnsignedInteger(value: bigint, width: bigint) {
try {
const bigIntValue = BigInt(value);
return bigIntValue >= 0 && bigIntValue <= BigInt(2) ** BigInt(width) - 1n;
} catch (e) {
return false; // Not a valid integer
}
}
17 changes: 17 additions & 0 deletions tooling/noir_js/src/serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { WitnessMap, compressWitness } from '@noir-lang/acvm_js';
import { decompressSync as gunzip } from 'fflate';
import { base64Decode } from './base64_decode.js';

// After solving the witness, to pass it a backend, we need to serialize it to a Uint8Array
export function witnessMapToUint8Array(solvedWitness: WitnessMap): Uint8Array {
// TODO: We just want to serialize, but this will zip up the witness
// TODO so its not ideal
const compressedWitness = compressWitness(solvedWitness);
return gunzip(compressedWitness);
}

// Converts an bytecode to a Uint8Array
export function acirToUint8Array(base64EncodedBytecode): Uint8Array {
const compressedByteCode = base64Decode(base64EncodedBytecode);
return gunzip(compressedByteCode);
}
24 changes: 24 additions & 0 deletions tooling/noir_js/src/witness_generation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { abiEncode } from '@noir-lang/noirc_abi';
import { validateInputs } from './input_validation.js';
import { base64Decode } from './base64_decode.js';
import { WitnessMap, executeCircuit } from '@noir-lang/acvm_js';

// Generates the witnesses needed to feed into the chosen proving system
export async function generateWitness(compiledProgram, inputs): Promise<WitnessMap> {
// Validate inputs
const { isValid, error } = validateInputs(inputs, compiledProgram.abi);
if (!isValid) {
throw new Error(error?.toString());
}
const witnessMap = abiEncode(compiledProgram.abi, inputs, null);

// Execute the circuit to generate the rest of the witnesses
try {
const solvedWitness = await executeCircuit(base64Decode(compiledProgram.bytecode), witnessMap, () => {
throw Error('unexpected oracle during execution');
});
return solvedWitness;
} catch (err) {
throw new Error(`Circuit execution failed: ${err}`);
}
}
Loading

0 comments on commit e79f1ed

Please sign in to comment.