Susumu Tomita

Advanced Cryptography Program 2026 / Week 1 自習ノート

算術回路の作り方と、壊し方

このノートは講義スライド 37 枚と、課題 proof-of-exploit の README と同じ順番で進みます。 1 カード = スライド 1〜2 枚(または README の 1 概念)= 主張 1 つ。宿題の 2 関数 build / attack が どこに当たるかを、毎回「宿題ではここ」で示します。

数は 0〜7 の整数と 1 桁の掛け算だけ。「やってみる」を紙で解いてから答えを開いてください。 制約を 1 本ずつ外して回路を壊せるラボと、検問クイズはそのまま残してあります。

カード 20 枚・演習 6 問 対話ラボあり 前提知識なし 外部通信なし

出発点

このノートは、講義スライド 37 枚と課題 README の順番で進む

Week 1 のスライドは前半(1〜14)がプログラム紹介、後半(15〜37)が「なぜ Programmable Cryptography か」と「ZK / MPC / FHE の違い」、 そして技術の実体は課題 proof-of-exploit の README にあります。 このノートはスライド 15〜37 をカードにしたあと、README を同じ形(1 カード = 1 主張 + 1 例)でカードにします。 宿題で書く 2 関数 build(Part A)と attack(Part B)がどこに当たるかを「宿題ではここ」で示します。

読み方

上から順に、1 カードずつ。カード末尾の Q. に自分の言葉で答えられたら次へ。章の終わりの「やってみる」は紙と鉛筆で。数は 0〜7 の整数と 1 桁の掛け算だけです。

この週の 1 行

回路 = 「全部 0 になるべき式の束」。信号は、式で縛らない限り自由。だから式を 1 本忘れると、嘘の値が通る(アンダー制約)。Week 2〜4 は、この「式の束」を分けて計算し(MPC)、見せずに納得させ(ZK)、多項式にまとめる(SNARK)話です。

この頁の構成

    対話ドリル — 回路を、1 行ずつ自分の手で

    1 行打って、出た値を貼って、1 文読む

    宿題 proof-of-exploit の Part A(組む)と Part B(壊す)を、関数を書く前に手で 1 回通します。1 行打つ → 出た値を貼る → その値を指す 1〜2 文が開く。回路とは何か・アンダー制約とは何かが、説明ではなく数で入ります。

    準備(1 行だけ)

    python3

    ターミナルで python3 と打つだけです。宿題のファイルは要りません(mod 11 の計算だけ)。行頭に空白を入れると IndentationError になるので、貼るときは行頭から。

    ドリル 1 — 「= 0 の式」で条件を書く

    移項して = 0 → ビット制約 → メンバーシップ → f が嘘をつくと落ちる → AND は掛け算 → 7 本の残りが全部 0。10 行。

    ドリル 1 — 制約を作る

    0 / 0

    ドリル 2 — 1 本抜くと、嘘が通る

    正常系 → 嘘は落ちる → リンクを抜く → 通る値が 2 通りから 8 通りへ → 攻撃 witness を選ぶ → 修復 → 正常系は無事。9 行。

    ドリル 2 — 壊して、直す

    0 / 0

    スライド 15〜24 / Programmable Cryptography が重要となっている背景

    監査の外で起きる事故を、数学で止めたい

    15/37本日の流れ

    背景 → Programmable Cryptography とは → それを使ったシステム開発。午後はホワイトボードセッション(題材: KelpDAO 事件)。

    17〜18/37背景: サイバー攻撃範囲の拡大/KelpDAO 事件

    被害はコードのバグより、監査対象外(設定ミス・運用・ブリッジの信頼設定)で起きる割合が増えている。KelpDAO × LayerZero は 2026 年想定の仮想シナリオとして、その典型を示す。

    Q. 「監査で見つからない」事故に、監査を増やす以外の手はあるか。(次のカード)

    19〜20/37Q1 / Proof-of-Exploit × Circuit-Breaker

    「攻撃手順 W でこのコントラクトが壊れる」ことを、W を見せずに証明できれば、証明が通った瞬間にコントラクトを自動停止(circuit breaker)できる。原発の非常停止(スクラム)と同じ発想: 人の判断を待たない安全装置。

    宿題ではここ: 課題名 proof-of-exploit の由来。Part B の attack() が「壊れる手順(witness)」を作る側、採点器の solver が「壊れないことを確かめる」側。

    21/37ProgCrypto は、防御の範囲を押し上げるためにある

    監査は「人が読んで見つける」。ProgCrypto は「壊れないことを数学で保証する」か「壊れたら数学が検知する」。守れる範囲が変わる。

    22〜24/37なぜ今か — 耐量子・プライバシーのデフォルト化・機関投資家
    耐量子: 署名・コミットメントをハッシュベースへ、実行の検証は zkVM の証明が担う(lean Ethereum)
    プライバシー: イーサリアムのプライバシーをデフォルト化する(PSE、Kohaku、PIR)
    機関投資家: 取引内容を隠したまま規則どおりだと示せないと DeFi に入れない

    覚える 1 点: 3 つとも「見せずに正しいと示す」= ZK の需要。Week 3・4 の題材はこの需要の中身。

    スライド 26〜31 / Programmable Cryptography とは

    ZK・MPC・FHE は「何を、誰から隠すか」が違う。共通の中間表現が回路

    26/37「ゼロ知識証明」の「証明」とは何を指すか

    数学の証明(文章)ではなく、「検証者が機械的に確かめられるデータ」。Goldwasser–Micali–Rackoff(1985)が「対話しながら確かめる」形に広げ、そこから「知識を漏らさない証明」が定義された。

    Week 3 で本番: 証明 = (R, s) というデータ、検証 = s·G = R + e·P という計算。

    27/37ZK / MPC / FHE
            隠す対象              信頼前提                        敵対モデル                     用途例
    ZK      prover の witness w   prover を信頼しなくてよい(数学)  prover(偽証)/verifier(窃取) 年齢証明 — 生年月日を出さず 18 以上
    MPC     各参加者の入力         n 台中 k を信頼                  参加者(最大 t 名が逸脱)        秘密入札 — 額を漏らさず最高値
    FHE     計算データと中間状態    User の鍵管理。Server は見ない     盗聴者/Server(正しさは別途)    暗号文のまま機械学習を委託
    ZK: π : ∃w. C(x, w) = 1        MPC: f(x₁, x₂, x₃)        FHE: E(x) → E(f(x))

    Q. 「1 者が自分のデータを見せずに計算してもらう」はどれか。(FHE)「複数者が持ち寄る」は?(MPC)「条件を満たすことだけ示す」は?(ZK)

    28/37Programmable Cryptography — 暗号とシステムを一体化するパラダイム
    1. SOURCE(用途別のコード)   age_check.rs(ZK)/auction.rs(MPC)/diagnose.rs(FHE)
            ↓ compile(circom / halo2 / noir / arkworks)
    2. CIRCUIT(共通の中間表現)  y = (a × b) + c     ゲートとワイヤ。低レベルの数学的表現
            ↓ run(prover / protocol / FHE eval)
    3. EXECUTABLE(共通)         end user には backend が裏で動くだけ

    宿題ではここ: 課題は 2 の CIRCUIT を手で書く。「回路」が ZK / MPC / FHE の共通の土台だから、Week 1 に置かれている。

    29〜31/37ZK の実用化 10 年で 2 桁以上/身近な例/Longfellow

    SumCheck(1992)→ GKR(2008)→ Groth16(2016)→ Spartan・Nova・Lasso・Jolt と、証明時間は 10 年で 2 桁以上速くなった。Google の Longfellow は既存の身分証規格の上で ZK を動かす。

    Week 4 との接続: SumCheck・GKR・Groth16 が Week 4 の 3 部品表に出てくる。

    スライド 33〜37 / ProgCrypto を使ったシステム開発

    4 つの問いが決まってから、暗号を選ぶ

    33/37ProgCrypto をサービスに組み込むときの考え方
    STEP 1 誰が   STEP 2 何の情報を誰から隠して   STEP 3 どういう計算をして   STEP 4 誰が検証するか
    KelpDAO: off-chain の AI Auditor が/攻撃手順 W を事業者・第三者・他ユーザーから隠して/「W でコントラクトが壊れる」を計算し/on-chain コントラクトが検証(成功したら自動遮断)

    Week 2・4 でも同じ問い: MPC の party / input / function / output、Week 4 の 5 パート(証明者・方法・対象・検証・検証者)。

    34〜35/37どの暗号を使うのかは要件次第/パターン A を実装する(RISC Zero × Rust)

    要件が決まると、ZK か MPC か FHE か、対話型か非対話型か、が決まる。KelpDAO のパターン A は zkVM(RISC Zero)で「攻撃手順を実行したら壊れた」の実行を証明する。

    36〜37/37ホワイトボードセッション/今週の課題
    ホワイトボード: KelpDAO 事件を防ぐシステムを 1 つ(アーキテクチャ図・要件・選定技術)
    課題: ① 健全な回路を組む(許可された資格だけを通す制約)  ② 壊れた回路を破る(制約の抜けを突く witness)  ③ PR で提出

    課題 README / proof-of-exploit

    回路は「0 になるべき式の束」— 信号は縛らない限り自由

    ここからが技術の実体です。README の言葉を 1 つずつ、中学の方程式から橋を架けて読みます。

    用語 1信号・制約・witness — 中学の方程式で言うと
    信号(signal)   回路に出てくる変数。値は有限体 F_p の元(大きな素数の余りの世界)
    制約(constraint) 「ある式が 0 になる」という条件。 例: b·b − b = 0 は「b は 0 か 1」
    witness          全部の信号に値を入れた割当。全制約を満たす witness があれば「証明できた」
    
    中学: 「x + 3 = 5」を移項して「x + 3 − 5 = 0」。右辺を 0 にそろえるのが制約の書き方

    宿題ではここ: cs.input(name, value) が入力の信号、cs.aux(name, value) が補助信号、cs.assert_zero(expr) が制約、cs.set_output(granted) が出力。

    用語 2健全性(soundness)と、この課題の肝

    本当は条件を満たしていない入力で制約を満たす witness が作れてしまうと、回路は「嘘の証明」を受理する。防げていれば健全。信号は、制約で縛らない限り好きな値にできる。だから必要な制約を書き忘れると健全性が壊れる(アンダー制約)。

    Q. 「制約を 1 本忘れる」と「制約を 1 本多く書く」、危ないのはどっちか。(忘れる方。多い方は正直な人が通らなくなる=完全性の問題で、嘘は通さない)

    題材アクセス制御 — 3 つの許可リスト
    資格 (role, clearance, region) は各 0〜7。 ROLE_OK = {2, 5, 6},  CLEARANCE_OK = {3, 4, 7},  REGION_OK = {1, 6}
    authorized ⇔ role ∈ ROLE_OK かつ clearance ∈ CLEARANCE_OK かつ region ∈ REGION_OK      (通る組は 3 × 3 × 2 = 18 通り/512 通り)
    作るもの: granted == 1 にできる ⇔ その資格が authorized
    部品 1ビット制約 b·b − b = 0 — 因数分解で読む
    b·b − b = b·(b − 1) = 0  ⇔  b = 0 または b = 1        (中 3 の因数分解: 積が 0 なら、どちらかが 0)
    b = 2 なら 4 − 2 = 2 ≠ 0 → 通らない。 b = 1 なら 0 ✓、b = 0 なら 0 ✓

    宿題ではここ: cs.assert_zero(f * f - f) を f_role, f_clearance, f_region, granted の 4 本。「フラグ」を 0/1 に縛る。

    部品 2メンバーシップ f·(x − 2)(x − 5)(x − 6) = 0 — 「x はリストの中」を式にする
    (x − 2)(x − 5)(x − 6) = 0  ⇔  x ∈ {2, 5, 6}        (因数分解の解。x = 3 なら (1)(−2)(−3) = 6 ≠ 0、x = 5 なら 0)
    前に f を掛ける: f·(x − 2)(x − 5)(x − 6) = 0  ⇔  f = 0 または x ∈ {2, 5, 6}
    → 「f = 1 なら x はリストの中」。f = 0 のときは x に何も言わない(それでよい: granted は f の AND だから)

    宿題ではここ: _membership_product(x, allowlist) が積を作り、cs.assert_zero(f_role * _membership_product(r, ROLE_OK)) など 3 本。この 3 本のどれかを忘れるのが Part B の穴

    部品 3AND は掛け算 — granted = f_role · f_clearance · f_region
    3 つが全部 1 のときだけ積が 1、どれか 0 なら 0。 制約: granted − f_role·f_clearance·f_region = 0
    (Week 2 の Boolean 回路の AND、Week 4 の PLONK の掛け算ゲートと同じもの)

    宿題ではここ: cs.assert_zero(granted - f_role * f_clearance * f_region)。これで build は 8 本の制約(ビット 4 + リンク 3 + AND 1)。

    Part Bアンダー制約を突く — 抜けている 1 本を読む
    tests/challenge.py の制約:  ビット 4 本 ✓、role のリンク ✓、clearance のリンク ✓、region のリンク ✗(無い)、AND ✓
    → f_region は「1 なのに region が {1, 6} の外」という値を取れる。region の値そのものに触れる式が 1 本も無い
    
    attack():  honest_witness(2, 3, 1) から始めて  region = 0,  f_region = 1,  granted = 1 に書き換える
    検算: ビット制約 4 本 → 0 ✓、role リンク f·(2−2)(…) = 0 ✓、clearance リンク f·(3−3)(…) = 0 ✓、AND 1 − 1·1·1 = 0 ✓
          region = 0 は REGION_OK に無いのに granted = 1 → 本来入れない人が通った

    宿題ではここ: attack() は全信号名 → 値の dict を返す。「どの信号に触れている式が無いか」を探すのが解き方。

    採点完全性・構造・健全性・exploit — 4 つは別々に壊れる
    完全性: authorized な 18 通りは全部 granted = 1 で通るか        (制約が多すぎると落ちる)
    構造:   制約の集合が入力値に依存しないか                          (if で制約を出し分けると落ちる)
    健全性: authorized でない 494 通りで granted = 1 にできないか      (solver が攻撃者として探す。制約が足りないと落ちる)
    exploit: attack() が challenge を破っているか

    覚える 1 点: 完全性は「通すべき人を通す」、健全性は「通してはいけない人を通さない」。Week 3・4 でも同じ 2 語が主役。

    やってみる — 紙と鉛筆で。答えは開くまで隠れる

    制約を数値で読む(0〜7 の整数)

    問 1. b·b − b を b = 0, 1, 2, 3 で計算せよ。0 になるのはどれか。
    b=0: 0、b=1: 0、b=2: 2、b=3: 6。0 になるのは 0 と 1 だけ → 「ビット」の制約になっている。
    問 2. (x − 2)(x − 5)(x − 6) を x = 2, 4, 6 で計算せよ。
    x=2: 0·(−3)·(−4) = 0、x=4: 2·(−1)·(−2) = 4、x=6: 4·1·0 = 0。リストの中(2, 6)だけ 0。
    問 3. f·(x − 2)(x − 5)(x − 6) = 0 で、x = 4 のとき f に許される値は? x = 5 のときは?
    x=4: 積が 4 なので f·4 = 0 → f = 0 だけ(F_p では 4 に逆元があるので f = 0 が唯一)。
    x=5: 積が 0 なので f は 0 でも 1 でもよい(ビット制約が別にある)。
    → 「リストの外なら f は 0 に固定、中なら自由」。granted = 1 にするには f = 1 が要るので、外の人は通れない。
    問 4. (f_role, f_clearance, f_region) = (1, 1, 0) のとき granted は? (1, 1, 1) では?
    積なので 0 と 1。3 つ全部 1 のときだけ 1(AND)。
    問 5. 次の 7 本の制約のうち、抜けているのはどれか。「f_role·f_role − f_role, f_c·f_c − f_c, f_g·f_g − f_g, g·g − g, f_role·Π(r − ROLE_OK), f_g·Π(region − REGION_OK), g − f_role·f_c·f_g」
    clearance のリンク f_c·Π(clearance − CLEARANCE_OK) が無い。
    → 攻撃: honest_witness(2, 3, 1) から clearance = 0, f_clearance = 1, granted = 1 に書き換えると全制約が通る。
    問 6. 「制約の集合が入力値に依存してはいけない」のはなぜか。
    制約は verifier が事前に固定して持つ「型」。prover の入力に合わせて式を出し分けられるなら、prover が都合のよい式を選べてしまう。
    Week 4 の PLONK で「セレクター(回路の型)は公開、witness は秘密」と分けたのと同じ線引き。

    宿題の 2 関数

    宿題の関数 ↔ 部品 ↔ 学校で習ったこと

    関数 / 部品スライド・README学校で習ったこと小さい数の例落ちやすい所
    build: ビット制約README「制約」因数分解 b(b−1) = 0(中 3)b=2 → 2 ≠ 04 本(granted も)
    build: メンバーシップREADME「考え方」因数分解の解 (x−a)(x−b)… = 0(中 3)x=4 → 4、x=5 → 03 本のうち 1 本忘れる
    build: ANDスライド 28 の × ゲート掛け算 = 全部 1 のときだけ 11·1·0 = 0
    build: 構造採点 2if で制約を出し分けない
    attackREADME Part B「式が触れていない変数は自由」region = 0, f_region = 1honest_witness から書き換える

    提出前の自己チェック — 5 問、自分の言葉で

    (1) 「回路」とは何か、1 文で。 (2) b·b − b = 0 がビットを表す理由。 (3) f·(x−2)(x−5)(x−6) = 0 で f = 0 のとき x に何も言えない理由と、それで困らない理由。 (4) 制約が 1 本抜けると何が起きるか、具体的な witness で。 (5) 完全性と健全性の違いを、この課題の採点で言うと。

    用語集

    用語の意味・なぜその言葉か・宿題での実物

    用語意味なぜその言葉か宿題では
    Programmable CryptographyZK / MPC / FHE などの総称。「何を計算するか」をプログラムのように書ける暗号0xPARC の造語。programmable = 書ける
    回路(circuit)+ と × のゲートとワイヤで書いた計算。= 「0 になるべき式の束」電気回路の比喩(ゲート・ワイヤ)。ZK / MPC / FHE 共通の中間表現build が組むもの
    信号(signal)/ワイヤ回路の変数。値は F_p の元回路の線に乗る値cs.input / cs.aux
    制約(constraint)「この式は 0」という条件constrain = 縛る。中学の方程式を移項したものcs.assert_zero
    witness全信号への値の割当。全制約を満たせば「証明できた」法廷の「証人」。見せれば真だと分かる値attack() が返す dict
    aux(補助信号)入力でも出力でもない中間の信号(フラグなど)auxiliary = 補助のf_role など
    完全性(completeness)正しい入力は必ず通るcomplete = 取りこぼしがない採点 1
    健全性(soundness)正しくない入力は通らないsound = 確かな採点 3(solver)
    アンダー制約(under-constrained)縛る式が足りず、嘘の値が通る状態under = 不足challenge.py の穴
    メンバーシップ「x はリストの中」の制約membership = 所属f·Π(x − a) = 0
    Proof-of-Exploit「この手順で壊れる」ことを、手順を見せずに証明exploit = 脆弱性を突く手順課題名
    circuit breaker異常を検知したら自動で止める仕組み電気のブレーカーKelpDAO の例
    ZK / MPC / FHE見せずに証明/集めずに計算/暗号化したまま計算Zero-Knowledge / Multi-Party Computation / Fully Homomorphic EncryptionWeek 3・2・5
    zkVMプログラムの実行そのものを ZK で証明する仮想マシンRISC Zero、SP1 などスライド 35
    DSL回路を書くための小さな専用言語Domain-Specific Language。circom / noir など。課題の aclib はトイ DSLtests/aclib.py
    F_p(有限体)素数 p で割った余りの世界。信号の値の住処土台編信号の値

    ラボ

    実際に壊してみる

    左のパネルで prover の主張(witness)を組み立て、右の台帳で 8 本の式が 0 になるかを見る。 各行のスイッチを切ると、その 1 本を書き忘れた回路になり、 下の採点器が「その回路なら何通りの不正資格が通るか」をブラウザ内で総当たりして数える。

    prover の主張(witness)

    7 signals
    role許可 2, 5, 6
    clearance許可 3, 4, 7
    region許可 1, 6
    補助信号(フラグと出力)bit
    この witness は

    採点器(悪意ある prover 役)

    brute force

    いま有効な制約だけを使い、権限のない 494 通りの資格それぞれについて 「フラグをどう置けば granted = 1 のまま全制約を満たせるか」を総当たりする。 1 つでも見つかれば、その回路は嘘の証明を受理する。

    完全性 completeness 18 / 18 正当な資格が通る数(多いほどよい)
    健全性 soundness 通ってしまう不正資格の数(0 でなければ欠陥)

    この探索器はフラグを 0/1 に限って総当たりする簡易版。本物の tests/solver.py は ビットを場合分けしつつ、残りを F_p 上の連立一次方程式として解く。 結論は今回の回路では一致することを確認済み。

    実験 1

    1 本の書き忘れを再現する

    1. 「正直な資格」を押して、全部 0 が揃う状態を見る
    2. #6(region のリンク)のスイッチを切る
    3. region を 0 に、f_region1 のままにする

    起きること: 全制約を満たしたまま granted = 1。 これが tests/challenge.py の状態そのもので、Part B の答えでもある。

    実験 2

    被害の大きさを比べる

    1. #4(role のリンク)だけを切り、採点器の数字を見る → 30
    2. 戻して #6(region)だけを切る → 54
    3. 戻して #7(AND)だけを切る → 494

    読み取ること: 穴の大きさは「その 1 本が守っていた自由度」で決まる。 AND を落とすと 3 フラグ全部が無意味になるため、全滅する。

    実験 3

    ビット制約を切っても壊れないことを確かめる

    1. #0〜#3(ビット制約)を好きに切ってみる
    2. 採点器の健全性が 0 のまま動かないことを確認する

    ただし結論を急がない: 「反例が出ない」と「安全である」は別のこと。 理由は次の節で。

    理解度チェック

    制約検問 — あなたが verifier になる

    読んだだけでは定着しない。ここではあなたが検証者の席に座り、 提出された witness と回路を審査する。問題は毎回その場で生成され、 判定は上のラボとまったく同じ計算器が行う。3 回誤判定したら、その回は終了。

    検問モードを選ぶ

    10 問・ライフ 3

    誤答するとその場で解説が出る。分野を絞って弱点だけ潰すこともできる。

    この先

    ここで掴んだものが、そのまま次に効く

    今日の 7 信号・8 制約は、R1CS の一歩手前の形。 中間信号を切って次数 2 に割れば、そのまま Circom が吐く R1CS になる (今回のメンバーシップリンクは次数 4、AND は次数 3 なので、まだ R1CS そのものではない)。 aclib.py が使う素数 p は BN254 の scalar field で、実際の Groth16 / PLONK でそのまま出てくる数。 cs.input / cs.aux / cs.assert_zero は、Circom の signal input / signal / === にほぼ対応する (Circom の === は次数 2 までしか受け付けない点だけ違う)。

    ZK・MPC・FHE はどれも「計算を算術回路に翻訳してから暗号をかける」構造を共有している。 だから 回路が間違っていれば、その上の暗号がどれだけ正しくても意味がない。 実際、ZK プロジェクトの脆弱性報告の多くは暗号の破れではなく、今日と同じアンダー制約だ。

    持ち帰る問いは 1 つ。「この信号を縛っている式は、どれか」。 答えられない信号が 1 本でもあれば、そこが次の穴になる。

    付録

    深掘り本文 — 各節を開いて読む(旧版の本文)

    上のカード列で骨組みが入ったあとで、細部を足したいときに開いてください。なぜ + と × だけなのか、なぜ回路にするとプライバシーが守れるのか、leave-one-out で「どの制約が効いていたか」の実測、Circom / Noir での同じバグ、はここにあります。

    このプログラムは、何を目指しているのか(旧・全体像)

    全体像

    このプログラムは、何を目指しているのか

    Week 1 だけ見ると「なぜ 8 本の式をいじっているのか」が分からない。先に地図を置く。

    プログラムの目的

    ゼロ知識証明の理論と実装の理解を促し、Ethereum エコシステムや実社会に コミットする技術者を養成する。

    ゴールは知識ではなく成果物。成果物テーマ案として、実際の企業が出した課題 (Intmax・Nyx Foundation・SMBC日興証券・キリフダ ほか)が並んでいる。 MPC ウォレットの鍵復元、プライバシー決済の匿名性、規制産業での機密 LLM、 AI エージェントの行動証跡——どれも「見せずに正しさを示す」という同じ形をしている。

    各週に共通する構造

    このプログラムには一貫した型がある。自分で作り、自分で壊す。 Week 1 は回路を組んでその穴を突く。Week 3 は Schnorr 署名を作って nonce の使い回しから鍵を抜く。 Week 6 は zkVM の中で動く exploit を書く。

    攻撃側に立たないと、何を守っていたのかが分からない——という思想で組まれている。 だから Week 1 の課題名は proof-of-exploit(脆弱性の証明)になっている。

    週の地図

    講義(スライドの 7 週間カリキュラム)課題(リポジトリ)
    0有限体と楕円曲線(事前学習スライド)
    1Programmable Cryptography 概要proof-of-exploit ← 今ここ
    2MPC 秘密計算未公開
    3ZKP・SNARK / STARKschnorr-from-scratch
    4FHE・格子暗号 / LWE未公開
    5Advanced(vFHE / zkVM)tfhe-toy-python
    6–7プロジェクト/Demo Dayco-snark-prove/zkvm-exploit

    毎週土曜に講義 2 時間 + ホワイトボードセッション 2.5 時間、日〜木で自習・実装、金曜に課題提出という周期。 Week 3 の課題は有限体 → 楕円曲線 → シグマプロトコル → Fiat-Shamir と積み上げる構成で、 講義の「ZKP・SNARK / STARK」の土台にあたる。 Week 5 だけ講義(vFHE / zkVM)と課題(TFHE)がずれて見えるので、 進行に合わせて確認する。

    Week 1 が最初に来る理由

    ZK も MPC も FHE も、計算を「算術回路」に翻訳してから暗号をかける。回路は 3 つの共通言語。

    つまり 回路が間違っていれば、その上にどんな暗号を積んでも意味がない。 Week 1 が暗号を一切使わず、Python の小さなライブラリだけで回路を扱うのは、そこが土台だから。

    そして今週学ぶ「制約の書き忘れ(アンダー制約)」は、実務でそのまま使える。 成果物テーマにも Proof-of-Exploit による DeFi 自動停止ZK/FHE/MPC 回路のバグ発見形式証明された ZK 回路の実装が並んでいる。 Week 1 でやることは、その入口そのもの。

    このページの構成

      なぜ、わざわざ「回路」で書くのか

      出発点

      なぜ、わざわざ「回路」で書くのか

      ここが腑に落ちていないと、以降の制約がただの奇妙な数式に見えてしまう。 「そもそも何がしたくて、なぜ普通のコードでは駄目なのか」から順に片付ける。

      やりたいこと

      今回の題材で言えば、こういう主張を通したい。

      「私はこのシステムにアクセスする権限を持っている。
      ただし、自分の役職も、クリアランスも、所属地域も一切明かさない。」

      年齢確認なら「18 歳以上である。ただし生年月日は見せない」。 送金なら「残高は足りている。ただし残高は見せない」。 結論だけを、根拠を隠したまま、相手に確信させたい——これが共通の願い。

      素朴な方法は、どれも壊れる

      やること何が起きるか
      ① 自己申告「権限あります」と送る嘘がつける。検証が存在しない
      ② 資格を全部見せるrole/clearance/region を送る通るが、隠したかった情報が全部漏れる
      ③ サーバに預けて判定してもらう暗号化して送り、向こうで復号サーバを全面的に信用することになる。漏洩・内部犯行に無防備
      ④ 信頼できる第三者に証明書を出させる「権限あり」と署名してもらうその第三者は全員の資格を知っている。信頼の一点集中が残る

      欲しいのは「本人にしか作れず、中身を明かさず、相手が数学的に確認できる証拠」。 誰かを信用する話に落とさずに、これを作る道具が ZK(ゼロ知識証明)であり、 同じ土台を共有するのが MPC(複数人で分担計算)と FHE(暗号化したまま計算)。

      ところが、暗号が貸してくれる操作は 2 つしかない

      1. 使えるのは「足し算」と「掛け算」だけ

        FHE なら暗号文のまま E(a) + E(b) = E(a+b)E(a) × E(b) = E(a×b) ができる。 MPC なら秘密分散されたシェアどうしで加算・乗算ができる。 ZK の証明系が扱うのは多項式で、多項式を作る操作もやはり加算と乗算。

        共通点: どの技術も「+ と ×」だけを提供する。 比較・分岐・ループ・配列アクセスといった、普段プログラムを書くときの道具は1 つも入っていない

      2. だから計算を「+ と × だけの式」に翻訳する

        この翻訳結果が算術回路。プログラムを、加算と乗算だけで組み立てた 巨大な式の集まりに書き直したものだ。

        なぜ「回路」と呼ぶのか: 論理回路が AND/OR/NOT ゲートの組み合わせであるように、 こちらは加算ゲートと乗算ゲートの組み合わせだから。ただし電気は流れないし、 実行順序という概念もない。全部の条件が同時に成り立っているかどうかだけが問われる。

      3. 分岐は「両方計算して混ぜる」で置き換える

        if s: out = a else: out = b は、回路では out = s·a + (1−s)·b と書く(s は 0 か 1)。 どちらの枝も必ず計算され、選択子で片方を消す

        なぜこうなるのか: ① 値が見えないので、どちらに進むか判断できない。 ② かりに判断できても、どちらに進んだかという事実自体が秘密を漏らす。 実行時間や回路の形が入力によって変われば、そこから中身が推測できてしまう。 だから回路は常に全経路を計算する。後の「構造を入力値に依存させるな」という要求は、ここに根がある。

      4. 判定はすべて「= 0」の形に揃える

        verifier ができるのは、突き詰めれば「この等式は成り立っているか」の確認だけ。 だから「x はリストに入っている」「フラグは 0 か 1」といった条件を、 全部「ある式の値が 0」という同じ形に書き直す。

        揃えると何が嬉しいか: 形が同じなら、何千本もの条件をまとめて 1 本の多項式に畳み込み、 ランダムな 1 点で評価するだけで全部を一括検査できる。 ZK の効率はこの「形の統一」に支えられている。 true/false という型が存在しないのは、手抜きではなく設計。

      そして最大の転換 — 回路は計算しない。答え合わせをする

      普通のプログラムは、入力から答えを計算して返す。 回路は違う。prover が答えを先に主張し、回路はその主張が嘘でないことだけを確かめる

      分かりやすい例が割り算だ。回路に除算ゲートは無い。ではどうするか—— prover に「1/x の答えは y です」と言わせる。 回路が書く制約はたった 1 本、x · y − 1 = 0。 これで y が本当に逆数であることが保証される。 難しい計算は prover にやらせ、verifier は掛け算 1 回で検算する。

      今回のフラグもまったく同じ発想でできている。 「role が許可リストに入っているか」を回路が計算するのではない。 prover が f_role = 1主張し、 回路は「その主張が嘘なら 0 にならない式」を 1 本置くだけ。

      同じ判定を、2 つの世界で書くと

      # 普通のプログラム — 計算して決める
      if role in {2, 5, 6}:
          f_role = 1
      else:
          f_role = 0
      
      # 回路 — prover が主張し、嘘を禁じる
      #   f_role の値は prover が勝手に決めてよい
      #   回路が言えるのは「1 と言うなら嘘は許さない」だけ
      f_role * f_role - f_role        == 0
      f_role * (role-2)*(role-5)*(role-6) == 0

      上は手続き、下は条件。下には「f_role を 1 にせよ」と命じる行がどこにも無いことに注目。 値を入れるのは prover の自由で、回路にできるのは禁止だけ

      ここから、回路に固有のバグが生まれる

      主張ベースの設計なので、「その主張が嘘でないことの確認」を書き忘れると、嘘がそのまま通る。

      普通のプログラムなら、計算を書き忘れれば動かない。すぐバグとして表に出る。 しかし回路では、確認を書き忘れても正直に使っている限り完璧に正常動作する。 テストも通る。壊れていることに誰も気づかない。

      これがアンダー制約で、Week 1 の主題であり、 現実の ZK プロジェクトで報告される脆弱性の最頻出パターンでもある。 以降のページは全部、この 1 種類のバグを見る目を作るためにある。

      なぜ、この課題が proof-of-exploit という名前なのか

      動機

      なぜ、この課題が proof-of-exploit という名前なのか

      Week 1 の講義は、ある事件を題材にしています。課題名はそこから来ていて、 今週書くコードは講義の主題そのものです。

      題材 — ブリッジが壊された

      講義では KelpDAO のブリッジが攻撃された事件(想定シナリオ)を扱います。 この種の攻撃で厄介なのは、発見から実行までが分単位だという点です。 人間のガバナンス投票では間に合いません。

      では自動で止めればいいかというと、そう簡単でもない。 誤検知で勝手に止められるリスクがあるからです。 「危ないと思ったので止めました」を無条件に信じる仕組みは、それ自体が攻撃対象になります。

      対策 — 証拠を出させて、自動で遮断する

      講義が示す答えが Proof-of-Exploit × Circuit-Breaker です。

      off-chain の AI Auditor が攻撃手順を見つけたら、その手順を誰にも見せないまま 「この手順でコントラクトが壊れる」ことを証明する。on-chain の検証が通ったときだけ、自動で止める。

      止める権限を、人の判断ではなく検証可能な証拠に紐づける。 これがこの週のいちばん大きな考え方です。

      仕組みを 1 枚で

      攻撃手順 W を発見(AI Auditor / off-chain)
              │
              │  W は見せない
              ▼
      「W を入れるとこの契約は壊れる」
        という証明だけを作る
              │
              ▼
      on-chain のコントラクトが検証
              │
              ├─ 通った → 自動で遮断
              └─ 通らない → 何も起きない

      W を公開してしまうと、それを見た誰かが先に攻撃できます。 だから隠したまま、壊れることだけを示す必要がある。 ゼロ知識証明がここで効く理由がこれです。

      今週の課題との対応

      Part B で書く attack() は、この「W」を素手で作る練習。

      回路の穴を突く witness を見つけ、それが本当に全制約を満たすことを示す—— スケールは小さいですが、やっていることは Proof-of-Exploit と同じ形です。 そして Part A は、そもそも穴を作らない側の練習になります。

      成果物テーマにも「AI エージェント × Proof-of-Exploit による DeFi 自動停止」と 「SPECA を使った ZK / FHE / MPC 回路のバグ発見」が並んでいます。 今週の 8 本の式は、その入口ではなく縮小版です。

      なぜ「回路」にするとプライバシーが守れるのか

      核心

      なぜ「回路」にするとプライバシーが守れるのか

      ここが一番よく分からないところ。先に結論から言うと——

      先に、今週作るものを 1 分で

      この節の話をするために、完成形を先に見せます。詳しくは後の節でやるので、形だけ眺めてください

      作るのは信号が 7 本、「0 になるべき式」が 8 本の小さな回路です。 資格 3 本(role / clearance / region)、フラグ 3 本、出力 granted が 1 本。

      検証の手続きはこれだけ —— 7 本すべてに値を入れて、8 本の式が全部 0 になるか確かめる。 この「7 本の値の組」を witness と呼びます(用語は次の次の節でまとめます)。

      # 信号 7 本
      role  clearance  region        ← 資格
      f_role  f_clearance  f_region  ← フラグ
      granted                        ← 出力
      
      # 0 になるべき式 8 本(抜粋)
      f_role² − f_role                        = 0
      f_role · (role−2)(role−5)(role−6)       = 0
      granted − f_role·f_clearance·f_region   = 0

      そのうえで、誤解を外す

      回路そのものは、プライバシーを守らない。

      いま見たとおり、この検証は 7 本の値をまるごと検証側に渡しています。 role も clearance も丸見えで、隠れているものは何一つありません。 「回路を書いた=秘密が守られた」ではない、ということです。

      プライバシーを作るのは、その上に乗る暗号のほう。 ただしその暗号は「等式が成り立つかを確かめる」ことしかできない。 だから回路の役割は、確かめたいことを全部「等式」に翻訳しておくこと—— 暗号をかけられる形に、計算をあらかじめ整えておく係だ。

      では、その暗号は何をしているのか

      1. 値に「印」をつける。印から値は戻せない

        まず、値ごとにを作る道具があるとします。ルールは 2 つだけ。

        ① 印を見ても、元の値は分からない。 ② 一度つけた印に合う値は、あとから変えられない。

        両方そろって初めて意味があります: ①だけなら、あとから「実は違う値でした」と嘘をつけます。 ②だけなら、中身が丸見えです。「隠したまま、逃げられなくする」のがこの道具の役割。

      2. 印どうしを組み合わせると、足し算した値の印になる

        いちばん簡単な作り方は「2 を値の回数だけ掛ける」です。実際に数字でやってみます。

        3 の印 = 2³ = 8  4 の印 = 2⁴ = 16 8 × 16 = 128 = 2⁷ = 7 の印

        印を掛けただけなのに、中身は足し算されました(3 + 4 = 7)。 指数法則 2³ × 2⁴ = 2³⁺⁴ がそのまま効いているだけで、 誰かが「足し算機能」を作ったわけではありません。

        「8 を見たら 2³ だと分かるのでは」——小さい数ならそのとおりです。 だから本物は巨大な素数の世界でやります。そこでは印から値を逆算するのが現実的に不可能で、 これが Week 0 で出てきた離散対数問題です。

      3. だから「値を読む」代わりに「印を比べる」で済む

        誰かが「x + y = z です」と主張したとします。確かめる側がやることは——

        x の印 × y の印 と z の印 が一致するかを見るだけ

        一致すれば主張は正しい。しなければ嘘。x も y も z も、値は 1 つも分かっていません。

        ここがプライバシーの正体です。 検証という行為が「値を読んで判定する」から 「印を比べる」に変わりました。読んでいないので、漏れようがない。 そして比べられるのは「同じか違うか」だけで、「どちらが大きいか」は比べられません。 だから、確かめたいことを全部等式に書き直しておく必要があります。

      4. 確かめられるのは「等式」だけ。だから全部 = 0 に直す

        箱の外から言えるのは「この箱とこの箱は同じ中身か」まで。 「x のほうが大きいか」は言えない(言えたら箱が壊れている——次の節で説明する)。

        だから回路が要る: 「権限がある」という言いたいことを、 足し算と掛け算と等号だけで書き直す。その翻訳結果が算術回路。 回路は暗号を「かけられる形」を用意する工程であって、隠す仕事はしていない。

      5. ここで問題。8 本を 1 本ずつ確かめるのは大変すぎる

        今週の回路は式が 8 本なので全部見ればいい。でも本物の回路は制約が 100 万本を超えます。 1 本ずつ確かめていたら、検証が計算そのものと同じくらい重くなってしまう。

        欲しいのは: 100 万本まとめて、ほんの数回の計算で「全部 0 だ」と確かめる方法。 これができないと、そもそも実用になりません。

      6. 解決 — 8 本を 1 本の式に畳む

        8 本の制約に値を入れた結果を c₀, c₁, …, c₇ と呼びます。 全部 0 であってほしい数です。ここで検証者がでたらめな数 r を 1 つ選び、次の式を作ります。

        S = c₀ + c₁·r + c₂·r² + c₃·r³ + … + c₇·r⁷

        順に読みます。 c₀ はそのまま、c₁ には r を 1 回、c₂ には r を 2 回……と掛けて、全部足す。それだけです。

        正直な場合: c₀ から c₇ まで全部 0 なら、どの項も「0 × 何か」なので 0。 足しても S = 0 です。必ず 0 になります。

        嘘の場合: たとえば c₆ だけ 0 でないとします。すると S は c₆·r⁶ の項が残るので、r の式として 0 ではありません

        ここが肝心: 0 でない式でも、特定の r を入れれば偶然 0 になることはあります。 しかし r⁷ までしか出てこない式が 0 になる r は、多くても 7 個しかありません (2 次方程式の解が最大 2 個、3 次なら 3 個、と同じ話)。 一方 r の候補は p ≈ 2²⁵⁴ 個あります。
        偶然 0 を引く確率 ≈ 7 ÷ 2²⁵⁴ —— 事実上ゼロです。

      7. だから「1 個の数を見るだけ」で全部を検査できる

        検証者がやることは S を 1 個受け取って S = 0 かを見るだけ。 制約が 8 本でも 100 万本でも変わりません。

        そして S を渡すときに前の節の「印」を使えば、witness そのものは見せずに済みます。 しかも S には乱数が混ぜてあるので、S から元の値をたどることもできません。

        これで 2 つが同時に成立します。
        隠せる —— 渡すのは印と 1 個の数だけで、witness は渡さない。
        騙せない —— 制約が 1 本でも破れていれば、r をどう引いても S はほぼ確実に 0 にならない。

      8. この仕組みの総称が SNARK

        頭文字を 1 つずつ開くと、そのまま性質の説明になっています。

        Succinct / Non-interactive / ARgument of Knowledge

        Succinct(簡潔) = 回路が巨大でも証明は数百バイトで済む。いま見た「1 個の数に畳む」がこれ。
        Non-interactive(非対話) = 証明を 1 回送れば終わり。何度もやりとりする必要がない。
        Argument of Knowledge(知識の主張) = 「答えを知っている」ことの主張である。

        Groth16 や PLONK は、この SNARK の具体的な作り方の名前です。 Week 1 では一切使いませんが、そこへ渡すために回路を用意しているという関係になります。

      担当今週やること
      算術回路言いたいことを等式に翻訳するこれ(暗号は使わない)
      コミットメント値を隠しつつ、後から変えられなくする
      証明系全制約をまとめて 1 点で検査する

      下の 2 層があってもいちばん上が間違っていれば、「嘘の主張に対する完璧な証明」ができあがる。 Week 1 が土台と呼ばれるのはそういう意味。

      ZK・MPC・FHE は、それぞれ違う「信用できなさ」を解いている

      三兄弟

      ZK・MPC・FHE は、それぞれ違う「信用できなさ」を解いている

      いつも 3 つ並べて語られるので混ざりやすい。だが解いている問題はまったく別物で、 共通しているのは道具立てのほうだけ。まず言葉の意味から。

      ZK / zero-knowledge proof
      秘密を渡さずに、条件を満たしていることだけを相手に納得させる技術。 「ゼロ知識」は相手に知識を一切与えないという意味で、 「証明」はそれでも嘘はつけないという意味。この 2 つが同時に立つのが肝。
      例: 身分証を見せずに「20 歳以上」だけを示す。残高を見せずに「足りている」だけを示す。
      今回なら: 資格 (2, 3, 1) を明かさずに「granted = 1 にできる」ことだけを示す。
      MPC / multi-party computation
      複数人が秘密を持ち寄り、誰も他人の値を知らないまま、計算結果だけを全員で得る技術。 値を意味のない断片(シェア)に割って配り、断片のまま計算する。 断片を 1 つ見ても何も分からないが、規定の人数分そろえば元に戻る。
      例: 3 人が年収を明かさずに、平均年収だけを知る。
      今回なら: 人事部が role、セキュリティ部が clearance、拠点管理が region を持ち、 互いの値を知らないまま granted を出す。
      FHE / fully homomorphic encryption
      暗号化したまま計算できる暗号。 「準同型」は構造を保つという意味で、暗号文どうしを操作すると 中身にも同じ計算が反映される。「完全」は何回でも、どんな組み合わせでもできるということ。 計算する側は最後まで中身を知らない。
      例: 暗号化した医療データをクラウドに解析させる。クラウドは何も学ばない。
      今回なら: 暗号化した資格を送り、暗号のまま判定した暗号化 granted を受け取る。
      ZK ゼロ知識証明MPC 秘密計算FHE 準同型暗号
      解く問題本当に正しく計算したか自分の値を他人に見せたくない計算は任せたいが中身は渡したくない
      秘密を持つ人1 人(prover)参加者それぞれ依頼者 1 人
      計算する人prover 自身参加者全員で分担受託者(クラウド)
      相手に渡るもの証明(SNARK 系なら数百バイト)計算結果だけ暗号化された結果
      相手が学ぶこと主張が正しいという事実のみ結果のみ(他人の入力は不明)何も学ばない
      信頼前提prover を信頼しなくてよい(数学が保証)n 人中 k 人を信頼(不正が t 人未満なら安全)利用者の鍵管理が信頼の起点
      敵対モデルprover の偽証/verifier による witness 窃取参加者の逸脱(最大 t 名・準正直/悪意)盗聴者/サーバ(計算の正しさは別途 = vFHE)
      典型例残高を見せずに送金の正当性を示す各社の給与を明かさず平均だけ出す暗号化した医療データをクラウドで解析

      今回のアクセス制御回路を、3 方式で動かすと

      ZK で使うと

      本人が証明を作る

      資格を持っているのは本人。granted = 1 を満たす witness を知っていることの証明を作り、 サーバに送る。サーバは role も clearance も region も永遠に知らないまま、 アクセスを許可する。

      MPC で使うと

      3 部署が分担して判定する

      人事部が role、セキュリティ部が clearance、拠点管理が region を持つ。 誰も他部署の値を知らないまま、3 者で判定を計算して granted を得る。 主張する人がいないので、証明は登場しない

      FHE で使うと

      クラウドが暗号のまま判定する

      資格を暗号化してクラウドに送る。クラウドは暗号文のまま判定を計算し、 暗号化された granted を返す。復号できるのは依頼者だけ。 ここにも証明は無い

      選ぶときの軸 — 「誰を信頼しないで済むか」

      3 方式の本当の違いは、隠し方ではなく信頼の置きどころ

      ZK は相手を信頼しなくていい(嘘は数学が弾く)。 MPC は全員は信頼しなくていいが、一定数は信頼する(t 名未満の逸脱なら安全)。 FHE は計算する相手にデータの機密は預けないが、鍵の管理は自分の責任で、 しかも計算が正しく行われた保証は別物(そこを埋めるのが vFHE)。

      要件が「相手が嘘をつくかもしれない」なら ZK、「複数人の秘密を突き合わせたい」なら MPC、 「計算を外に出したい」なら FHE。隠したいものではなく、疑いたい相手から決まる。

      そして 3 つは、真ん中で同じものを使う

      ① SOURCE — 用途ごとに書く
        ZK  : 年齢証明     age_check(dob, today)
        MPC : 秘密入札     auction(b1, b2, b3)
        FHE : AI 診断      diagnose(data)
              │
              │ compile(circom / halo2 / noir / arkworks)
              ▼
      ② CIRCUIT — 共通の中間表現
              y = (a × b) + c
              gates と wires だけの低レベル表現
              │
              ▼
      ③ EXECUTABLE — backend が裏で動く
        ZK prover / MPC protocol / FHE eval

      上と下は用途ごとに違いますが、真ん中の「回路」は共通です。 書く言語も、最後に走る暗号も入れ替わるのに、間にある表現だけは同じ。

      Week 1 でやっているのは、この真ん中の層を素手で組む練習です。 暗号を一切使わないのは手抜きではなく、そこが 3 方式の交差点だから。

      そして交差点が間違っていれば、上に何を載せても間違ったまま動きます。

      だから、回路は ZK 固有の産物ではない

      3 方式とも、同じ判定ロジック(メンバーシップと AND)を算術回路にする。

      ただし組み方の形が違う。ZK は検証の形—— フラグは prover が主張し、回路は「その主張が嘘なら 0 にならない式」を置くだけ(これが今週の 8 本)。 MPC と FHE は計算の形——主張してくれる人がいないので、 フラグ自体を「メンバーシップ積が 0 か」から計算しなければならない。

      それでも「加算と乗算だけの式に翻訳する」という土台は完全に共通で、 そこだけを扱う Week 1 が「暗号を一切使わない」のはそういう理由。

      この課題で使う 3 つの言葉

      語彙

      この課題で使う 3 つの言葉

      signal
      回路に出てくる変数。値は有限体 F_p の元。今回は 7 本(資格 3 本 + フラグ 3 本 + 出力 1 本)。
      constraint
      「この式は 0」という条件。今回は 8 本。回路の安全性は、この 8 本で全部決まる。
      witness
      7 本すべてに値を入れた割当。prover が「これで全制約を 0 にできる」と主張して差し出すもの。

      検証の手続き — たった 2 行

      ① prover が witness を出す ② verifier が 8 本の式に代入して、全部 0 かを見る

      本物の ZK では ② を「witness を見ずに」やるための暗号がかぶさるが、 判定内容そのものは何も変わらない。だから回路が間違っていれば、上にどんな暗号を載せても無駄。 Week 1 が暗号抜きで回路だけを扱うのは、そこが土台だから。

      有限体 F_p について、必要な分だけ

      前提

      有限体 F_p について、必要な分だけ

      Week 0 のスライド「有限体と楕円曲線」で通った道。今回使うのは、そのうち 3 つの性質だけ。

      事前学習(Week 0)との接続

      Week 0 で出てきたもの今週どこで効くか
      群 ⊃ 環 ⊃ 体 の包含関係
      「環 = 加法と乗法」「体 = 環 + 乗法逆元」
      「+ と × しかない」の根拠そのもの。環の定義に演算が 2 つしか無いことが、この講座全体の制約になる
      mod 演算・時計の比喩(Z₁₃)p 回足すと 0 に戻る = 順序が存在しない理由。だから比較が書けない
      加法逆元・乗法逆元引き算・割り算ができる根拠。採点器 solver.pypow(a, P−2, P)(フェルマーの小定理)で逆元を取っている
      位数(要素の個数)今週の p は BN254 の scalar field、位数は約 2²⁵⁴
      離散対数問題コミットメント g^x を開けられない根拠。前の節の「箱」が箱でいられるのはこれのおかげ
      有限体上の楕円曲線/演習の拡張ユークリッド法Week 3 の schnorr-from-scratch でそのまま実装する

      Week 0 が「数学の準備」に見えて実は伏線だった、という構造。 とくに群・環・体の図は、今週の「なぜ + と × だけなのか」への直接の答えになっている。

      1. 巨大な素数 p で割った余りの世界

        p = 21888242871839275222246405745257275088548364400416034343698204186575808495617(BN254 の scalar field)。 足し算も掛け算も、やったあとに p で割った余りを取る。数がいくら大きくなっても、この 1 本の帯の中に収まる。

        なぜ余りの世界なのか: 暗号は「決まった大きさの数」を扱う必要があるから。 実数のように無限に伸びる値は、暗号の道具に載せられない。

      2. 足す・引く・掛ける・割るが全部できる(=体)

        p が素数なので、0 以外のどの元にも逆数がある。負の数も普通に扱える (−6p − 6 と同じもの)。

        実務上の注意: 一方で「大小比較」と「範囲」は自然には存在しない。 x < 8 のような条件は、それ専用の制約(range proof)を書かないと表現できない。 今回は許可リストが値を 2〜3 個に固定するので、範囲の心配が要らない設計になっている。

      3. 零因子がない — これが今回の主役。そして証明できる

        主張はこれです。a · b = 0 なら、a = 0b = 0 の どちらかが必ず成り立つ。「どちらも 0 でないのに掛けたら 0」は起きません。

        これは「整数と同じ感覚だから」で済ませず、さっきの ②(逆数がある)から 4 行で出ます。 覚えるものではなく、導けるものです。

        仮定: a · b = 0 で、しかも a ≠ 0 だとする

        ① a ≠ 0 なので、逆数 a⁻¹ が存在する (体の定義そのもの)
        ② 両辺に a⁻¹ を掛ける  a⁻¹ · (a · b) = a⁻¹ · 0
        ③ 左辺を組み替える   (a⁻¹ · a) · b = 0  → 1 · b = 0
        ④ よって        b = 0

        つまり 「a ≠ 0 なら、b は 0 でしかありえない」。 対偶を取れば「a·b = 0 なら a = 0 または b = 0」で、主張のとおりです。

        どこで「体であること」を使ったか: ① だけです。 0 以外に逆数がある、という一点から零因子の不在が出ます。 逆に言えば、逆数が無い世界ではこれは成り立ちません (例: 時計算 mod 12 では 3 × 4 = 12 ≡ 0。3 も 4 も 0 でないのに積が 0 になる。 12 は素数でないので体ではなく、3 に逆数が無いのが原因です)。 p が素数でなければならない理由が、ここに出ています。

        そして今回の回路がこの上に乗ります: f · (何かの積) = 0 と書いておけば、 f ≠ 0 と分かった瞬間、いま証明したことから積の側が 0 だと断言できる。 許可リストの強制も、ビット制約も、まるごとこの 4 行の帰結です。

      なぜ「足し算と掛け算」だけなのか

      原理

      なぜ「足し算と掛け算」だけなのか

      制限があるから 2 つに絞っている、のではない。2 つしか存在しないから 2 つ、という話。

      その前に —— 準同型とは何か、身近な例で

      準同型とは「先に計算してから変換」と「変換してから計算」が一致する変換のこと。

      f(a ○ b) = f(a) ● f(b)

      左辺は「a と b を先に計算してから f で変換」、右辺は「a と b をそれぞれ f で変換してから計算」。 この 2 つが必ず同じ答えになるとき、f を準同型と呼びます。それだけです。

      例 1 — 対数(log)

      log(a × b) = log(a) + log(b)

      掛け算の世界を、足し算の世界に移しています。 昔の計算尺や対数表が便利だったのは、これのおかげでした。2.34 × 5.67 を筆算する代わりに、 log を引いて足し算して戻せばいい。難しい演算を、簡単な演算に移せる —— これが準同型の「だから何」の第一の答えです。

      例 2 — 偶数か奇数か

      3(奇)+ 5(奇)= 8(偶)  奇 + 奇 = 偶

      整数を「偶か奇か」に潰す変換も準同型です。 元の数を知らなくても、偶奇さえ分かれば和の偶奇が分かる。 情報をほとんど捨てているのに、足し算の構造だけは生き残っています。 これが「隠しても計算できる」の原型です。

      例 3 — 時計(mod 12)

      7 時 + 8 時間 = 15 → 3 時

      整数の足し算が、時計の文字盤の足し算にそのまま対応します。Week 0 の mod 演算がこれでした。

      では暗号は何をしているか

      2³ × 2⁴ = 2³⁺⁴ = 2⁷

      「2 を x 回掛ける」という変換 x ↦ 2ˣ は、log とちょうど逆向きの準同型です。 log が掛け算を足し算に移すのに対して、こちらは足し算を掛け算に移す。 だから前の節で 8 × 16 = 128 と印を掛けただけで、中身が 3 + 4 = 7 になりました。

      暗号がやっているのは、この性質を一つ借りているだけです。 新しい機能を発明したのではなく、もともとある構造をそのまま壁の向こうへ持ち越している

      そして「比較が運べない」理由もここに出ます

      log は単調なので、大小関係も一緒に運べます(a < b なら log a < log b)。 ところが 2ˣ mod p は巻き戻るので、大小の関係は完全に壊れます運ばれるのは「構造として保たれる演算」だけで、順序はその中に入っていない。

      (教科書の「準同型定理」は、この写像で何が潰れて何が残るかを分類するための定理です。 暗号が使っているのは定理そのものではなく、準同型という性質のほう—— 「壁の向こうで計算ができる」という一点だけです。だから定理を覚えていなくても、ここは読めます。)

      1. 暗号がすり抜けさせられるのは「構造がもともと持っている演算」だけ

        準同型(homomorphism)とは構造を保つ写像のこと。 暗号化を E とすると、E(a) ⊕ E(b) = E(a + b) のような性質が成り立つとき、 暗号文のまま計算ができる。

        ここが肝: 保てるのは、その構造がもともと持っている演算だけ。 暗号が新しい操作を発明することはできない。すでにある演算を、暗号の壁の向こう側に そのまま持ち越すのが準同型だから。

      2. そして、体や環という構造には演算が 2 つしかない

        平文が住んでいるのは (足し算と掛け算ができる世界)か (さらに割り算もできる世界)。 そして環の定義に書かれている演算は、加法と乗法のちょうど 2 つ。それだけ。

        だから比較は「直接には」運べない: 「<」は環の公理に入っていないので、 準同型で保ちようがない。「+ と × だけ」は暗号の都合ではなく、代数の定義そのもの。 (欲しければビットに分解して + と × から組み立て直す。 Week 5 で扱う TFHE の Programmable Bootstrapping は、その組み立てを暗号側で肩代わりして 暗号文に任意の表引きを適用できるようにする技術——底では同じ 2 つの演算の上に立っている。)

      3. 引き算と割り算は、新しい演算ではない

        「四則演算ができる」と「演算は 2 つ」は矛盾しません。 引き算と割り算は、足し算と掛け算の裏返しとして定義されるからです。

        引き算は「逆元を足すこと」。 a − b の正体は a + (−b) で、 −b は「足すと 0 になる相手」(加法逆元)。 F₅ なら −2 = 3(2 + 3 = 5 ≡ 0)なので、 3 − 2 = 3 + 3 = 6 ≡ 1

        割り算は「逆数を掛けること」。 a ÷ b の正体は a × b⁻¹ で、 b⁻¹ は「掛けると 1 になる相手」(乗法逆元)。 F₅ なら 2⁻¹ = 3(2 × 3 = 6 ≡ 1)なので、 3 ÷ 2 = 3 × 3 = 9 ≡ 4。検算すると 4 × 2 = 8 ≡ 3 で戻ります。

        体の定義そのもの: 「環(加法と乗法がある)であって、0 以外のすべての元に乗法逆元がある」。 四則のうち 2 つは、残り 2 つの逆演算として後から出てくるのであって、 演算が 4 つあるわけではありません。Week 0 のスライドで加法逆元・乗法逆元の表を作ったのは、 まさにこれを確認するためでした。

      4. 回路では、引き算はタダ・割り算は制約 1 本に化ける

        引き算はゲートを消費しません。 a − ba + (−1)·b と書けて、 定数倍は無料だからです。今回の role − 2 にコストがかからないのはこれが理由。

        割り算にはゲートがありません。 代わりに答えを prover に主張させて、掛け算 1 回で検算しますy = 1/x を主張させ、回路が課すのは x · y − 1 = 0 の 1 本だけ。

        おまけで 0 除算も塞がる: x = 0 のとき x·y − 1 = 0 を満たす y は存在しません。「0 では割れない」が制約として自動的に表現されるわけです。 そして 0 に逆数を認めない理由も同じで、認めると 0·y = 10·y = 0 が衝突します。

      5. 実例 — 指数法則を借りるだけで、加算が 1 つ手に入る

        平文 m を g^m の形で隠す方式なら、暗号文どうしを掛けると g^a · g^b = g^(a+b)平文の足し算になっている。 教科書的な RSA なら m₁^e · m₂^e = (m₁m₂)^e で、こちらは平文の掛け算。

        何をしているのか: どちらも新しい操作を作ってはいない。 指数法則というすでにある構造をそのまま借りているだけ。 そして片方しか手に入らない。+ と × を任意の回数こなせる方式(FHE)は 2009 年の Gentry まで作れなかった——回数制限つきで両方持つ方式はそれ以前からあったが、 制限を外すのがそれほど難しかった。

      6. 比較は、原理的に存在できない

        有限体には、+ と × と両立する大小関係が存在しない。 仮に 0 < 1 と決めると、両辺に 1 を足して 1 < 2、 さらに足して……と続き、いつまでも 0 に戻れないはずだが、 有限体では p 回足すと 0 に戻ってしまう。順序と矛盾する。

        実務での意味: 「x < 8」という順序は体に備わっていないので、そのままでは書けない。 書きたければ + と × から組み立て直す —— x をビットに分解して各ビットに b·b−b=0 を課し Σ bᵢ2ⁱ = x で繋ぐか、範囲が狭いなら (x−0)(x−1)…(x−7) = 0 と積で書く。 range proof が高価なのはこれが理由で、今回の課題が「許可リスト方式」なのも、 それを避けた設計になっている。

      7. MPC でも、まったく同じ壁がある

        秘密分散では、各人が持つシェアを足すだけで秘密の和のシェアになる(通信ゼロ・無料)。 掛け算もできるが、多項式の次数が倍になるので次数を戻すための通信が要る

        だから MPC のコストは乗算ゲートの数で測る。 加算は無料、乗算は有料、 それ以外の操作はそもそも定義されていない。ZK とは技術がまったく違うのに、 使える演算が同じ 2 つに落ち着くのは、根が同じ代数だから。

      8. ZK の検証も、多項式でなければ成立しない

        証明系は「全制約が満たされている」を「ある多項式が恒等的に 0」に翻訳し、 ランダムな 1 点で評価するだけで検査する。 次数 d の 0 でない多項式は零点を高々 d 個しか持てないので、 でたらめに選んだ点でたまたま 0 になる確率は無視できるほど小さい。

        この「1 点で全体を代表できる」性質は多項式だけのもので、 多項式は + と × で作られる。だから制約は多項式でなければならない。 SNARK 系の検証がミリ秒で終わる理由は、ここまで遡る。

      「4 つ持てば便利では」への答え

      増やせないのではなく、増やしても何も増えない。そして本当に増やせるものは、増やすと壊れる。

      ① 引き算と割り算は独立していない。 a − b = a + (−b) で完全に書けるものを 4 つ目として数えても、表現力は 1 ミリも増えません。 数学が構造を「2 つの演算」で定義するのは、独立に必要な最小の組を取るからです。

      ② 本当に独立な 4 つ目(比較など)は、持てたら暗号が壊れる。 中身を見ずに大小が分かる道具があれば、二分探索で平文が割れます。 増やせないのは代数の都合ではなく、安全性の要請です。

      ③ 除算をゲートにすると、回路がむしろ壊れる。 x = 0 のとき答えが定義できないからです。回路は「全部の等式が同時に成り立つか」を問う世界なので、 入力によって答えが無い関数をそのまま置けません。 制約 x·y − 1 = 0 に翻訳するほうが素直で、0 除算も自動的に塞がります。

      ④ そして決定的なのがこれ —— 2 つ + 「主張させて検算する」は、4 つより強い。

      逆数は x·y = 1、平方根は y·y = x、ソートは「並べ替えたものを主張させて、 置換であることと単調であることを検査」。どれも専用ゲートを持たずに、掛け算数本で片が付きます。 回路の設計思想は「演算を増やす」ではなく「難しい計算は prover にやらせ、安く検算する」—— 前の節で見た「回路は計算しない、答え合わせをする」が、ここで効いてきます。

      (この手が使えるのは、答えを主張してくれる prover がいる ZK だからです。 MPC と FHE には主張する人がいないので、逆数もソートも本当に計算する必要があり、ずっと高くつきます。)

      言い換え — 比較が無いのは、暗号が弱いからではない。強いからだ

      もし「箱の中身を見ずに大小を判定して、答えをそのまま返してくれる」道具があったとする。

      その道具があれば、x > 100? x > 50? x > 75? …… と 二分探索するだけで、中身の値が完全に特定できてしまう。 つまりその道具の存在は「この暗号は破れている」と同じ意味になる。 比較がすり抜けてこないのは、すり抜けたら暗号として成立しないから。

      では比較は永久に不可能かというと、そうではない。答えを暗号のまま返すなら問題ない (中身が漏れないので)。ただしそれは環の演算ではないので、 ビットに分解して + と × から組み立てるしかない。だから高くつく。 「+ と × しかない」の正体は、不可能ではなくこの値段のこと。

      (順序をわざと漏らす暗号も存在する = order-preserving encryption。速い代わりに、 設計として大小関係が漏れる。何を諦めるかのトレードオフになっている。)

      では、これで全部表現できるのか

      できる。表現力は失われていない。

      有限体の上では、どんな関数も必ず多項式で書ける(ラグランジュ補間)。 入力の全パターンに対して望む出力を通る多項式を、必ず 1 本作れるからだ。 比較も、ハッシュも、機械学習の推論も、原理的には + と × だけで書ける。

      失われるのは表現力ではなくコスト。素直に補間すると次数が p 級(p は 2²⁵⁴ 程度)になり、制約が天文学的な本数になって誰も検証できない。

      だから回路設計とは、「書けるか」ではなく「安く書けるか」を考える仕事になる。

      今回のメンバーシップ判定がまさにその実例。「role が {2,5,6} に入っているか」を (x−2)(x−5)(x−6) と書けば次数 3 で済む。 許可リストが 3 個しかないから安い。仕様が回路に優しく作られているのは偶然ではない。

      なぜ「回路」と呼ぶのか — ゲート、同時性、そして次数

      呼び名

      なぜ「回路」と呼ぶのか — ゲート、同時性、そして次数

      比喩ではなく、構造が本当に論理回路と同じ形をしている。ただし決定的に違う点が 1 つある。

      論理回路が AND / OR / NOT ゲートを線で繋いだ図であるように、算術回路は 加算ゲートと乗算ゲートを線で繋いだ図。 入力信号がゲートを通って中間信号になり、最後に出力信号になる。 ループのない有向グラフ(DAG)になる。

      右は、今回の f_role · (role−2)(role−5)(role−6) をゲートに分解したもの。 乗算ゲートが 3 つ並び、各ゲートの出口に中間信号が生まれる。 教材の前半で「中間結果に名前を付ける」と言ったのは、この中間信号のこと。

      そして 乗算ゲート 1 個が、制約 1 本になる。 t1 = (role−2)·(role−5) のような等式を 0 の形に直すと t1 − (role−2)(role−5) = 0。掛け算は 1 回だけなので次数 2。 R1CS が「次数 2 の制約の集まり」と説明されるのは、これが正体で、 今回の DSL が次数 4 の式を許しているのは学習用の簡略化。

      ゲートに分解すると

      (role−2) ─┐
                ├─[ × ]──→ t1      制約: t1 − (role−2)(role−5) = 0
      (role−5) ─┘
      
      t1       ─┐
                ├─[ × ]──→ t2      制約: t2 − t1·(role−6) = 0
      (role−6) ─┘
      
      t2       ─┐
                ├─[ × ]──→ t3      制約: t3 − t2·f_role  = 0
      f_role   ─┘
      
                             t3 = 0 ← これが最終的に課す条件

      乗算ゲート 3 個 → 中間信号 3 本 → 制約 3 本。R1CS 系では加算(引き算)は ゲートを消費しないので role−2 は無料で、コストは乗算の数で決まる (MPC も同じ。ただし PLONK のように加算も行を消費する方式もある)。

      論理回路と決定的に違うところ — 時間が無い

      回路は「実行する」ものではなく「満たす」もの。順序も状態も存在しない。

      プログラムは時間軸に沿って状態が変わる。1 行目のあとに 2 行目が動く。 回路にはそれが無く、全部の等式が同時に成り立っているかどうかだけが問われる。 制約を並べる順番を入れ替えても、まったく同じ回路。

      だから cs.assert_zero(...) は「実行される命令」ではなく 「提出される条件」。8 本書いたら、8 本が同時に成り立つ witness があるかどうか、 という一枚の連立方程式になる。

      この「時間が無い」性質が、コストに直結する

      ループは全部展開される。 回数が入力によって変わるループは書けない。 最大回数まで必ず展開され、回路のサイズはその最大回数ぶん膨らむ。

      配列アクセスは全要素を読む。 a[i] は「i 番目だけ取り出す」ことができない (どこを読んだかが秘密を漏らすし、そもそも添字で分岐できない)。 全要素を読み、選択子を掛けて他を 0 で潰す。要素数に比例したコストがかかる。

      そして回路の形は、入力によって変わってはいけない。 形が変われば形そのものが秘密を漏らすから。 —— 後半で出てくる採点器の test_structure_is_value_independent は、 この性質を守らせているテストだ。

      作るもの — アクセス制御

      お題

      作るもの — アクセス制御

      先に「なぜこの題材なのか」を書きます。適当に選ばれた練習問題ではありません。

      なぜアクセス制御なのか

      理由 1 ZK で最も多いのがこの形 「私はこの集合の一員だ」という主張は、実際の ZK アプリの大半を占めます。 年齢証明、資格証明、KYC 済みリスト、投票権、Merkle tree の包含証明—— 全部メンバーシップ判定です。最初にやる価値がここにあります。
      理由 2 範囲証明の沼を避けられる 「0 以上 8 未満」を回路で書くとビット分解が必要で、いきなり高くつきます。 許可リスト方式なら積 1 本で済む。Week 1 で本題(アンダー制約)に集中するための設計です。
      理由 3 条件が 3 つあるから比較できる 単一条件だと「どの制約がどれだけ効くか」を比べられません。 3 つあるから 1 本ずつ外して被害を測れるし、AND の合成という 回路設計の基本動作も自然に必要になります。
      理由 4 全部数え上げられる小ささ 512 通りしかないので、健全性を総当たりで確かめられます。 本物の回路ではこの全探索は不可能で、だからこそ形式検証や証明が要る—— という次の話への足がかりにもなっています。

      資格は (role, clearance, region) の 3 つ組。それぞれ 0〜7 の整数で、 3 つとも許可リストに入っているときだけ「正当(authorized)」とし、出力 granted を 1 にする。

      許可リストは公開・固定(tests/spec.py)。秘密なのは資格の方だ。 資格の候補は 8 × 8 × 8 = 512 通り、うち正当なのは 3 × 3 × 2 = 18 通り、 残り 494 通りは本来 1 つも通ってはいけない。

      この 494 という数字を覚えておくと、下のラボの採点器が出す数字がそのまま 「どれだけ壊れているか」の目盛りになる。

      数え方 — 順列でも組み合わせでもない

      nPrnCr は出てきません。使うのは積の法則だけです。 3 つの枠が区別されていて、それぞれ独立に選べるから、単純に掛けます。

      順列ではない理由: 順列は「同じ集合から取り出して並べる」話です。 ここは role・clearance・region という役割の違う 3 つの枠で、並べ替えという概念がありません。 (2, 3, 1) の数字を入れ替えた (3, 2, 1) は「別の並び」ではなく、 role = 3 という許可リスト外の別人です。

      組み合わせでもない理由: nCr は順序を区別せずに選ぶ話です。 ここでは枠が区別されるので、role の 2 と region の 2 は別物として扱われます。

      3・3・2 という数字は、許可リストの要素数がそのまま出てきたものです。 spec.pyROLE_OK = (2, 5, 6) と固定で書いてあるから role は 3 通り、 REGION_OK = (1, 6) だから region は 2 通り。 全候補の 8 も同じで、CODE_MIN = 0CODE_MAX = 7 と決まっているから 8 通りです。 数え上げができるのは、リストが固定で公開されているから

      許可リスト(公開)

      ROLE_OK      = {2, 5, 6}   # 3 個
      CLEARANCE_OK = {3, 4, 7}   # 3 個
      REGION_OK    = {1, 6}      # 2 個
      
      authorized := role∈ROLE_OK かつ
                    clearance∈CLEARANCE_OK かつ
                    region∈REGION_OK

      region だけ許可が 2 個しかない。この非対称が、あとで「どこを壊すと一番被害が大きいか」に効いてくる。

      roleclearanceregion掛けると
      資格の全候補888512
      正当(authorized)33218
      不正(512 − 18)494

      3 桁のダイヤル錠と同じ数え方。1 つ目が 3 通り、2 つ目が 3 通り、3 つ目が 2 通りなら、 全部で 3 × 3 × 2 通り。

      実際に書き出してみる

      # role を 2 に固定すると、残りは 3 × 2 = 6 通り。全部書ける
      (2, 3, 1)  (2, 3, 6)
      (2, 4, 1)  (2, 4, 6)
      (2, 7, 1)  (2, 7, 6)
      
      # role は 2 / 5 / 6 の 3 通りあるので、6 × 3 = 18 通り
      role=2 → 6 通り
      role=5 → 6 通り   ← (5,3,1) (5,3,6) (5,4,1) (5,4,6) (5,7,1) (5,7,6)
      role=6 → 6 通り
                          合計 18 通り
      
      # 不正になる例。どれか 1 つでも許可リストの外にあれば不正
      (0, 3, 1)  role が外
      (2, 0, 1)  clearance が外
      (2, 3, 0)  region が外   ← Part B で使う witness
      (0, 0, 0)  全部外

      この同じ掛け算が、あとの「実測」の表の数字を全部説明します。 region のリンクを外すと region だけ 8 通りに広がるので 3 × 3 × 8 = 72、そこから正当な 18 を引いて 54。 role のリンクを外すと 8 × 3 × 2 = 48 から 18 を引いて 30。 被害の大きさは「自由になった枠の選択肢が何倍に増えたか」で決まります —— region は 2 → 8 で 4 倍、role は 3 → 8 で 2.7 倍。だから region を外したときが一番大きい。

      制約を、1 本ずつ数値で読む

      部品

      制約を、1 本ずつ数値で読む

      使う部品は 3 種類だけ。どれも「代入してみれば分かる」ものなので、実際に値を入れて確かめる。

      先に —— この 3 つが何の代わりなのか

      回路には bool 型も in 演算子もありません。だから自分で作ります。

      部品① は「真偽値」の自作。 型が無いので「この信号は 0 か 1」は 自分で 1 行書かないと存在しません。ZK 回路でいちばん多く書く制約で、 選択子 s·a + (1−s)·bs、Merkle パスの左右、 範囲証明のビット分解——全部この 1 行が土台になります。

      部品② は「in 演算子」の自作。 集合に属することを掛け算だけで表します。 そして「私はこの集合の一員だ」はZK アプリの主役級の主張です (年齢証明・ホワイトリスト・KYC・投票権・Merkle 包含証明)。 この積は、その主張を + と × の世界に翻訳する方法そのものです。

      そして部品③ が心臓です。① は主張を入れる、② は事実を表す。 ③ はその 2 つを縛りつける鎖で、Part B で抜けているのがまさにこの 1 本です。

      ① だけあっても、フラグは値と無関係のまま。② だけあっても、主張と結びついていない。 ③ が「1 と主張するなら、リストに入っていろ」を強制して初めて意味が生まれます。

      部品① ビット制約 b · b − b = 0

      これは「b は 0 か 1」を意味します。1 行ずつ変形すれば出ます。

      b · b − b = 0
      b² − b = 0   b·b を b² と書いただけ
      b(b − 1) = 0  共通の b でくくった(分配法則の逆)

      ここでさっき証明した「零因子がない」を使います。 積が 0 なら、どちらかの因子が 0。つまり b = 0b − 1 = 0。 後者は b = 1 なので、答えは 0 か 1 の 2 つだけです。

      右の表で確かめられます。b = 2 なら 4 − 2 = 2 で 0 になりません。b = 7 なら 49 − 7 = 42。 0 と 1 以外は必ず余りが出ます。

      回路に「型」はないので、ブール値という概念はこの 1 行で自作する。 書き忘れれば、そのフラグは 0/1 以外の値も取れる、ただの数になる。

      bb · bb·b − b判定
      000満たす
      110満たす
      242違反
      396違反
      74942違反

      部品② メンバーシップ積 (x − a₁)(x − a₂)…

      許可リストの各要素を引いて、全部掛ける。 x がリストのどれかに一致するとき、その因子が 0 になり、積全体が 0 になる。 一致しなければ、どの因子も 0 でないので積も 0 にならない(零因子がないから)。

      つまりこの積は「x ∈ 許可リスト」という条件を、掛け算だけで表した式になっている。 回路で in 演算子を自作するとこうなる、ということ。

      role(role−2)(role−5)(role−6)結果意味
      20 · (−3) · (−4)0リスト内
      53 · 0 · (−1)0リスト内
      64 · 1 · 00リスト内
      0(−2) · (−5) · (−6)−60リスト外
      31 · (−2) · (−3)6リスト外

      部品③ リンク — ①と②を結ぶ 1 行が、回路の心臓

      f · (x − a₁)(x − a₂)… = 0

      これが「f = 1 を主張するなら、x は許可リストに入っていなければならない」を強制する。 零因子がないので、f ≠ 0 なら積の方が 0 でなければならず、x はリストのどれかに一致するしかない。

      逆に f = 0 なら、この式は x が何であっても 0 になる。 フラグを下ろしている限り、x は自由ということ。これは仕様どおりで問題ない —— 権限を主張していないのだから。

      向きに注意 — 保証されるのは片道だけ

      この 1 行が保証するのは f = 1 ⟹ x ∈ S の一方向だけで、 逆(x ∈ S ⟹ f = 1)は保証されない。正当な資格を持っている人が、自分の意思で f = 0 と置いて「権限がない」ことにするのは自由なままだ。

      それで困らない。健全性が要求しているのは 「granted = 1 を主張したときに、本当に権限があるか」だけで、 これは前向きの含意で足りる。逆向きは、正直な prover がどう値を選ぶかという witness 生成側の話にすぎない。

      回路を組み立てる — 実際の思考順序

      Part A

      回路を組み立てる — 実際の思考順序

      部品が揃ったので順に積む。各ステップの「なぜ」まで押さえると、次週以降そのまま使える型になる。

      1. 資格を入力信号として宣言する

        cs.input("role", role) のように、名前を決めて 3 本置く。

        なぜ名前が固定なのか: 採点器は名前で信号を探す。ここが違うと、回路が正しくても評価されない。 現実でも、証明系は「どの信号が公開入力か」を名前や位置で識別する。

      2. フィールドごとに補助フラグを 1 本ずつ足す

        f_role, f_clearance, f_region。 「このフィールドは許可リストに入っている」という主張を表す信号。

        なぜ補助信号が要るのか: 3 条件の AND を 1 本の巨大な式で書くと次数が跳ね上がり、 実際の証明系では扱えない。中間結果に名前を付けて次数を下げるのは回路設計の基本動作で、 Circom を書くときも同じことをする。

      3. フラグをビットに縛る(部品①を 3 本 + 出力用に 1 本)

        f · f − f = 0 を、3 つのフラグと granted に課す。

        なぜ granted にも要るのか: granted は外に出る出力だから。 ビットに縛らなければ granted = 6 のような値も許され、 「1 かどうか」で判定する検証側が壊れる。

      4. リンクを 3 本張る(部品③)— ここを 1 本でも落とすと壊れる

        f_role · (role−2)(role−5)(role−6) = 0 を、3 フィールドすべてに。

        落とすとどうなるか: そのフィールドはフラグと切り離され、 「リストに入っていないのにフラグ 1」が成立してしまう。 これが今回の Part B で突く穴そのもの。下のラボでスイッチを切れば実物が見られる。

      5. AND を取って出力にする

        granted − f_role · f_clearance · f_region = 0。 フラグがビットなら、この積は「3 つとも 1 のときだけ 1」になる。最後に cs.set_output(granted)

        これが健全性の要: granted = 1 を主張した瞬間、積が 1 になり、 3 つのフラグはどれも 0 でなくなる。すると 3 本のリンクが一斉に発動し、 3 フィールドすべてを許可リストに縛りにいく。ここまでが一本の論理の鎖で、 どの環が欠けても嘘の証明が通る。

      6. 信号と制約の「形」を入力値に依存させない

        if role in ROLE_OK: で制約を出し分けてはいけない。値は入力ごとに変わってよいが、 式の集合はどの資格でも完全に同じでなければならない。

        なぜそんな制限が: 現実の回路は一度コンパイルされ、全員が同じものを使う。 形が秘密で変わるなら、形そのものが秘密を漏らすし、verifier は検証すべき式を固定できない。 採点器の test_structure_is_value_independent はこれを見ている。

      完成した Part A(提出した solution.py の中核)

      # (1) フラグはすべてビット
      cs.assert_zero(f_role * f_role - f_role)
      cs.assert_zero(f_clearance * f_clearance - f_clearance)
      cs.assert_zero(f_region * f_region - f_region)
      cs.assert_zero(granted * granted - granted)
      
      # (2) メンバーシップリンク(3 フィールドすべてに必要)
      cs.assert_zero(f_role      * _membership_product(r, ROLE_OK))
      cs.assert_zero(f_clearance * _membership_product(c, CLEARANCE_OK))
      cs.assert_zero(f_region    * _membership_product(g, REGION_OK))
      
      # (3) granted は 3 フラグの AND
      cs.assert_zero(granted - f_role * f_clearance * f_region)

      健全性の証明を、言葉で 3 行

      ① granted = 1 と主張 → 積 = 1 → 3 フラグはどれも 0 ではない
      ② フラグ ≠ 0 なら、リンクの残り(メンバーシップ積)が 0 でなければならない
      ③ 積が 0 ⟺ その値は許可リストのどれかに一致

      よって「granted = 1 にできる」なら 3 フィールドとも許可リストの中。 これが健全性そのもの。 逆に、正当な資格ならフラグを全部 1 に置けて全式が 0 になる —— こちらが完全性。

      完全性と健全性は、別々に壊れる

      2 つの正しさ

      完全性と健全性は、別々に壊れる

      片方だけ満たす回路はいくらでも作れる。だから採点器も別々にテストしている。

      完全性 completeness健全性 soundness
      主張正当な資格なら granted=1 にできる不正な資格では granted=1 にできない
      守る相手正直な利用者システムそのもの
      壊れると正しい人が入れない(不便)誰でも入れる(事故)
      壊し方制約を足しすぎる制約を書き忘れる(アンダー制約)
      採点器の見方18 通りを実際に組んで確認494 通りに対し反例を全探索
      気づきやすさすぐ気づく(自分が困る)誰も困らないので気づかない

      最後の行がこの課題の主題。制約を書き忘れた回路は、正直に使っている限り完璧に正常動作する。 テストも通る。攻撃されて初めて分かる。だから「壊れているかを自分から探しにいく」道具(=採点器の全探索)が要る。

      どの制約が、実際に効いていたか(leave-one-out の実測)

      実測

      どの制約が、実際に効いていたか

      8 本を 1 本ずつ外して採点器を回した結果。上のラボで同じことが再現できる。

      #外した制約採点器の結果通る不正資格
      0f_role がビット破れず0
      1f_clearance がビット破れず0
      2f_region がビット破れず0
      3granted がビット破れず0
      4role のメンバーシップリンク破れる (0, 3, 1)30
      5clearance のメンバーシップリンク破れる (2, 0, 1)30
      6region のメンバーシップリンク破れる (2, 3, 0)54
      7AND(granted = 3 フラグの積)破れる (0, 0, 0)494

      数字の出どころ: region のリンクを外すと region が 8 通りすべて自由になり、 role と clearance は許可内に縛られたままなので 3 × 3 × 8 = 72、 うち正当な 18 を引いて 54。role のリンクを外した場合は 8 × 3 × 2 = 48 から 18 を引いて 30。 許可リストが狭いフィールドほど、リンクを失ったときの被害が大きい。

      「反例なし」は「安全」ではない

      ビット制約 4 本は、外しても採点器を破れない。それでも消してはいけない。

      理屈のうえでも破れないのは事実で、granted = 1 を主張すれば積が 1 になり各フラグは 0 でなくなるので、 リンクは効き続ける。ここまでは正しい。

      理由①: 「採点器が反例を出さなかった」は「安全である」の証明ではない。 本物の tests/solver.py は自分の説明文で「これは形式的な健全性証明ではない」と明言しており、 ブールフラグと線形構造を前提にした発見的な探索にすぎない。 (この回路では実測したところ、ビット制約を外しても採点器は諦めずに 正しく全ケースを棄却していた。つまり表の 0 は本物の 0。ただしそれはこの回路がたまたま健全だったからで、 一般には別の話。)

      理由②: この回路は「granted = 1 を主張したとき」だけを検査対象にしているから成立している。 フラグを他の回路と共有したり、出力を別の式で使い回した瞬間、 「フラグは 0 か 1」という前提が要る。安全性が周囲の文脈に依存している状態は、それ自体が危うい。

      理由③: 出力の値域は、回路が自分で保証すべきもの。 いまは AND 制約と 3 つのフラグのビット制約が結果的に granted を 0/1 に押し込んでいるが、 それは他の制約に頼っているだけで、granted 自身は何も主張していない。 依存先が 1 本消えれば前提も消える。

      攻撃側に回る — 4 手で破る

      Part B

      攻撃側に回る — 4 手で破る

      tests/challenge.py は同じアクセス制御の回路だが、制約が 1 本足りない。読んで、突く。

      1. 回路を読んで、無い制約を探す

        challenge.py には 7 本しか制約がない。自分が組んだ 8 本と突き合わせると、 f_region · (region−1)(region−6) = 0 だけが無い(コメントとして残っている)。

        探し方のコツ: 「何が書いてあるか」ではなく「対称性が崩れている場所」を見る。 3 フィールドを同じ形で扱っているはずの回路で、1 つだけ扱いが違えば、そこが穴。

      2. その 1 本が何を守っていたかを言葉にする

        守っていたのは「f_region = 1 なら region ∈ {1, 6}」という含意。 無いということは、f_region は region の値と完全に無関係になれる

        残っている制約を確認する: f_region をビットに縛る制約と AND の制約は残っている。 だが、どちらも region の値そのものには一切触れていない。 誰も region を見ていない——これが攻撃可能である証拠。

      3. 正直な witness から出発する

        challenge.honest_witness(2, 3, 1) で、正当な資格の割当を作る。 これで 7 本の信号名がすべて正しく揃う。

        ハマりどころ: challenge.py のフラグ名は f_clearance であって f_clear ではない。名前を 1 つでも間違えると、採点器は制約を評価する前に 「attack() の witness に足りない信号」として不合格にする (DSL の _resolve を直接叩いた場合は KeyError)。 正直な割当から作れば、この事故は起きない。

      4. 縛られていない信号だけを書き換える

        region を許可リスト外の 0 に。f_region1 のまま。 granted は 1·1·1 = 1 のまま。role と clearance には触らない —— こちらのリンクは生きているので、動かせば即座に違反になる。

        成立の確認: ビット制約 4 本 ✓/role・clearance のリンク 2 本 ✓(値は正当なまま)/ AND ✓。7 本すべて 0 で、granted = 1、しかし資格 (2, 3, 0) は不正。 本来入れない人が入れた。

      提出した attack() の中核

      witness = challenge.honest_witness(2, 3, 1)
      witness["region"]   = 0   # 許可リスト {1,6} の外
      witness["f_region"] = 1   # 止める制約が無い
      witness["granted"]  = 1   # 1·1·1 = 1
      return witness

      同じ witness を、自分の回路にぶつけると

      challenge.py(制約 7 本)→ 全部 0。通る。

      自分の build()(制約 8 本)→ #6 で 1 · (0−1) · (0−6) = 6。0 でないので拒否。

      違いは、書いた式が 1 本多いこと。ただそれだけで、 同じ嘘が通る回路と通らない回路に分かれる

      値を決めるのは prover、条件を課すのは verifier

      分業

      値を決めるのは prover、条件を課すのは verifier

      この 2 つを混同していると、「なぜ書き忘れが致命傷になるのか」がずっと腑に落ちない。

      cs.aux("f_role", 1 if role in ROLE_OK else 0) には 2 つの役割が同居している。 第 2 引数は witness の値で prover の仕事、 cs.assert_zero(...)制約で verifier の仕事。

      証拠に、フラグの値を 1 とベタ書きしても Week 1 のテストは 5 本とも通る。 完全性テストは正当な資格しか組まないし、健全性テストは値を捨てて制約だけから解き直すので、 採点器には区別がつかない

      つまり solution.py を書くとき、あなたは2 人分の仕事を同時にしている。 そして攻撃者が守ってくれるのは verifier 側だけ。prover 側のコードを攻撃者が 自分用に書き直すのは自由だ ——それが Part B でやったことそのもの。

      だから、こう読み替える

      witness の値 = 攻撃者が好きに書き換えられるもの
      制約 = 攻撃者が絶対に避けて通れないもの

      回路を書き終えたら、witness の値の部分を全部消して読み直す。 それでも仕様が守られているなら健全。守られないなら、そこに制約を足す。 これが今週いちばん持ち帰る価値のある習慣。

      採点器が値を無視する理由

      solver.py は role/clearance/region と granted=1 だけを固定し、 残りの信号を未知数として解き直す。 あなたが用意した witness の値は一切見ない。攻撃者は自分で witness を作るのだから、 それが正しいシミュレーションになる。

      実際に組み込むときは、4 つの問いから始める

      設計

      実際に組み込むときは、4 つの問いから始める

      暗号を選ぶのが先ではありません。要件を 4 つの問いで確定させてから、 それを満たすプロトコルを探しにいきます。順番を逆にすると、道具に合わせて問題を歪める羽目になります。

      1. 誰が

        証明したり計算したりする主体は誰か。1 人か、複数人か、委託先か。

        KelpDAO の例: off-chain の AI Auditor。

      2. 何の情報を、誰から隠して

        隠す対象と、隠す相手を両方書き出す。ここを「誰から」まで詰めないと、方式が決まりません。

        KelpDAO の例: 発見した攻撃手順 W を、事業者・サードパーティ・他の利用者から隠す。 先に公開すると、見た人が先に攻撃できてしまうから。

      3. どういう計算をして

        隠したまま何を計算・主張するのか。これが回路になる部分です。

        KelpDAO の例: 「W を入れるとこのコントラクトが壊れる」という計算。

      4. 誰が検証するか

        結果を受け取って判断するのは誰か。人か、コントラクトか、別のサーバか。

        KelpDAO の例: on-chain のコントラクトが検証し、通れば自動で遮断する。 検証者がコントラクトだからこそ、人の判断を待たずに止められる。

      この 4 つが埋まって、はじめて方式が決まる

      「相手が嘘をつくかもしれない」が問題なら ZK。「複数人の秘密を突き合わせたい」なら MPC。 「計算を外に出したい」なら FHE。4 つの問いの答えが、そのまま選択の根拠になります。

      今週の課題を 4 つに当てはめると——
      ① prover が ② 資格 (role, clearance, region) を verifier から隠して ③「権限がある」ことを計算し ④ verifier が制約 8 本で検証する。

      ただし Week 1 では ② の「隠す」部分だけを外してあります。 witness は丸見えのまま渡していて、隠すのは上に暗号を載せてからの話。 今週は ③ と ④ を正しく作ることに集中している、という位置づけです。

      実際は何で書くのか — Circom / Noir で同じバグがどう現れるか

      現場の道具

      実際は何で書くのか — そして、同じバグがどう現れるか

      今週使った Python の DSL は学習用。実務では専用の言語やライブラリを使う。 どれを選んでも、今日のバグは消えない。

      道具書き方位置づけ
      Circom専用 DSL(signal / <== / ===R1CS を自分の手で組む。制約が見える
      NoirRust の専用言語(Aztec 製)Rust そのものではない。コンパイラが Rust 製で、構文が Rust に似ている
      halo2 / arkworks本物の Rust(ライブラリ)回路を Rust のコードとして組み立てる
      gnark本物の Go(ライブラリ)同上の Go 版
      zkVM(RISC Zero / SP1)普通の Rust プログラム回路を書かない。書いた Rust の実行を証明する ← Week 6 の zkvm-exploit

      「Noir は Rust で書ける」はほぼ当たり。ただし正確には Rust に似た独自言語で、 ACIR という中間表現に落ちるため証明系を差し替えられる。 本当に Rust そのものを書くのは halo2 / arkworks(回路をコードで組む)と zkVM(普通のプログラムを書く)

      Circom — 代入と制約が別の記号になっている

      // <== は「代入 + 制約」
      inter <== in1 * in2;
      
      // <-- は「代入だけ」。制約は作られない
      bit <-- (in1 >> 0) & 1;
      bit * (bit - 1) === 0;   // ← 手で書く

      <-- は値を入れるだけで、回路には何も約束させない。 公式ドキュメントの range proof の例ですら、<-- の直後に === 0 を手で書いている。この 1 行を忘れたら、今日の Part B と完全に同じ穴が空く。

      ついでに公開/秘密の指定も Circom にはある: component main {public [in1, in2]} と 書いた入力だけが公開で、それ以外の入力は秘密。今週の aclib.py に この区別が無いのは、学習用に秘匿性を外してあるから。

      Noir — 「制約されていない」ことを言語が明示させる

      // Safety: 下の assert で out を縛っている
      let out = unsafe { u72_to_u8(num) };
      
      let mut reconstructed: u72 = 0;
      for i in 0..8 {
          reconstructed += (out[i] as u72 << (56 - 8*i));
      }
      assert(num == reconstructed);  // ← ここで初めて縛られる

      unconstrained fn の戻り値は制約されていないので、 呼ぶ側が unsafe { } で囲み、// Safety: コメントで理由を書き、 自分で assert して縛ることをコンパイラが要求する。

      これは今週の f_role とまったく同じ構造 —— 重い計算は prover にやらせ、回路は答え合わせだけする。 縛り忘れたらそのまま通る、というリスクの形も同じ。

      つまり

      言語が変わっても、「主張させて、縛り忘れる」というバグの形は変わらない。

      Circom は <--、Noir は unconstrained、halo2 なら値を置いただけの cell。 どれも「値は入るが、回路は何も約束していない」状態を作れる。 今週 8 本の式で覚えた「この信号を縛っている式はどれか」という問いは、そのまま持ち運べる。

      zkVM(Week 6)は回路を書かないのでこの形のバグは減るが、代わりに 何を public にして何を witness にするかの設計が残る。 zkvm-exploit が「guest プログラムと public/witness 設計」を課題にしているのは、そこが勝負どころだから。

      ここまでに出た言葉(旧・用語)

      用語

      ここまでに出た言葉

      signal回路の変数。F_p の元。入力信号と補助信号がある。
      constraint「この式は 0」という条件。回路の安全性はここだけで決まる。
      witness全信号への値の割当。prover が作って差し出すもの。
      completeness正当な入力なら受理できること。壊れると正直な人が困る。
      soundness不正な入力では受理できないこと。壊れても誰も困らないので気づかない。
      under-constrained必要な制約が足りない状態。ZK の脆弱性の最頻出パターン。
      membership値が集合に入っていること。積 ∏(x−aᵢ) が 0 になることで表す。
      R1CS今日の「0 になるべき式の集まり」を、次数 2 に揃えて整理した標準形式。
      prover秘密を持ち、witness を作って「条件を満たせる」と主張する側。攻撃者もここに座る。
      verifier主張を受け取り、制約が全部 0 になるかだけを確かめる側。秘密は見ない(見せない)。
      zero-knowledge proof秘密を渡さずに「条件を満たしている」ことだけを納得させる技術。渡すのは短い証明データ。
      multi-party computation複数人が秘密を持ち寄り、互いの値を知らないまま計算結果だけを得る技術。値を断片に割って扱う。
      fully homomorphic encryption暗号化したまま何度でも計算できる暗号。計算する側は中身を最後まで知らない。
      homomorphism構造を保つ写像。暗号文への操作が中身の演算に対応すること。保てるのは + と × だけ。
      commitment値を「中身は見えないが、後から差し替えられない」箱に入れる仕掛け。ZK の秘匿性の土台。
      finite field巨大な素数 p で割った余りの世界。四則ができ、零因子が無く、順序は無い。