From 1b95d5e36b5adb313f68a48031b5e2d3239cdde9 Mon Sep 17 00:00:00 2001 From: oleh Date: Thu, 6 Feb 2025 03:02:58 +0100 Subject: [PATCH] feat: support UltraHonk (#5) Switches to UltraHonk by default. Introduces `flavor` config variable. Supported values are `ultra_keccak_honk` (default), `ultra_plonk` or both as an array. Solidity verifiers are generated according to the `flavor` set in the config. Minimum supported bb.js version is 0.67.0 due to `ultra_keccak_honk` proof generation introduction only in that version: https://github.com/AztecProtocol/aztec-packages/pull/10489. --- README.md | 37 +++++++--- src/Noir.ts | 26 ++++--- src/index.ts | 8 ++- src/tasks.ts | 65 ++++++++++++++--- src/type-extensions.ts | 7 +- .../hardhat-project/contracts/MyContract.sol | 16 +++-- .../hardhat-project/hardhat.config.ts | 2 + test/noir.test.ts | 71 ++++++++++++++++++- 8 files changed, 192 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 00a8b58..c7bc537 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,10 @@ Use the verifier contract in Solidity: ```solidity // contracts/MyContract.sol -import {UltraVerifier} from "../noir/target/my_noir.sol"; +import {HonkVerifier} from "../noir/target/my_noir.sol"; contract MyContract { - UltraVerifier public verifier = new UltraVerifier(); + HonkVerifier public verifier = new HonkVerifier(); function verify(bytes calldata proof, uint256 y) external view returns (bool) { bytes32[] memory publicInputs = new bytes32[](1); @@ -110,19 +110,25 @@ it("proves and verifies on-chain", async () => { const { noir, backend } = await hre.noir.getCircuit("my_noir"); const input = { x: 1, y: 2 }; const { witness } = await noir.execute(input); - const { proof, publicInputs } = await backend.generateProof(witness); + const { proof, publicInputs } = await backend.generateProof(witness, { + keccak: true, + }); // it matches because we marked y as `pub` in `main.nr` expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y)); // Verify the proof on-chain - const result = await contract.verify(proof, input.y); + // slice the proof to remove length information + const result = await contract.verify(proof.slice(4), input.y); expect(result).to.eq(true); // You can also verify in JavaScript. - const resultJs = await backend.verifyProof({ - proof, - publicInputs: [String(input.y)], - }); + const resultJs = await backend.verifyProof( + { + proof, + publicInputs: [String(input.y)], + }, + { keccak: true }, + ); expect(resultJs).to.eq(true); }); ``` @@ -141,7 +147,7 @@ output of `npx hardhat help example` This plugin extends the Hardhat Runtime Environment by adding a `noir` field. -You can call `hre.noir.getCircuit(name)` to get a compiled circuit JSON. +You can call `hre.noir.getCircuit(name, backendClass)` to get a compiled circuit JSON. ## Configuration @@ -158,6 +164,19 @@ export default { }; ``` +Change the proof flavor. It will generate different Solidity verifiers. If you switch to `ultra_plonk`, use `noir.getCircuit(name, UltraPlonkBackend)` to get ultra plonk backend. + +```js +export default { + noir: { + // default is "ultra_keccak_honk" + flavor: "ultra_plonk", + // you can also specify multiple flavors + // flavor: ["ultra_keccak_honk", "ultra_plonk"], + }, +}; +``` + The default folder where Noir is located is `noir`. You can change it in `hardhat.config.js`: ```js diff --git a/src/Noir.ts b/src/Noir.ts index f4d5654..df34648 100644 --- a/src/Noir.ts +++ b/src/Noir.ts @@ -1,6 +1,5 @@ -import type { UltraPlonkBackend } from "@aztec/bb.js"; +import type { UltraHonkBackend } from "@aztec/bb.js"; import type { CompiledCircuit, Noir } from "@noir-lang/noir_js"; -import type { Backend } from "@noir-lang/types"; import { HardhatPluginError } from "hardhat/plugins"; import type { HardhatConfig, HardhatRuntimeEnvironment } from "hardhat/types"; import { PLUGIN_NAME } from "./utils"; @@ -35,25 +34,24 @@ export class NoirExtension { * Call this only once per circuit as it creates a new backend each time. * * @param name name of the circuit - * @param createBackend an optional function that creates a backend for the given circuit. By default, it creates a `BarretenbergBackend`. + * @param backendClass Backend class. Depends on the `noir.flavor` type you have set in Hardhat config. Either {@link UltraHonkBackend} or {@link UltraPlonkBackend} */ - async getCircuit( + async getCircuit( name: string, - createBackend?: (circuit: CompiledCircuit) => T | Promise, + backendClass?: new (bytecode: string) => T, ): Promise<{ circuit: CompiledCircuit; noir: Noir; backend: T; }> { + backendClass ||= await (async () => { + const { UltraHonkBackend } = await import("@aztec/bb.js"); + return UltraHonkBackend as unknown as NonNullable; + })(); const circuit = await this.getCircuitJson(name); const { Noir } = await import("@noir-lang/noir_js"); const noir = new Noir(circuit); - createBackend ||= async (circuit: CompiledCircuit) => { - const { UltraPlonkBackend } = await import("@aztec/bb.js"); - const ultraPlonk = new UltraPlonkBackend(circuit.bytecode); - return ultraPlonk as unknown as T; - }; - const backend = await createBackend(circuit); + const backend = new backendClass(circuit.bytecode); return { circuit, noir, backend }; } } @@ -63,3 +61,9 @@ export async function getTarget(noirDir: string | HardhatConfig) { const path = await import("path"); return path.join(noirDir, "target"); } + +export type ProofFlavor = keyof typeof ProofFlavor; +export const ProofFlavor = { + ultra_keccak_honk: "ultra_keccak_honk", + ultra_plonk: "ultra_plonk", +} as const; diff --git a/src/index.ts b/src/index.ts index 6a523a8..c4ce85e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { extendConfig, extendEnvironment } from "hardhat/config"; import { HardhatPluginError, lazyObject } from "hardhat/plugins"; import { HardhatConfig, HardhatUserConfig } from "hardhat/types"; import path from "path"; -import { NoirExtension } from "./Noir"; +import { NoirExtension, ProofFlavor } from "./Noir"; import "./tasks"; import "./type-extensions"; import { PLUGIN_NAME } from "./utils"; @@ -54,9 +54,15 @@ extendConfig( `cannot infer bb version for noir@${version}. Please specify \`noir.bbVersion\` in Hardhat config`, ); } + const flavor: ProofFlavor[] = u.flavor + ? Array.isArray(u.flavor) + ? u.flavor + : [u.flavor] + : [ProofFlavor.ultra_keccak_honk]; return { version, bbVersion, + flavor, skipNargoWorkspaceCheck: u.skipNargoWorkspaceCheck ?? false, }; } diff --git a/src/tasks.ts b/src/tasks.ts index 2bbb7ce..bd30371 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -8,12 +8,11 @@ import { HardhatPluginError } from "hardhat/plugins"; import { HardhatConfig } from "hardhat/types"; import { NoirCache } from "./cache"; import { installBb, installNargo } from "./install"; -import { getTarget } from "./Noir"; +import { getTarget, ProofFlavor } from "./Noir"; import { makeRunCommand, PLUGIN_NAME } from "./utils"; task(TASK_COMPILE, "Compile and generate circuits and contracts").setAction( async (args, { config }, runSuper) => { - const path = await import("path"); const noirDir = config.paths.noir; const targetDir = await getTarget(noirDir); @@ -42,16 +41,19 @@ task(TASK_COMPILE, "Compile and generate circuits and contracts").setAction( return; } - const name = path.basename(file, ".json"); - console.log(`Generating Solidity verifier for ${name}...`); - await runCommand( - `${bbBinary} write_vk -b ${targetDir}/${name}.json -o ${targetDir}/${name}_vk`, - ); - await runCommand( - `${bbBinary} contract -k ${targetDir}/${name}_vk -o ${targetDir}/${name}.sol`, - ); + for (const flavor of Object.values(ProofFlavor) as ProofFlavor[]) { + if (!config.noir.flavor.includes(flavor)) { + continue; + } + await generateSolidityVerifier( + config, + file, + bbBinary, + targetDir, + flavor, + ); + } await cache.saveJsonFileHash(file); - console.log(`Generated Solidity verifier for ${name}`); }), ); @@ -105,6 +107,47 @@ task( }, ); +async function generateSolidityVerifier( + config: HardhatConfig, + file: string, + bbBinary: string, + targetDir: string, + flavor: ProofFlavor, +) { + const path = await import("path"); + + const runCommand = makeRunCommand(config.paths.noir); + + const name = path.basename(file, ".json"); + console.log(`Generating Solidity ${flavor} verifier for ${name}...`); + let writeVkCmd: string, contractCmd: string; + switch (flavor) { + case "ultra_plonk": { + writeVkCmd = "write_vk"; + contractCmd = "contract"; + break; + } + case "ultra_keccak_honk": { + writeVkCmd = "write_vk_ultra_keccak_honk"; + contractCmd = "contract_ultra_honk"; + break; + } + default: { + flavor satisfies never; + return; + } + } + const nameSuffix = + flavor === ProofFlavor.ultra_keccak_honk ? "" : `_${flavor}`; + await runCommand( + `${bbBinary} ${writeVkCmd} -b ${targetDir}/${name}.json -o ${targetDir}/${name}${nameSuffix}_vk`, + ); + await runCommand( + `${bbBinary} ${contractCmd} -k ${targetDir}/${name}${nameSuffix}_vk -o ${targetDir}/${name}${nameSuffix}.sol`, + ); + console.log(`Generated Solidity ${flavor} verifier for ${name}`); +} + async function checkNargoWorkspace(config: HardhatConfig) { if (config.noir.skipNargoWorkspaceCheck) { return; diff --git a/src/type-extensions.ts b/src/type-extensions.ts index 42a9941..0ad7c5a 100644 --- a/src/type-extensions.ts +++ b/src/type-extensions.ts @@ -3,7 +3,7 @@ // To extend one of Hardhat's types, you need to import the module where it has been defined, and redeclare it. import "hardhat/types/config"; import "hardhat/types/runtime"; -import { NoirExtension } from "./Noir"; +import { NoirExtension, ProofFlavor } from "./Noir"; declare module "hardhat/types/config" { // This is an example of an extension to one of the Hardhat config values. @@ -29,12 +29,15 @@ declare module "hardhat/types/config" { noir: { version: string; bbVersion?: string; + flavor?: ProofFlavor | ProofFlavor[]; skipNargoWorkspaceCheck?: boolean; }; } export interface HardhatConfig { - noir: NonNullable>; + noir: Omit, "flavor"> & { + flavor: ProofFlavor[]; + }; } } diff --git a/test/fixture-projects/hardhat-project/contracts/MyContract.sol b/test/fixture-projects/hardhat-project/contracts/MyContract.sol index 46d8eb9..37baf9b 100644 --- a/test/fixture-projects/hardhat-project/contracts/MyContract.sol +++ b/test/fixture-projects/hardhat-project/contracts/MyContract.sol @@ -1,12 +1,18 @@ // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.27; -import {UltraVerifier} from "../noir2/target/my_circuit.sol"; +import {HonkVerifier} from "../noir2/target/my_circuit.sol"; contract MyContract { - UltraVerifier public verifier; + HonkVerifier public verifier = new HonkVerifier(); - constructor(UltraVerifier _verifier) { - verifier = _verifier; - } + function verify( + bytes calldata proof, + uint256 y + ) external view returns (bool) { + bytes32[] memory publicInputs = new bytes32[](1); + publicInputs[0] = bytes32(y); + bool result = verifier.verify(proof, publicInputs); + return result; + } } diff --git a/test/fixture-projects/hardhat-project/hardhat.config.ts b/test/fixture-projects/hardhat-project/hardhat.config.ts index c1733ff..80e29e0 100644 --- a/test/fixture-projects/hardhat-project/hardhat.config.ts +++ b/test/fixture-projects/hardhat-project/hardhat.config.ts @@ -1,4 +1,5 @@ // We load the plugin here. +import "@nomicfoundation/hardhat-ethers"; import { HardhatUserConfig } from "hardhat/types"; import "../../../src/index"; @@ -20,6 +21,7 @@ const config: HardhatUserConfig = { }, noir: { version: TEST_NOIR_VERSION, + flavor: ["ultra_keccak_honk", "ultra_plonk"], }, }; diff --git a/test/noir.test.ts b/test/noir.test.ts index 171b866..657c328 100644 --- a/test/noir.test.ts +++ b/test/noir.test.ts @@ -1,4 +1,5 @@ // tslint:disable-next-line no-implicit-dependencies +import { UltraPlonkBackend } from "@aztec/bb.js"; import { assert, expect } from "chai"; import fs from "fs"; import { TASK_CLEAN, TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; @@ -43,6 +44,74 @@ describe("Integration tests examples", function () { expect(exists).to.be.eq(true); fs.rmSync(dir, { recursive: true }); }); + + it("proves and verifies on-chain", async function () { + await this.hre.run("compile"); + + // Deploy a verifier contract + const contractFactory = + await this.hre.ethers.getContractFactory("MyContract"); + const contract = await contractFactory.deploy(); + await contract.waitForDeployment(); + + // Generate a proof + const { noir, backend } = await this.hre.noir.getCircuit("my_circuit"); + const input = { x: 1, y: 2 }; + const { witness } = await noir.execute(input); + const { proof, publicInputs } = await backend.generateProof(witness, { + keccak: true, + }); + // it matches because we marked y as `pub` in `main.nr` + expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y)); + + // Verify the proof on-chain + const result = await contract.verify(proof.slice(4), input.y); + expect(result).to.eq(true); + + // You can also verify in JavaScript. + const resultJs = await backend.verifyProof( + { + proof, + publicInputs: [String(input.y)], + }, + { keccak: true }, + ); + expect(resultJs).to.eq(true); + }); + + it("proves and verifies on-chain ultra_plonk", async function () { + await this.hre.run("compile"); + + // Deploy a verifier contract + const contractFactory = + await this.hre.ethers.getContractFactory("UltraVerifier"); + const contract = await contractFactory.deploy(); + await contract.waitForDeployment(); + + // Generate a proof + const { noir, backend } = await this.hre.noir.getCircuit( + "my_circuit", + UltraPlonkBackend, + ); + const input = { x: 1, y: 2 }; + const { witness } = await noir.execute(input); + const { proof, publicInputs } = await backend.generateProof(witness); + // it matches because we marked y as `pub` in `main.nr` + expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y)); + + // Verify the proof on-chain + const result = await contract.verify(proof, [ + this.hre.ethers.toBeHex(input.y, 32), + ]); + expect(result).to.eq(true); + + // You can also verify in JavaScript. + const resultJs = await backend.verifyProof({ + proof, + publicInputs: [String(input.y)], + }); + expect(resultJs).to.eq(true); + }); }); describe("HardhatConfig extension", function () { @@ -63,7 +132,7 @@ describe("Integration tests examples", function () { await this.hre.run("compile"); const contractFactory = - await this.hre.ethers.getContractFactory("UltraVerifier"); + await this.hre.ethers.getContractFactory("HonkVerifier"); const contract = await contractFactory.deploy(); await contract.waitForDeployment(); console.log("verifier", await contract.getAddress());