ゲストプログラム
zkVM 内で実行されるゲストプログラムの設計を解説します。
ゲストプログラムは、投票コミットメントの再計算、RFC 6962 包含証明の検証、集計の実行、ビットマップルートの計算を行い、結果をジャーナルにコミットします。ゲスト内の全処理は STARK 証明により正当性が保証されるため、このプログラムが信頼の根幹となります。
概要
ゲストプログラムは RISC Zero zkVM 上で動作する Rust プログラムです。ホストから投票データ(コミットメント、Merkle パス、選挙メタデータ)を受け取り、以下の処理を行います:
- 各投票の正当性検証(コミットメント再計算 + 包含証明)
- 有効投票の集計
- カウント状態のビットマップ計算
- 入力コミットメントと STH ダイジェストの計算
- 結果のジャーナルへのコミット
ゲスト内の処理はすべて STARK 証明に含まれるため、出力(ジャーナル)の正しさが暗号学的に保証されます。
入力構造
ゲストプログラムが受け取る入力(AggregatorInput)の構造を示します。
| フィールド | 型 | 説明 |
|---|---|---|
| election_id | 16 バイト | 選挙の UUID v4 バイナリ表現 |
| bulletin_root | 32 バイト | 掲示板 Merkle ツリーの最終ルート |
| tree_size | u32 | 掲示板のリーフ数(= 投票スロット数) |
| log_id | 32 バイト | 掲示板のログ識別子 |
| timestamp | u64 | 入力構築時に採用された最新 STH スナップショットの Unix タイムスタンプ |
| total_expected | u32 | 想定される総投票数 |
| election_config_hash | 32 バイト | 選挙設定のハッシュ値 |
| votes | VoteWithProof[ ] | 投票データと Merkle パスの配列 |
各 VoteWithProof は以下のフィールドを持ちます:
| フィールド | 型 | 説明 |
|---|---|---|
| index | u32 | 掲示板上のインデックス |
| choice | u8 | 選択肢(0 = A, 1 = B, 2 = C, 3 = D, 4 = E) |
| random | 32 バイト | コミットメント計算に使用した乱数 |
| commitment | 32 バイト | 投票コミットメント値 |
| merkle_path | 32 バイト[ ] | RFC 6962 Merkle 包含証明のパスノード |
処理パイプライン
ゲストプログラムの処理は、入力検証、投票検証・集計、出力構築の 3 フェーズで構成されます。
flowchart TD
subgraph "フェーズ 1: 入力検証"
I1[入力デシリアライズ] --> I2{掲示板ルート<br/>が非ゼロ?}
I2 -->|Yes| I3{ツリーサイズ<br/>が正の値?}
I2 -->|No| FAIL[不正入力]
I3 -->|Yes| I4{投票数 ≤<br/>ツリーサイズ?}
I3 -->|No| FAIL
I4 -->|Yes| NEXT[フェーズ 2 へ]
I4 -->|No| FAIL
end
subgraph "フェーズ 2: 投票検証と集計"
NEXT --> LOOP[各投票に対して]
LOOP --> V1[6 段階検証]
V1 -->|有効| TALLY[集計に加算]
V1 -->|無効| EXCL[除外リストに追加]
TALLY --> BIT[ビットマップ更新]
end
subgraph "フェーズ 3: 出力構築"
LOOP -->|全投票完了| O1[ビットマップルート計算]
O1 --> O2[入力コミットメント計算]
O2 --> O3[STH ダイジェスト計算]
O3 --> O4[ジャーナルにコミット]
end
投票の 6 段階検証
各投票に対して、以下の 6 つの検証が順に実行されます。いずれかが失敗した投票は即座に「無効」として除外され、以降の検証はスキップされます。
flowchart TD
V[投票] --> C1{1. インデックス<br/>範囲チェック}
C1 -->|失敗| INV[無効として除外]
C1 -->|成功| C2{2. インデックス<br/>重複チェック}
C2 -->|失敗| INV
C2 -->|成功| C3{3. 選択肢<br/>範囲チェック}
C3 -->|失敗| INV
C3 -->|成功| C4{4. コミットメント<br/>再計算と照合}
C4 -->|失敗| INV
C4 -->|成功| C5{5. コミットメント<br/>重複チェック}
C5 -->|失敗| INV
C5 -->|成功| C6{6. 包含証明検証}
C6 -->|失敗| INV
C6 -->|成功| VALID[有効: 集計に加算]
1. インデックス範囲チェック
投票のインデックスが 0 以上 tree_size 未満であることを確認します。範囲外のインデックスは掲示板上に存在し得ないため、不正入力として検出されます。
2. インデックス重複チェック
同一インデックスの投票が既に処理されていないことを確認します。重複するインデックスは二重カウント攻撃を意味するため、2 番目以降の同一インデックスは除外されます。
3. 選択肢範囲チェック
選択肢の値が 0 から 4(A から E)の範囲内であることを確認します。
4. コミットメント再計算と照合
ゲスト内で投票者の(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、入力として渡されたコミットメント値と照合します。
この検証により、投票者が主張する選択肢が掲示板上のコミットメントと一致することが保証されます。コミットメントの計算規則は コミットメントスキーム を参照してください。
5. コミットメント重複チェック
同一コミットメントが既に処理されていないことを確認します。コミットメント値が重複した場合は、入力の異常または二重投入の兆候として無効化されます。
6. RFC 6962 包含証明検証
投票のコミットメントが掲示板 Merkle ツリーに含まれることを、RFC 6962 PATH 関数ベースの CT スタイル包含証明で検証します。投票のインデックスと Merkle パスから掲示板ルートを再計算し、入力の bulletin_root と一致するかを確認します。
ハッシュ規則は CT Merkle ツリー の RFC 6962 参照規則に合わせます:
- リーフ:
SHA-256(0x00 || "stark-ballot:leaf|v1" || data) - ノード:
SHA-256(0x01 || left || right)
集計ロジック
6 段階検証をすべて通過した投票は「有効」として集計に加算されます。
- 集計は選択肢ごとの配列(5 要素)で管理
- 有効投票のインデックスに対応するビットマップのビットを
trueに設定 - 無効投票はカウントされず、ビットマップのビットも
falseのまま
三分類の除外モデル
ゲストプログラムは、ツリーサイズ分の全スロットを以下の 3 カテゴリに分類します。
flowchart LR TS["ツリーサイズ<br/>(全スロット)"] TS --> CNT["カウント済み<br/>countedIndices"] TS --> INV["無効<br/>invalidIndices"] TS --> MIS["欠損<br/>missingIndices"]
| カテゴリ | 条件 | 意味 |
|---|---|---|
| カウント済み | 6 段階検証をすべて通過した投票 | 集計に含まれた |
| 無効 | 入力として渡されたが、いずれかの検証に失敗した投票 | 不正データとして除外された |
| 欠損 | 入力に含まれなかったスロット(ツリーサイズとの差分) | サーバーが投票を提示しなかった |
通常のファイナライズ経路(buildZkVMInputFromSession が生成する正規入力)では、これら 3 つの合計はツリーサイズと一致します:
countedIndices + invalidIndices + missingIndices = treeSize
実装上は、範囲外インデックスや重複インデックスを含む非正規入力が直接渡された場合、この恒等式が崩れる余地があります。公開検証では、通常フローで生成された入力を前提に扱います。
後方互換フィールドとして excludedCount も出力されます:
excludedCount = invalidIndices + missingIndices
excludedCount > 0 は検証失敗の決定的な指標です。1 票でも除外された場合、集計結果の完全性が損なわれていることを意味します。
ジャーナル出力
ゲストプログラムがジャーナルにコミットする出力構造(VerificationOutput)を示します。
| フィールド | 型 | 説明 |
|---|---|---|
| electionId | UUID | 対象選挙 ID(入力の election_id をエコー) |
| electionConfigHash | 32 バイト | 選挙設定ハッシュ(入力の election_config_hash をエコー) |
| bulletinRoot | 32 バイト | 掲示板ルート(入力の bulletin_root をエコー) |
| treeSize | u32 | 掲示板のツリーサイズ(入力をエコー) |
| totalExpected | u32 | 想定総投票数(入力をエコー) |
| sthDigest | 32 バイト | STH ダイジェスト |
| verifiedTally | u32[5] | 選択肢 A〜E ごとの得票数 |
| totalVotes | u32 | zkVM が受け取った投票レコード数 |
| validVotes | u32 | 検証に成功した投票数 |
| invalidVotes | u32 | 検証に失敗した投票数 |
| seenIndicesCount | u32 | 一意インデックスとして処理した件数 |
| missingIndices | u32 | 入力に含まれなかったスロット数 |
| invalidIndices | u32 | 検証失敗または重複/範囲外として無効化された件数 |
| countedIndices | u32 | 正常にカウントされた投票数 |
| includedBitmapRoot | 32 バイト | ビットマップ Merkle ルート |
| excludedCount | u32 | 除外された投票の総数(= missing + invalid) |
| inputCommitment | 32 バイト | 入力コミットメント |
| methodVersion | u32 | ゲストプログラムのバージョン(v1.0 = 10) |
ジャーナルの信頼モデル
ジャーナルの各フィールドは、対応する STARK 証明により「ゲストプログラムが正しく計算した結果」であることが保証されます。
| ジャーナル項目 | STARK 証明で保証される内容 | 補足 |
|---|---|---|
verifiedTally | 有効投票のみを正しく集計した結果である | validVotes / invalidVotes との整合もジャーナル上で確認可能 |
excludedCount | 除外された票数がゲストの計算結果と一致する | excludedCount > 0 は完全性違反の重要シグナル |
inputCommitment | ゲストが処理した入力データを正準エンコードで束縛した値である | 公開入力側から再計算して照合できる |
includedBitmapRoot | 各インデックスのカウント状態から計算したルートである | 自票包含の証明(bitmap proof)の検証基準になる |
sthDigest | その実行で参照した掲示板状態から計算した値である | 第三者 STH 合意そのものは別チェックで確認する |
第三者はレシートの STARK 検証を行うだけで、上記の保証を取得できます。ゲストプログラムのロジックを信頼する必要はありますが、ホストやサーバーの正直性を信頼する必要はありません。
ビットマップルートの計算
ゲストプログラムは投票検証と並行して、各インデックスのカウント状態を記録するビットマップを構築します。
- ツリーサイズと同じ長さのブール配列を初期化(全
false) - 有効と判定された投票のインデックスに対応するビットを
trueに設定 - ビットマップを LSB-first でバイト列にパッキング
- 32 バイトチャンクに分割し、CT スタイルの Merkle ツリーを構築
- ルート値(
includedBitmapRoot)をジャーナルにコミット
ビットマップの詳細な構造とハッシュ規則は ビットマップ Merkle を参照してください。
入力コミットメントと STH ダイジェスト
ゲストプログラムは投票処理の後、2 つの追加ハッシュ値を計算してジャーナルにコミットします。
入力コミットメント
ゲストに渡されたすべての投票データ(コミットメント値と Merkle パス)を正準エンコーディングで連結し、SHA-256 で圧縮します。第三者は公開された入力データから同じ値を再計算し、ジャーナルの値と照合することで、zkVM が処理した入力の同一性を検証できます。
詳細は 入力コミットメント を参照してください。
STH ダイジェスト
掲示板のログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを結合して SHA-256 で圧縮します。このダイジェストは第三者の STH ソースとの照合に使用され、サーバーが異なる投票者に異なる掲示板ビューを提示する分割ビュー攻撃を緩和します。
詳細は STH ダイジェスト を参照してください。
ゲストプログラムのバージョニング
ゲストプログラムにはバージョン番号が割り当てられ、ジャーナルの methodVersion フィールドに記録されます。現在のバージョンは 10(v1.0)です。
バージョン番号は Image ID の管理と連動しており、ゲストプログラムの変更は新しい Image ID の生成を伴います。検証時には、レシートの Image ID がバージョンに対応する期待値と一致するかが確認されます。