メむンコンテンツたでスキップ

🧠 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);

入力デヌタの生成手順​

  1. パスワヌドの線集任意:
    pkgs/circuit/scripts/generateInput.jsファむルを開き、input倉数の倀を奜きなパスワヌド英数字に倉曎できたす。

    const input = "serverless"; // 👈 ここを奜きなパスワヌドに倉曎
  2. 入力デヌタの生成: スクリプトを実行しお、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に蚭定したすので、控えおおいおください。

🔑 蚌明キヌず怜蚌キヌの生成Groth16​

次に、Groth16ずいうZK-SNARKsプロトコルを䜿っお、蚌明キヌProving Keyず怜蚌キヌVerification Keyを生成したす。

このプロセスは 「Trusted Setup」 ずも呌ばれたす。

以䞋のコマンドを実行しおください。

pnpm circuit run executeGroth16

実行されるスクリプトは次のような内容です。

#!/bin/bash

# Variable to store the name of the circuit
CIRCUIT=PasswordHash

# Variable to store the number of the ptau file
PTAU=14

# In case there is a circuit name as an input
if [ "$1" ]; then
CIRCUIT=$1
fi

# In case there is a ptau file number as an input
if [ "$2" ]; then
PTAU=$2
fi

# Check if the necessary ptau file already exists. If it does not exist, it will be downloaded from the data center
if [ -f ./ptau/powersOfTau28_hez_final_${PTAU}.ptau ]; then
echo "----- powersOfTau28_hez_final_${PTAU}.ptau already exists -----"
else
echo "----- Download powersOfTau28_hez_final_${PTAU}.ptau -----"
wget -P ./ptau https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_${PTAU}.ptau
fi

# Compile the circuit
circom ./src/${CIRCUIT}.circom --r1cs --wasm --sym --c

# Generate the witness.wtns
node ${CIRCUIT}_js/generate_witness.js ${CIRCUIT}_js/${CIRCUIT}.wasm ./data/input.json ${CIRCUIT}_js/witness.wtns

echo "----- Generate .zkey file -----"
# Generate a .zkey file that will contain the proving and verification keys together with all phase 2 contributions
snarkjs groth16 setup ${CIRCUIT}.r1cs ptau/powersOfTau28_hez_final_${PTAU}.ptau ./zkey/${CIRCUIT}_0000.zkey

echo "----- Contribute to the phase 2 of the ceremony -----"
# Contribute to the phase 2 of the ceremony
snarkjs zkey contribute ./zkey/${CIRCUIT}_0000.zkey ./zkey/${CIRCUIT}_final.zkey --name="1st Contributor Name" -v -e="some random text"

echo "----- Export the verification key -----"
# Export the verification key
snarkjs zkey export verificationkey ./zkey/${CIRCUIT}_final.zkey ./zkey/verification_key.json

echo "----- Generate zk-proof -----"
# Generate a zk-proof associated to the circuit and the witness. This generates proof.json and public.json
snarkjs groth16 prove ./zkey/${CIRCUIT}_final.zkey ${CIRCUIT}_js/witness.wtns ./data/proof.json ./data/public.json

echo "----- Verify the proof -----"
# Verify the proof
snarkjs groth16 verify ./zkey/verification_key.json ./data/public.json ./data/proof.json

echo "----- Generate Solidity verifier -----"
# Generate a Solidity verifier that allows verifying proofs on Ethereum blockchain
snarkjs zkey export solidityverifier ./zkey/${CIRCUIT}_final.zkey ${CIRCUIT}Verifier.sol

# Update the solidity version in the Solidity verifier
sed 's/0.6.11;/0.8.20;/g' ${CIRCUIT}Verifier.sol > ${CIRCUIT}Verifier2.sol
# Update the contract name in the Solidity verifier
sed "s/contract Verifier/contract ${CIRCUIT}Verifier/g" ${CIRCUIT}Verifier2.sol > ${CIRCUIT}Verifier.sol
rm ${CIRCUIT}Verifier2.sol

echo "----- Generate and print parameters of call -----"
# Generate and print parameters of call
snarkjs generatecall data/public.json data/proof.json | tee ./data/calldata.json

以䞋のような出力結果が出おいればOKです 

----- Export the verification key -----
[INFO] snarkJS: EXPORT VERIFICATION KEY STARTED
[INFO] snarkJS: > Detected protocol: groth16
[INFO] snarkJS: EXPORT VERIFICATION KEY FINISHED
----- Generate zk-proof -----
----- Verify the proof -----
[INFO] snarkJS: OK!
----- Generate Solidity verifier -----
[INFO] snarkJS: EXPORT VERIFICATION KEY STARTED
[INFO] snarkJS: > Detected protocol: groth16
[INFO] snarkJS: EXPORT VERIFICATION KEY FINISHED
----- Generate and print parameters of call -----
["0x15ae6792cbe82731dfdcef2012a32cf9e2d1d81d6a71086ad14afd229bbab166", "0x1e5fade2062037da2c5309da71a6d5fd7da656274e358e71603579720fde74ee"],[["0x110f6f6bc5846e31b20b1e1cacb8a431759640644e56a342d90fee20b51d961f", "0x204970c9f05d739f5ae4b20d0b086c779cbfa69a3fc23acfb3df558d2fb6abb9"],["0x10b3a650597a08686299d817a1f7868a26d0eb1699117deb01cbb052d7262755", "0x02ca1580581ed8b05f10db03aaaa3479b1b50024574bc1cb894d58ec535b634d"]],["0x0dc789b08391b6a5150a4435b9fa0193baffb7a63ef01780b0a4c89da576e587", "0x19c5d7b33a7b1a14eb61d8147b3bab95d25abaee0b830ef8c34ad110093ee85b"],["0x2186bb937db8faf41e671589c116c1f8491df908baaf0da11aedd7fedb5437b9"]

このスクリプトは、内郚でいく぀かのステップを実行したす。

  1. Powers of Tau:
    䞀般的なセットアップファむルをダりンロヌドしたす。

  2. Proving Keyの生成:
    PasswordHash_final.zkeyずいう蚌明キヌを生成したす。
    これは、蚌明を生成する際に必芁ずなりたす。

  3. Verification Keyの生成:
    verification_key.jsonずいう怜蚌キヌを生成したす。
    これは、蚌明を怜蚌する際に必芁ずなりたす。

  4. 蚌明デヌタの生成:
    proof.json蚌明デヌタずpublic.json公開シグナルを生成したす。
    さらに、Solidityコントラクトでの怜蚌に䜿甚できる圢匏のcalldata.jsonも出力したす。

  5. Solidity Verifierの生成:
    PasswordHashVerifier.solずいう、オンチェヌンで蚌明を怜蚌するためのスマヌトコントラクトを生成したす。

🧟 Witnessの蚈算​

Witnessは、特定の入力我々の堎合はパスワヌドに察する回路の䞭間状態をすべお含んだデヌタです。

蚌明を生成するために必芁ずなりたす。

以䞋のコマンドでwitness.wtnsファむルを生成したす。

pnpm circuit run generateWitness

ここでは以䞋のようなスクリプトが実行されたす

#!/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

# Generate the witness.wtns
node ${CIRCUIT}_js/generate_witness.js ${CIRCUIT}_js/${CIRCUIT}.wasm ./data/input.json ${CIRCUIT}_js/witness.wtns

以䞋のような出力結果が出おいれば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

✅ 回路のテスト​

ここたでのステップが正しく完了したかを確認するために、テストを実装したしょう

pkgs/circuit/test/verify.test.jsを䜜成しお以䞋のコヌドをコピヌ&ペヌストしおください。

const snarkjs = require("snarkjs");
const fs = require("fs");

/**
* 怜蚌甚のサンプルスクリプト
*/
async function run() {
// 各ファむルたでのパス
const WASM_PATH = "./PasswordHash_js/PasswordHash.wasm";
const ZKEY_PATH = "./zkey/PasswordHash_final.zkey";
const VKEY_PATH = "./zkey/verification_key.json";
// input data
const inputData = JSON.parse(fs.readFileSync("./data/input.json"));

console.log("Input Data: ");
console.log(inputData);

const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputData,
WASM_PATH,
ZKEY_PATH,
);

console.log("Proof: ");
console.log(JSON.stringify(proof, null, 1));

const vKey = JSON.parse(fs.readFileSync(VKEY_PATH));
// 怜蚌
const res = await snarkjs.groth16.verify(vKey, publicSignals, proof);

if (res === true) {
console.log("Verification OK");
} else {
console.log("Invalid proof");
}
}

run().then(() => {
process.exit(0);
});

このテストは、生成したキヌずWitnessを䜿っお、実際に蚌明が正しく生成・怜蚌できるかを確認したす。

以䞋のコマンドでテストを実行したす。

pnpm circuit run test

タヌミナルに以䞋のような内容が出力されおいればOKです 

Verification OK

📊 アヌティファクトのコピヌ​

最埌に、生成されたアヌティファクトPasswordHashVerifier.sol、PasswordHash.wasm、PasswordHash_final.zkeyを、backendずfrontendの各コンポヌネントから参照できる堎所にコピヌしたす。

# 怜蚌コントラクトをbackendにコピヌ
pnpm circuit run cp:verifier

# ZK関連ファむルをbackendずfrontendにコピヌ
pnpm circuit run cp:zk

これで、れロ知識蚌明回路の構築は完了です

次のセクションでは、ここで生成したPasswordHashVerifier.solを䜿っお、スマヌトコントラクトを開発しおいきたす。

🙋‍♂ 質問する​

ここたでの䜜業で䜕かわからないこずがある堎合は、Discordの#zkで質問をしおください。

ヘルプをするずきのフロヌが円滑になるので、゚ラヌレポヌトには䞋蚘の3点を蚘茉しおください ✹

  1. 質問が関連しおいるセクション番号ずレッスン番号
  2. 䜕をしようずしおいたか
  3. ゚ラヌ文をコピヌ&ペヌスト
  4. ゚ラヌ画面のスクリヌンショット