Skip to content

Commit

Permalink
feat: support UltraHonk (#5)
Browse files Browse the repository at this point in the history
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:
AztecProtocol/aztec-packages#10489.
  • Loading branch information
olehmisar authored Feb 6, 2025
1 parent 57887be commit 1b95d5e
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 40 deletions.
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
```
Expand All @@ -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

Expand All @@ -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
Expand Down
26 changes: 15 additions & 11 deletions src/Noir.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<T extends Backend = UltraPlonkBackend>(
async getCircuit<T = UltraHonkBackend>(
name: string,
createBackend?: (circuit: CompiledCircuit) => T | Promise<T>,
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<typeof backendClass>;
})();
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 };
}
}
Expand All @@ -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;
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
};
}
Expand Down
65 changes: 54 additions & 11 deletions src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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}`);
}),
);

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/type-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -29,12 +29,15 @@ declare module "hardhat/types/config" {
noir: {
version: string;
bbVersion?: string;
flavor?: ProofFlavor | ProofFlavor[];
skipNargoWorkspaceCheck?: boolean;
};
}

export interface HardhatConfig {
noir: NonNullable<Required<HardhatUserConfig["noir"]>>;
noir: Omit<Required<HardhatUserConfig["noir"]>, "flavor"> & {
flavor: ProofFlavor[];
};
}
}

Expand Down
16 changes: 11 additions & 5 deletions test/fixture-projects/hardhat-project/contracts/MyContract.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions test/fixture-projects/hardhat-project/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// We load the plugin here.
import "@nomicfoundation/hardhat-ethers";
import { HardhatUserConfig } from "hardhat/types";

import "../../../src/index";
Expand All @@ -20,6 +21,7 @@ const config: HardhatUserConfig = {
},
noir: {
version: TEST_NOIR_VERSION,
flavor: ["ultra_keccak_honk", "ultra_plonk"],
},
};

Expand Down
71 changes: 70 additions & 1 deletion test/noir.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 () {
Expand All @@ -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());
Expand Down

0 comments on commit 1b95d5e

Please sign in to comment.