🎁

ZKP(zkSNARKs)の使い方と活用事例

2023/12/25に公開

本記事はEthereum Advent Calender 25日目の記事として書いてます。

ブロックチェーン業界ではゼロ知識証明(以下ZKP:Zero Knowledge Proof)が注目を集めていますが、
ZKPに関する記事は概要や理論に関する記事が多く、実際の使い方やコードベースの記事は少ないと感じていいます。
実際に、ZKPを使ったアプリケーションを開発しようとした時、実務的な内容の記事が少なく、開発に入るまで多くの時間を要しました。

https://twitter.com/_ywzx/status/1721127312194781215

したがってZKPの理論よりも、ZKPの活用事例、ZKPライブラリの使い方、ZKPを使ったサンプルアプリの解説など、より実務に近いの内容の記事を書きたいと思い執筆しました。

ZKPの理論自体に興味がある方はこちらの記事をご参考ください。
とても分かりやすく詳しくまとめてありました。

目次

  • ゼロ知識証明(ZKP)とは
  • ゼロ知識証明(ZKP)の種類
  • ゼロ知識証明(ZKP)の活用事例
    • zk rollup
    • Tornado cash
  • zkSNARKsを使った開発の流れ
  • サンプルアプリケーション(zk-distributer)の紹介

ゼロ知識証明(ZKP)とは

ゼロ知識証明は英語ではZero Knowledge Proofと呼ばれ、ZKPとよく略されています。

ゼロ知識証明とは、「ある命題が真であることを、命題が真であるという情報以外を伝えずに証明できること」 です。
関数だと 「C(x, w) = z」 でwを明かすことなくC(x, w) = trueであることを証明できる と表現することができます。xはpublic inputでwはprivate inputになります。

また、ゼロ知識証明では、ある命題が真であることを証明する人を「Prover」、Proverが真であることを証明するデータを「Proof」、そのProofを検証する人を「Verifier」、とそれぞれ呼ばれます。

「ログイン認証」はゼロ知識証明の定義を理解する上でわかりやすい例えです。
需要はないと感じますが、例えばスマートコントラクトでログイン認証を実装しようとしたらパスワードをブロックチェーンに保存する必要があります。そうすると、ブロックチェーンのデータは公開されているため、パスワードが晒されてしまい、ログイン認証をブロックチェーンで実現することはできませんでした。

しかし、ゼロ知識証明を利用すると、パスワードを持っていることを、そのパスワードを誰にもに見せることなく、パスワードを持っているということを数学的に証明することができます。そのため、スマートコントラクトでも「ログイン認証」のような機能を実装することができます。

実際に、「パスワードを相手に見せることなく、パスワードを持っているということ」を証明するコードは下記のようになります。

・・・
template Main() {
    signal input secretHash;  // ①
    signal input secret;      // ①

    component hasher = SecretHasher();  // ②
    hasher.secret <== secret;                     // ②

    secretHash === hasher.secretHash;     // ③
}

component main {public [secretHash]} = Main();  // ①

https://github.com/0xywzx/event/tree/main/20231111_web3_global_hackathon/zk-secret#1-circomでcircuit回路の作成

①でpublic inputとprivate inputを指定しています。
②でprivate inputであるsecretSercetHasher()を利用してハッシュ化しています。
③でpublic inputであるsecretHashと②のsercetHashを比較して、同じであればtrueを返します。

ゼロ知識証明(ZKP)の種類

ゼロ知識証明の歴史は意外と長く、時代の流れとともに数多くの種類が出ていています。

また、ゼロ知識証明を利用しているプロジェクトは多く存在していますが、それぞれのプロジェクトは別々の種類のゼロ知識証明を利用しています。
StarkWareはSTARKを、scrollやAnomaはHaloを、MoneroはBulletproofを、tornado cashはGroth16を、AztexとMinaはPlonlK...などなど、それぞれバラバラです。

Scrollの開発者コミュニティーによって生成されたHalo2-ceもScrollでは使われているらしく、多くの種類のゼロ知識証明が生まれ使われています。
https://open.spotify.com/episode/1gh9GZyQ7dExtEObxAHUYa

zk-SNARKs と zk-STARKs

ZKPの中でもzk-SNARKsとzk-STARKsはよく耳にするかと思います。
zk-SNARKsは「証明の生成は遅いがProofのサイズが小さく検証が早い」と言われており、zk-STARKsは「証明の生成は早いが、Proofのサイズが大きく検証が遅い」といわれています。
また、zk-SNARKsは量子耐性がないと言われていますが、zk-STARKsはTrusted setupが必要ないことから量子耐性があると言われています。
加えて、zk-SNARKSは証明するデータが多くなるとproofのサイズが大きくなってしまいますが、zkSTARKsだと証明するデータが多くなってもproofのサイズが変わらないため、zkSTARKsはスケーラビリティーがあると言われています。

ただ、ZKP周りの技術の進展は早くTrusted Setupが必要ないzkSNARKsが出てたり、そもそもSNARKsとSTARKsの比較自体がナンセンスだったりするそうです。

ひとまず、ZKPにも様々な種類があることだけ覚えてもらえたらと思います。

https://github.com/matter-labs/awesome-zero-knowledge-proofs?ref=blog.pantherprotocol.io#comparison-of-the-most-popular-zkp-systems

https://ethereum.stackexchange.com/questions/59145/zk-snarks-vs-zk-starks-vs-bulletproofs-updated

https://blog.chain.link/zk-snarks-vs-zk-starks/

ブロックチェーンにおけるZKPのメリット

ざっくりとしたZKPの概要を説明できたところで、ブロックチェーンでZKPを使うメリットについて、具体的なアプリケーションの事例を元に解説していきます。

ZKPがブロックチェーンのもたらす影響として 「データ量・計算量の圧縮」「情報の秘匿化」 が挙げられます。

「データ量・計算量の圧縮」

例えば、「1, 2, 3 , 4 ... 10000」と1万個の数字があり、これらの数値を順番に足すような処理をブロックチェーンで実行しようとすると多額のガス代がかかってしまいます。単純に足すだけの関数であれば高いガス代を払えば実行することができるのですが、もっと複雑な計算を行う関数などはそもそもオンチェーンで実行できない可能性があります。なぜかというと、Ethereum系のブロックチェーンは一つのblockで扱えるGasUnit(ブロックチェーン上の処理の単位)の上限があり、複雑な計算はその上限に達する可能性があるためです。

データ量・計算量の圧縮のユースケースとしてzk rollupHyperOracleが挙げられます。

情報の秘匿化

ブロックチェーン上の情報は全て公開されているため、公開したくない情報などは扱うことができませんでした。しかし、ZKPを使うとその情報を持っているということを、その情報を明かすことなく証明できるので、パブリックが前提のブロックチェーンにプライベートの概念を持ち込むことができます。
情報の秘匿化のユースケースとしてtornado cashが挙げられます。

実際のサービスを用いて、この2つの利点について詳しく解説していきます。

データ量・計算量の圧縮のユースケース : zk rollup

Ethereumに関連する定義や情報が細かくまとめられているEthereum.orgではRollupは下記のように説明されています。

Rollups bundle (or ’roll up’) hundreds of transactions into a single transaction on layer 1. This distributes the L1 transaction fees across everyone in the rollup, making it cheaper for each user.
 Rollup transactions get executed off layer 1 but the transaction data is submitted to layer 1. By submitting transaction data onto layer 1, rollups inherit the security of Ethereum.

現状のEthereum(layer1)はガス代が高く、また多くのトランザクションを一度に処理することができません。
Rollup(L2)とは、それらの課題を解決するためにEthereum外でトランザクションを処理することで、ガス代を削減し処理速度を上げようとする取り組みです。
しかし、Ethereum外でトランザクションを処理してしまうとセキュリティーが損なわれてします。そのため、Ethereum外での処理の結果をまとめてEthereumに書き込むことで、rollupではEthereumのセキュリティーも継承しつつ、ガス代の削減と処理速度の向上を実現しています。

zk Rollupでは、rollupの情報をEthereumに書き込む際にZKPが活用されています。

rollupのトランザクションのEthereumへの書き込み

rollupには、Ethereumに書き込まれるrollupのトランザクションは正しいものとし、一定期間異議申し立てがなかったらrollupのトランザクションを確定する「Optimistic rollup」と、Etheruemに書き込まれるrollupのトランザクションの正しさをZKPを利用して証明し確定する「zk rollup」があります。

zkRollupをより理解するためにも、Optimistic rollupの仕組みとともに解説します。

Optimistic rollup : Optimismの例

Optimistic rollupでもOptimismのトランザクションの見てみます。Optimismのトランザクションは、Op-Batcherと呼ばれるOptimismが管理するアドレスによってEtheruemに書き込まれています。

上の画像は、実際にOptimismのトランザクションがEthereumに書き込まれているトランザクションです。
"Input Data"の箇所をスクロールしていただけたらわかると思いますが、かなりのデータがEthereumに保存されており、このデータは一定期間のOptimismのトランザクションをまとめたものになります。

Optimistic rollupの場合はこれらのデータの検証は行わずに、7日間たっても異議申し立てをする人が現れなければrollupのトランザクションは正しかったとみなされます。

zk rollup : zkSync Eraの例

zkRollupの一つであるzkSync Eraの場合、zkSyncのトランザクションをまとめてハッシュ化し、そのハッシュ値とstateの差分のみをEthereumに保存しています(①Commit)。そして、そのハッシュ値が正しいことをZKPを用いて証明しています(②prove)。

①Commit

ハッシュ化とはあるデータを固定長のデータに変換することを指し、ハッシュの特徴として、ハッシュ化する前のデータからハッシュ化した後のデータは簡単にわかりますが、ハッシュ化した後のデータからハッシュ化する前のデータは推測することが事実的に不可能である「不可逆性」を持っています。

実際にcommit()のトランザクションを見てみると、commitmentがrollupのトランザクションをまとめてハッシュ化したデータになり、Optimismと比べると非常に小さいことがわかります。

②prove

ハッシュ値をEthereumに保存したものの、まだこのハッシュ値が本当にrollupのトランザクションをまとめてハッシュ化したものかはわかりません。
そのため、rollupのトランザクションをまとめたという情報をZKPを活用して証明しています。

「ゼロ知識証明(ZKP)とは」のセクションでパスワードを持っていることをパスワードを明かすことなく証明することができると紹介しましたが、その例に当てはめて考えるとより理解しやすいかと思います。
パスワードを証明するために、パスワードをハッシュ化し、そのハッシュ値とproofのみでパスワードを明かすことなくパスワードも持っていることを証明できました。
zkRollupでは、パスワードがrollupの一定期間のトランザクションにあたり、それらのトランザクションをまとめたハッシュ値とそれらのトランザクションを持っているというproofのみで、それらのトランザクションを持っていることをEthereumに渡すことなくEthereum上で証明しています。

実際のトランザクションを見てみると、proofが渡されていることがわかります。

このようにzkRollupでは、rollupのトランザクションが存在していることを、Ethereumに渡すことなくEthereum上で証明することができています。
つまり、本来であればEthereum上ですべき計算をゼロ知識証明を使うことでEthereum外で行うことができ、計算量・データの圧縮できています。

情報の秘匿化のユースケース : tornado cash

次に「情報の秘匿化」のユースケースとしてtornado cashを紹介します。
tornado cashとは、暗号資産のミキシングサービスで、アドレスの結びつきを分断するために利用されています。
例えば、筆者が筆者の友人にEthereumを送った場合、筆者の友人は筆者のアドレスがわかってしまうので、筆者のアドレスの過去の履歴やこれからの行動全てが友人にわかってしまうことになります。そうなると、プライバシーがなくなるため、tornado cashを使ってアドレスを追跡を困難にすることができます。

tornado cashのZKPの活用方法をサービスの流れを追って説明します。

① note(nullifier + secret)を生成
② 利用していたアドレス(0x111...)で、noteのハッシュ値を引数にETHをスマートコントラクトにデポジットする関数を実行
③ スマートコントラクトの処理でハッシュ値はmerkle treeに追加
④ 新しいアドレス(0x555...)をベースに、noteを保有していることを証明するproofを作成 (←ここでZKPが利用されています)
⑤ RelayerにproofとnulifierHashを渡し、Relayerがそれらを引数に引き出し関数を実行することで、新しいアドレス(0x555...)にETHが送信される

まず、①のステップで31bytesのnullifierとsecretを生成し、この2つの値を合わせたものをnoteと呼びます。noteはパスワードと同等のものと捉えていただいて大丈夫です。
noteをnullifierとsecretの2つの値から生成している理由は、2つあります。
1つ目は、資産の引き出しの際にnullfierのハッシュ値のみを登録してnoteの2重利用を防ぐためです。
2つ目は、アドレスとnoteが結びつかないようにすることです。②ではnoteのハッシュ値を登録しますが、1つ目の理由としてあげたnoteの2重利用を防ぐために、資金の引き出しの際にnoteのハッシュ値を保存してしまうとアドレスが紐付いてしまいます。

③と④のステップがtornado cashではとても重要なステップです。
noteのハッシュ値はMerkle treeに追加されます。そして、新しいアドレスでETHを引き出す際に、「noteを保有していること」と「そのnoteのhash値がスマートコントラクトにあるMerkle Treeの中に存在していること」の2つを証明するproofを生成します。
noteのハッシュ値をMerkle treeに追加することで、新しいアドレスで引き出す際にMerkleTreeに保存されている特定のnoteを指定せずとも関数を実行することができ、フロントランやアドレスの結びつきを防いでいます。

⑤のステップでRelayerにETHの引き出しを以上する理由は、新しいアドレスではETHを持っていないためgasを支払えず引き出す関数を実行できないから、手数料を払ってRelayerに引き出しを実行してもらっています。

このように、tornado cashでは、ZKPを活用することにより「noteを保有していること」「そのnoteがMerkleTreeに保存されていること」を秘匿化してオンチェーンで検証することで、暗号資産のミキシングを実現しています。

zkSNARKsを使った開発

ここからは実際にZKPを活用したアプリケーションを開発する流れについて解説します。
本記事ではZKPの中でもzkSNARKsのGroth16を利用します。

zkSNARKsを使ったアプリケーションとこれまでのdappsと比較した違いとして、開発者側ではCircuit(回路)とTrusted Setupの作成、利用者側ではproofの生成があります。

ただ、そこまで大きな違いはないです。

zkSNARKsとは

zkSNARKsとはSuccinct Non interactive ARgument of Knowledge の頭文字をとっています。各単語の意味は下記のようになります。

  • Succint = 簡潔:実際のデータ量よりもproofのサイズ非常に小さい
  • Non Interactive = 非対話式 : proverからverifierへの情報の送信は一度きりで、両者間での往復するやりとりはない
  • ARguement of Knowledge = 知識の根拠:proofは計算上確かなものとみなされるため、proofの元となる情報を保持していない限り、システムをハックすることは困難

Trusted Setup と Ceremony

zkSNARKsの開発においてTrusted Setupは特に重要なプロセスになります。
そのため、実際の開発の流れを説明する前にTrusted Setupについて解説します。

Trusted SetupとはzkSNARKsを使ってproofを生成する際に必要となる最初のパラメーターを生成するプロセスのことです。

また、Trusted Setupを生成するために、不特定多数の参加者が生成する秘密情報をリレー形式に繋いでZKPに必要なパラメーターを生成する作業のことをCeremonyと呼びます。

Ceremonyの各参加者が利用した秘密情報が漏洩してしまうと、虚偽のproofが生成できてしまいます。つまり、パスワードを持っていることを証明するアプリの場合、パスワードを持っていなくとも持っていると証明できてしまいます。

したがって、zkSNARKsを使う場合は、Trusted Setupの分散性と堅牢性を担保するためにも多くの方に参加してもらう必要があります。

実際にzkSync Eraは200人以上Tornado cashは1,114人KZG Ceremonyは141,416人参加したと言われています。

開発の流れ

パスワードを持っていることを証明するプログラムを作成したので、こちらのプログラムをもとに実際の開発の流れを解説していきます。

https://github.com/0xywzx/event/tree/main/20231111_web3_global_hackathon/zk-secret

開発のステップは下記になります。

  1. circomでcircuit(回路)の作成
  2. circuitのcompile
  3. Trusted Setup* (Power Of Tau, phase2)
  4. Proofの生成
  5. Proofの検証
  6. Solidity Verifierとproofの作成

環境構築

まず、zkSnarkとcircomをインストールします。

circomに関してはこちらを参考にしてください。

https://docs.circom.io/getting-started/installation/

zkSnarkに関してはこちらのコマンドでインストールできます。

npm install -g snarkjs@latest

1. circomでcircuit(回路)の作成

まず、Circomと呼ばれる言語を利用して、パスワードを持っていることを証明するcircuit(回路)を作成します。

パスワードはsecretという名称を付けています。

cat <<EOT > circuit.circom
pragma circom 2.0.0;

include "./node_modules/circomlib/circuits/bitify.circom";
include "./node_modules/circomlib/circuits/pedersen.circom";

template SecretHasher() {
    signal input secret;
    signal output secretHash;

    component secretHasher = Pedersen(248);
    component secretBits = Num2Bits(248);

    secretBits.in <== secret;

    for (var i = 0; i < 248; i++) {
        secretHasher.in[i] <== secretBits.out[i];
    }

    secretHash <== secretHasher.out[0];
}

template Main() {
    signal input secretHash;
    signal input secret;

    component hasher = SecretHasher();
    hasher.secret <== secret;

    secretHash === hasher.secretHash;
}

component main {public [secretHash]} = Main();

EOT

この回路は「ZKP(ゼロ知識証明)とは」のセクションで説明したものと同じで、secretのhash値をpublic input、secretをprivate inputとし、circuitの中でprivate inputのsecretをハッシュ化してpublic inputのsecretHashと一致していたらtrueを返すproofを生成するものです。

2. circuitのcompile

circuitのcompileは下記コードで行えます。今回のcircuitではcircomlibを利用しているので、package.jsonを参考にインストールしてください。

circom circuit.circom --r1cs --wasm --sym

3. Trusted Setup* (Power Of Tau, phase2)

zkSnarkのgroth16の場合、Trusted Setupは2つのフェーズに別れます。最初のフェーズはSNARKsのためのものPower of Tauと呼ばれで、2つ目のフェーズは特定のcircuitのためのものになります。

https://tornado-cash.medium.com/tornado-cash-trusted-setup-ceremony-b846e1e00be1

Trusted Setupは下記のコマンドで行うことができます。

Phase1 : Power of tau

// ceremonyの開始
snarkjs powersoftau new bn128 14 pot14_0000.ptau -v

// first contribution
snarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau --name="First contribution" -v -e "random text 1"

// second contribution
snarkjs powersoftau contribute pot14_0001.ptau pot14_0002.ptau --name="First contribution" -v -e "random text 2"

// third contribution
snarkjs powersoftau contribute pot14_0002.ptau pot14_0003.ptau --name="First contribution" -v -e "random text 3"

・・・・

// power of tau の完成
snarkjs powersoftau beacon pot14_0003.ptau pot14_beacon.ptau 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n="Final Beacon"

// phase2の準備
snarkjs powersoftau prepare phase2 pot14_beacon.ptau pot14_final.ptau -v

snarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau --name="First contribution" -v -e "random text 1"のコマンドは、秘密情報である"random text 1"をもとにpot14_0000.ptauからpot14_0001.ptauを作成しています。
その次のコマンドでは、秘密情報である"ramdom text 1"とpot14_0001.ptauをもとに、pot14_0002.ptauを生成して...と言ったように、参加者で秘密情報をリレーすることでzkSNARKsに必要となるパラメーターを生成しています。

phase 2

// setup
snarkjs groth16 setup circuit.r1cs pot14_final.ptau circuit_0000.zkey

// first contribute
snarkjs zkey contribute circuit_0000.zkey circuit_0001.zkey --name="1st Contributor Name" -v

・・・

// apply random beacon
snarkjs zkey beacon circuit_0001.zkey circuit_final.zkey 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n="Final Beacon phase2"

// export verification key
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json

phase2もphase1と同様に、秘密情報をリレーしています。
最後の2つのコマンドで、circuit_final.zkeyverification_key.jsonを生成しています。
circuit_final.zkeyはproofを生成するために必要となるもので、verification_key.jsonはproofを検証するために必要なものになります。

4. Proofの生成

proofを生成するためのinputを作成します。
circomでsignal input secretsignal output secretHashの2つを指定していたので、2つの値のinputを作成します。

// inputの作成
cat <<EOT > input.json
{
  "secretHash": "6652720879077241532616383128614021269153945618627832693739893997014067042416",
  "secret": "47147923612045898161641070995147120065030130353900809301484784528935861854"
}
EOT

このinputをもとに下記コマンドでproofを生成します。

// witnessの作成
cd circuit_js
node generate_witness.js circuit.wasm ../input.json ../witness.wtns
cd ..

// proofの生成
snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json

5. Proofの検証

下記コマンドでproofを検証することができ、proofが正しければ「OK」とでます。

snarkjs groth16 verify verification_key.json public.json proof.json
[INFO]  snarkJS: OK!

6. Solidity Verifierの作成

5ではコマンドラインでproofを検証しましたが、ブロックチェーンで活用する場合はオンチェーンでproofを証明する必要があります。
snarkjsにはproofを検証できるsolidityのコントラクトを生成するコマンドがあり、下記を実行するとverifier.solが生成されます。

snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol

solidityの関数でproofを検証したい場合は以下のコマンドを実行すると取得できます。

snarkjs zkey export soliditycalldata public.json proof.json

実際に生成されたsolidityのスマートコントラクトをデプロイして、solidity用のproofを渡したところ、trueが返却されるのが確認できます。

サンプルアプリ : zk distributer

ZKPを使ったアプリケーションの流れを掴んだところで、筆者が実際に作成したアプリケーションを紹介します。

作成した背景

先日、スマートコントラクトの仕組みと法律という本を出版して、本の購入特典としてNFTを配りたいと考えていました。

配布の方法として、本に個別のコードを記載し、そのコードを使って本の購入者がNFTをmintする設計を考えたのですが、この方法にはいくつか問題点がありました。

コードを引数としてmintを実行してしまうと、トランザクションが取り込まれる前に第三者に横取りされる(フロントラン)される可能性があります。かといって、フロントランを防ごうとすると、配布者側の運用の手間が増えてしまいます。

したがって、配布者が手間をかけることなく、確実に特典を届けることが難しかったため、妥協した設計になっていました。

しかし、ZKPを活用すれば、コードを保持していることを明かすことなくコードを持っていることを証明できるので、それらの問題を解決できると考え作成しました。

アプリの流れ

実際に作成したもののコードはこちらになります。
https://github.com/0xywzx/event/tree/main/20231225_zkp_zenn

具体的なアプリの流れとしては下記になります。

  • 配布者の準備
    • 配布者がランダムな文字列(コード)を複数生成し、それらのハッシュ値をMerkle tree形式でまとめる
    • Merkle treeをコントラクトに保存する
    • 生成されたコードを本に綴じる
  • 本の購入者が行うこと
    • 購入者がコードを持っているという証明を作成
    • 証明をもとにNFTをmint

重要な部分だけコードとともに解説まします。

配布者の準備

まずこちらのスクリプトでランダムな文字列を複数生成し、rootを生成しています。

  for (let i = 0; i < 20; i++) {
    const secret = rbigint(31);
    const hash = pedersenHash(leInt2Buff(secret, 31));
    secrets.push(secret);
    leaves.push(hash);
    ...
  };
  ...
  const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves);
  const root = tree.root();
  ...
  const hexRoot = "0x" + BigInt(root).toString(16);
  ...

ランダムな文字列であるsecretがそれぞれ本の購入者に配られるものになります。
snarkjsのexportSolidityCallDataでは、inputがhexに変換されていたので今回はhexで扱うことにします。

上記で生成したrootをコントラクトdepoly時に保存しています。

    constructor(
        IVerifier _verifier,
        uint _root
    )
        ERC721("ZKDistributerCTF", "ZKD")
    {
        verifier = _verifier;
        root = _root;
    }

本の購入者が行うこと

こちらのコードでランダムな文字列を
proofを生成する際にtreeのどの位置にあるランダムな文字列を利用しているかの情報が必要になるのでindexも指定する必要があります。
実際にTornadoCashもtreeの全てのデータを取得して、leafの位置を指定してproofを生成しています。

https://github.com/0xywzx/event/blob/main/20231225_zkp_zenn/contract/scripts/generateProof.js

proofが生成できたら、そのproofを元にsafeMint()を実行して、NFTをmintします。

    function safeMint(
        uint[2] calldata _pA,
        uint[2][2] calldata _pB,
        uint[2] calldata _pC,
        uint[2] calldata _pubSignals
    )
        public
    {
        uint256 tokenId = _nextTokenId++;

        // ① Check if secretHash has not been used
        require(!secretHashes[_pubSignals[1]], "The secret has been already used");

        // ② Check if the Merkle tree root matches the registered root
        require(_pubSignals[0] == root, "The root does not match the registered root");

        // ③ Verify the proof
        require(
            verifier.verifyProof(
                _pA,
                _pB,
                _pC,
                _pubSignals
            ),
            "Invalid proof"
        );

        // ④ Update the secretHash status
        secretHashes[_pubSignals[1]] = true;

        _safeMint(msg.sender, tokenId);
    }

ランダムな文字列を複数回使われないように、①で使われてないかをチェックし、④で使用済みのラベルを貼っています。
②ではrootは保存されたものを正しいかを検証して、③でproofの検証を行っています。

クリスマスプレゼント

その① : NFTをゲットしよう

実際に今回作成したシークレット値を知っていたらNFTがミントできるスマートコントラクトをテストネットでデプロイしました。

https://sepolia.etherscan.io/address/0x936BAF258E18D5C52F8298DA1c0E75E9E91707c5#code

このリポジトリのどこかに20個のシークレット値を配置してます。(結構簡単に見つけられると思います。)

https://github.com/0xywzx/event/tree/main/20231225_zkp_zenn

そのチークレット値を元にproofを生成してNFTをゲットしてみましょう!!

ヒント

*ほぼ答えになります。

.envの設定は必要になりますが、下記スクリプトを実行し、indexとシークレット値を記入すれば、proofの作成からmintを行ってくれます。

https://github.com/0xywzx/event/blob/main/20231225_zkp_zenn/contract/scripts/mint.js

その② : 脆弱性を見つけよう

(時間がなくて実装できませんでした...)
まだ、今回作成したアプリには脆弱性があります。その脆弱性を見つけてみましょう!!

おわりに

本記事を最後まで読んでくださりありがとうございます!!
この記事をきっかけにゼロ知識証明を活用したアプリケーションが生まれれば幸いです。

Happy ZK life 🎅

Discussion