Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ゲストが票を調べ、数える

集計ゲストは、渡された票をそのまま足すプログラムではありません。B の一票が掲示板のインデックス 0 にあるなら、その位置、opening、コミットメント、包含証明を順に検査し、すべてを通過した場合だけ B の得票数へ加えます。

同時に、「検査に通らなかったレコード」と「掲示板にはあるのに渡されなかった位置」を記録します。64 スロットある掲示板に対し、63 票しか提示されなかったことを、計算結果自身に残すためです。

処理パイプライン

flowchart TB
  I[入力全体の形式と量を確認] --> V[各票を検査]
  V --> T[有効票を候補別に加算]
  V --> C[提示・除外・却下を記録]
  T --> J[ジャーナルを出力]
  C --> J
  I --> H[入力と締切状態のハッシュ]
  H --> J

入力全体が処理可能かという検査と、一票を有効と認める検査は分けています。処理量の上限を超えれば実行を拒否します。一方、範囲内の入力に無効票や重複があれば、それを却下した結果をジャーナルに残します。

投票の 6 段階検証

入力レコードを受け取った順に検査します。最初に失敗したところで、そのレコードの検査を打ち切ります。

順序確かめること失敗する例
1インデックスが 0 <= index < tree_size か64 スロットなのに位置 64 を指定
2そのインデックスが初出か位置 0 の票をもう一度渡す
3選択肢が 0〜4 か候補 A〜E に対応しない値
4opening から再計算したコミットメントが一致するかB を C に変えて元の乱数を使う
5同じコミットメントをすでに処理していないか別の位置へ同じ記録を複製
6最終掲示板への包含証明が成立するか記録やパスが掲示板ルートと合わない

1、2 を通過した時点で、そのスロットを seenBitmap に記録します。3〜6 で失敗しても「提示された」という事実は残ります。6 まで通過すれば includedBitmap も立て、対応する候補へ 1 票を加えます。

重複の検査は先着順です。先のレコードが検証に失敗しても、その位置を後続レコードでやり直すことはできません。コミットメントも、照合と重複検査を通過して登録された後は、包含検査に失敗しても予約されたままです。

コミットメントの計算は投票コミットメントの式、包含検査は CT Merkle の規則を使います。

スロット / レコード分離モデル

スロットは掲示板上の位置、レコードはゲストに渡された票データです。一つの位置に重複レコードを何件送っても、掲示板のスロット数は増えません。この二つを分けると、何が不足したのかを説明できます。

状況validVotesinvalidPresentedSlotsmissingSlotsrejectedRecords
64 票すべてを正しく提示64000
B の位置 0 を提示せず、ほかの 63 票は有効63010
位置 0 を不正な opening で提示、ほかは有効63101
正しい 64 票の後に位置 0 の重複を追加64001

スロットは「計上済み」「提示されたが未計上」「一度も提示されなかった」の三つに分かれます。

validVotes + invalidPresentedSlots + missingSlots = treeSize
seenIndicesCount = validVotes + invalidPresentedSlots
excludedSlots = missingSlots + invalidPresentedSlots

excludedSlots > 0 なら、少なくとも一つの掲示板スロットを数えられていません。rejectedRecords は、それとは別に、重複や範囲外を含む却下レコードの総数です。最後の例のように除外が 0 でも却下が 1 になり得ますが、アプリが Verified として受け入れるには却下も 0 であることを要求します。

入力レコードと得票数には、さらに次の関係が成立します。

sum(verifiedTally) = validVotes
validVotes + invalidVotes = totalVotes
rejectedRecords = invalidVotes
rejectedRecords >= invalidPresentedSlots

totalVotes はゲストへ提示したレコード数で、掲示板のスロット数ではありません。totalExpected は選挙設定が要求する票数です。ゲストはこれを入力から出力へ引き継ぎ、treeSize との一致は検証側が確認します。

ジャーナル出力

ジャーナルは、上の計算を検証者へ伝える公開出力です。下表は公開 JSON の名前と論理的な値を定義します。

フィールド型説明
electionIdUUID対象選挙 ID(入力の election_id をエコー)
electionConfigHash32 バイト選挙設定ハッシュ(入力の election_config_hash をエコー)
bulletinRoot32 バイト掲示板ルート(入力の bulletin_root をエコー)
treeSizeu32掲示板のツリーサイズ(入力をエコー)
totalExpectedu32想定総投票数(入力をエコー)
sthDigest32 バイトSTH ダイジェスト
verifiedTallyu32[5]選択肢 A〜E ごとの得票数
totalVotesu32zkVM が受け取った投票レコード数
validVotesu32検証に成功した投票数
invalidVotesu32検証に失敗した投票数
seenIndicesCountu32範囲内かつ初出のインデックスとして処理した件数
missingSlotsu32一度も提示されなかった掲示板スロット数(スロット / レコード分離モデル を参照)
invalidPresentedSlotsu32提示はされたが計上されなかった範囲内スロット数(同上)
rejectedRecordsu32却下されたレコード数(同上)
seenBitmapRoot32 バイトprover に提示されたインデックス集合の ビットマップ Merkle ルート
includedBitmapRoot32 バイト実際にカウントされたインデックス集合の ビットマップ Merkle ルート
excludedSlotsu32除外されたスロットの総数(定義は スロット / レコード分離モデル の式)
inputCommitment32 バイト入力コミットメント
methodVersionu32ゲストプログラムのバージョン(現行 = 14)

ジャーナルの信頼モデル

期待する Image ID で本物のレシート検証が成功したとき、そのレシート内のジャーナルを、指定ゲストが出した結果として受け入れられます。得票数や不足数はゲストの計算結果ですが、選挙 ID、設定ハッシュ、掲示板ルート、期待票数は入力から引き継いだ値です。これらが対象の選挙・掲示板に一致することは、公開入力や投票時の記録との照合で確かめます。

inputCommitment は決められた公開フィールドだけを束縛します。sthDigest はゲストに渡された締切状態のハッシュであり、外部の記録源が同じ状態を観測したことを、それだけで示すものではありません。

レシート内部と JSON の表現

Rust の VerificationOutput は election_id: [u8; 16] などのフィールドを持ち、env::commit(&output) が RISC Zero serde で直列化します。この receipt 内部のバイト配置は公開 JSON の仕様ではありません。固定オフセットや手書きの packed layout で読みません。

ホストがデコードした camelCase JSON では UUID とハッシュがバイト配列で出力され、配布用 journal.json では UUID 文字列・16 進数文字列へ変換されます。検証サービスの decode-journalは、同じレシートを検証してから Rust の契約でデコードします。

ビットマップルートの計算

seenBitmap と includedBitmap はそれぞれ treeSize 個のビットを持ち、提示された位置と計上した位置を表します。全ビットをジャーナルへ載せる代わりに、それぞれの Merkle ルートを載せます。

これにより、自票の位置 0 について「提示されて数えられた」「提示されたが無効だった」「提示されなかった」を証明に結びつけて調べられます。未計上の提示スロットがなければ、両ビットマップの集合もルートも一致します。バイト表現と個票の証明はビットマップ Merkleで定義します。

入力データ構造

入力の Rust フィールドは次のとおりです。opening を含む入力全体は公開 ZIP に入れません。

フィールド型説明
election_id16 バイト選挙の UUID v4 バイナリ表現
bulletin_root32 バイト掲示板 Merkle ツリーの最終ルート
tree_sizeu32掲示板のリーフ数(= 投票スロット数)
log_id32 バイト掲示板のログ識別子
timestampu64入力構築時に採用された最新 STH スナップショットの Unix 時刻ミリ秒
total_expectedu32想定される総投票数
election_config_hash32 バイト選挙設定のハッシュ値
votesVoteWithProof[ ]投票データと Merkle パスの配列

各 VoteWithProof は以下のフィールドを持ちます。

フィールド型説明
indexu32掲示板上のインデックス
choiceu8選択肢(0 = A, 1 = B, 2 = C, 3 = D, 4 = E)
random32 バイトコミットメント計算に使用した乱数
commitment32 バイト投票コミットメント値
merkle_path32 バイト[ ]RFC 6962 Merkle 包含証明のパスノード

入力の境界条件

bulletin_root の全ゼロ、tree_size=0、timestamp=0 は拒否します。さらに、ツリーサイズ・期待票数・レコード数・各候補の得票数は最大 1,000,000、各パス長は最大 u16::MAX、全パスノード数は最大 1,000,000、推定構造化入力サイズは最大 64 MiB です。

votes.length > tree_size は、それだけでは入力全体の拒否理由になりません。上限内であれば個々の重複・範囲外レコードを検査し、却下数を出力できます。

ゲストプログラムのバージョニング

現在の methodVersion は 14 です。これはジャーナル契約の識別に使います。実際に実行したゲストバイナリの識別は Image IDで行い、バージョン番号の一致だけで証明を受け入れません。

投票の却下・スロット分割・処理量の上限は、Lean の抽象モデルと Rust の共有ベクトルでも検査しています。その対応検査は、ゲストを含むシステム全体の実装証明ではありません。