跳到主要内容

使用 Noir 的前端证明

信息

教程代码可在此处查看

本指南演示如何开发一款由 Noir 支持前端生成证明、并通过 Kurier 验证的 NextJS 应用。我们将从零编写简单 Noir 电路,再用 bbjs 在客户端生成证明。

首先用命令创建 NextJS 应用:

npx create-next-app@latest

创建过程中会遇到以下选项:

What is your project named? my-app
Would you like to use TypeScript? No / Yes # Select yes
Would you like to use ESLint? No / Yes # Select yes
Would you like to use Tailwind CSS? No / Yes # Select yes
Would you like your code inside a `src/` directory? No / Yes # Select yes
Would you like to use App Router? (recommended) No / Yes # Select no
Would you like to use Turbopack for `next dev`? No / Yes # Select yes
Would you like to customize the import alias (`@/*` by default)? No / Yes # Select no
What import alias would you like configured? @/*

接着安装依赖 axios@noir-lang/noir_js@aztec/bb.js,先进入项目目录再执行 npm 安装:

cd your-project-name
npm i axios @noir-lang/[email protected] @aztec/[email protected]

在 IDE 中打开 NextJS 应用,查看目录结构。我们将在 public 目录中创建新的 Noir 项目,执行:

cd public
nargo new multiply

multiplysrc 目录下有 main.nr。不深讲 DSL,仅提供必要代码:简单电路接收 x、y、z,约束 z === (x*y)。将文件内容替换为:

fn main(x: Field, y: Field, z: pub Field) {
assert(z == x*y);
}

然后编译项目,进入 multiply 目录使用 nargo

cd multiply
nargo compile

此时 public 目录应类似:

alt_text

在项目根目录创建 .env 存放 Kurier API key,后续用于验证:

API_KEY = "向 Horizen Labs 获取的 API Key"

接下来在 NextJS 创建后端 API,接收证明产物并调用 Kurier 验证,供前端使用。于 api 子目录新建 kurier.ts

首先引入 axios、bbjs、aztecjs 等依赖。然后创建处理函数,接受 POST 请求,读取请求体中的证明产物进行验证;声明 Kurier 的 API_URL。还需编写 concatenatePublicInputsAndProof()Ultraplonk 证明格式化为 Kurier 所需格式。

在提交验证前需注册 verification key(每个 proof 类型一次即可,可降低验证成本)。注册完成后调用 Kurier API 提交 proof 并轮询状态;收到 IncludedInBlock 事件即向前端返回交易数据。可参考Kurier 教程

import axios from "axios";
import { NextApiRequest, NextApiResponse } from "next";
import { Buffer } from "buffer";
const API_URL = "https://api-testnet.kurier.xyz/api/v1";
export default async function handler(req: NextApiRequest, res: NextApiResponse){
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try{
const proofUint8 = new Uint8Array(Object.values(req.body.proof));
const params = {
"proofType": "ultraplonk",
"vkRegistered": false,
"proofOptions": {
"numberOfPublicInputs": 1
},
"proofData": {
"proof": Buffer.from(concatenatePublicInputsAndProof(req.body.publicInputs, proofUint8)).toString("base64"),
"vk": req.body.vk
}
}
const requestResponse = await axios.post(`${API_URL}/submit-proof/${process.env.API_KEY}`, params)
console.log(requestResponse.data)
if(requestResponse.data.optimisticVerify != "success"){
console.error("Proof verification, check proof artifacts");
return;
}
while(true){
const jobStatusResponse = await axios.get(`${API_URL}/job-status/${process.env.API_KEY}/${requestResponse.data.jobId}`);
if(jobStatusResponse.data.status === "IncludedInBlock"){
console.log("Job Included in Block successfully");
res.status(200).json(jobStatusResponse.data);
return;
}else{
console.log("Job status: ", jobStatusResponse.data.status);
console.log("Waiting for job to finalize...");
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds before checking again
}
}
}catch(error){
console.log(error)
}
}
function hexToUint8Array(hex: any) {
if (hex.startsWith('0x')) hex = hex.slice(2);
if (hex.length % 2 !== 0) hex = '0' + hex;
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
function concatenatePublicInputsAndProof(publicInputsHex: any, proofUint8: any) {
const publicInputBytesArray = publicInputsHex.flatMap((hex: any) =>
Array.from(hexToUint8Array(hex))
);
const publicInputBytes = new Uint8Array(publicInputBytesArray);
console.log(publicInputBytes.length, proofUint8.length)
import axios from "axios";
import { NextApiRequest, NextApiResponse } from "next";
import { Buffer } from "buffer";
import fs from "fs";
import path from "path";
export default async function handler(req: NextApiRequest, res: NextApiResponse){
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try{
const proofUint8 = new Uint8Array(Object.values(req.body.proof));
if(fs.existsSync(path.join(process.cwd(), "public", "multiply", "vkey.json")) === false) {
await registerVk(req.body.vk);
await new Promise(resolve => setTimeout(resolve, 5000));
}
const vk = fs.readFileSync(
path.join(
process.cwd(),
"public",
"multiply",
"vkey.json"
),
"utf-8"
);
const params = {
"proofType": "ultraplonk",
"vkRegistered": true,
"proofOptions": {
"numberOfPublicInputs": 1
},
"proofData": {
"proof": Buffer.from(concatenatePublicInputsAndProof(req.body.publicInputs, proofUint8)).toString("base64"),
"vk": JSON.parse(vk).vkHash || JSON.parse(vk).meta.vkHash,
}
}
const requestResponse = await axios.post(`${API_URL}/submit-proof/${process.env.API_KEY}`, params)
console.log(requestResponse.data)
if(requestResponse.data.optimisticVerify != "success"){
console.error("Proof verification, check proof artifacts");
return;
}
while(true){
try{
const jobStatusResponse = await axios.get(`${API_URL}/job-status/${process.env.API_KEY}/${requestResponse.data.jobId}`);
console.log("Job Status: ", jobStatusResponse.data);
if(jobStatusResponse.data.status === "IncludedInBlock"){
console.log("Job Included in Block successfully");
res.status(200).json(jobStatusResponse.data);
return;
}else{
console.log("Job status: ", jobStatusResponse.data.status);
console.log("Waiting for job to finalize...");
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds before checking again
}
} catch (error: any) {
if(error.response.status === 503){
console.log("Service Unavailable, retrying...");
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds before retrying
}
}
}
}catch(error){
console.log(error)
}
}
function hexToUint8Array(hex: any) {
if (hex.startsWith('0x')) hex = hex.slice(2);
if (hex.length % 2 !== 0) hex = '0' + hex;
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
function concatenatePublicInputsAndProof(publicInputsHex: any, proofUint8: any) {
const publicInputBytesArray = publicInputsHex.flatMap((hex: any) =>
Array.from(hexToUint8Array(hex))
);
const publicInputBytes = new Uint8Array(publicInputBytesArray);
console.log(publicInputBytes.length, proofUint8.length)
const newProof = new Uint8Array(publicInputBytes.length + proofUint8.length);
newProof.set(publicInputBytes, 0);
newProof.set(proofUint8, publicInputBytes.length);
return newProof;
}
async function registerVk(vk: any){
const API_URL = "https://api-testnet.kurier.xyz/api/v1";
const params = {
proofType: "ultraplonk",
vk: vk,
proofOptions: {
"numberOfPublicInputs": 1
},
};
try{
const res = await axios.post(
`${API_URL}/register-vk/${process.env.API_KEY}`,
params
)
console.log(res)
fs.writeFileSync(
path.join(process.cwd(), "public", "multiply", "vkey.json"),
JSON.stringify(res.data)
);
}catch(error: any) {
console.log(error.response)
fs.writeFileSync(
path.join(process.cwd(), "public", "multiply", "vkey.json"),
JSON.stringify(error.response.data)
);
}
}

src/pages 下创建 components 子目录用于存放新组件,并在其中新建 proof.tsx

alt_text

下面细看 proof 组件。先从 react 引入 useState,从 bbjs 引入 UltraPlonkBackend,从 noir_js 引入 Noir。声明多个 state 维护应用状态,再编写 handleProofGeneration(),使用 UltraPlonkBackend.generateProof() 结合产物与输入生成 groth16 证明,然后调用之前的 Kurier 后端 API 验证并更新状态。return 中渲染所需 UI。

"use client";
import { useState } from "react";
import { UltraPlonkBackend } from "@aztec/bb.js";
import { abi, Noir } from "@noir-lang/noir_js";
export default function ProofComponent() {
const [x, setX] = useState("");
const [y, setY] = useState("");
const [result, setResult] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [proofResult, setProofResult] = useState(null);
const [errorMsg, setErrorMsg] = useState("");
const [verificationStatus, setVerificationStatus] = useState("");
const [txHash, setTxHash] = useState<string | null>(null);
const handleGenerateProof = async () => {
setIsLoading(true);
setProofResult(null);
setErrorMsg("");
setVerificationStatus("");
setTxHash(null);
try {
const circuit_json = await fetch("/multiply/target/multiply.json");
const noir_data = await circuit_json.json();
const input = {
x: x,
y: y,
z: result,
};
const noir = new Noir({
bytecode: noir_data.bytecode,
abi: noir_data.abi as any,
});
const execResult = await noir.execute(input);
console.log("Witness Generated:", execResult);
const plonk = new UltraPlonkBackend(noir_data.bytecode, { threads: 2 });
const { proof, publicInputs } = await plonk.generateProof(
execResult.witness
);
const vk = await plonk.getVerificationKey();
setProofResult({
proof: "0x" + Buffer.from(proof).toString("hex"),
publicInputs,
});
// Send to backend for verification
const res = await fetch("/api/kurier", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
proof: proof,
publicInputs: publicInputs,
vk: Buffer.from(vk).toString("base64"),
}),
});
const data = await res.json();
if (res.ok) {
setVerificationStatus("✅ Proof verified successfully!");
if (data.txHash) {
setTxHash(data.txHash);
}
} else {
setVerificationStatus("❌ Proof verification failed.");
}
} catch (error) {
console.error("Error generating proof or verifying:", error);
setErrorMsg(
"❌ Error generating or verifying proof. Please check your inputs and try again."
);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<h1 className="text-4xl font-bold mb-6">zkVerify Noir NextJS</h1>
{/* Inputs */}
<div className="flex flex-col space-y-4 w-64 mb-6">
<input
type="number"
placeholder="Enter value x"
value={x}
onChange={(e) => setX(e.target.value)}
className="px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="Enter value y"
value={y}
onChange={(e) => setY(e.target.value)}
className="px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="Enter value x * y"
value={result}
onChange={(e) => setResult(e.target.value)}
className="px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Generate Proof Button */}
<button
onClick={handleGenerateProof}
disabled={isLoading}
className={`${
isLoading
? "bg-gray-400 cursor-not-allowed"
: "bg-green-600 hover:bg-green-700"
} text-white font-semibold px-6 py-2 rounded-lg`}
>
{isLoading ? "Processing..." : "Generate Proof"}
</button>
{/* Loading */}
{isLoading && (
<div className="mt-6 text-blue-600 font-semibold">
Working on it, please wait...
</div>
)}
{/* Error Message */}
{errorMsg && (
<div className="mt-6 text-red-600 font-medium">{errorMsg}</div>
)}
{/* Verification Result */}
{verificationStatus && (
<div className="mt-4 text-lg font-medium text-blue-700">
{verificationStatus}
</div>
)}
{/* TX Hash */}
{txHash && (
<div className="mt-2 text-blue-800 underline">
<a
href={`https://zkverify-testnet.subscan.io/extrinsic/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
🔗 View on Subscan (txHash: {txHash.slice(0, 10)}...)
</a>
</div>
)}
{/* Output */}
{proofResult && (
<div className="mt-8 bg-white shadow-md p-4 rounded-lg w-full max-w-xl">
<h2 className="text-xl font-bold mb-2 text-green-700">
✅ Proof Generated
</h2>
<pre className="text-sm overflow-x-auto whitespace-pre-wrap break-words">
{JSON.stringify(proofResult, null, 2)}
</pre>
</div>
)}
</div>
);
}

打开 index.tsx 引入 proof 组件,替换为:

import Image from "next/image";
import { Geist, Geist_Mono } from "next/font/google";
import ProofComponent from "./components/proof";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export default function Home() {
return (
<>
<ProofComponent />
</>
);
}

至此 NextJS 应用已就绪,运行:

npm run dev

在终端提供的地址访问,界面如下:

alt_text

输入任意 x、y、result(满足 result = x*y),点击 Generate Proof,生成后显示在按钮下方:

alt_text

生成后会自动通过 Kurier 提交验证并显示 “working on it”。验证通过后会返回 txHash,可点击在 Explorer 查看。

alt_text