diff --git a/js/package.json b/js/package.json index 8764f83..655215c 100644 --- a/js/package.json +++ b/js/package.json @@ -14,6 +14,7 @@ "@mach-34/noir-bignum-paramgen": "^1.0.2", "@noir-lang/backend_barretenberg": "=0.34.0", "@noir-lang/noir_js": "=0.34.0", + "@noir-lang/noirc_abi": "^0.35.0", "@zk-email/helpers": "=6.1.5" }, "devDependencies": { @@ -51,7 +52,8 @@ "testPathIgnorePatterns": [ "/dist/", "/node_modules/" - ] + ], + "testTimeout": 100000 }, "babel": { "presets": [ diff --git a/js/src/index.ts b/js/src/index.ts index 526743e..c93de3f 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -17,7 +17,7 @@ import { u8ToU32 } from "./utils"; // with a few modifications for noir input generation // also removes some of the unused functionality like masking -type CircuitInput = { +export type CircuitInput = { header: string[]; header_length: string; pubkey: string[]; @@ -30,7 +30,7 @@ type CircuitInput = { body_hash_index?: string; }; -type InputGenerationArgs = { +export type InputGenerationArgs = { ignoreBodyHashCheck?: boolean; shaPrecomputeSelector?: string; maxHeadersLength?: number; // Max length of the email header including padding diff --git a/js/src/prover.ts b/js/src/prover.ts new file mode 100644 index 0000000..371cbc4 --- /dev/null +++ b/js/src/prover.ts @@ -0,0 +1,145 @@ +import { + BarretenbergBackend, + CompiledCircuit, + ProofData, + UltraHonkBackend, +} from "@noir-lang/backend_barretenberg"; +import { Noir } from "@noir-lang/noir_js"; +import { InputValue } from "@noir-lang/noirc_abi"; +import { CircuitInput } from "./index"; + +type ProvingBackend = "honk" | "plonk" | "all"; + +export class ZKEmailProver { + private plonk?: BarretenbergBackend; + private honk?: UltraHonkBackend; + private noir: Noir; + + constructor( + /* The ACIR of the Noir circuit to prove */ + circuit: CompiledCircuit, + /* Define the prover backend to use */ + private provingBackend: ProvingBackend = "plonk" + ) { + // initialize the backends + if (provingBackend === "plonk" || provingBackend === "all") { + this.plonk = new BarretenbergBackend(circuit); + } + if (provingBackend === "honk" || provingBackend === "all") { + this.honk = new UltraHonkBackend(circuit); + } + // initialize the Noir instance + this.noir = new Noir(circuit); + } + + /** + * Compute the witness for a given input to the circuit without generating a proof + * + * @param input - the input that should produce a satisfying witness for the circuit + * @returns - the witness for the input and the output of the circuit if satisfiable + */ + async simulateWitness( + input: CircuitInput + ): Promise<{ witness: Uint8Array; returnValue: InputValue }> { + return await this.noir.execute(input); + } + + /** + * Generate a proof of a satisfying input to the circuit using a provided witness + * + * @param input - a satisfying witness for the circuit + * @param provingBackend - optionally provided if the class was initialized with both proving schemes + * @returns proof of valid execution of the circuit + */ + async prove( + witness: Uint8Array, + provingBackend?: ProvingBackend + ): Promise { + // determine proving backend to use + let backend: BarretenbergBackend | UltraHonkBackend; + if (provingBackend) { + // check that the asserted backend is initialized + if (provingBackend === "plonk" && this.plonk) { + backend = this.plonk; + } else if (provingBackend === "honk" && this.honk) { + backend = this.honk; + } else { + throw new Error(`Proving scheme ${provingBackend} not initialized`); + } + } else { + // default to the backend used to initialize the class + if (this.provingBackend === "honk" && this.honk) { + backend = this.honk; + } else if (this.provingBackend === "plonk" && this.plonk) { + backend = this.plonk; + } else { + throw new Error(`Proving scheme ${this.provingBackend} not initialized`); + } + } + // generate the proof + return await backend.generateProof(witness); + } + + /** + * Simulate the witness for a given input and generate a proof + * + * @param input - the input that should produce a satisfying witness for the circuit + * @param provingBackend - optionally provided if the class was initialized with both proving schemes + * @returns proof of valid execution of the circuit + */ + async fullProve( + input: CircuitInput, + provingBackend?: ProvingBackend + ): Promise { + const { witness } = await this.simulateWitness(input); + return await this.prove(witness, provingBackend); + } + + /** + * Verify a proof of a satisfying input to the circuit for a given proving scheme + * + * @param proof - the proof to verify + * @param provingBackend - optionally provided if the class was initialized with both proving schemes + * @returns true if the proof is valid, false otherwise + */ + async verify( + proof: ProofData, + provingBackend?: ProvingBackend + ): Promise { + // determine proving backend to use + let backend: BarretenbergBackend | UltraHonkBackend; + if (provingBackend) { + // check that the asserted backend is initialized + if (provingBackend === "plonk" && this.plonk) { + backend = this.plonk; + } else if (provingBackend === "honk" && this.honk) { + backend = this.honk; + } else { + throw new Error(`Proving scheme ${provingBackend} not initialized`); + } + } else { + // default to the backend used to initialize the class + if (this.provingBackend === "honk" && this.honk) { + backend = this.honk; + } else if (this.provingBackend === "plonk" && this.plonk) { + backend = this.plonk; + } else { + throw new Error(`Proving scheme ${this.provingBackend} not initialized`); + } + } + // verify the proof + return await backend.verifyProof(proof); + } + + /** + * End the prover wasm instance(s) and clean up resources + */ + async destroy() { + if (this.plonk) { + await this.plonk.destroy(); + } + if (this.honk) { + await this.honk.destroy(); + } + } +} diff --git a/js/tests/circuits.test.ts b/js/tests/circuits.test.ts index e4f122a..9911018 100644 --- a/js/tests/circuits.test.ts +++ b/js/tests/circuits.test.ts @@ -1,13 +1,8 @@ import fs from "fs"; import path from "path"; -import { - BarretenbergBackend, - CompiledCircuit, - UltraHonkBackend, -} from "@noir-lang/backend_barretenberg"; -import { Noir, acvm } from "@noir-lang/noir_js"; -import { generateEmailVerifierInputs } from "../src"; -import circuit1024 from "../../examples/verify_email_1024_bit_dkim/target/verify_email_1024_bit_dkim.json"; +import { ZKEmailProver } from "../src/prover"; +import { generateEmailVerifierInputs } from "../src/index"; +// import circuit1024 from "../../examples/verify_email_1024_bit_dkim/target/verify_email_1024_bit_dkim.json"; import circuit2048 from "../../examples/verify_email_2048_bit_dkim/target/verify_email_2048_bit_dkim.json"; import partialHash from "../../examples/partial_hash/target/partial_hash.json"; const emails = { @@ -17,118 +12,97 @@ const emails = { ), }; -type Prover = { - noir: Noir; - barretenberg: BarretenbergBackend; - ultraHonk: UltraHonkBackend; +// default header/ body lengths to use for input gen +const inputParams = { + maxHeadersLength: 512, + maxBodyLength: 1024, }; -function makeProver(circuit: CompiledCircuit): Prover { - return { - noir: new Noir(circuit), - barretenberg: new BarretenbergBackend(circuit), - ultraHonk: new UltraHonkBackend(circuit), - }; -} - -async function teardownProver(prover: Prover) { - await prover.barretenberg.destroy(); - await prover.ultraHonk.destroy(); -} - describe("Fixed Size Circuit Input", () => { - let prover1024: Prover; - let prover2048: Prover; - let proverPartialHash: Prover; - const inputParams = { - maxHeadersLength: 512, - maxBodyLength: 1024, - }; - jest.setTimeout(100000); - beforeAll(async () => { + // todo: get a github email from a throwaway account to verify + // let prover1024: ZKEmailProver; + let prover2048: ZKEmailProver; + let proverPartialHash: ZKEmailProver; + + beforeAll(() => { //@ts-ignore - prover1024 = makeProver(circuit1024); + // prover1024 = new ZKEmailProver(circuit1024, "all"); //@ts-ignore - prover2048 = makeProver(circuit2048); + prover2048 = new ZKEmailProver(circuit2048, "all"); //@ts-ignore - proverPartialHash = makeProver(partialHash); + proverPartialHash = new ZKEmailProver(partialHash, "all"); }); + afterAll(async () => { - teardownProver(prover1024); - teardownProver(prover2048); - teardownProver(proverPartialHash); + // await prover1024.destroy(); + await prover2048.destroy(); + await proverPartialHash.destroy(); }); - describe("UltraHonk", () => { - it("UltraHonk::SmallEmail", async () => { + + describe("UltraPlonk", () => { + it("UltraPlonk::SmallEmail", async () => { const inputs = await generateEmailVerifierInputs( emails.small, inputParams ); - const { witness } = await prover2048.noir.execute(inputs); - const proof = await prover2048.ultraHonk.generateProof(witness); - const result = await prover2048.ultraHonk.verifyProof(proof); + const proof = await prover2048.fullProve(inputs, "plonk"); + const result = await prover2048.verify(proof, "plonk"); expect(result).toBeTruthy(); }); - it("UltraHonk::LargeEmail", async () => { + it("UltraPlonk::LargeEmail", async () => { const inputs = await generateEmailVerifierInputs( emails.large, inputParams ); - const { witness } = await prover2048.noir.execute(inputs); - const proof = await prover2048.ultraHonk.generateProof(witness); - const result = await prover2048.ultraHonk.verifyProof(proof); + const proof = await prover2048.fullProve(inputs, "plonk"); + const result = await prover2048.verify(proof, "plonk"); expect(result).toBeTruthy(); }); }); - describe("UltraPlonk", () => { - it("UltraPlonk::SmallEmail", async () => { + describe("UltraHonk", () => { + it("UltraHonk::SmallEmail", async () => { const inputs = await generateEmailVerifierInputs( emails.small, inputParams ); - const { witness } = await prover2048.noir.execute(inputs); - const proof = await prover2048.barretenberg.generateProof(witness); - const result = await prover2048.barretenberg.verifyProof(proof); + const proof = await prover2048.fullProve(inputs, "honk"); + const result = await prover2048.verify(proof, "honk"); expect(result).toBeTruthy(); }); - it("UltraPlonk::LargeEmail", async () => { + it("UltraHonk::LargeEmail", async () => { const inputs = await generateEmailVerifierInputs( emails.large, inputParams ); - const { witness } = await prover2048.noir.execute(inputs); - const proof = await prover2048.barretenberg.generateProof(witness); - const result = await prover2048.barretenberg.verifyProof(proof); + const proof = await prover2048.fullProve(inputs, "honk"); + const result = await prover2048.verify(proof, "honk"); expect(result).toBeTruthy(); }); }); describe("Partial Hash", () => { + const selectorText = "All nodes in the Bitcoin network can consult it"; it("UltraPlonk::PartialHash", async () => { - const selectorText = "All nodes in the Bitcoin network can consult it"; const inputs = await generateEmailVerifierInputs(emails.large, { shaPrecomputeSelector: selectorText, maxHeadersLength: 512, maxBodyLength: 192, }); - const { witness } = await proverPartialHash.noir.execute(inputs); - const proof = await proverPartialHash.barretenberg.generateProof(witness); - const result = await proverPartialHash.barretenberg.verifyProof(proof); + const proof = await proverPartialHash.fullProve(inputs, "plonk"); + const result = await proverPartialHash.verify(proof, "plonk"); expect(result).toBeTruthy(); }); it("UltraHonk::PartialHash", async () => { - const selectorText = "All nodes in the Bitcoin network can consult it"; const inputs = await generateEmailVerifierInputs(emails.large, { shaPrecomputeSelector: selectorText, maxHeadersLength: 512, maxBodyLength: 192, }); - const { witness } = await proverPartialHash.noir.execute(inputs); - const proof = await proverPartialHash.ultraHonk.generateProof(witness); - const result = await proverPartialHash.ultraHonk.verifyProof(proof); + const proof = await proverPartialHash.fullProve(inputs, "honk"); + const result = await proverPartialHash.verify(proof, "honk"); expect(result).toBeTruthy(); }); }); diff --git a/js/yarn.lock b/js/yarn.lock index ce9e663..4ed35e8 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -1694,11 +1694,23 @@ dependencies: "@noir-lang/types" "0.34.0" +"@noir-lang/noirc_abi@^0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@noir-lang/noirc_abi/-/noirc_abi-0.35.0.tgz#6b56f356a056ef4f935503d70460afa2aae76018" + integrity sha512-iTlHWHdnxgwQveGNCaeQKj1UenFqtnWvh62mfY7m/GD9cs9LB5PFs57rmYMEGaQysEix3sM3BmSX1h9C93pHDg== + dependencies: + "@noir-lang/types" "0.35.0" + "@noir-lang/types@0.34.0": version "0.34.0" resolved "https://registry.yarnpkg.com/@noir-lang/types/-/types-0.34.0.tgz#909d4a170177c7ab5adfeed5dc8660d69825ced2" integrity sha512-04A14JMPt5G6/cCD4n59W0DUtAbcN8YByvSJ6gj0VQramd11tWK3ZqhKadnQrzhA0GwEvROd6jvn/blKdHlSXQ== +"@noir-lang/types@0.35.0": + version "0.35.0" + resolved "https://registry.yarnpkg.com/@noir-lang/types/-/types-0.35.0.tgz#47e73db1c545b0a68a7b1c4d551843ed71e47291" + integrity sha512-m4zzTuWVeYI/lgAKU4XwS61lfU/vr8laal8mLm2YEym8GTyVfezjjVrzQd0/mOZtpZ6hO7r3rbd3W2Vs9IkMrg== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -4783,9 +4795,9 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -"snarkjs@git+https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e": +"snarkjs@https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e": version "0.5.0" - resolved "git+https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e" + resolved "https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e" dependencies: "@iden3/binfileutils" "0.0.11" bfj "^7.0.2"