🧠 ZK証明生成APIの実装
このレッスンでは、ユーザー が入力したパスワードを基に、**サーバーサイド(APIルート)**でゼロ知識証明を生成する機能を実装します。
snarkjs
をサーバーサイドで利用し、バックエンドでコンパイルした回路(.wasm
)と証明鍵(.zkey
)を使って、効率的に証明を計算します。
🛠 準備: 回路ファイルの配置
まず、section-2
で生成した、証明の生成に不可欠な2つのファイル(circuit.wasm
とcircuit.zkey
)を、フロントエンドからアクセスできるpublic
ディレクトリに配置する必要があります。
pkgs/frontend/public
ディレクトリ内に、zk
という名前のサブディレクトリを作成します。
mkdir -p pkgs/frontend/public/zk
次に、circuit
パッケージのbuild
ディレクトリから、生成されたファイルをコピーします。
cp pkgs/circuit/zkey/PasswordHash.wasm pkgs/frontend/public/zk/PasswordHash.wasm
cp pkgs/circuit/zkey/PasswordHash_final.zkey pkgs/frontend/public/zk/PasswordHash_final.zkey
public
ディレクトリに置かれたファイルは、Webサーバーのルートパスとして扱われます。
これにより、フロントエンドのコードから/zk/PasswordHash.wasm
や/zk/PasswordHash_final.zkey
といったURLでこれらのファイルに直接アクセスできるようになります。
🧠 ZK証明生成APIの実装
ゼロ知識証明の生成は計算量が多いため、APIルートとして実装し、サーバーサイドで処理するのが効率的です。
app/api/generateProof/route.ts
を作成し、以下のコードを記述します:
// pkgs/frontend/app/api/generateProof/route.ts
import { NextRequest, NextResponse } from "next/server";
import { buildPoseidon } from "circomlibjs";
const snarkjs = require("snarkjs");
// スマートコントラクトの関数に渡すためのデータ構造を定義
export interface Calldata {
pA: string[];
pB: string[][];
pC: string[];
pubSignals: string[];
}
export async function POST(req: NextRequest) {
try {
const { password } = await req.json();
if (!password) {
return NextResponse.json(
{ error: "Password is required" },
{ status: 400 }
);
}
// --- ステップ1: パスワードから回路への入力データを準備 ---
const poseidon = await buildPoseidon();
// パスワードを16進数に変換し、BigIntとして扱う
const passwordNumber = BigInt(
Buffer.from(password).toString("hex"),
16
).toString();
// パスワードのハッシュ値を計算
const hash = poseidon.F.toString(poseidon([passwordNumber]));
const inputs = {
password: passwordNumber,
hash: hash,
};
// --- ステップ2: ZK証明(Proof)と公開シグナル(Public Signals)を生成 ---
// publicディレクトリに配置した.wasmと.zkeyファイルを使用
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
"/zk/PasswordHash.wasm",
"/zk/PasswordHash_final.zkey"
);
// --- ステップ3: スマートコントラクトに渡せる形式にデータを整形 ---
// Groth16証明データを直接使用して、スマートコントラクト用の形式に変換
const calldata: Calldata = {
pA: [proof.pi_a[0], proof.pi_a[1]],
pB: [
[proof.pi_b[0][1], proof.pi_b[0][0]], // Groth16のpB形式は転置が必要
[proof.pi_b[1][1], proof.pi_b[1][0]],
],
pC: [proof.pi_c[0], proof.pi_c[1]],
pubSignals: publicSignals,
};
return NextResponse.json({ calldata });
} catch (error) {
console.error("Proof generation failed:", error);
return NextResponse.json(
{ error: "Failed to generate proof" },
{ status: 500 }
);
}
}
🔍 コード解説
-
APIルートパターン:
Next.js 13+のApp Routerでは、app/api/
ディレクトリ内にroute.ts
ファイルを配置することでAPIエンドポイントを作成できます。 -
Calldata
インタフェース:
スマートコントラクトのsafeMint
関数が期待する引数の型に合わせて、証明データを整形するためのデータ構造を定義します。 -
証明生成プロセス:
-
入力データの準備:
ユーザーが入力したpassword
文字列を、回路が理解できる数値(passwordNumber
)に変換し、そのハッシュ値(hash
)を計算します。 -
証明の生成:
snarkjs.groth16.fullProve
を呼び出します。これが魔法の核心部分です。
回路の入力(
inputs
)、コンパイルされた回路(.wasm
)、そして証明鍵(.zkey
)を渡すことで、proof
とpublicSignals
を生成します。- データ整形:
生成されたproof
とpublicSignals
を直接使用して、スマートコントラクトのsafeMint
関数が期待する形式に変換します。
Groth16証明の
pi_b
要素は、Solidityコントラクトでの使用時に転置(transpose)が必要なため、配列の順序を調整しています。 -
-
エラーハンドリング:
証明生成に失敗した場合は、適切なエラーレスポンスを返します。