Susumu Tomita

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

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

Week 1 の課題 proof-of-exploit を、前提から順に理解するためのページ。 「そもそもなぜ回路で書くのか」から始めて、制約を 1 本ずつ数値で読み、 最後は実際に制約を外して回路を壊せる対話型のラボまで通します。

この週で掴むことを 1 行にすると—— 回路には if が無く、あるのは「この式は 0 でなければならない」という条件だけ。 条件を 1 本書き忘れると、その信号は攻撃者の持ち物になる。

読了 15〜25 分 対話ラボあり 前提知識なし 外部通信なし

全体像

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

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(脆弱性の証明)になっている。

週の地図

テーマ課題
0導入スライドのみ
1Programmable Cryptography の全体像/算術回路proof-of-exploit ← 今ここ
2準備中
3楕円曲線暗号と Schnorr プロトコルschnorr-from-scratch
4準備中
5TFHE(Programmable Bootstrapping)tfhe-toy-python
6zkVM / vFHE / co-SNARK の組み合わせco-snark-prove/zkvm-exploit

Week 3 は有限体 → 楕円曲線 → シグマプロトコル → Fiat-Shamir を下から積み上げる構成。 Week 5 は暗号文のまま NAND を計算する。Week 6 でそれらを組み合わせて 1 つのスタックにする。

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 種類のバグを見る目を作るためにある。

    核心

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

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

    まず誤解を外す

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

    証拠に、今週作った回路は witness を 7 個まるごと検証側に渡している。 role も clearance も丸見えで、隠れているものは何もない。

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

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

    1. 秘密の値を「箱」に入れる

      x を、乱数 r と一緒に C(x) = g^x · h^r のような形に潰す (コミットメント)。中身は見えない(r が隠す)が、 後から x を差し替えることもできない(拘束)。

      この 2 つが同時に成り立つのが重要: 見えないだけなら、あとで嘘をつける。 変えられないだけなら、中身が漏れる。両方あって初めて「隠したまま約束する」ができる。

    2. 箱を開けずに、箱のまま足し算ができる

      C(x) · C(y) = g^(x+y) · h^(r+r') = C(x+y)。 箱どうしを掛け合わせると、中身が足し算された箱が出てくる。 これは指数法則の副作用で、誰かが機能として足したわけではない。

      ここでプライバシーが生まれる: 「x + y = z です」という主張は、 C(x)·C(y)C(z) が同じものを指すかを見るだけで確かめられる。 一度も箱を開けていないのに、等式が正しいことだけが分かった。

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

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

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

    4. 実際の SNARK は、これを一気にやる

      信号を 1 つずつ箱に入れるのではなく、witness 全体を多項式にまとめてコミットする。 そして「全制約が満たされている」を「ある多項式が恒等的に 0」に変換し、 検証者が選んだランダムな 1 点での値だけを開示させる。

      なぜ漏れないのか: 1 点の値から元の多項式(=witness)は復元できないうえ、 あらかじめ乱数で撹乱してある。なぜ騙せないのか: 制約が 1 本でも破れていれば その多項式は 0 でなくなり、ランダムな点でたまたま 0 になる確率は無視できるほど小さいから。 「隠す」と「騙せない」が同時に立つ。

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

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

    三兄弟

    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 系なら数百バイト)計算結果だけ暗号化された結果
    相手が学ぶこと主張が正しいという事実のみ結果のみ(他人の入力は不明)何も学ばない
    典型例残高を見せずに送金の正当性を示す各社の給与を明かさず平均だけ出す暗号化した医療データをクラウドで解析

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

    ZK で使うと

    本人が証明を作る

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

    MPC で使うと

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

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

    FHE で使うと

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

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

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

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

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

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

    語彙

    この課題で使う 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 について、必要な分だけ

    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 = 0 か b = 0 のどちらかが必ず成り立つ。 「どちらも 0 でないのに掛けたら 0」は起きない(整数と同じ感覚でよい)。

      ここから何が言えるか: f · (何かの積) = 0 という制約を書いておけば、 f ≠ 0 だと分かった瞬間、積の方が 0 でなければならないと断言できる。 許可リストの強制は、まるごとこの性質の上に乗っている。

    原理

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

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

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

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

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

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

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

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

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

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

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

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

      有限体には、+ と × と両立する大小関係が存在しない。 仮に 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 が高価なのはこれが理由で、今回の課題が「許可リスト方式」なのも、 それを避けた設計になっている。

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

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

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

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

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

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

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

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

    その道具があれば、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 は、 この性質を守らせているテストだ。

    お題

    作るもの — アクセス制御

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

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

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

    許可リスト(公開)

    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 個しかない。この非対称が、あとで「どこを壊すと一番被害が大きいか」に効いてくる。

    部品

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

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

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

    これは「b は 0 か 1」を意味する。理由は代入すれば見える。 b·b = b を満たす数は、この世界に 0 と 1 の 2 つしかない (b(b−1) = 0 と書き直すと、零因子がないので b = 0 か b = 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 通りに対し反例を全探索
    気づきやすさすぐ気づく(自分が困る)誰も困らないので気づかない

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

    ラボ

    実際に壊してみる

    左のパネルで 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 のまま動かないことを確認する

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

    実測

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

    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 本消えれば前提も消える。

    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

    この 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 を作るのだから、 それが正しいシミュレーションになる。

    現場の道具

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

    今週使った 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 で割った余りの世界。四則ができ、零因子が無く、順序は無い。

    理解度チェック

    制約検問 — あなたが 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 本でもあれば、そこが次の穴になる。