🧠 ZK回路の構築
このセクションでは、プロジェクトの心臓部である ゼロ知識証明の「回路(Circuit)」 を実装します!
Circom
という特別な言語を使って、「ある秘密のパスワードを知っている」という事実を証明するためのロジックを設計図のように定義し、証明と検証に必要なファイルを生成していきます。
📜 Circomとは?
Circomは、ゼロ知識証明の回路を記述するために作られた、いわば 「証明の設計図」を作成するための言語 です。
算術回路(足し算や掛け算などの組み合わせ)を定義し、それを R1CS(Rank-1 Constraint System) という、多くの ZK-SNARKs プロトコルが理解できる標準形式に変換(コンパイル)することができます。
このプロジェクトでは、PasswordHash.circom
というファイルで、「秘密のパスワード」をハッシュ化する計算が正しく行われたこと を検証する回路を定義します。
✍️ 回路の設計と実装
それでは、pkgs/circuit/src/PasswordHash.circom
ファイルを作成し、以下のコードを記述しましょう。
この回路の目的は、「私は、ある秘密のパスワードを知っています。そして、そのパスワードをハッシュ化すると、公開されているこのハッシュ値になります」 ということを、パスワードそのものを見せることなく証明することです。
// pkgs/circuit/src/PasswordHash.circom
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/poseidon.circom";
/**
* ハッシュ値を生成するメソッド
*/
template PasswordCheck() {
// 入力値を取得
signal input password;
signal input hash;
component poseidon = Poseidon(1);
poseidon.inputs[0] <== password;
hash === poseidon.out; // ハッシュ値の一致を検証(true or falseを返す)
}
// パブリック入力: パスワードのハッシュ値
// プライベート入力: パスワード
component main {public [hash]} = PasswordCheck();
🔍 コード解説
-
signal input password
:
証明者(Prover)、つまりパスワードを知っている人だけが持つ秘密の入力です。
この値は証明を生成するために使われますが、外部には一切公開されません。 -
signal output hash
:
証明者(Prover) と 検証者(Verifier) の両方が知っている公開情報です。
この回路は、 「password
をハッシュ化した結果が、このhash
の値とピッタリ一致する」 ことを保証します。 -
component hasher = Poseidon(1)
:
circomlib
という便利なライブラリから、Poseidon
ハッシュ関数の機能を持つ部品(コンポーネント)を呼び出しています。
Poseidonは、ゼロ知識証明と非常に相性が良い(ZKフレンドリーな)ハッシュ関数として広く使われています。 -
hasher.inputs[0] <== password
:
秘密の入力password
を、Poseidonハッシュ関数の入力としてセットしています。 -
hash <== hasher.out
:
これが回路の最も重要な部分、「制約(Constraint)」 です。ハッシュ関数の計算結果(
hasher.out
)が、公開されているhash
と等しくなければならない、というルールを課しています。この制約を満たすpassword
を知っていることこそが、この回路が証明したい内容そのものなのです。
🔢 回路のコンパイル
設計図が完成したので、次はこのPasswordHash.circom
ファイルをコンパイルします。
コンパイルすることで、証明を生成するために必要なwasm
ファイル と、回路の制約を数学的に記述したr1cs
ファイル が生成されます。
ターミナルで以下のコマンドを実行してください。
pnpm circuit run compile
ここでは以下の様なスクリプトを実行することになります。
#!/bin/bash
# Variable to store the name of the circuit
CIRCUIT=PasswordHash
# In case there is a circuit name as input
if [ "$1" ]; then
CIRCUIT=$1
fi
# Compile the circuit
circom ./src/${CIRCUIT}.circom --r1cs --wasm --sym --c
このスクリプトは、pkgs/circuit/package.json
に定義されたスクリプトを実行し、pkgs/circuit/src/PasswordHash.circom
を読み込みます。
そして、pkgs/circuit/build
ディレクトリ内にPasswordHash.r1cs
とPasswordHash_js/PasswordHash.wasm
という2つの重要なファイルを出力します。
最終的に以下のような出力結果になっていればOKです! !
template instances: 71
non-linear constraints: 216
linear constraints: 199
public inputs: 1
private inputs: 1
public outputs: 0
wires: 417
labels: 583
Written successfully: ./PasswordHash.r1cs
Written successfully: ./PasswordHash.sym
Written successfully: ./PasswordHash_cpp/PasswordHash.cpp and ./PasswordHash_cpp/PasswordHash.dat
Written successfully: ./PasswordHash_cpp/main.cpp, circom.hpp, calcwit.hpp, calcwit.cpp, fr.hpp, fr.cpp, fr.asm and Makefile
Written successfully: ./PasswordHash_js/PasswordHash.wasm
Everything went okay
🔐 パスワードの設定と入力データの生成
次に、証明したい「秘密のパスワード」を具体的に設定し、それを回路への入力データとしてJSON形式で生成します。
スクリプトの役割
pkgs/circuit/scripts/generateInput.js
は、私たちが設定したパスワードをPoseidonハッシュ関数で計算し、回路への入力となるinput.json
ファイルを自動で作成してくれる便利なヘルパースクリプトです。
// pkgs/circuit/scripts/generateInput.js
const { buildPoseidon } = require("circomlibjs");
const fs = require("fs");
const path = require("path");
async function main() {
// Poseidonハッシュ関数を使えるように準備
const poseidon = await buildPoseidon();
// これが今回の「秘密の合言葉」
const input = "serverless";
// パスワードを回路が扱える数値(BigInt)に変換
// 注意: 実際のアプリケーションでは、より安全な方法で文字列をフィールド要素に変換することが推奨されます
const passwordNumber = BigInt(
Buffer.from(input).toString("hex"),
16
).toString();
// パスワードのハッシュ値を計算
const hash = poseidon.F.toString(poseidon([passwordNumber]));
console.log(`Input (passwordNumber): ${passwordNumber}`);
console.log(`Hash: ${hash}`);
// input.jsonファイルを作成するためのデータ構造
const inputs = {
password: passwordNumber,
hash: hash,
};
fs.writeFileSync(
path.join(__dirname, "../data/input.json"),
JSON.stringify(inputs, null, 2)
);
}
main().catch(console.error);
入力データの生成手順
-
パスワードの編集(任意):
pkgs/circuit/scripts/generateInput.js
ファイルを開き、input
変数の値を好きなパスワード(英数字)に変更できます。const input = "serverless"; // 👈 ここを好きなパスワードに変更
-
入力データの生成: スクリプトを実行して、
input.json
を生成します。pnpm circuit run generateInput
以下のような出力結果が得られればOKです!
InputData: {
input: 'serverless',
inputNumber: '544943514572763559195507',
hash: '15164376112847237158131942621891884356916177189690069125192984001689200900025'
}このコマンドは
generateInput.js
を実行し、pkgs/circuit/data/input.json
に以下のような内容を書き込みます。{
"password": "YOUR_GENERATED_PASSWORD_NUMBER",
"hash": "YOUR_GENERATED_HASH_VALUE"
}ターミナルに出力される
hash
の値は、後でフロントエンドの環境変数PASSWORD_HASH
に設定しますので、控えておいてください。