Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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