Skip to content

Commit 1b95d5e

Browse files
authored
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: AztecProtocol/aztec-packages#10489.
1 parent 57887be commit 1b95d5e

File tree

8 files changed

+192
-40
lines changed

8 files changed

+192
-40
lines changed

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ Use the verifier contract in Solidity:
7979

8080
```solidity
8181
// contracts/MyContract.sol
82-
import {UltraVerifier} from "../noir/target/my_noir.sol";
82+
import {HonkVerifier} from "../noir/target/my_noir.sol";
8383
8484
contract MyContract {
85-
UltraVerifier public verifier = new UltraVerifier();
85+
HonkVerifier public verifier = new HonkVerifier();
8686
8787
function verify(bytes calldata proof, uint256 y) external view returns (bool) {
8888
bytes32[] memory publicInputs = new bytes32[](1);
@@ -110,19 +110,25 @@ it("proves and verifies on-chain", async () => {
110110
const { noir, backend } = await hre.noir.getCircuit("my_noir");
111111
const input = { x: 1, y: 2 };
112112
const { witness } = await noir.execute(input);
113-
const { proof, publicInputs } = await backend.generateProof(witness);
113+
const { proof, publicInputs } = await backend.generateProof(witness, {
114+
keccak: true,
115+
});
114116
// it matches because we marked y as `pub` in `main.nr`
115117
expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y));
116118

117119
// Verify the proof on-chain
118-
const result = await contract.verify(proof, input.y);
120+
// slice the proof to remove length information
121+
const result = await contract.verify(proof.slice(4), input.y);
119122
expect(result).to.eq(true);
120123

121124
// You can also verify in JavaScript.
122-
const resultJs = await backend.verifyProof({
123-
proof,
124-
publicInputs: [String(input.y)],
125-
});
125+
const resultJs = await backend.verifyProof(
126+
{
127+
proof,
128+
publicInputs: [String(input.y)],
129+
},
130+
{ keccak: true },
131+
);
126132
expect(resultJs).to.eq(true);
127133
});
128134
```
@@ -141,7 +147,7 @@ output of `npx hardhat help example`
141147

142148
This plugin extends the Hardhat Runtime Environment by adding a `noir` field.
143149

144-
You can call `hre.noir.getCircuit(name)` to get a compiled circuit JSON.
150+
You can call `hre.noir.getCircuit(name, backendClass)` to get a compiled circuit JSON.
145151

146152
## Configuration
147153

@@ -158,6 +164,19 @@ export default {
158164
};
159165
```
160166

167+
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.
168+
169+
```js
170+
export default {
171+
noir: {
172+
// default is "ultra_keccak_honk"
173+
flavor: "ultra_plonk",
174+
// you can also specify multiple flavors
175+
// flavor: ["ultra_keccak_honk", "ultra_plonk"],
176+
},
177+
};
178+
```
179+
161180
The default folder where Noir is located is `noir`. You can change it in `hardhat.config.js`:
162181

163182
```js

src/Noir.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { UltraPlonkBackend } from "@aztec/bb.js";
1+
import type { UltraHonkBackend } from "@aztec/bb.js";
22
import type { CompiledCircuit, Noir } from "@noir-lang/noir_js";
3-
import type { Backend } from "@noir-lang/types";
43
import { HardhatPluginError } from "hardhat/plugins";
54
import type { HardhatConfig, HardhatRuntimeEnvironment } from "hardhat/types";
65
import { PLUGIN_NAME } from "./utils";
@@ -35,25 +34,24 @@ export class NoirExtension {
3534
* Call this only once per circuit as it creates a new backend each time.
3635
*
3736
* @param name name of the circuit
38-
* @param createBackend an optional function that creates a backend for the given circuit. By default, it creates a `BarretenbergBackend`.
37+
* @param backendClass Backend class. Depends on the `noir.flavor` type you have set in Hardhat config. Either {@link UltraHonkBackend} or {@link UltraPlonkBackend}
3938
*/
40-
async getCircuit<T extends Backend = UltraPlonkBackend>(
39+
async getCircuit<T = UltraHonkBackend>(
4140
name: string,
42-
createBackend?: (circuit: CompiledCircuit) => T | Promise<T>,
41+
backendClass?: new (bytecode: string) => T,
4342
): Promise<{
4443
circuit: CompiledCircuit;
4544
noir: Noir;
4645
backend: T;
4746
}> {
47+
backendClass ||= await (async () => {
48+
const { UltraHonkBackend } = await import("@aztec/bb.js");
49+
return UltraHonkBackend as unknown as NonNullable<typeof backendClass>;
50+
})();
4851
const circuit = await this.getCircuitJson(name);
4952
const { Noir } = await import("@noir-lang/noir_js");
5053
const noir = new Noir(circuit);
51-
createBackend ||= async (circuit: CompiledCircuit) => {
52-
const { UltraPlonkBackend } = await import("@aztec/bb.js");
53-
const ultraPlonk = new UltraPlonkBackend(circuit.bytecode);
54-
return ultraPlonk as unknown as T;
55-
};
56-
const backend = await createBackend(circuit);
54+
const backend = new backendClass(circuit.bytecode);
5755
return { circuit, noir, backend };
5856
}
5957
}
@@ -63,3 +61,9 @@ export async function getTarget(noirDir: string | HardhatConfig) {
6361
const path = await import("path");
6462
return path.join(noirDir, "target");
6563
}
64+
65+
export type ProofFlavor = keyof typeof ProofFlavor;
66+
export const ProofFlavor = {
67+
ultra_keccak_honk: "ultra_keccak_honk",
68+
ultra_plonk: "ultra_plonk",
69+
} as const;

src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { extendConfig, extendEnvironment } from "hardhat/config";
22
import { HardhatPluginError, lazyObject } from "hardhat/plugins";
33
import { HardhatConfig, HardhatUserConfig } from "hardhat/types";
44
import path from "path";
5-
import { NoirExtension } from "./Noir";
5+
import { NoirExtension, ProofFlavor } from "./Noir";
66
import "./tasks";
77
import "./type-extensions";
88
import { PLUGIN_NAME } from "./utils";
@@ -54,9 +54,15 @@ extendConfig(
5454
`cannot infer bb version for noir@${version}. Please specify \`noir.bbVersion\` in Hardhat config`,
5555
);
5656
}
57+
const flavor: ProofFlavor[] = u.flavor
58+
? Array.isArray(u.flavor)
59+
? u.flavor
60+
: [u.flavor]
61+
: [ProofFlavor.ultra_keccak_honk];
5762
return {
5863
version,
5964
bbVersion,
65+
flavor,
6066
skipNargoWorkspaceCheck: u.skipNargoWorkspaceCheck ?? false,
6167
};
6268
}

src/tasks.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import { HardhatPluginError } from "hardhat/plugins";
88
import { HardhatConfig } from "hardhat/types";
99
import { NoirCache } from "./cache";
1010
import { installBb, installNargo } from "./install";
11-
import { getTarget } from "./Noir";
11+
import { getTarget, ProofFlavor } from "./Noir";
1212
import { makeRunCommand, PLUGIN_NAME } from "./utils";
1313

1414
task(TASK_COMPILE, "Compile and generate circuits and contracts").setAction(
1515
async (args, { config }, runSuper) => {
16-
const path = await import("path");
1716
const noirDir = config.paths.noir;
1817
const targetDir = await getTarget(noirDir);
1918

@@ -42,16 +41,19 @@ task(TASK_COMPILE, "Compile and generate circuits and contracts").setAction(
4241
return;
4342
}
4443

45-
const name = path.basename(file, ".json");
46-
console.log(`Generating Solidity verifier for ${name}...`);
47-
await runCommand(
48-
`${bbBinary} write_vk -b ${targetDir}/${name}.json -o ${targetDir}/${name}_vk`,
49-
);
50-
await runCommand(
51-
`${bbBinary} contract -k ${targetDir}/${name}_vk -o ${targetDir}/${name}.sol`,
52-
);
44+
for (const flavor of Object.values(ProofFlavor) as ProofFlavor[]) {
45+
if (!config.noir.flavor.includes(flavor)) {
46+
continue;
47+
}
48+
await generateSolidityVerifier(
49+
config,
50+
file,
51+
bbBinary,
52+
targetDir,
53+
flavor,
54+
);
55+
}
5356
await cache.saveJsonFileHash(file);
54-
console.log(`Generated Solidity verifier for ${name}`);
5557
}),
5658
);
5759

@@ -105,6 +107,47 @@ task(
105107
},
106108
);
107109

110+
async function generateSolidityVerifier(
111+
config: HardhatConfig,
112+
file: string,
113+
bbBinary: string,
114+
targetDir: string,
115+
flavor: ProofFlavor,
116+
) {
117+
const path = await import("path");
118+
119+
const runCommand = makeRunCommand(config.paths.noir);
120+
121+
const name = path.basename(file, ".json");
122+
console.log(`Generating Solidity ${flavor} verifier for ${name}...`);
123+
let writeVkCmd: string, contractCmd: string;
124+
switch (flavor) {
125+
case "ultra_plonk": {
126+
writeVkCmd = "write_vk";
127+
contractCmd = "contract";
128+
break;
129+
}
130+
case "ultra_keccak_honk": {
131+
writeVkCmd = "write_vk_ultra_keccak_honk";
132+
contractCmd = "contract_ultra_honk";
133+
break;
134+
}
135+
default: {
136+
flavor satisfies never;
137+
return;
138+
}
139+
}
140+
const nameSuffix =
141+
flavor === ProofFlavor.ultra_keccak_honk ? "" : `_${flavor}`;
142+
await runCommand(
143+
`${bbBinary} ${writeVkCmd} -b ${targetDir}/${name}.json -o ${targetDir}/${name}${nameSuffix}_vk`,
144+
);
145+
await runCommand(
146+
`${bbBinary} ${contractCmd} -k ${targetDir}/${name}${nameSuffix}_vk -o ${targetDir}/${name}${nameSuffix}.sol`,
147+
);
148+
console.log(`Generated Solidity ${flavor} verifier for ${name}`);
149+
}
150+
108151
async function checkNargoWorkspace(config: HardhatConfig) {
109152
if (config.noir.skipNargoWorkspaceCheck) {
110153
return;

src/type-extensions.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// To extend one of Hardhat's types, you need to import the module where it has been defined, and redeclare it.
44
import "hardhat/types/config";
55
import "hardhat/types/runtime";
6-
import { NoirExtension } from "./Noir";
6+
import { NoirExtension, ProofFlavor } from "./Noir";
77

88
declare module "hardhat/types/config" {
99
// This is an example of an extension to one of the Hardhat config values.
@@ -29,12 +29,15 @@ declare module "hardhat/types/config" {
2929
noir: {
3030
version: string;
3131
bbVersion?: string;
32+
flavor?: ProofFlavor | ProofFlavor[];
3233
skipNargoWorkspaceCheck?: boolean;
3334
};
3435
}
3536

3637
export interface HardhatConfig {
37-
noir: NonNullable<Required<HardhatUserConfig["noir"]>>;
38+
noir: Omit<Required<HardhatUserConfig["noir"]>, "flavor"> & {
39+
flavor: ProofFlavor[];
40+
};
3841
}
3942
}
4043

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
22
pragma solidity ^0.8.27;
33

4-
import {UltraVerifier} from "../noir2/target/my_circuit.sol";
4+
import {HonkVerifier} from "../noir2/target/my_circuit.sol";
55

66
contract MyContract {
7-
UltraVerifier public verifier;
7+
HonkVerifier public verifier = new HonkVerifier();
88

9-
constructor(UltraVerifier _verifier) {
10-
verifier = _verifier;
11-
}
9+
function verify(
10+
bytes calldata proof,
11+
uint256 y
12+
) external view returns (bool) {
13+
bytes32[] memory publicInputs = new bytes32[](1);
14+
publicInputs[0] = bytes32(y);
15+
bool result = verifier.verify(proof, publicInputs);
16+
return result;
17+
}
1218
}

test/fixture-projects/hardhat-project/hardhat.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// We load the plugin here.
2+
import "@nomicfoundation/hardhat-ethers";
23
import { HardhatUserConfig } from "hardhat/types";
34

45
import "../../../src/index";
@@ -20,6 +21,7 @@ const config: HardhatUserConfig = {
2021
},
2122
noir: {
2223
version: TEST_NOIR_VERSION,
24+
flavor: ["ultra_keccak_honk", "ultra_plonk"],
2325
},
2426
};
2527

test/noir.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// tslint:disable-next-line no-implicit-dependencies
2+
import { UltraPlonkBackend } from "@aztec/bb.js";
23
import { assert, expect } from "chai";
34
import fs from "fs";
45
import { TASK_CLEAN, TASK_COMPILE } from "hardhat/builtin-tasks/task-names";
@@ -43,6 +44,74 @@ describe("Integration tests examples", function () {
4344
expect(exists).to.be.eq(true);
4445
fs.rmSync(dir, { recursive: true });
4546
});
47+
48+
it("proves and verifies on-chain", async function () {
49+
await this.hre.run("compile");
50+
51+
// Deploy a verifier contract
52+
const contractFactory =
53+
await this.hre.ethers.getContractFactory("MyContract");
54+
const contract = await contractFactory.deploy();
55+
await contract.waitForDeployment();
56+
57+
// Generate a proof
58+
const { noir, backend } = await this.hre.noir.getCircuit("my_circuit");
59+
const input = { x: 1, y: 2 };
60+
const { witness } = await noir.execute(input);
61+
const { proof, publicInputs } = await backend.generateProof(witness, {
62+
keccak: true,
63+
});
64+
// it matches because we marked y as `pub` in `main.nr`
65+
expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y));
66+
67+
// Verify the proof on-chain
68+
const result = await contract.verify(proof.slice(4), input.y);
69+
expect(result).to.eq(true);
70+
71+
// You can also verify in JavaScript.
72+
const resultJs = await backend.verifyProof(
73+
{
74+
proof,
75+
publicInputs: [String(input.y)],
76+
},
77+
{ keccak: true },
78+
);
79+
expect(resultJs).to.eq(true);
80+
});
81+
82+
it("proves and verifies on-chain ultra_plonk", async function () {
83+
await this.hre.run("compile");
84+
85+
// Deploy a verifier contract
86+
const contractFactory =
87+
await this.hre.ethers.getContractFactory("UltraVerifier");
88+
const contract = await contractFactory.deploy();
89+
await contract.waitForDeployment();
90+
91+
// Generate a proof
92+
const { noir, backend } = await this.hre.noir.getCircuit(
93+
"my_circuit",
94+
UltraPlonkBackend,
95+
);
96+
const input = { x: 1, y: 2 };
97+
const { witness } = await noir.execute(input);
98+
const { proof, publicInputs } = await backend.generateProof(witness);
99+
// it matches because we marked y as `pub` in `main.nr`
100+
expect(BigInt(publicInputs[0])).to.eq(BigInt(input.y));
101+
102+
// Verify the proof on-chain
103+
const result = await contract.verify(proof, [
104+
this.hre.ethers.toBeHex(input.y, 32),
105+
]);
106+
expect(result).to.eq(true);
107+
108+
// You can also verify in JavaScript.
109+
const resultJs = await backend.verifyProof({
110+
proof,
111+
publicInputs: [String(input.y)],
112+
});
113+
expect(resultJs).to.eq(true);
114+
});
46115
});
47116

48117
describe("HardhatConfig extension", function () {
@@ -63,7 +132,7 @@ describe("Integration tests examples", function () {
63132
await this.hre.run("compile");
64133

65134
const contractFactory =
66-
await this.hre.ethers.getContractFactory("UltraVerifier");
135+
await this.hre.ethers.getContractFactory("HonkVerifier");
67136
const contract = await contractFactory.deploy();
68137
await contract.waitForDeployment();
69138
console.log("verifier", await contract.getAddress());

0 commit comments

Comments
 (0)