diff --git a/tooling/noir_js/test/node/e2e.test.ts b/tooling/noir_js/test/node/e2e.test.ts index 8921314e8ea..979841c47e6 100644 --- a/tooling/noir_js/test/node/e2e.test.ts +++ b/tooling/noir_js/test/node/e2e.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import assert_lt_json from '../noir_compiled_examples/assert_lt/target/assert_lt.json' assert { type: 'json' }; import { Noir } from '@noir-lang/noir_js'; -import { BarretenbergBackend as Backend } from '@noir-lang/backend_barretenberg'; +import { BarretenbergBackend as Backend, BarretenbergVerifier as Verifier } from '@noir-lang/backend_barretenberg'; import { CompiledCircuit } from '@noir-lang/types'; const assert_lt_program = assert_lt_json as CompiledCircuit; @@ -47,6 +47,28 @@ it('end-to-end proof creation and verification (outer) -- Program API', async () expect(isValid).to.be.true; }); +it('end-to-end proof creation and verification (outer) -- Verifier API', async () => { + // Noir.Js part + const inputs = { + x: '2', + y: '3', + }; + + // Initialize backend + const backend = new Backend(assert_lt_program); + // Initialize program + const program = new Noir(assert_lt_program, backend); + // Generate proof + const proof = await program.generateProof(inputs); + + const verificationKey = await backend.getVerificationKey(); + + // Proof verification + const verifier = new Verifier(); + const isValid = await verifier.verifyProof(proof, verificationKey); + expect(isValid).to.be.true; +}); + // TODO: maybe switch to using assert_statement_recursive here to test both options it('end-to-end proof creation and verification (inner)', async () => { // Noir.Js part diff --git a/tooling/noir_js_backend_barretenberg/src/backend.ts b/tooling/noir_js_backend_barretenberg/src/backend.ts new file mode 100644 index 00000000000..d07681dd8c1 --- /dev/null +++ b/tooling/noir_js_backend_barretenberg/src/backend.ts @@ -0,0 +1,143 @@ +import { decompressSync as gunzip } from 'fflate'; +import { acirToUint8Array } from './serialize.js'; +import { Backend, CompiledCircuit, ProofData, VerifierBackend } from '@noir-lang/types'; +import { BackendOptions } from './types.js'; +import { deflattenPublicInputs } from './public_inputs.js'; +import { reconstructProofWithPublicInputs } from './verifier.js'; +import { type Barretenberg } from '@aztec/bb.js'; + +// This is the number of bytes in a UltraPlonk proof +// minus the public inputs. +const numBytesInProofWithoutPublicInputs: number = 2144; + +export class BarretenbergVerifierBackend implements VerifierBackend { + // These type assertions are used so that we don't + // have to initialize `api` and `acirComposer` in the constructor. + // These are initialized asynchronously in the `init` function, + // constructors cannot be asynchronous which is why we do this. + + protected api!: Barretenberg; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected acirComposer: any; + protected acirUncompressedBytecode: Uint8Array; + + constructor( + acirCircuit: CompiledCircuit, + protected options: BackendOptions = { threads: 1 }, + ) { + const acirBytecodeBase64 = acirCircuit.bytecode; + this.acirUncompressedBytecode = acirToUint8Array(acirBytecodeBase64); + } + + /** @ignore */ + async instantiate(): Promise { + if (!this.api) { + if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) { + this.options.threads = navigator.hardwareConcurrency; + } else { + try { + const os = await import('os'); + this.options.threads = os.cpus().length; + } catch (e) { + console.log('Could not detect environment. Falling back to one thread.', e); + } + } + const { Barretenberg, RawBuffer, Crs } = await import('@aztec/bb.js'); + const api = await Barretenberg.new(this.options); + + const [_exact, _total, subgroupSize] = await api.acirGetCircuitSizes(this.acirUncompressedBytecode); + const crs = await Crs.new(subgroupSize + 1); + await api.commonInitSlabAllocator(subgroupSize); + await api.srsInitSrs(new RawBuffer(crs.getG1Data()), crs.numPoints, new RawBuffer(crs.getG2Data())); + + this.acirComposer = await api.acirNewAcirComposer(subgroupSize); + await api.acirInitProvingKey(this.acirComposer, this.acirUncompressedBytecode); + this.api = api; + } + } + + /** @description Verifies a proof */ + async verifyProof(proofData: ProofData): Promise { + const proof = reconstructProofWithPublicInputs(proofData); + await this.instantiate(); + await this.api.acirInitVerificationKey(this.acirComposer); + return await this.api.acirVerifyProof(this.acirComposer, proof); + } + + async getVerificationKey(): Promise { + await this.instantiate(); + await this.api.acirInitVerificationKey(this.acirComposer); + return await this.api.acirGetVerificationKey(this.acirComposer); + } + + async destroy(): Promise { + if (!this.api) { + return; + } + await this.api.destroy(); + } +} + +export class BarretenbergBackend extends BarretenbergVerifierBackend implements Backend { + /** @description Generates a proof */ + async generateProof(compressedWitness: Uint8Array): Promise { + await this.instantiate(); + const proofWithPublicInputs = await this.api.acirCreateProof( + this.acirComposer, + this.acirUncompressedBytecode, + gunzip(compressedWitness), + ); + + const splitIndex = proofWithPublicInputs.length - numBytesInProofWithoutPublicInputs; + + const publicInputsConcatenated = proofWithPublicInputs.slice(0, splitIndex); + const proof = proofWithPublicInputs.slice(splitIndex); + const publicInputs = deflattenPublicInputs(publicInputsConcatenated); + + return { proof, publicInputs }; + } + + /** + * Generates artifacts that will be passed to a circuit that will verify this proof. + * + * Instead of passing the proof and verification key as a byte array, we pass them + * as fields which makes it cheaper to verify in a circuit. + * + * The proof that is passed here will have been created using a circuit + * that has the #[recursive] attribute on its `main` method. + * + * The number of public inputs denotes how many public inputs are in the inner proof. + * + * @example + * ```typescript + * const artifacts = await backend.generateRecursiveProofArtifacts(proof, numOfPublicInputs); + * ``` + */ + async generateRecursiveProofArtifacts( + proofData: ProofData, + numOfPublicInputs = 0, + ): Promise<{ + proofAsFields: string[]; + vkAsFields: string[]; + vkHash: string; + }> { + await this.instantiate(); + const proof = reconstructProofWithPublicInputs(proofData); + const proofAsFields = ( + await this.api.acirSerializeProofIntoFields(this.acirComposer, proof, numOfPublicInputs) + ).slice(numOfPublicInputs); + + // TODO: perhaps we should put this in the init function. Need to benchmark + // TODO how long it takes. + await this.api.acirInitVerificationKey(this.acirComposer); + + // Note: If you don't init verification key, `acirSerializeVerificationKeyIntoFields`` will just hang on serialization + const vk = await this.api.acirSerializeVerificationKeyIntoFields(this.acirComposer); + + return { + proofAsFields: proofAsFields.map((p) => p.toString()), + vkAsFields: vk[0].map((vk) => vk.toString()), + vkHash: vk[1].toString(), + }; + } +} diff --git a/tooling/noir_js_backend_barretenberg/src/index.ts b/tooling/noir_js_backend_barretenberg/src/index.ts index bfdf1005a93..f28abb9a658 100644 --- a/tooling/noir_js_backend_barretenberg/src/index.ts +++ b/tooling/noir_js_backend_barretenberg/src/index.ts @@ -1,150 +1,7 @@ -import { decompressSync as gunzip } from 'fflate'; -import { acirToUint8Array } from './serialize.js'; -import { Backend, CompiledCircuit, ProofData } from '@noir-lang/types'; -import { BackendOptions } from './types.js'; -import { deflattenPublicInputs, flattenPublicInputsAsArray } from './public_inputs.js'; -import { type Barretenberg } from '@aztec/bb.js'; - +export { BarretenbergBackend } from './backend.js'; +export { BarretenbergVerifier } from './verifier.js'; export { publicInputsToWitnessMap } from './public_inputs.js'; -// This is the number of bytes in a UltraPlonk proof -// minus the public inputs. -const numBytesInProofWithoutPublicInputs: number = 2144; - -export class BarretenbergBackend implements Backend { - // These type assertions are used so that we don't - // have to initialize `api` and `acirComposer` in the constructor. - // These are initialized asynchronously in the `init` function, - // constructors cannot be asynchronous which is why we do this. - - private api!: Barretenberg; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private acirComposer: any; - private acirUncompressedBytecode: Uint8Array; - - constructor( - acirCircuit: CompiledCircuit, - private options: BackendOptions = { threads: 1 }, - ) { - const acirBytecodeBase64 = acirCircuit.bytecode; - this.acirUncompressedBytecode = acirToUint8Array(acirBytecodeBase64); - } - - /** @ignore */ - async instantiate(): Promise { - if (!this.api) { - if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) { - this.options.threads = navigator.hardwareConcurrency; - } else { - try { - const os = await import('os'); - this.options.threads = os.cpus().length; - } catch (e) { - console.log('Could not detect environment. Falling back to one thread.', e); - } - } - const { Barretenberg, RawBuffer, Crs } = await import('@aztec/bb.js'); - const api = await Barretenberg.new(this.options); - const [_exact, _total, subgroupSize] = await api.acirGetCircuitSizes(this.acirUncompressedBytecode); - const crs = await Crs.new(subgroupSize + 1); - await api.commonInitSlabAllocator(subgroupSize); - await api.srsInitSrs(new RawBuffer(crs.getG1Data()), crs.numPoints, new RawBuffer(crs.getG2Data())); - - this.acirComposer = await api.acirNewAcirComposer(subgroupSize); - await api.acirInitProvingKey(this.acirComposer, this.acirUncompressedBytecode); - this.api = api; - } - } - - /** @description Generates a proof */ - async generateProof(compressedWitness: Uint8Array): Promise { - await this.instantiate(); - // TODO: Change once `@aztec/bb.js` version is updated to use methods without isRecursive flag - const proofWithPublicInputs = await this.api.acirCreateProof( - this.acirComposer, - this.acirUncompressedBytecode, - gunzip(compressedWitness), - ); - - const splitIndex = proofWithPublicInputs.length - numBytesInProofWithoutPublicInputs; - - const publicInputsConcatenated = proofWithPublicInputs.slice(0, splitIndex); - const proof = proofWithPublicInputs.slice(splitIndex); - const publicInputs = deflattenPublicInputs(publicInputsConcatenated); - - return { proof, publicInputs }; - } - - /** - * Generates artifacts that will be passed to a circuit that will verify this proof. - * - * Instead of passing the proof and verification key as a byte array, we pass them - * as fields which makes it cheaper to verify in a circuit. - * - * The proof that is passed here will have been created using a circuit - * that has the #[recursive] attribute on its `main` method. - * - * The number of public inputs denotes how many public inputs are in the inner proof. - * - * @example - * ```typescript - * const artifacts = await backend.generateRecursiveProofArtifacts(proof, numOfPublicInputs); - * ``` - */ - async generateRecursiveProofArtifacts( - proofData: ProofData, - numOfPublicInputs = 0, - ): Promise<{ - proofAsFields: string[]; - vkAsFields: string[]; - vkHash: string; - }> { - await this.instantiate(); - const proof = reconstructProofWithPublicInputs(proofData); - const proofAsFields = ( - await this.api.acirSerializeProofIntoFields(this.acirComposer, proof, numOfPublicInputs) - ).slice(numOfPublicInputs); - - // TODO: perhaps we should put this in the init function. Need to benchmark - // TODO how long it takes. - await this.api.acirInitVerificationKey(this.acirComposer); - - // Note: If you don't init verification key, `acirSerializeVerificationKeyIntoFields`` will just hang on serialization - const vk = await this.api.acirSerializeVerificationKeyIntoFields(this.acirComposer); - - return { - proofAsFields: proofAsFields.map((p) => p.toString()), - vkAsFields: vk[0].map((vk) => vk.toString()), - vkHash: vk[1].toString(), - }; - } - - /** @description Verifies a proof */ - async verifyProof(proofData: ProofData): Promise { - const proof = reconstructProofWithPublicInputs(proofData); - await this.instantiate(); - await this.api.acirInitVerificationKey(this.acirComposer); - // TODO: Change once `@aztec/bb.js` version is updated to use methods without isRecursive flag - return await this.api.acirVerifyProof(this.acirComposer, proof); - } - - async destroy(): Promise { - if (!this.api) { - return; - } - await this.api.destroy(); - } -} - -function reconstructProofWithPublicInputs(proofData: ProofData): Uint8Array { - // Flatten publicInputs - const publicInputsConcatenated = flattenPublicInputsAsArray(proofData.publicInputs); - - // Concatenate publicInputs and proof - const proofWithPublicInputs = Uint8Array.from([...publicInputsConcatenated, ...proofData.proof]); - - return proofWithPublicInputs; -} - // typedoc exports -export { Backend, BackendOptions, CompiledCircuit, ProofData }; +export { Backend, CompiledCircuit, ProofData } from '@noir-lang/types'; +export { BackendOptions } from './types.js'; diff --git a/tooling/noir_js_backend_barretenberg/src/verifier.ts b/tooling/noir_js_backend_barretenberg/src/verifier.ts new file mode 100644 index 00000000000..fe9fa9cfffd --- /dev/null +++ b/tooling/noir_js_backend_barretenberg/src/verifier.ts @@ -0,0 +1,78 @@ +import { ProofData } from '@noir-lang/types'; +import { BackendOptions } from './types.js'; +import { flattenPublicInputsAsArray } from './public_inputs.js'; +import { type Barretenberg } from '@aztec/bb.js'; + +export class BarretenbergVerifier { + // These type assertions are used so that we don't + // have to initialize `api` and `acirComposer` in the constructor. + // These are initialized asynchronously in the `init` function, + // constructors cannot be asynchronous which is why we do this. + + private api!: Barretenberg; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private acirComposer: any; + + constructor(private options: BackendOptions = { threads: 1 }) {} + + /** @ignore */ + async instantiate(): Promise { + if (!this.api) { + if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) { + this.options.threads = navigator.hardwareConcurrency; + } else { + try { + const os = await import('os'); + this.options.threads = os.cpus().length; + } catch (e) { + console.log('Could not detect environment. Falling back to one thread.', e); + } + } + const { Barretenberg, RawBuffer, Crs } = await import('@aztec/bb.js'); + + // This is the number of CRS points necessary to verify a Barretenberg proof. + const NUM_CRS_POINTS_FOR_VERIFICATION: number = 0; + const [api, crs] = await Promise.all([Barretenberg.new(this.options), Crs.new(NUM_CRS_POINTS_FOR_VERIFICATION)]); + + await api.commonInitSlabAllocator(NUM_CRS_POINTS_FOR_VERIFICATION); + await api.srsInitSrs( + new RawBuffer([] /* crs.getG1Data() */), + NUM_CRS_POINTS_FOR_VERIFICATION, + new RawBuffer(crs.getG2Data()), + ); + + this.acirComposer = await api.acirNewAcirComposer(NUM_CRS_POINTS_FOR_VERIFICATION); + this.api = api; + } + } + + /** @description Verifies a proof */ + async verifyProof(proofData: ProofData, verificationKey: Uint8Array): Promise { + const { RawBuffer } = await import('@aztec/bb.js'); + + await this.instantiate(); + // The verifier can be used for a variety of ACIR programs so we should not assume that it + // is preloaded with the correct verification key. + await this.api.acirLoadVerificationKey(this.acirComposer, new RawBuffer(verificationKey)); + + const proof = reconstructProofWithPublicInputs(proofData); + return await this.api.acirVerifyProof(this.acirComposer, proof); + } + + async destroy(): Promise { + if (!this.api) { + return; + } + await this.api.destroy(); + } +} + +export function reconstructProofWithPublicInputs(proofData: ProofData): Uint8Array { + // Flatten publicInputs + const publicInputsConcatenated = flattenPublicInputsAsArray(proofData.publicInputs); + + // Concatenate publicInputs and proof + const proofWithPublicInputs = Uint8Array.from([...publicInputsConcatenated, ...proofData.proof]); + + return proofWithPublicInputs; +} diff --git a/tooling/noir_js_types/src/types.ts b/tooling/noir_js_types/src/types.ts index 3a62d79a807..456e5a57f40 100644 --- a/tooling/noir_js_types/src/types.ts +++ b/tooling/noir_js_types/src/types.ts @@ -29,7 +29,17 @@ export type Abi = { return_witnesses: number[]; }; -export interface Backend { +export interface VerifierBackend { + /** + * @description Verifies a proof */ + verifyProof(proofData: ProofData): Promise; + + /** + * @description Destroys the backend */ + destroy(): Promise; +} + +export interface Backend extends VerifierBackend { /** * @description Generates a proof */ generateProof(decompressedWitness: Uint8Array): Promise; @@ -49,14 +59,6 @@ export interface Backend { /** @description A Field containing the verification key hash */ vkHash: string; }>; - - /** - * @description Verifies a proof */ - verifyProof(proofData: ProofData): Promise; - - /** - * @description Destroys the backend */ - destroy(): Promise; } /**