ゲストプログラム
zkVM 内のゲストが、入力検証から集計・ビットマップ計算までをどう構成するかを扱う章です。
ゲストプログラムは、投票コミットメントの再計算、RFC 6962 包含証明の検証、集計の実行、ビットマップルートの計算を行い、結果をジャーナルにコミットします。
契約上重要なヘルパー(コミットメント計算、正準エンコーディング、RFC 6962 包含証明、ビットマップルートなど)は zkvm/contract-core/ に集約されており、ゲストとホストが同じ実装を参照します。
概要
ゲストプログラムは 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{Phase 4 境界と<br/>Merkle パス長が有効?}
I3 -->|No| FAIL
I4 -->|Yes| NEXT[フェーズ 2 へ]
I4 -->|No| FAIL
end
subgraph "フェーズ 2: 投票検証と集計"
NEXT --> LOOP[各投票に対して]
LOOP --> V1[6 段階検証]
V1 -->|有効| TALLY[集計に加算]
V1 -->|無効| EXCL[却下カウントと<br/>スロット統計に反映]
TALLY --> BIT[ビットマップ更新]
end
subgraph "フェーズ 3: 出力構築"
LOOP -->|全投票完了| O1[ビットマップルート計算]
O1 --> O2[入力コミットメント計算]
O2 --> O3[STH ダイジェスト計算]
O3 --> O4[ジャーナルにコミット]
end
Note: 次のいずれかに該当する入力は、ジャーナル生成前に fail-closed で拒否されます。
bulletin_rootがゼロtree_sizeが 0tree_size > 1,000,000total_expected > 1,000,000votes.length > 1,000,000- 候補別 tally bucket が 1,000,000 を超える
- Merkle パス長が
u16::MAXを超える
votes.length > tree_sizeのような入力は、上記境界内であれば事前 reject されません。重複や範囲外は record 単位でrejectedRecordsに反映されます。
投票の 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検証<br/>包含証明検証"}
C6 -->|失敗| INV
C6 -->|成功| VALID[有効: 集計に加算]
各検証の失敗条件と、失敗したレコードがどのカウンタに反映されるかを示します。
| # | 検証 | 失敗条件 | 反映先 |
|---|---|---|---|
| 1 | インデックス範囲 | index >= tree_size(guest contract 上の index は u32) | rejectedRecords |
| 2 | インデックス重複 | 既に処理済みの index(2 番目以降) | rejectedRecords |
| 3 | 選択肢範囲 | choice が 0..=4(A..E)の外 | rejectedRecords + seenBitmap 反映 |
| 4 | コミットメント照合 | 再計算したコミットメントが入力の commitment と不一致 | rejectedRecords + seenBitmap 反映 |
| 5 | コミットメント重複 | 既に処理済みのコミットメント値(範囲内かつ初出スロットでも無効化) | rejectedRecords + seenBitmap 反映 |
| 6 | RFC 6962 包含証明 | Merkle パスから再計算したルートが bulletin_root と不一致 | rejectedRecords + seenBitmap 反映 |
範囲内かつ初出のスロットが #3〜#6 で無効化された場合、seenBitmap ではビットが立ち(提示はされた)、includedBitmap ではビットが立たない(計上されない)ため、invalidPresentedSlots として観測されます。範囲外(#1)や既処理スロットへの重複レコード(#2)は rejectedRecords のみに反映され、スロット単位の指標には影響しません。
コミットメント再計算と照合(#4)
ゲスト内で投票者の(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、入力として渡された値と照合します。これにより、投票者が主張する選択肢が掲示板上のコミットメントと一致することが保証されます。計算規則は コミットメントスキーム を参照してください。
RFC 6962 包含証明検証(#6)
投票のコミットメントが掲示板 Merkle ツリーに含まれることを、RFC 6962 PATH 関数ベースの CT スタイル包含証明で検証します。投票のインデックスと Merkle パスから掲示板ルートを再計算し、入力の bulletin_root と一致するかを確認します。リーフ・ノードハッシュの規則とドメインタグは CT Merkle ツリー を参照してください。
集計ロジック
6 段階検証をすべて通過した投票は「有効」として集計に加算されます。
- 集計は選択肢ごとの配列(5 要素)で管理
- 有効投票のインデックスに対応する
includedBitmapのビットをtrueに設定 - 無効投票はカウントされず、
includedBitmapのビットはfalseのまま - 範囲内かつ初出として提示された無効票は
seenBitmapではtrueになり、提示済みだが未計上のスロットとして扱われる
スロット / レコード分離モデル
現行のゲストプログラムは、掲示板スロットに対する完全性と、入力レコードの異常を別々に記録します。
flowchart LR TS["ツリーサイズ<br/>(全スロット)"] TS --> CNT["カウント済み<br/>validVotes"] TS --> INV["提示されたが未計上<br/>invalidPresentedSlots"] TS --> MIS["未提示<br/>missingSlots"] REC["入力レコード"] --> REJ["却下レコード<br/>rejectedRecords"]
| 指標 | 条件 | 意味 |
|---|---|---|
validVotes | 6 段階検証をすべて通過した範囲内かつ初出の投票 | 集計に含まれたスロット数 |
invalidPresentedSlots | 範囲内かつ初出のスロットが提示されたが、最終的に計上されなかった | 提示はされたがカウントに失敗したスロット数 |
missingSlots | 範囲内スロットが一度も提示されなかった | サーバーが prover に提示しなかったスロット数 |
rejectedRecords | 検証に失敗したレコード全体 | 重複 index、範囲外 index、重複 commitment なども含むレコード単位の却下数 |
スロット単位の 3 分類は、現行実装では常に次の関係を満たします:
validVotes + invalidPresentedSlots + missingSlots = treeSize
fail-closed 判定に使われる除外数は、スロット単位の excludedSlots です:
excludedSlots = missingSlots + invalidPresentedSlots
excludedSlots > 0 は検証失敗の決定的な指標です。1 スロットでも未提示または未計上であれば、集計結果の完全性が損なわれていることを意味します。
一方で rejectedRecords はレコード単位の補助指標です。たとえば次のようなケースでは rejectedRecords は増えても excludedSlots は増えません。
- 既に正しくカウント済みのスロットに対する重複インデックスレコード
tree_sizeの外側を指す範囲外レコード
旧 public contract の互換名は現行 journal にも公開レスポンスにも現れませんが、参考までに 1 対 1 対応を示します。
| 旧名 (compatibility mirror) | 現行 journal フィールド |
|---|---|
missingIndices | missingSlots |
invalidIndices | invalidPresentedSlots |
countedIndices | validVotes |
excludedCount | excludedSlots |
※ rejectedRecords は record 単位の新設カウントで、旧 invalidIndices の mirror は invalidPresentedSlots 側。
ジャーナル出力
ゲストプログラムがジャーナルにコミットする出力構造(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 | 範囲内かつ初出のインデックスとして処理した件数 |
| missingSlots | u32 | 一度も提示されなかった掲示板スロット数 |
| invalidPresentedSlots | u32 | 提示はされたが計上されなかった範囲内スロット数 |
| rejectedRecords | u32 | 却下されたレコード数(重複・範囲外・各種検証失敗を含む) |
| seenBitmapRoot | 32 バイト | prover に提示されたインデックス集合の ビットマップ Merkle ルート |
| includedBitmapRoot | 32 バイト | 実際にカウントされたインデックス集合の ビットマップ Merkle ルート |
| excludedSlots | u32 | 除外されたスロットの総数(= missingSlots + invalidPresentedSlots) |
| inputCommitment | 32 バイト | 入力コミットメント |
| methodVersion | u32 | ゲストプログラムのバージョン(現行 = 14) |
ジャーナルの信頼モデル
ジャーナルの各フィールドは、対応する STARK 証明により「ゲストプログラムが正しく計算した結果」であることが保証されます。
| ジャーナル項目 | STARK 証明で保証される内容 |
|---|---|
verifiedTally | 有効投票のみを正しく集計した結果である |
excludedSlots | 未提示または未計上のスロット数がゲストの計算結果と一致する |
rejectedRecords | 却下されたレコード数がゲストの計算結果と一致する |
inputCommitment | ゲストが処理した入力データを正準エンコードで束縛した値である |
seenBitmapRoot | prover に提示された範囲内かつ初出のインデックス集合から計算したルートである |
includedBitmapRoot | 実際にカウントされたインデックス集合から計算したルートである |
sthDigest | その実行で参照した掲示板状態から計算した値である |
一方、STARK 証明だけでは保証されないものがあります。「ゲストに提示されなかった票」「第三者 STH との合意」「ホストやサーバーの正直性」はジャーナル外の独立チェックで確認します。第三者はレシートの STARK 検証を行うだけで上記の保証を取得でき、ゲストロジックの信頼以外にホスト・サーバーを信頼する必要はありません。
ビットマップルートの計算
ゲストプログラムは投票検証と並行して seenBitmap(範囲内かつ初出として提示されたインデックス集合)と includedBitmap(6 段階検証を通過したインデックス集合)の 2 種類を構築し、それぞれの Merkle ルートをジャーナルにコミットします。この 2 つのルートを併用することで、公開検証側は「prover に提示されたが無効化された票」と「そもそも提示されなかった票」を区別できます。
LSB-first のバイト列パッキング、32 バイト境界による単一リーフ / 分割リーフの扱い、leaf / node hash 規則は ビットマップ Merkle を参照してください。
入力コミットメントと STH ダイジェスト
ゲストプログラムは投票処理の後、2 つの追加ハッシュ値を計算してジャーナルにコミットします。
入力コミットメント
ゲストに渡された入力のうち、公開フィールドを正準エンコーディングで連結し SHA-256 で圧縮します。現行実装では固定のドメインタグと format version を先頭に付与した上で、electionId・bulletinRoot・treeSize・totalExpected・votesCount と各投票の index・コミットメント値・Merkle パスを束縛します。投票列はハッシュ前に index 昇順で正規化されます(異常入力時の tie-break 補助ルールは 入力コミットメント > ソート規則 を参照)。
第三者は public-input.json などの公開検証用レコードから同じ値を再計算し、ジャーナルの値と照合することで、zkVM が処理した入力の同一性を検証できます。
詳細は 入力コミットメント を参照してください。
STH ダイジェスト
掲示板のログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを結合して SHA-256 で圧縮します。このダイジェストは第三者の STH ソースとの照合に使用され、サーバーが異なる投票者に異なる掲示板ビューを提示する分割ビュー攻撃を緩和します。
詳細は STH ダイジェスト を参照してください。
ゲストプログラムのバージョニング
ゲストプログラムにはバージョン番号が割り当てられ、ジャーナルの methodVersion フィールドに記録されます。現行のジャーナル契約は 14 です。
バージョン番号は Image ID の管理と連動しており、ゲストプログラムの変更は新しい Image ID の生成を伴います。検証時には、期待 Image ID との一致が確認されます。
ゲストの抽象 tally / rejection model と guest bounds は、Lean による形式化 で説明しています。Rust 側の guest-vector tests は、抽象モデルと実装の対応付けを検査します。