zkPI: ゼロ知識証明付き決済指図
zkPI は証明を伴う決済指図である。市場が下した判断を、その判断のもとになった入力を見せずに証明できる形にしたものである。決済層が知るのは次のことだけである。登録済みの誰かが指図を持っている。その金額と価格は申告した範囲に入っている。価格は定足数が署名したものと一致する。期限は過ぎていない。nullifier(同じ指図の二度使いを防ぐ使用済み印)はまだ使われていない。それ以外は何も知らない。
出典: zkpi/ZKPI_WIRE.md、zkpi/README.md、defmi/POSITION.md §7 · zkpi リポジトリのガジェット・指図・証明の crate
外へ出るもの、出ないもの
flowchart LR
subgraph known["定足数が計算に使うもの"]
direction TB
R["依頼
銘柄、数量、売買方向"]
P["各メイカーの
値付け規則"]
W["勝者と
その価格"]
end
subgraph published["公開されるもの"]
direction TB
C["commitment 化した
指図"]
N["nullifier"]
D["期限"]
S["定足数
署名"]
end
subgraph venue["決済する側が確かめるもの"]
direction TB
V["範囲内
未使用
期限内
定足数の署名あり"]
end
R --> C
P --> C
W --> C
C --> V
N --> V
D --> V
S --> V
known -. "外へ出ない" .-> venue
classDef secret stroke:#f2b880,stroke-width:2px
classDef mech stroke:#9bb0ff,stroke-width:2px
classDef ok stroke:#5ee0c1,stroke-width:2px
class R,P,W secret
class C,N,D,S mech
class V ok
公開検証できる MPC の守りは、出力を開いた時点で終わる。この指図が必要なのはそのためである。見積を開いてから平文で決済すると、計算が守っていたものが全部漏れる。銘柄、数量、取引当事者が漏れ、差分を取れば値付け規則も分かる。zkPI は決済指図そのものを commitment(値を隠したまま、後から変えられない形で固定したもの)と証明にする。こうすると、監査で見た値と決済した値が、開かれていない同じ commitment になる。
指図が証明すること
- 承認。市場のノード 7 台のうち 3 台の定足数(3-of-7)が作る、ristretto255 上の FROST 閾値署名。検証器はグループ鍵しか持たないので、どの 3 台が署名したかは分からない。
- 金額と価格の範囲。範囲証明が 2 本ある。どちらも、各ノードが手元の Shamir シェアから作った部分を合わせて組み立てる。そのため、証明の元になる秘密(witness)を一台で持つノードはない。バージョン 2 はこれを閾値範囲証明として運ぶ。バージョン 1 は通常の Bulletproofs を 2 本運んでいた。
- 監査した判断への束縛。公開される quote proof(見積の選定が正しいことの証明)全体の SHA-256 ダイジェストを含む。ダイジェストからは、中に詰めた勝者も価格も分からない。検証者は指図と一緒に証明の全体を受け取り、自分で確かめる。
- 一度だけの使用。32 バイトの nonce から nullifier を導出する。決済層は使った nullifier を登録する。同じ指図が二度目に来たら、証明を調べる前に拒否する。
- 取引当事者は handle で表す。handle(当事者を表す曲線上の点)は圧縮した点 2 つである。一つの seed から市場ごとに導出するので、同じ法人でも市場が違えば無関係な点になる。台帳は署名済みの 2 つの handle から 4 つの口座名を導出する。指図と決済パッケージで口座が食い違えば拒否する。
wire 上の配置、バージョン 2
全体をビッグエンディアンで書く。各フィールドは固定長か、長さを前置した可変長で、次の順に並ぶ。省略できるフィールドはない。フィールドが一つ欠けた指図は「短い指図」ではなく、壊れた指図である。
| フィールド | バイト | 内容 |
|---|---|---|
| magic | 8 | QOMMZKPI |
| version | 2 | 現在は 2。知らない version を見た検証器は止まる。配置を推測して読もうとはしない。読み違えた commitment も曲線上の有効な点になってしまうからである。 |
| 金額の commitment | 32 | 圧縮 Ristretto |
| 価格の commitment | 32 | 圧縮 Ristretto |
| 資産の commitment | 32 | 圧縮 Ristretto |
| 支払側の handle | 32 | 圧縮 Ristretto |
| 受取側の handle | 32 | 圧縮 Ristretto |
| 期限 | 8 | Unix epoch からの秒数 |
| nonce | 32 | nullifier を一意にするもの |
| quote proof ダイジェスト | 32 | 公開される quote proof 全体の SHA-256 |
| 署名 | 64 | Ristretto255 上の FROST |
| 金額証明の長さ | 4 | |
| 金額の範囲証明 | n | 共同で組み立てた閾値範囲証明 |
| 価格証明の長さ | 4 | |
| 価格の範囲証明 | n | 共同で組み立てた閾値範囲証明 |
製品版の固定テストベクタは、数量 16 ビット、価格 32 ビットの範囲で 9,726 バイトである。バージョン 1 との互換用ベクタは 1,572 バイト。この差は、範囲証明を閾値方式で組み立てる代価である。小さい旧 wire の方を製品のコストとして見せることはせず、差をこのとおり明記する。
意図して省いたもの
圧縮、自己記述型のコンテナ、前方互換性は入れていない。どれも、二つの実装が読んだ内容を食い違わせる原因になる。上の表があれば、実装は 1 日で書ける。
実装を確かめる
テストベクタは artifacts/zkpi_vectors/ にある。wire のバージョンごとに、受理される指図 1 件と、それを 5 通りに壊したものが入っている。受理されるベクタは、復号して再符号化したとき、元と同じバイト列に戻る必要がある。これは「パースできた」よりも厳しい条件である。あるフィールドを捨てて再導出する実装は、パースの試験には通るが、この往復の試験には落ちる。二つの市場が同じ指図を決済できるかを決めるのは、この往復の試験である。
zkpi-verify --check-vectors artifacts/zkpi_vectors
zkpi-verify --self-test # issue v2, encode, decode, verify
zkpi-verify --quorum <hex> --now <seconds> < instruction.bin
zkpi-verify < instruction.bin # layout only, and it says so
終了コードは、0 が受理、1 が拒否、2 が判定できなかったことを表す。定足数の鍵を渡さなければ、ツールは配置だけを確かめる。そのときは「これはパースであって検証ではない」と出力する。形式が整っただけの未署名の指図に「ok」と出すツールは、無い方がましである。
どこで動くか、なぜ他では動かないか
実装の対象は、DeFMI のページで述べる、EVM を使わない専用の Avalanche L1 である。信頼境界(どこまでを信頼するかの線)ははっきり決めている。ノードの委員会は、署名する前に閾値範囲証明 2 本と quote proof の全体を検証する。各検証者は、状態遷移を適用する前に次を自分で確かめる。3-of-7 の Ed25519 承認、型付きの指図、quote proof の全体、共同で組み立てた 2 本の範囲証明、テイカーの価格制限、資産の紐付け、DvP の関係、チェーンとレールのドメイン、期限、シーケンス番号、直前の状態ルート、nullifier。一方で、検証者は非公開の MP-SPDZ のトランスクリプトを再実行しない。各ノードの内部でシェアが証明に渡される部分も証明しない。
実行層の比較、記録として残す
専用 VM を作る前に、バージョン 1 の検証器がどの実行層に収まるかを計測した。この検証は約 1.6 ms の曲線演算で、大半は Bulletproofs 2 本の検証である。
- EVM バイトコード。ed25519 のスカラー乗算 1 回で 302,401 gas を計測した。不採用。
- Subnet-EVM のプリコンパイル。実現はできる。検証器は自分で書く Go のコードになり、ガス代も自分で決められる。代価は、曲線の演算コードを自分で抱えることである。そこにバグがあれば、他に誰も見ていないチェーンのコンセンサスのバグになる。
- Solana。ランタイムに ristretto255 の syscall があり、これはここで使う曲線そのものである。しかし Groth–Kohlweiss の検証器は、点の演算に入る前に、commitment ごとに次数 log₂(n) の多項式をスカラー体で評価する。Solana にはスカラー体演算の syscall がない。
| Solana 上の演算(Agave 4.2.1) | 計算単位(compute units) |
|---|---|
| スカラー体: 乗算 1 回 | 5,450 |
| スカラー体: 加算 1 回 | 350 |
| Merlin トランスクリプト、32 バイトの要素 1 つあたり | 3,688 |
同じ処理を sha256 syscall 経由で | 132 |
| プログラム内での基点スカラー乗算 1 回 | ~863,000 |
| syscall としての ristretto スカラー乗算 1 回(公表値) | 2,208 |
| one-of-many 検証器、crowd 16 | 398,825 |
| one-of-many 検証器、crowd 32 | 951,503 |
| crowd 128(予測。上限は 1,400,000) | ~5,200,000 |
artifact: defmi/artifacts/solana_cu.json · stylus_gas.json
計算単位は時間を測った値ではなく、仮想機械が数えた値である。そのため上の数値は単位まで正確に再現でき、計測したホストの名前ではなくツールチェーンの版で記録できる。ボトルネックは曲線ではなく体(スカラーの演算)にあった。これで結論は出た。検証器をネイティブの Rust で動かせる VM を採る。
handle: 文書にはあってコードになかった性質
設計書は最初から、法人の handle は市場ごとに導出し、一つの法人が市場ごとに無関係な点になると書いていた。しかし最初のライブラリには handle を導出する手段がなかった。そのまま組み込むと、一つの名前をどこでも使うことになる。PvP(通貨同士の同時決済)の連結不能性を計測すると、観察者はスワップの回数がいくつでも確率 1.000 で両脚を結び付けられた。いまは zkpi::handles が一つの seed から handle を導出する。その隣に置いた試験では、最初に思いつく方式(一つの秘密に市場ごとの公開係数を掛ける)も試し、それが公開情報だけで連結できることを示している。導出した handle を使うと、観察者が当てる確率は 1/k に落ち、そこから上がらない。計測表のこの二つの行の間で、暗号は何も変わっていない。変わったのは名前の付け方だけである。
zkPI がしないこと
- 価格が妥当か、銘柄が正しいかは示さない。それは定足数の主張であり、署名が運ぶ。
- 現在の Triptych 系 note 証明は、送金者が知る
g^Sを nullifier として公開しない。支出秘密を隠す linking tag を使う。ただし、候補群、時刻、公開される取引の周辺情報からの関連付けは残る。 - ノードが、配られたシェアをそのまま計算エンジンに入れたことは証明しない。この隙間は QOMM の束縛に関する文書が主題として扱っており、委員会の信頼境界として残る。
- バージョン 1 は移行用の形式である。タグが付いていれば今も復号と再符号化はするが、製品としての発行経路ではない。