From 33ee6a7cf628b09199b13e74f59051c830e28682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 9 Oct 2023 14:55:29 +0100 Subject: [PATCH 1/7] feat: devex improvements to noirjs --- tooling/noir_js/tsconfig.json | 2 +- .../noir_js_backend_barretenberg/src/index.ts | 20 +++++++------- tooling/noir_js_types/.gitignore | 2 +- tooling/noir_js_types/src/types.ts | 26 +++++++++++++------ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/tooling/noir_js/tsconfig.json b/tooling/noir_js/tsconfig.json index 1e0fdea09c7..0fd439990a6 100644 --- a/tooling/noir_js/tsconfig.json +++ b/tooling/noir_js/tsconfig.json @@ -13,4 +13,4 @@ }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/tooling/noir_js_backend_barretenberg/src/index.ts b/tooling/noir_js_backend_barretenberg/src/index.ts index b3d3af79b9e..07d407ff49a 100644 --- a/tooling/noir_js_backend_barretenberg/src/index.ts +++ b/tooling/noir_js_backend_barretenberg/src/index.ts @@ -1,28 +1,28 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { acirToUint8Array } from './serialize.js'; -import { Backend, CompiledCircuit, ProofData } from '@noir-lang/types'; +import { Backend, BackendOptions, CompiledCircuit, ProofData } from '@noir-lang/types'; // 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: any; private acirComposer: any; private acirUncompressedBytecode: Uint8Array; private numberOfThreads = 1; - constructor(acirCircuit: CompiledCircuit, numberOfThreads = 1) { - const acirBytecodeBase64 = acirCircuit.bytecode; - this.numberOfThreads = numberOfThreads; - this.acirUncompressedBytecode = acirToUint8Array(acirBytecodeBase64); + public circuit: CompiledCircuit; + public options: BackendOptions; + + constructor(circuit: CompiledCircuit, options: BackendOptions = { numOfThreads: 1 }) { + this.circuit = circuit; + this.options = options; + this.numberOfThreads = options.numOfThreads; + this.acirUncompressedBytecode = acirToUint8Array(this.circuit.bytecode); } - private async instantiate(): Promise { + async instantiate(): Promise { if (!this.api) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore diff --git a/tooling/noir_js_types/.gitignore b/tooling/noir_js_types/.gitignore index 7951405f85a..a65b41774ad 100644 --- a/tooling/noir_js_types/.gitignore +++ b/tooling/noir_js_types/.gitignore @@ -1 +1 @@ -lib \ No newline at end of file +lib diff --git a/tooling/noir_js_types/src/types.ts b/tooling/noir_js_types/src/types.ts index 357e440f155..da03500d58d 100644 --- a/tooling/noir_js_types/src/types.ts +++ b/tooling/noir_js_types/src/types.ts @@ -1,17 +1,27 @@ -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. +interface BackendInternal { + circuit: CompiledCircuit; generateFinalProof(decompressedWitness: Uint8Array): Promise; - - // Generates an inner proof. This is the proof that will be verified - // in another circuit. - generateIntermediateProof(decompressedWitness: Uint8Array): Promise; - verifyFinalProof(proofData: ProofData): Promise; + instantiate(): Promise; + destroy(): Promise; +} +export type ProofArtifacts = { + proofAsFields: string[]; + vkAsFields: string[]; + vkHash: string; +}; + +export interface Backend extends BackendInternal { + generateIntermediateProof(decompressedWitness: Uint8Array): Promise; verifyIntermediateProof(proofData: ProofData): Promise; + generateIntermediateProofArtifacts(proofData: ProofData, numOfPublicInputs: number): Promise; } +export type BackendOptions = { + numOfThreads: number; +}; + export type ProofData = { publicInputs: Uint8Array[]; proof: Uint8Array; From 258f94c5052df33eefe314b7d644f8187749ae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 9 Oct 2023 15:11:38 +0100 Subject: [PATCH 2/7] feat: devex improvements to noirjs --- tooling/noir_js/src/index.ts | 37 ++++++++++++++++++++++++++------ tooling/noir_js/src/program.ts | 32 --------------------------- tooling/noir_js_types/.gitignore | 2 +- 3 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 tooling/noir_js/src/program.ts diff --git a/tooling/noir_js/src/index.ts b/tooling/noir_js/src/index.ts index 71914d59dbf..b4be1aa1871 100644 --- a/tooling/noir_js/src/index.ts +++ b/tooling/noir_js/src/index.ts @@ -1,9 +1,34 @@ -import * as acvm from '@noir-lang/acvm_js'; -import * as abi from '@noir-lang/noirc_abi'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Backend, ProofData } from '@noir-lang/types'; +import { generateWitness } from './witness_generation.js'; +import initAbi from '@noir-lang/noirc_abi'; +import initACVM from '@noir-lang/acvm_js'; -export { acvm, abi }; +export class Noir { + constructor(private backend: Backend) {} -export { generateWitness } from './witness_generation.js'; -export { acirToUint8Array, witnessMapToUint8Array } from './serialize.js'; + async init(): Promise { + // If these are available, then we are in the + // web environment. For the node environment, this + // is a no-op. + if (typeof initAbi === 'function') { + await Promise.all([initAbi(), initACVM()]); + } + await this.backend.instantiate(); + } -export { Noir } from './program.js'; + async destroy(): Promise { + await this.backend.destroy(); + } + + // Initial inputs to your program + async generateFinalProof(inputs: any): Promise { + await this.init(); + const serializedWitness = await generateWitness(this.backend.circuit, inputs); + return this.backend.generateFinalProof(serializedWitness); + } + + async verifyFinalProof(proofData: ProofData): Promise { + return this.backend.verifyFinalProof(proofData); + } +} diff --git a/tooling/noir_js/src/program.ts b/tooling/noir_js/src/program.ts deleted file mode 100644 index 1fd32862010..00000000000 --- a/tooling/noir_js/src/program.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Backend, CompiledCircuit, ProofData } from '@noir-lang/types'; -import { generateWitness } from './witness_generation.js'; -import initAbi from '@noir-lang/noirc_abi'; -import initACVM from '@noir-lang/acvm_js'; - -export class Noir { - constructor( - private circuit: CompiledCircuit, - private backend: Backend, - ) {} - - async init(): Promise { - // If these are available, then we are in the - // web environment. For the node environment, this - // is a no-op. - if (typeof initAbi === 'function') { - await Promise.all([initAbi(), initACVM()]); - } - } - - // Initial inputs to your program - async generateFinalProof(inputs: any): Promise { - await this.init(); - const serializedWitness = await generateWitness(this.circuit, inputs); - return this.backend.generateFinalProof(serializedWitness); - } - - async verifyFinalProof(proofData: ProofData): Promise { - return this.backend.verifyFinalProof(proofData); - } -} diff --git a/tooling/noir_js_types/.gitignore b/tooling/noir_js_types/.gitignore index a65b41774ad..7951405f85a 100644 --- a/tooling/noir_js_types/.gitignore +++ b/tooling/noir_js_types/.gitignore @@ -1 +1 @@ -lib +lib \ No newline at end of file From d5732274fd41edf68d79b632e41070448008e797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 9 Oct 2023 17:58:30 +0100 Subject: [PATCH 3/7] fixing tests, adding Tom suggestions on lazy backend init --- .../test/browser/compile_prove_verify.test.ts | 2 +- .../test/browser/recursion.test.ts | 8 +- .../integration-tests/test/browser/utils.ts | 46 ++++ .../test/node/smart_contract_verifier.test.ts | 2 +- tooling/noir_js/src/index.ts | 1 - tooling/noir_js/test/node/cjs.test.cjs | 16 +- tooling/noir_js/test/node/e2e.test.ts | 239 +++++++++--------- tooling/noir_js/test/node/smoke.test.ts | 2 +- 8 files changed, 177 insertions(+), 139 deletions(-) diff --git a/compiler/integration-tests/test/browser/compile_prove_verify.test.ts b/compiler/integration-tests/test/browser/compile_prove_verify.test.ts index a2d6c7e94ed..c8c83851fea 100644 --- a/compiler/integration-tests/test/browser/compile_prove_verify.test.ts +++ b/compiler/integration-tests/test/browser/compile_prove_verify.test.ts @@ -61,7 +61,7 @@ test_cases.forEach((testInfo) => { } const backend = new BarretenbergBackend(noir_program); - const program = new Noir(noir_program, backend); + const program = new Noir(backend); const prover_toml = await getFile(`${base_relative_path}/${test_case}/Prover.toml`); const inputs = TOML.parse(prover_toml); diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts index c773e80ea43..358acce6f47 100644 --- a/compiler/integration-tests/test/browser/recursion.test.ts +++ b/compiler/integration-tests/test/browser/recursion.test.ts @@ -4,17 +4,15 @@ import { TEST_LOG_LEVEL } from '../environment.js'; import { Logger } from 'tslog'; import { initializeResolver } from '@noir-lang/source-resolver'; import newCompiler, { compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm'; -import { acvm, abi, generateWitness } from '@noir-lang/noir_js'; +import newABICoder from '@noir-lang/noirc_abi'; +import initACVM from '@noir-lang/acvm_js'; import * as TOML from 'smol-toml'; import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'; -import { getFile } from './utils.js'; +import { getFile, generateWitness } from './utils.js'; const logger = new Logger({ name: 'test', minLevel: TEST_LOG_LEVEL }); -const { default: initACVM } = acvm; -const { default: newABICoder } = abi; - await newCompiler(); await newABICoder(); await initACVM(); diff --git a/compiler/integration-tests/test/browser/utils.ts b/compiler/integration-tests/test/browser/utils.ts index 35588407193..ffa7edd9a99 100644 --- a/compiler/integration-tests/test/browser/utils.ts +++ b/compiler/integration-tests/test/browser/utils.ts @@ -1,3 +1,8 @@ +import { abiEncode } from '@noir-lang/noirc_abi'; +import { executeCircuit, WitnessMap, compressWitness } from '@noir-lang/acvm_js'; +import { decompressSync as gunzip } from 'fflate'; +import { CompiledCircuit } from '@noir-lang/types'; + export async function getFile(file_path: string): Promise { const file_url = new URL(file_path, import.meta.url); const response = await fetch(file_url); @@ -6,3 +11,44 @@ export async function getFile(file_path: string): Promise { return await response.text(); } + +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.'); + } +} + +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); +} + +export async function generateWitness(compiledProgram: CompiledCircuit, inputs: unknown): Promise { + // Throws on ABI encoding error + const witnessMap = abiEncode(compiledProgram.abi, inputs, null); + + // Execute the circuit to generate the rest of the witnesses and serialize + // them into a Uint8Array. + try { + const solvedWitness = await executeCircuit(base64Decode(compiledProgram.bytecode), witnessMap, () => { + throw Error('unexpected oracle during execution'); + }); + return witnessMapToUint8Array(solvedWitness); + } catch (err) { + throw new Error(`Circuit execution failed: ${err}`); + } +} diff --git a/compiler/integration-tests/test/node/smart_contract_verifier.test.ts b/compiler/integration-tests/test/node/smart_contract_verifier.test.ts index 038c692220f..5a117057ed1 100644 --- a/compiler/integration-tests/test/node/smart_contract_verifier.test.ts +++ b/compiler/integration-tests/test/node/smart_contract_verifier.test.ts @@ -36,7 +36,7 @@ test_cases.forEach((testInfo) => { const noir_program = compile(noir_source_path); const backend = new BarretenbergBackend(noir_program); - const program = new Noir(noir_program, backend); + const program = new Noir(backend); // JS Proving diff --git a/tooling/noir_js/src/index.ts b/tooling/noir_js/src/index.ts index b4be1aa1871..c5b106470df 100644 --- a/tooling/noir_js/src/index.ts +++ b/tooling/noir_js/src/index.ts @@ -14,7 +14,6 @@ export class Noir { if (typeof initAbi === 'function') { await Promise.all([initAbi(), initACVM()]); } - await this.backend.instantiate(); } async destroy(): Promise { diff --git a/tooling/noir_js/test/node/cjs.test.cjs b/tooling/noir_js/test/node/cjs.test.cjs index b7b30d7dcdb..16e4851bf1f 100644 --- a/tooling/noir_js/test/node/cjs.test.cjs +++ b/tooling/noir_js/test/node/cjs.test.cjs @@ -2,14 +2,14 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const chai = require('chai'); const assert_lt_json = require('../noir_compiled_examples/assert_lt/target/assert_lt.json'); -const noirjs = require('@noir-lang/noir_js'); +const { generateWitness } = require('../../lib/witness_generation.cjs'); it('generates witnesses successfully', async () => { const inputs = { x: '2', y: '3', }; - const _solvedWitness = await noirjs.generateWitness(assert_lt_json, inputs); + const _solvedWitness = await generateWitness(assert_lt_json, inputs); }); it('string input and number input are the same', async () => { @@ -21,8 +21,8 @@ it('string input and number input are the same', async () => { x: 2, y: 3, }; - const solvedWitnessString = await noirjs.generateWitness(assert_lt_json, inputsString); - const solvedWitnessNumber = await noirjs.generateWitness(assert_lt_json, inputsNumber); + const solvedWitnessString = await generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await generateWitness(assert_lt_json, inputsNumber); chai.expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -36,8 +36,8 @@ it('string input and number input are the same', async () => { y: 3, }; - const solvedWitnessString = await noirjs.generateWitness(assert_lt_json, inputsString); - const solvedWitnessNumber = await noirjs.generateWitness(assert_lt_json, inputsNumber); + const solvedWitnessString = await generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await generateWitness(assert_lt_json, inputsNumber); chai.expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -48,7 +48,7 @@ it('0x prefixed string input for inputs will throw', async () => { }; try { - await noirjs.generateWitness(assert_lt_json, inputsHexPrefix); + await generateWitness(assert_lt_json, inputsHexPrefix); chai.expect.fail( 'Expected generatedWitness to throw, due to inputs being prefixed with 0x. Currently not supported', ); @@ -66,7 +66,7 @@ describe('input validation', () => { }; try { - await noirjs.generateWitness(assert_lt_json, inputs); + await generateWitness(assert_lt_json, inputs); chai.expect.fail('Expected generatedWitness to throw, due to x not being convertible to a uint64'); } catch (error) { const knownError = error; diff --git a/tooling/noir_js/test/node/e2e.test.ts b/tooling/noir_js/test/node/e2e.test.ts index fe0d26c7e3b..ab0f0fff782 100644 --- a/tooling/noir_js/test/node/e2e.test.ts +++ b/tooling/noir_js/test/node/e2e.test.ts @@ -1,139 +1,134 @@ 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 { Noir } from '../../src/program.js'; +import { generateWitness } from '../../src/witness_generation.js'; +import { Noir } from '../../src/index.js'; import { BarretenbergBackend as Backend } from '@noir-lang/backend_barretenberg'; -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 inputs = { + x: '2', + y: '3', +}; - // bb.js part - // - // Proof creation - const prover = new Backend(assert_lt_json); - const proof = await prover.generateFinalProof(serializedWitness); +describe('Outer proofs', () => { + let backend: Backend; + let noir: Noir; - // Proof verification - const isValid = await prover.verifyFinalProof(proof); - expect(isValid).to.be.true; -}); + before(() => { + backend = new Backend(assert_lt_json, { numOfThreads: 4 }); + noir = new Noir(backend); + }); -it('end-to-end proof creation and verification (outer) -- Program API', async () => { - // Noir.Js part - const inputs = { - x: '2', - y: '3', - }; - - // Initialize backend - const backend = new Backend(assert_lt_json); - // Initialize program - const program = new Noir(assert_lt_json, backend); - // Generate proof - const proof = await program.generateFinalProof(inputs); - - // Proof verification - const isValid = await program.verifyFinalProof(proof); - expect(isValid).to.be.true; -}); + it('Creates and verifies end-to-end outer proofs with underlying backend API', async () => { + // Noir.Js part + const serializedWitness = await generateWitness(assert_lt_json, inputs); -it('end-to-end proof creation and verification (inner)', async () => { - // Noir.Js part - const inputs = { - x: '2', - y: '3', - }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + // BackendBarretenberg part + const prover = new Backend(assert_lt_json, { numOfThreads: 4 }); + const proof = await prover.generateFinalProof(serializedWitness); + const isValid = await prover.verifyFinalProof(proof); - // bb.js part - // - // Proof creation - const prover = new Backend(assert_lt_json); - const proof = await prover.generateIntermediateProof(serializedWitness); + // tests + expect(isValid).to.be.true; + }); - // Proof verification - const isValid = await prover.verifyIntermediateProof(proof); - expect(isValid).to.be.true; -}); + it('Creates and verifies end-to-end outer proofs with Noir API', async () => { + const proof = await noir.generateFinalProof(inputs); + const isValid = await noir.verifyFinalProof(proof); -// The "real" workflow will involve a prover and a verifier on different systems. -// -// We cannot do this in our tests because they will panic with: -// `RuntimeError: null function or function signature mismatch` -// -// This happens when we we create a proof with one barretenberg instance and -// try to verify it with another. -// -// If this bug is fixed, we can remove this test and split barretenberg into -// a prover and verifier class to more accurately reflect what happens in production. -// -// If its not fixable, we can leave it in as documentation of this behavior. -it('[BUG] -- bb.js null function or function signature mismatch (different instance) ', async () => { - // Noir.Js part - const inputs = { - x: '2', - y: '3', - }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); - - // bb.js part - const prover = new Backend(assert_lt_json); - - const proof = await prover.generateFinalProof(serializedWitness); - - try { - const verifier = new Backend(assert_lt_json); - 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.', - ); - } catch (error) { - const knownError = error as Error; - expect(knownError.message).to.contain('null function or function signature mismatch'); - } + // tests + expect(isValid).to.be.true; + }); }); -// This bug occurs when we use the same backend to create an inner proof and then an outer proof -// and then try to verify either one of them. -// -// The panic occurs when we try to verify the outer/inner proof that was created. -// If we only create one type of proof, then this works as expected. -// -// If we do not create an inner proof, then this will work as expected. -it('[BUG] -- bb.js null function or function signature mismatch (outer-inner) ', async () => { - // Noir.Js part - const inputs = { - x: '2', - y: '3', - }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); - - // bb.js part +describe('Inner proofs', () => { + let backend: Backend; + let noir: Noir; + + before(() => { + backend = new Backend(assert_lt_json, { numOfThreads: 4 }); + noir = new Noir(backend); + }); + + it('Creates and verifies end-to-end inner proofs with underlying backend API', async () => { + // Noir.Js part + const inputs = { + x: '2', + y: '3', + }; + const serializedWitness = await generateWitness(assert_lt_json, inputs); + + // bb.js part + // + // Proof creation + const proof = await backend.generateIntermediateProof(serializedWitness); + + // Proof verification + const isValid = await backend.verifyIntermediateProof(proof); + expect(isValid).to.be.true; + }); + + // The "real" workflow will involve a prover and a verifier on different systems. // - // Proof creation + // We cannot do this in our tests because they will panic with: + // `RuntimeError: null function or function signature mismatch` // - const prover = new Backend(assert_lt_json); - // Create a proof using both proving systems, the majority of the time - // one would only use outer proofs. - const proofOuter = await prover.generateFinalProof(serializedWitness); - const _proofInner = await prover.generateIntermediateProof(serializedWitness); - - // Proof verification + // This happens when we we create a proof with one barretenberg instance and + // try to verify it with another. + // + // If this bug is fixed, we can remove this test and split barretenberg into + // a prover and verifier class to more accurately reflect what happens in production. + // + // If its not fixable, we can leave it in as documentation of this behavior. + it('Expects the "null function or function signature mismatch" if using different instance', async () => { + const serializedWitness = await generateWitness(assert_lt_json, inputs); + + // bb.js part + const proof = await backend.generateFinalProof(serializedWitness); + + try { + const verifier = new Backend(assert_lt_json); + 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.', + ); + } catch (error) { + const knownError = error as Error; + expect(knownError.message).to.contain('null function or function signature mismatch'); + } + }); + + // This bug occurs when we use the same backend to create an inner proof and then an outer proof + // and then try to verify either one of them. + // + // The panic occurs when we try to verify the outer/inner proof that was created. + // If we only create one type of proof, then this works as expected. // - try { - const isValidOuter = await prover.verifyFinalProof(proofOuter); - expect(isValidOuter).to.be.true; - // We can also try verifying an inner proof and it will fail. - // const isValidInner = await prover.verifyInnerProof(_proofInner); - // expect(isValidInner).to.be.true; - expect.fail('bb.js currently returns a bug when we try to verify an inner and outer proof with the same backend'); - } catch (error) { - const knownError = error as Error; - expect(knownError.message).to.contain('null function or function signature mismatch'); - } + // If we do not create an inner proof, then this will work as expected. + it('Expects the "null function or function signature mismatch" when mixing different proof types', async () => { + const serializedWitness = await generateWitness(assert_lt_json, inputs); + + // bb.js part + // + // Proof creation + // + const prover = new Backend(assert_lt_json); + // Create a proof using both proving systems, the majority of the time + // one would only use outer proofs. + const proofOuter = await prover.generateFinalProof(serializedWitness); + const _proofInner = await prover.generateIntermediateProof(serializedWitness); + + // Proof verification + // + try { + const isValidOuter = await prover.verifyFinalProof(proofOuter); + expect(isValidOuter).to.be.true; + // We can also try verifying an inner proof and it will fail. + // const isValidInner = await prover.verifyInnerProof(_proofInner); + // expect(isValidInner).to.be.true; + expect.fail('bb.js currently returns a bug when we try to verify an inner and outer proof with the same backend'); + } catch (error) { + const knownError = error as Error; + expect(knownError.message).to.contain('null function or function signature mismatch'); + } + }); }); diff --git a/tooling/noir_js/test/node/smoke.test.ts b/tooling/noir_js/test/node/smoke.test.ts index 4b0291c0f41..889fc43a971 100644 --- a/tooling/noir_js/test/node/smoke.test.ts +++ b/tooling/noir_js/test/node/smoke.test.ts @@ -1,6 +1,6 @@ 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 { generateWitness } from '../../src/witness_generation.js'; it('generates witnesses successfully', async () => { const inputs = { From b57b4c44cc6112ae4728afbf88c4c3053239674f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 9 Oct 2023 21:04:02 +0100 Subject: [PATCH 4/7] linter --- tooling/noir_js/test/node/e2e.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tooling/noir_js/test/node/e2e.test.ts b/tooling/noir_js/test/node/e2e.test.ts index ab0f0fff782..93f40f42abf 100644 --- a/tooling/noir_js/test/node/e2e.test.ts +++ b/tooling/noir_js/test/node/e2e.test.ts @@ -42,11 +42,9 @@ describe('Outer proofs', () => { describe('Inner proofs', () => { let backend: Backend; - let noir: Noir; before(() => { backend = new Backend(assert_lt_json, { numOfThreads: 4 }); - noir = new Noir(backend); }); it('Creates and verifies end-to-end inner proofs with underlying backend API', async () => { From ffba1108532767b435cff3608d72ae0c8d5405a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 10 Oct 2023 17:08:06 +0100 Subject: [PATCH 5/7] making changes as discussed with kev --- .../test/browser/recursion.test.ts | 9 ++-- .../integration-tests/test/browser/utils.ts | 46 ------------------- tooling/noir_js/src/index.ts | 39 +++++++++++++--- tooling/noir_js/src/witness_generation.ts | 22 --------- tooling/noir_js/test/node/cjs.test.cjs | 18 ++++---- tooling/noir_js/test/node/e2e.test.ts | 11 +++-- tooling/noir_js/test/node/smoke.test.ts | 18 ++++---- 7 files changed, 64 insertions(+), 99 deletions(-) delete mode 100644 tooling/noir_js/src/witness_generation.ts diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts index 358acce6f47..fc76ad5ab79 100644 --- a/compiler/integration-tests/test/browser/recursion.test.ts +++ b/compiler/integration-tests/test/browser/recursion.test.ts @@ -6,10 +6,11 @@ import { initializeResolver } from '@noir-lang/source-resolver'; import newCompiler, { compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm'; import newABICoder from '@noir-lang/noirc_abi'; import initACVM from '@noir-lang/acvm_js'; +import { Noir } from '@noir-lang/noir_js'; import * as TOML from 'smol-toml'; import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'; -import { getFile, generateWitness } from './utils.js'; +import { getFile } from './utils.js'; const logger = new Logger({ name: 'test', minLevel: TEST_LOG_LEVEL }); @@ -39,6 +40,8 @@ describe('It compiles noir program code, receiving circuit bytes and abi object. let circuit_main_toml; let circuit_recursion_source; + const noir = new Noir(); // backendless noir + before(async () => { circuit_main_source = await getFile(`${base_relative_path}/${circuit_main}/src/main.nr`); circuit_main_toml = await getFile(`${base_relative_path}/${circuit_main}/Prover.toml`); @@ -52,7 +55,7 @@ describe('It compiles noir program code, receiving circuit bytes and abi object. const main_backend = new BarretenbergBackend(main_program); - const main_witnessUint8Array = await generateWitness(main_program, main_inputs); + const main_witnessUint8Array = await noir.generateWitness(main_program, main_inputs); const main_proof = await main_backend.generateIntermediateProof(main_witnessUint8Array); const main_verification = await main_backend.verifyIntermediateProof(main_proof); @@ -81,7 +84,7 @@ describe('It compiles noir program code, receiving circuit bytes and abi object. const recursion_backend = new BarretenbergBackend(recursion_program); - const recursion_witnessUint8Array = await generateWitness(recursion_program, recursion_inputs); + const recursion_witnessUint8Array = await noir.generateWitness(recursion_program, recursion_inputs); const recursion_proof = await recursion_backend.generateFinalProof(recursion_witnessUint8Array); diff --git a/compiler/integration-tests/test/browser/utils.ts b/compiler/integration-tests/test/browser/utils.ts index ffa7edd9a99..35588407193 100644 --- a/compiler/integration-tests/test/browser/utils.ts +++ b/compiler/integration-tests/test/browser/utils.ts @@ -1,8 +1,3 @@ -import { abiEncode } from '@noir-lang/noirc_abi'; -import { executeCircuit, WitnessMap, compressWitness } from '@noir-lang/acvm_js'; -import { decompressSync as gunzip } from 'fflate'; -import { CompiledCircuit } from '@noir-lang/types'; - export async function getFile(file_path: string): Promise { const file_url = new URL(file_path, import.meta.url); const response = await fetch(file_url); @@ -11,44 +6,3 @@ export async function getFile(file_path: string): Promise { return await response.text(); } - -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.'); - } -} - -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); -} - -export async function generateWitness(compiledProgram: CompiledCircuit, inputs: unknown): Promise { - // Throws on ABI encoding error - const witnessMap = abiEncode(compiledProgram.abi, inputs, null); - - // Execute the circuit to generate the rest of the witnesses and serialize - // them into a Uint8Array. - try { - const solvedWitness = await executeCircuit(base64Decode(compiledProgram.bytecode), witnessMap, () => { - throw Error('unexpected oracle during execution'); - }); - return witnessMapToUint8Array(solvedWitness); - } catch (err) { - throw new Error(`Circuit execution failed: ${err}`); - } -} diff --git a/tooling/noir_js/src/index.ts b/tooling/noir_js/src/index.ts index c5b106470df..7cd6231e003 100644 --- a/tooling/noir_js/src/index.ts +++ b/tooling/noir_js/src/index.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Backend, ProofData } from '@noir-lang/types'; -import { generateWitness } from './witness_generation.js'; -import initAbi from '@noir-lang/noirc_abi'; -import initACVM from '@noir-lang/acvm_js'; +import { Backend, CompiledCircuit, ProofData } from '@noir-lang/types'; +import initAbi, * as abi from '@noir-lang/noirc_abi'; +import initACVM, * as acvm from '@noir-lang/acvm_js'; +import { base64Decode } from './base64_decode.js'; +import { witnessMapToUint8Array } from './serialize.js'; -export class Noir { - constructor(private backend: Backend) {} +class Noir { + constructor(private backend?: Backend) {} async init(): Promise { // If these are available, then we are in the @@ -17,17 +18,41 @@ export class Noir { } async destroy(): Promise { + if (!this.backend) throw new Error('No backend to destroy'); + await this.backend.destroy(); } // Initial inputs to your program async generateFinalProof(inputs: any): Promise { + if (!this.backend) throw new Error('Cannot generate proofs without a backend'); + await this.init(); - const serializedWitness = await generateWitness(this.backend.circuit, inputs); + const serializedWitness = await this.generateWitness(this.backend.circuit, inputs); return this.backend.generateFinalProof(serializedWitness); } async verifyFinalProof(proofData: ProofData): Promise { + if (!this.backend) throw new Error('Cannot verify proofs without a backend'); + return this.backend.verifyFinalProof(proofData); } + + async generateWitness(circuit: CompiledCircuit, inputs: unknown) { + // Throws on ABI encoding error + const witnessMap = abi.abiEncode(circuit.abi, inputs, null); + + // Execute the circuit to generate the rest of the witnesses and serialize + // them into a Uint8Array. + try { + const solvedWitness = await acvm.executeCircuit(base64Decode(circuit.bytecode), witnessMap, () => { + throw Error('unexpected oracle during execution'); + }); + return witnessMapToUint8Array(solvedWitness); + } catch (err) { + throw new Error(`Circuit execution failed: ${err}`); + } + } } + +export { Noir, abi, acvm }; diff --git a/tooling/noir_js/src/witness_generation.ts b/tooling/noir_js/src/witness_generation.ts deleted file mode 100644 index 3b1dfd90109..00000000000 --- a/tooling/noir_js/src/witness_generation.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { abiEncode } 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 { - // Throws on ABI encoding error - const witnessMap = abiEncode(compiledProgram.abi, inputs, null); - - // Execute the circuit to generate the rest of the witnesses and serialize - // them into a Uint8Array. - try { - const solvedWitness = await executeCircuit(base64Decode(compiledProgram.bytecode), witnessMap, () => { - throw Error('unexpected oracle during execution'); - }); - return witnessMapToUint8Array(solvedWitness); - } catch (err) { - throw new Error(`Circuit execution failed: ${err}`); - } -} diff --git a/tooling/noir_js/test/node/cjs.test.cjs b/tooling/noir_js/test/node/cjs.test.cjs index 16e4851bf1f..cd6ffa81252 100644 --- a/tooling/noir_js/test/node/cjs.test.cjs +++ b/tooling/noir_js/test/node/cjs.test.cjs @@ -2,14 +2,16 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const chai = require('chai'); const assert_lt_json = require('../noir_compiled_examples/assert_lt/target/assert_lt.json'); -const { generateWitness } = require('../../lib/witness_generation.cjs'); +const { Noir } = require('../../lib/index.cjs'); + +const noir = new Noir(); // backendless noir it('generates witnesses successfully', async () => { const inputs = { x: '2', y: '3', }; - const _solvedWitness = await generateWitness(assert_lt_json, inputs); + const _solvedWitness = await noir.generateWitness(assert_lt_json, inputs); }); it('string input and number input are the same', async () => { @@ -21,8 +23,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 noir.generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await noir.generateWitness(assert_lt_json, inputsNumber); chai.expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -36,8 +38,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 noir.generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await noir.generateWitness(assert_lt_json, inputsNumber); chai.expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -48,7 +50,7 @@ it('0x prefixed string input for inputs will throw', async () => { }; try { - await generateWitness(assert_lt_json, inputsHexPrefix); + await noir.generateWitness(assert_lt_json, inputsHexPrefix); chai.expect.fail( 'Expected generatedWitness to throw, due to inputs being prefixed with 0x. Currently not supported', ); @@ -66,7 +68,7 @@ describe('input validation', () => { }; try { - await generateWitness(assert_lt_json, inputs); + await noir.generateWitness(assert_lt_json, inputs); chai.expect.fail('Expected generatedWitness to throw, due to x not being convertible to a uint64'); } catch (error) { const knownError = error; diff --git a/tooling/noir_js/test/node/e2e.test.ts b/tooling/noir_js/test/node/e2e.test.ts index 93f40f42abf..e41cf7ee299 100644 --- a/tooling/noir_js/test/node/e2e.test.ts +++ b/tooling/noir_js/test/node/e2e.test.ts @@ -1,6 +1,5 @@ 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/witness_generation.js'; import { Noir } from '../../src/index.js'; import { BarretenbergBackend as Backend } from '@noir-lang/backend_barretenberg'; @@ -20,7 +19,7 @@ describe('Outer proofs', () => { it('Creates and verifies end-to-end outer proofs with underlying backend API', async () => { // Noir.Js part - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await noir.generateWitness(assert_lt_json, inputs); // BackendBarretenberg part const prover = new Backend(assert_lt_json, { numOfThreads: 4 }); @@ -42,9 +41,11 @@ describe('Outer proofs', () => { describe('Inner proofs', () => { let backend: Backend; + let noir: Noir; before(() => { backend = new Backend(assert_lt_json, { numOfThreads: 4 }); + noir = new Noir(); // backendless noir; }); it('Creates and verifies end-to-end inner proofs with underlying backend API', async () => { @@ -53,7 +54,7 @@ describe('Inner proofs', () => { x: '2', y: '3', }; - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await noir.generateWitness(assert_lt_json, inputs); // bb.js part // @@ -78,7 +79,7 @@ describe('Inner proofs', () => { // // If its not fixable, we can leave it in as documentation of this behavior. it('Expects the "null function or function signature mismatch" if using different instance', async () => { - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await noir.generateWitness(assert_lt_json, inputs); // bb.js part const proof = await backend.generateFinalProof(serializedWitness); @@ -103,7 +104,7 @@ describe('Inner proofs', () => { // // If we do not create an inner proof, then this will work as expected. it('Expects the "null function or function signature mismatch" when mixing different proof types', async () => { - const serializedWitness = await generateWitness(assert_lt_json, inputs); + const serializedWitness = await noir.generateWitness(assert_lt_json, inputs); // bb.js part // diff --git a/tooling/noir_js/test/node/smoke.test.ts b/tooling/noir_js/test/node/smoke.test.ts index 889fc43a971..1d4b50b36e2 100644 --- a/tooling/noir_js/test/node/smoke.test.ts +++ b/tooling/noir_js/test/node/smoke.test.ts @@ -1,13 +1,15 @@ 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/witness_generation.js'; +import { Noir } from '../../src/index.js'; + +const noir = new Noir(); // backendless noir it('generates witnesses successfully', async () => { const inputs = { x: '2', y: '3', }; - expect(() => generateWitness(assert_lt_json, inputs)).to.not.throw; + expect(() => noir.generateWitness(assert_lt_json, inputs)).to.not.throw; }); it('string input and number input are the same', async () => { @@ -19,8 +21,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 noir.generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await noir.generateWitness(assert_lt_json, inputsNumber); expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -34,8 +36,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 noir.generateWitness(assert_lt_json, inputsString); + const solvedWitnessNumber = await noir.generateWitness(assert_lt_json, inputsNumber); expect(solvedWitnessString).to.deep.equal(solvedWitnessNumber); }); @@ -46,7 +48,7 @@ it('0x prefixed string input for inputs will throw', async () => { }; try { - await generateWitness(assert_lt_json, inputsHexPrefix); + await noir.generateWitness(assert_lt_json, 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 +64,7 @@ describe('input validation', () => { }; try { - await generateWitness(assert_lt_json, inputs); + await noir.generateWitness(assert_lt_json, inputs); expect.fail('Expected generatedWitness to throw, due to x not being convertible to a uint64'); } catch (error) { const knownError = error as Error; From 4cdc983c0fe372efad52b3a90853cc9cb6a91126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Tue, 10 Oct 2023 17:49:55 +0100 Subject: [PATCH 6/7] chore: fixing conflicts --- .../test/browser/recursion.test.ts | 7 ++-- .../assert_lt/target/assert_lt.json | 37 +------------------ 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/compiler/integration-tests/test/browser/recursion.test.ts b/compiler/integration-tests/test/browser/recursion.test.ts index 64faa28b600..4d0e3c56dd3 100644 --- a/compiler/integration-tests/test/browser/recursion.test.ts +++ b/compiler/integration-tests/test/browser/recursion.test.ts @@ -4,9 +4,7 @@ import { TEST_LOG_LEVEL } from '../environment.js'; import { Logger } from 'tslog'; import { initializeResolver } from '@noir-lang/source-resolver'; import newCompiler, { compile, init_log_level as compilerLogLevel } from '@noir-lang/noir_wasm'; -import newABICoder from '@noir-lang/noirc_abi'; -import initACVM from '@noir-lang/acvm_js'; -import { Noir } from '@noir-lang/noir_js'; +import { Noir, acvm, abi } from '@noir-lang/noir_js'; import * as TOML from 'smol-toml'; import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'; @@ -15,6 +13,9 @@ import { Field, InputMap } from '@noir-lang/noirc_abi'; const logger = new Logger({ name: 'test', minLevel: TEST_LOG_LEVEL }); +const { default: initACVM } = acvm; +const { default: newABICoder } = abi; + await newCompiler(); await newABICoder(); await initACVM(); diff --git a/tooling/noir_js/test/noir_compiled_examples/assert_lt/target/assert_lt.json b/tooling/noir_js/test/noir_compiled_examples/assert_lt/target/assert_lt.json index 61e00859a43..fdf25a4743d 100644 --- a/tooling/noir_js/test/noir_compiled_examples/assert_lt/target/assert_lt.json +++ b/tooling/noir_js/test/noir_compiled_examples/assert_lt/target/assert_lt.json @@ -1,36 +1 @@ -{ - "backend": "acvm-backend-barretenberg", - "abi": { - "parameters": [ - { - "name": "x", - "type": { - "kind": "integer", - "sign": "unsigned", - "width": 64 - }, - "visibility": "private" - }, - { - "name": "y", - "type": { - "kind": "integer", - "sign": "unsigned", - "width": 64 - }, - "visibility": "public" - } - ], - "param_witnesses": { - "x": [ - 1 - ], - "y": [ - 2 - ] - }, - "return_type": null, - "return_witnesses": [] - }, - "bytecode": "H4sIAAAAAAAA/81WXW7DIAw20BJl0noWE6CBt6k3WbT0/kfYooJm0bQPxZZqKXKw4PPPB5Y/AOATbqL+Pl30F1nrsqaiiq52j+cQ1nlanXffOOUlRQxxOSeXXEzxZ0rerymkOS95xuyCX901Zn/Fm5jXsbDBcobE9yxm7BNn+LCQxnvY+dfEZoq2AjlB46et42nHxupcgqSDAO4R+C6/VN5Hfo6QQIrWFDvFwP9DoaKZY1aM90C/HpdrDTupvyP2nS/a6GzRA7GNhEfd7Nu4qJxvb/5CzimiFcG4kDN7e9QDnJHY6vkTiQX4aoICjR5FG3mdHCwp5rYe4H6SMAK+Kxbng+zFGkGmCbf1652eLGPOAyOvHfV72sC4J1nLyPOjptXKL8ADXjPGCwAA" -} +{"backend":"acvm-backend-barretenberg","abi":{"parameters":[{"name":"x","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"private"},{"name":"y","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"public"}],"param_witnesses":{"x":[1],"y":[2]},"return_type":null,"return_witnesses":[]},"bytecode":"H4sIAAAAAAAA/81WXW7DIAw20BJl0noWE6CBt6k3WbT0/kfYooJm0bQPxZZqKXKw4PPPB5Y/AOATbqL+Pl30F1nrsqaiiq52j+cQ1nlanXffOOUlRQxxOSeXXEzxZ0rerymkOS95xuyCX901Zn/Fm5jXsbDBcobE9yxm7BNn+LCQxnvY+dfEZoq2AjlB46et42nHxupcgqSDAO4R+C6/VN5Hfo6QQIrWFDvFwP9DoaKZY1aM90C/HpdrDTupvyP2nS/a6GzRA7GNhEfd7Nu4qJxvb/5CzimiFcG4kDN7e9QDnJHY6vkTiQX4aoICjR5FG3mdHCwp5rYe4H6SMAK+Kxbng+zFGkGmCbf1652eLGPOAyOvHfV72sC4J1nLyPOjptXKL8ADXjPGCwAA"} \ No newline at end of file From a0897ff685d32fbf9d9b668f723ea1ce5379b77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Wed, 11 Oct 2023 12:41:39 +0100 Subject: [PATCH 7/7] lost the destroy function with the conflict solving --- tooling/noir_js/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tooling/noir_js/src/index.ts b/tooling/noir_js/src/index.ts index 314e237a8da..ff345a13130 100644 --- a/tooling/noir_js/src/index.ts +++ b/tooling/noir_js/src/index.ts @@ -19,6 +19,12 @@ class Noir { } } + async destroy(): Promise { + if (!this.backend) throw new Error('No backend to destroy'); + + await this.backend.destroy(); + } + private getBackend(): Backend { if (this.backend === undefined) throw new Error('Operation requires a backend but none was provided'); return this.backend;