入力コミットメント
zkVM に渡す入力データ全体に対する正準エンコーディングとコミットメントの設計を解説します。
入力コミットメントにより、「証明されたデータセット」と「主張されたデータセット」の一致を検証可能にします。バイトレベルの正準化により、TypeScript と Rust の間で決定的な一致を保証します。
概要
入力コミットメントは、zkVM ゲストプログラムに渡されるすべての投票データ(コミットメント値と Merkle パス)を単一のハッシュ値に集約するプロトコルです。
このハッシュ値は zkVM のジャーナル(公開出力)にコミットされるため、第三者はジャーナルに記録された入力コミットメントと、公開されている投票データから再計算した値を照合することで、zkVM が実際にどのデータセットを処理したかを独立に検証できます。
flowchart TB IN["入力データ<br/>electionId / bulletinRoot / treeSize<br/>/ totalExpected / votes (index 昇順)"] SPEC["固定値<br/>domainTag: stark-ballot:input|v1.0<br/>version: 10"] IN --> ENC[正準エンコーディング] SPEC --> ENC ENC --> H[SHA-256] H --> IC[入力コミットメント<br/>32 バイト] IC --> CMP["第三者照合<br/>再計算値 = journal.inputCommitment"]
入力コミットメントが解決する問題
zkVM の STARK 証明は「ゲストプログラムが正しく実行された」ことを証明しますが、「どの入力に対して実行されたか」は証明のスコープ外です。入力コミットメントがなければ、悪意あるサーバーは以下の攻撃が可能になります:
- 投票を除外した入力で zkVM を実行し、有効な STARK 証明を取得する
- 公開用の入力データには除外されていない投票を含めて提示する
- 第三者は STARK 証明が有効であることを確認できるが、実際に処理された入力は異なる
入力コミットメントをジャーナルに含めることで、第三者は「公開データから再計算した入力コミットメント」と「ジャーナルに記録された入力コミットメント」を照合し、不一致を検出できます。
正準エンコーディング
入力コミットメントの計算には、すべてのフィールドを決定的な順序・エンコーディングで連結する正準化が不可欠です。
バイトレイアウト
input_commitment = SHA-256(
domain_tag ← "stark-ballot:input|v1.0" (23 バイト, UTF-8)
|| version ← u32 リトルエンディアン (4 バイト) = 10
|| election_id ← UUID v4 バイナリ (16 バイト)
|| bulletin_root ← 32 バイト
|| tree_size ← u32 リトルエンディアン (4 バイト)
|| total_expected← u32 リトルエンディアン (4 バイト)
|| votes_count ← u32 リトルエンディアン (4 バイト)
|| [投票データ] ← インデックス昇順でソートされた各投票
)
各投票のエンコーディング
投票配列の各要素は以下の形式でエンコードされます:
vote_entry =
index ← u32 リトルエンディアン (4 バイト)
|| commitment_len← u16 リトルエンディアン (2 バイト) = 32 (固定)
|| commitment ← 32 バイト
|| path_len ← u16 リトルエンディアン (2 バイト)
|| path_nodes ← path_len × 32 バイト
フィールド一覧
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ドメインタグ | 23 バイト | UTF-8 固定文字列 | "stark-ballot:input|v1.0" |
| バージョン | 4 バイト | u32 LE | v1.0 = 10 |
| 選挙 ID | 16 バイト | UUID バイナリ | 選挙スコープの識別子 |
| 掲示板ルート | 32 バイト | ハッシュ値 | 最終的な Merkle ルート |
| ツリーサイズ | 4 バイト | u32 LE | 掲示板のリーフ数 |
| 期待投票数 | 4 バイト | u32 LE | 想定される総投票数 |
| 投票数 | 4 バイト | u32 LE | 実際に含まれる投票数 |
| 各投票インデックス | 4 バイト | u32 LE | 掲示板上の位置 |
| コミットメント長 | 2 バイト | u16 LE | 固定値 32 |
| コミットメント | 32 バイト | ハッシュ値 | 投票コミットメント |
| パス長 | 2 バイト | u16 LE | Merkle パスのノード数 |
| パスノード | 各 32 バイト | ハッシュ値 | 包含証明の兄弟ハッシュ |
正準化規則
エンコーディングの決定性を保証するために、以下の規則が厳守されます。
ソート規則
投票はインデックスの昇順にソートしてからエンコードする必要があります。
入力データの投票順序は任意であり得ますが、エンコーディング前にインデックスでソートすることで、同じ投票集合から常に同一のバイト列が生成されます。この規則に違反すると、TypeScript と Rust で異なるハッシュ値が計算され、検証が失敗します。
前提となる不変条件:
- 各投票の
indexは一意である(重複インデックスは不正入力)
重複インデックスが存在する入力はプロトコル違反であり、正常系の正準化対象ではありません。したがって、正準順序の主キーは index の昇順として定義されます。
実装上は、重複インデックスという異常入力に対して Rust 側で追加の tie-break(commitment と merklePath)を行いますが、これはあくまで異常系の決定性補助であり、正常系仕様を変更するものではありません。
エンディアン規則
すべての整数フィールドはリトルエンディアンでエンコードされます。
| 型 | バイト数 | エンコーディング |
|---|---|---|
| u16 | 2 | リトルエンディアン |
| u32 | 4 | リトルエンディアン |
16 進数正規化
コミットメント値やパスノードなどの 16 進数表現は、0x プレフィックスを除去した上でバイト列にデコードされます。16 進数文字列のまま連結するのではなく、常にバイナリ表現を使用します。
TypeScript と Rust の同期
入力コミットメントは TypeScript(サーバー側)と Rust(zkVM ゲスト内)の双方で独立に計算され、結果が一致する必要があります。
flowchart LR
subgraph TypeScript
TS_IN[公開入力データ] --> TS_CALC[正準エンコーディング<br/>+ SHA-256]
TS_CALC --> TS_IC[入力コミットメント A]
end
subgraph "Rust (zkVM ゲスト)"
RS_IN[ゲスト入力] --> RS_CALC[正準エンコーディング<br/>+ SHA-256]
RS_CALC --> RS_IC[入力コミットメント B]
end
TS_IC --> CMP{A = B ?}
RS_IC --> CMP
CMP -->|一致| OK[検証成功]
CMP -->|不一致| NG[検証失敗:<br/>入力データが異なる]
同期が破壊される典型的な原因:
- ソート順序の不一致
- エンディアンの不一致
- ドメインタグの文字列差異
- バージョン番号の不一致
- 16 進数正規化規則の差異(大文字/小文字、
0xプレフィックスの有無)
検証パイプラインにおける役割
入力コミットメントは、Counted-as-Recorded 段階での検証チェック counted_input_commitment_match として使用されます。
| チェック ID | 検証内容 |
|---|---|
counted_input_commitment_match | 公開入力データから再計算した入力コミットメントがジャーナルの値と一致するか |
このチェックが失敗する場合、zkVM が処理した入力データと公開されている入力データが異なることを意味し、結果の信頼性が根本的に損なわれます。
注意事項
入力コミットメントには投票者の秘密データ(選択肢や乱数)は含まれません。含まれるのはコミットメント値と Merkle パスのみであり、これらはすべて掲示板上で公開されているデータです。したがって、入力コミットメントの公開は投票の秘密性に影響しません。