zkp: ゼロ知識証明
2025/05/10
仕事として Bitcoin 開発をやっていたこともあり、ゼロ知識証明という名前自体は聞いたことがある。
が、ゼロ知識証明がブロックチェーンでよく出てくる、という認識は持ってなかった。
ブロックチェーンは難しい
ブロックチェーンの考え方はよくできているのだが、 どこで使うのかというか、どこでなら使えるのかというのは結構難しい。
Bitcoin みたいに誰でもノードを作って接続できる public なタイプは
まっとうな参加者が大量にいる状況じゃないと一から立ち上げるのは難しかろう。
なにより、それぞれの人が趣味でノードを立ち上げてくれるだけという状況だと
ネットワークを維持するのが難しいので、何か金銭的なメリットも付与させるくらいしないと立ちゆかなくなる可能性が高い。
関係者しか参加しない private なタイプだったら、そもそもサーバ・クライアントモデルの方がコストも低くなるのでブロックチェーンを選択する必要があまりない。
将来 public にするかも、みたいな感じでネットワークが広がるまでそうするとかだろうか。
public にしたとして、コンセンサスアルゴリズムを考えるのがまた難しい。
Bitcoin は Proof of Work だけど、昔は Bitcoin Core の機能としてあったらしいが(伝聞)、
今はもう専門企業 or 個人的な趣味くらいじゃないとやらなく(やれなく)なってしまった。
偏っている状況はよろしくないのだけど、これを覆すのはもはや難しいんじゃなかろうか。
もしかしたらマイニング報酬が下がって割に合わなくなった企業から止めていくのかもしれないけど、
その頃には Bitcoin の価格がさらに上がっていて手数料報酬だけでも十分、みたいなことになってるかもしれない。
などなど考えると、ブロックチェーンは Bitcoin や Ethereum などの徐々に育って現在に至っているものはよいとして、 新規で長期間運用できるものを立ち上げるのは大変そうだ。
こう書いていると私がすごく考察しているように見えてしまうが、たぶん社長の受け売りだと思う。 私はそんなに深く考えるタイプの人間ではないのだ。。。
ZKP
なので、今動いているブロックチェーンに乗っかろう、という考え方もある。
Ethereum の ERC-20 みたいにトークンを作るのもそういう感じだと思う。
それとは違う流れで、ブロックチェーンの上にネットワークを作ってしまおうというものがある。 階層のように考えて、ブロックチェーンを 1層目ということで Layer 1(L1)、 その上で動くネットワークを 2層目ということで Layer 2(L2)と呼んだりする。
L2 もいろいろあって、L1 と同じようなブロックチェーンになっているタイプもあれば 必要なときだけ L1 に記録を残すタイプもある。
タイプはいろいろあるにしても、プライバシーを守りたいというのはみんなの要望だ。
public なブロックチェーンは誰でも参照できるので、
アドレスから個人名はわからないにしても、お金の流れを追うことは誰でもできる。
もしちょっとしたことでアドレスと個人が結びついてしまうと
今の時代ではどこまで個人のことが解析されるか分かったものではない。
本人であることはしっかり確認したい。
が、個人のデータは表に出したくない。
ならばコンピュータで計算できる形で、
かつ個人のデータを素のままでない状態にしておき、
それでも正しいかどうかだけの確認はできるようにしたい。
おおざっぱにはそういう流れでブロックチェーンとゼロ知識証明が近しいことになったんだろう。
前置きが長くて済まんね。
案外と身近
ネット上でログインするときにパスワードを打ち込むが、 比較するのはパスワードそのものではなくそのハッシュ値を使う、というのはよく聞くと思う。
あれもゼロ知識証明の一種になるそうだ。 他に身近な例は出てこなかったけど、ゼロ知識証明の雰囲気だけはわかりそうだ。
まあ、ブロックチェーンで使うようなゼロ知識証明はやたらと難しいのだがね。。。
相手とやりとりするタイプのゼロ知識証明と、相手とやりとりしない非対話のゼロ知識証明があって、
ブロックチェーンは後者の方がやりやすいのだった(相手が誰か分からんので)。
zkSNARK と zkSTARK
ゼロ知識証明には定義があって、3つくらいの○○性、みたいなのがあったと思う。 調べたら出てくる。
ブロックチェーンでよく出てくるのは、 それにプラスして SNARK 特性(頭文字)がある zkSNARK と STARK 特性がある zkSTARK だと思う。
どっちがよいかというのは、いろいろ書いてあって良くわからなかった。。。
SNARK は Trusted Setup という信頼できる第三者がいるが検証が速いが STARK は Trusted Setup が不要だけど検証が重たいとか、
いやいや SNARK もひとくくりにできなくて Trusted Setup が不要なものあるよとか、
そもそも比較するようなもんでもないんじゃないとか。
正直なところ、よくわからん。。。
zkSNARKs みたいに “s” が付くかどうかもわからん。。。
理論的なところは・・・あきらめた
数学的なことはわからんのであきらめるとして、
こうだからこうなる、みたいな理論的なところだけでも把握したいと思った。
が・・・
数式は「そうなのねー」で読み飛ばしたのだが、それでもダメだった。
読み飛ばしたからダメなのかもしれないけど、とにかく何が何だかわからない。
演算回路を作って、相手とは非対話で検証だけできるようにする、というくらいだろうか。
これでも「ざっくり」らしい。
もしかしたら将来分かるようになる日が来るかもしれない、と自分に少しだけ期待してあきらめよう。
実践的なところ
理論は分からなくても、zkSNARK は少なくとも動いているのでやってみたら理解が進むかもしれない。
ちなみに「わかりやすかった」という記事に↑の理論サイトもあった。
わかる人にはわかりやすいのね。。。
iden3 という会社が提供している “circom” という回路コンパイラ(circom は回路の言語でもある) と、 回路で使うことができるライブラリの “circomlib”、 Proof の作成などで使う JavaScript ライブラリの “circomlibjs”、 いろいろやってくれる “snarkjs” を使っている。
このリポジトリの package.json を見ると iden3 ではなく tornadocash のリポジトリを参照していた。
このサイトで proof 作るのに使う input.json
のデータは載っているのだけど、
せっかくだから自分でもデータを作って確認したいではないか。
なので JavaScript のファイルだけ持ってきて package.json は自分で npm i
で iden3 のものをインストールして作っていたのだけどちゃんと動かないのだ。
その違いがリポジトリにあった、ということだ。
だいたい Pedersen Hash ってなんじゃ。
ハッシュといえば SHA 系でいいじゃないか、と思ったのだが ChatGPT 氏に訊くと ZKP の回路にしたときに SHA より小さくなるようなことをいっていた。
Poseidon とか Rescue とか全然知らないハッシュ演算の名前を教えてくれた。
ちなみに ChatGPT 氏に Pedersen Hash を iden3/circomlibjs@0.1.7 でサンプル実装してくれるよう頼んだのだけどダメダメだった。
戻り値の型が違うので伝えると「それは異常です」とまで言いきりやがったこいつめ。。。
どうも tornadocash の方は pedersen.hash() の結果を BabyJubjub という楕円曲線(べびーじゃぶじゃぶ?)に当てはめて X, Y 座標を取ってきて X座標を使っているのだが、 iden3/circomlibjs は BabyJubjub の packPoint というものを返しているようなのだ。 だいたい Y座標の方。
Pedersen Hash までは値が一致する。
オリジナルその値で babyJub.unpackPoint()
を呼び出す。
そうすると、その戻り値の[1]
、つまり Y座標が Pedersen Hash を little endian で数値化した値と一致するのだ。
しかし iden3/circomlibjs にしてオリジナルと同じようにハッシュ値を babyJub.unpackPoint()
に与えると [0]
も [1]
もオリジナルに出てこない数値になる。
エンディアンの違いでもなさそうだし、どこが違うのか分からない。
しょうがないので circomlibjs の方では Pedersen Hash の戻り値をそのまま使うように改造したのだが、
Proof を作成するときにオリジナルだと成功して circomlibjs 置き換え版だと失敗することがある、ということになってしまった。
なので修正するなら自分で改造した方になるのだが、皆目分からぬ。
この人も PedersenHash があわない、といっているが input を little endian 変換すれば済むだけだった。
それ以外のところでは BabyJubjub をしているわけでもないのでだいたい同じなのよねぇ。
なんだかわからないけど、2番目に回答がある人のまねをして babyJub.unpackPoint()
の結果を babyJub.F.toString()
にそれぞれ入れると tornadocash のと同じ値になった。
なんだ、なんなんだ・・・。
データ長を 31 バイトにしているところもよくわからない。
推測では、BabyJubjub の unpackPoint()
を見ると [31]
(つまり末尾)の最上位ビットが立ってるかどうかで分岐があったので、31 バイトにして [31]
は 0x00
にしたかったんじゃないかなー、と思っている。
そしていくつか値を見てみたが、普通に [31]
にも値が入っていた。
ハッシュ値だから input の桁数とか関係ないよね、うんうん。。。
おわりに
全然参考にならない話ばかりになってしまった。