隠す・分ける・示すを組み合わせる――co-SNARK・zkVM・Tornado Cash と、抜けた制約 1 行
zk-tokyo Advanced Cryptography Program 2026 の Week 6 は「理論を応用設計へ」でした。ZK は示す、MPC は分ける、FHE は隠す。3 つを競合ではなく部品として、要件から逆算して組みます。co-SNARK・zkVM・Tornado Cash を宿題と同じ数値の対話ドリル 38 行で通し、制約が 1 行抜けると二重引き出しが通る瞬間まで手で確かめました。
zk-tokyo のAdvanced Cryptography Program 2026を受講しています。Week 6 のテーマは「Application(理論を応用設計へ)」で、これまで別々に学んだゼロ知識証明(ZK)・秘密計算(MPC)・完全準同型暗号(FHE)を、1 つのシステムに組み合わせる週でした。
今回の収穫は、3 つの道具は「どれを使うか」ではなく「何が足りないか」で選ぶという設計法が腹に落ちたことです。
- 自習ノート: 隠す・分ける・示すを組み合わせる
- 週ごとの目次: Advanced Cryptography 2026 自習ノート
- リポジトリ: susumutomita/advanced-cryptography-note
3 つの道具は、それぞれ穴がある
講義は ZK・MPC・FHE を「誰が誰と何をやり取りするか」の表で並べ直すところから始まりました。表で効いてくるのは「信頼前提」の行です。
| ZK | MPC | FHE | |
|---|---|---|---|
| 何をする | 事実を示す(秘密は出さず) | 秘密を持ち寄って計算 | データを見せずに計算させる |
| 隠す対象 | 証明者の witness | 各参加者の入力 | 計算データと中間状態 |
| 信頼前提 | 証明者を信頼しなくてよい | t 人未満の共謀まで | サーバが正しく計算するか |
FHE は「サーバを信頼」と書いてあります。中身は見えませんが、正しく計算したかは分かりません。MPC は「t 人未満の共謀」までしか守れません。この穴が、講義の合成パターンを生みます。FHE に正しさが要るなら ZK を足して Verifiable FHE、秘密を持ち寄ったまま 1 つの証明にしたいなら MPC と ZK を足して co-SNARK です。足りない保証を、別の道具で埋める。この週の設計はこれに尽きます。
秘密を分けたまま、証明を組む
宿題co-snark-proveは、witness を秘密分散したまま証明者が走らせる計算を書く問題です。ノートのドリル A は宿題と同じp = 97とw = [3, 5]で、この計算を 14 行で通します。
秘密を 2 つの share に分けると、片方だけ見ても秘密は分かりません。share 1 から始まる組を 3 つ並べると、秘密は 3 にも 50 にも 96 にもなります。足し算と定数倍は、各自が自分の share に同じ計算をするだけで、結果の share になります。だから証明生成の大半を占める MSM や FFT は手元で済み、通信が要りません。
ところが掛け算はそうなりません。share 同士を掛けて開くと 21、正解は 13 × 17 mod 97 = 27 で、合いません。ここで Beaver triple が出てきます。秘密と無関係な乱数 a と b、その積 c の share を前もって配っておき、A − a と B − b だけを公開して積の share を組み立てると、開いた値は 27 になります。
share 同士の積を開く : 21 ← 積の share ではない
Beaver triple で組む : 27 ← 13 × 17 mod 97 と一致
公開した d = A − a : 8 ← A が 13 でも 50 でも 90 でも d = 8 になりうる
最後の行が co-SNARK の肝です。公開した差からも秘密は絞れません。「線形はローカル、掛け算だけ通信」という講義の一言は、この 3 行のことでした。
実行そのものを表にして、1 行ずつ確かめる
宿題zkvm-exploitは、zkVM の中で走る guest プログラムを Rust で書く問題です。題材は 16 ビットで合計を取るクーポン検証器で、65535 + 1001 は 16 ビットでは 1000 に化けます。真の合計は 66536 なのに上限 1000 を通ってしまう。これが攻撃です。
zkVM は回路を手で書く代わりに、実行を 1 ステップ 1 行の表(trace)にして、隣り合う 2 行の関係を制約にします。ドリル B では表が「16 ビットの合計」1 列だけで、制約も 1 種類です。
trace : [0, 65535, 1000]
隣接行の残差 : [0, 0] ← 全部 0 なら正しい実行
表を 1 か所書き換え : [0, 65535] ← 0 でない行が出て捕まる
証明の中身は「制約が全部 0 である」ことでした。Week 4 の AIR と同じ考え方で、命令が 1 つしかない極端な例です。講義で紹介された LeanVM は命令が 4 つしかなく、少ないほど制約が減って証明が軽くなるという理由も、この表を見たあとなら納得できます。
公開するのはプログラムの識別子と「攻撃が存在する」だけで、攻撃入力は witness に残します。ドリルでは 2 つの違う攻撃入力が同じ公開出力を返すことを確かめました。これが Proof of Exploit の「どの攻撃かは言わない」の中身です。
預けたことは示す、どれかは言わない
Tornado Cash は、預け入れと引き出しのリンクを断つ仕組みです。ドリル C は sha256 の先頭 6 桁をハッシュにして、4 枚の葉の Merkle 木を手で作ります。
預けるときは乱数 k と r から預かり証 C = h(k, r) を木に入れます。引き出すときは「木のどれかの葉の k, r を知っている」ことと、使用済み札 h(r) だけを見せます。k は最後まで一度も出てきません。2 回目は h(r) が既出なので拒否されます。
ここまでは教科書どおりです。面白いのは最後の 1 行でした。偽造者が別の r2 を持ってくると、使用済み検査は素通りし、C = h(k, r2) も成り立ちません。回路が「C を作った r で使用済み札を作った」ことを要求していれば、ここで止まります。要求していなければ、使用済み検査の素通りだけで 2 回目が通ります。
講義が取り上げた Zcash Orchard の偽造脆弱性は、まさにこの形でした。witness を置くだけで、上流の値との一致を制約しない 1 行が、4 年間の人手監査をすり抜けていました。壊れたのは soundness で、zero-knowledge ではありません。この 2 つが別物であることを、ドリルの最後の 1 行で体感できます。
要件から逆算する
講義の第 3 部は「欲しい成果物から primitive を選ぶ」決定木でした。証明が欲しいか計算結果が欲しいか、witness は単独か複数者か、計算するのは自分たちか第三者か、正しさも証明したいか。上から答えるだけで 1 つに決まります。
グループワークのお題「医療データを見せずにクラウドの AI に診断させ、正しく計算されたことも確認したい」は、計算結果 → 第三者に委託 → 正しさも証明したい、で Verifiable FHE に着きます。FHE だけでは正しさが保証されず、そこに ZK を重ねる。第 1 部の表の「穴」がそのまま答えになっていました。
もう 1 つ、性能の実測が印象に残りました。同じ主張を 3 方式で証明させると、証明生成は 241.6 秒から 22.0 秒に縮む一方、証明サイズは 0.93 KB から 716 KB に膨らみます。オンチェインで検証するなら小さい方が今も優位です。「一番速い方式」は存在せず、証明を誰がどこで検証するかで方式が決まる、という講義の言葉は、この 2 つの数字を並べると自明に見えました。
まとめ
- ZK は示す、MPC は分ける、FHE は隠す。3 つは競合ではなく、足りない保証を補い合う部品です。
- co-SNARK では線形計算は手元で済み、掛け算だけ Beaver triple で 1 往復します。公開した差からも秘密は絞れません。
- zkVM の証明は「実行の表で制約が全部 0」です。表を 1 か所いじれば 0 でない行が出ます。
- Tornado Cash の匿名性は commitment・nullifier・Merkle 所属の 3 点セットで、結びつきを要求する制約が 1 行抜けると二重引き出しが通ります。
ノートのドリルは宿題と同じ数値なので、書いた関数にドリルの入力を渡せば、ドリルの出力がそのまま期待値になります。宿題に取りかかる前に、38 行を手で打っておくことを勧めます。