はじめに
最終更新: 2026-08-10
このドキュメントは、STARK Ballot Simulator の公開向けガイドです。
STARK Ballot Simulator は投票の完全性を段階的に検証する教育・ポートフォリオ向けの PoC であり、実運用の選挙基盤ではありません。
目的
- システムの全体像を短時間で把握できるようにする
- 暗号プロトコルと検証パイプラインの設計根拠を説明する
- 検証手順を再現できる情報を提供する
公開状態
本書は、ライブデモと公開用ソース snapshot の読者に向けたドキュメントです。 公開 repository snapshot は hwatanabe-jp/stark-ballot-simulator-public で確認できます。
再現手順は、対象リリースの bundle.zip と対応する公開 repository snapshot を照合して実行してください。
必要な材料と bundle.zip 単体では揃わない証拠は 第三者検証ガイド にまとめています。
公開仕様は、runtime、検証、artifact の 3 つの境界を説明します。 これらは利用者、監査者、実装追跡者が確認できる範囲です。 内部の作業記録や運用証跡は本書の範囲外であり、完了状態として主張しません。
想定読者
- 暗号検証や監査、本アプリケーションの設計に関心のある技術者
本書の用語表記
本書では、語彙の揺れを避けるために次の表記へ統一します。 詳細な定義は 用語集 を参照してください。
- 日本語に統一する語:コミットメント(文脈に応じて「投票コミットメント」「入力コミットメント」を区別)、包含証明、整合性証明、投票レシート、掲示板、ジャーナル
- 英語のまま使う語:STARK、zkVM、Image ID、RFC 6962、capability、
bundle.zip、fail-closed、finalization(集計確定) - 識別子は原文のまま:ファイル名、API フィールド、契約名(
journal.json、verificationStatusなど)はコードフォントの英語表記を保ちます。 - コンポーネント名:概念としては「検証サービス」、バイナリ名・パスとしては
verifier-serviceを使います。 - 役割名とリソース名:概念・役割としては「プローバー」「検証 worker」、リソース・コンポーネント固有名(prover semaphore、
proof-dispatcher、verification-workerなど)は原文の英語表記を保ちます。
バンドル関連は、次の 3 語を使い分けます。
| 指すもの | 表記 |
|---|---|
| 配布されるファイル本体 | bundle.zip |
| 配布対象としての論理名 | 配布対象アーカイブ |
| 非公開アーティファクトを含む上位概念 | 証明バンドル |
verification.json は protected report artifact であり、public bundle.zip のメンバーではありません。
階層関係は バンドル構造 と 公開境界 を参照してください。
本書の読み方
標準ルート
- まず 全体像 でシステムの概要を掴む
- 利用フロー で Home から Verify までの画面と API の流れを確認する
- 暗号プロトコル でコミットメントや Merkle ツリーなどの基盤を理解する
- zkVM 設計 でゲストプログラムと証明生成の仕組みを学ぶ
- 検証パイプライン で 4 段階検証モデルの全体と公開境界を把握する
- 改ざんシナリオ で教育的シミュレーションの動作を確認する
- 品質保証と形式手法 でテスト、PBT、Lean による品質境界を確認する
- AWS アーキテクチャ で runtime の責務分担を理解する
- API リファレンス でエンドポイント仕様を参照する
- 実際に検証する場合は 第三者検証ガイド で
bundle.zipを使ったローカル検証手順を実行する
PoC として受け入れている制約は 全体像、 設計背景の一次資料は 参考文献 から参照してください。
読者別ルート
いずれも標準ルートの 1〜2(全体像、利用フロー)を読み終えていることを前提に、そこから先の重点だけを示します。
監査者向け
bundle.zip を検証ページから取得し、独立にローカル監査したい読者向けです。
- 検証パイプライン で
/verifyの最終判定ロジックを理解する - 公開境界 で public
bundle.zipと protected report artifact(verification.json)の境界を確認する - チェック一覧 で各チェック ID と判定条件を確認する
- 第三者検証ガイド で
bundle.zipのローカル監査手順を実行する
用語集 を手元に置き、テストと形式化が守る境界は 品質保証と形式手法 で補足してください。 飛ばしてよい: 暗号プロトコル の数式詳細、AWS アーキテクチャ のインフラ詳細
実装者向け
クライアント、サーバー、zkVM のいずれかの実装を変更または追従したい読者向けです。
- 暗号プロトコル でコミットメント、Merkle、入力コミットメントの正準形を把握する
- zkVM 設計 でゲストとホストの責務分担と Image ID 管理を理解する
- 検証パイプライン でチェック評価、ゲーティング、公開境界を把握する
- API リファレンス でエンドポイント仕様と session-scoped 認可を確認する
テストのレイヤー分担は 品質保証と形式手法 を参照してください。 飛ばしてよい: 第三者検証ガイド(実装変更後の動作確認には 改ざんシナリオ を使う方が早い)
運用者向け
AWS インフラ、非同期プローバー、デプロイを担当する読者向けです。
- AWS runtime 境界 で frontend、API、DynamoDB、async prover、artifact delivery の責務分担を把握する
- AWS アーキテクチャ で runtime 構成、環境分離、Terraform-managed infrastructure の連携点を把握する
- 非同期プローバー で SQS、Step Functions、ECS の責務を理解する
- 可観測性設計 で構造化ログ、検出、相関、通知境界を確認する
- イメージ署名 と Image ID で署名検証と Image ID 解決の連動を確認する
- 公開境界 と バンドル構造 で artifact の公開 / 保護境界を把握する
観測対象となるエンドポイントの契約は API リファレンス を参照してください。 飛ばしてよい: 暗号プロトコル の数式、改ざんシナリオ の教育的デモ詳細
全体像
STARK Ballot Simulator は、投票の完全性を段階的に検証するための PoC です。
AWS runtime は個人プロジェクトとして運用できるよう、WAF を採用せず、artifact lifecycle やオンデマンド実行を前提にコストを抑える設計です。
flowchart LR A[Cast-as-Intended] --> B[Recorded-as-Cast] B --> C[Counted-as-Recorded] C --> D[STARK Verification]
投票から検証までの流れ
sequenceDiagram
participant V as 投票者
participant S as サーバー
participant B as 掲示板
participant Z as zkVM
participant VS as 検証サービス
V->>S: 投票意図(選択肢・乱数)+ コミットメント
S->>B: 掲示板に追記
S-->>V: 投票レシート
Note over S: ボット投票を自動追加
V->>S: POST /api/sessions/:sessionId/finalizations で finalization を要求
S->>Z: 同期または SQS / Step Functions / ECS 経由で証明生成
Z-->>S: STARK レシート + ジャーナル
S-->>V: /result で tally と finalization output を表示
V->>S: POST /api/sessions/:sessionId/finalizations/:finalizationId/verification
alt local profile
S->>VS: local bundle の verifier-service を同期実行
else target AWS profile
S->>VS: durable SQS queue 経由で verification worker を実行
end
VS-->>S: receipt を検証し protected report artifact を保存
V->>S: /verify が同じ finalization-scoped verification GET をポーリング
S-->>V: 段階別チェックと overall verdict を表示
図から読み取れない要点は次の 3 点です。
- finalization と STARK receipt verification は別の lifecycle です。finalization は集計結果、zkVM ジャーナル、STARK レシート、配布用バンドルを作る段階であり、STARK receipt verification が常に同時に完了するわけではありません。
- 実行形態は profile で分かれます。ローカル構成は finalization と
verifier-serviceを同期実行できますが、AWS target runtime は finalization を SQS、Step Functions、ECS Fargate で非同期実行し、STARK receipt verification は durable SQS queue 経由の verification worker が担います。非同期のあいだ、ブラウザはGET /api/sessions/:sessionId/finalizations/currentと finalization-scoped verification GET をポーリングします。 - verification POST 応答の
verificationStatusと、available な verification GET resource のverifierResult.statusは STARK receipt verification の状態であり、どちらも overall verdict ではありません。ブラウザはローカルの Cast-as-Intended 証拠を server checks に重ね、共有 presentation policy が 4 段階の必須チェックと hard-failure 条件を評価して、最終的な「Verified / 失敗 / 制限付き」などの表示を導きます。
検証で扱う要素
| 段階 | 主な証拠 |
|---|---|
| Cast-as-Intended | 投票コミットメントと投票レシート |
| Recorded-as-Cast | RFC 6962 / CT スタイルの掲示板 |
| Counted-as-Recorded | zkVM ジャーナル、入力整合、ビットマップ証明 |
| STARK Verification | RISC Zero レシート検証 |
各段階の目的、必要な証拠、失敗モードは 4 段階検証モデル を参照してください。
バンドル用語の階層
検証で扱うアーティファクト群は、証明バンドル ⊃ 配布対象アーカイブ ⊃ bundle.zip(ファイル) の 3 層で呼び分けます。
定義は 用語集 > 証明バンドル、詳細は バンドル構造 を参照してください。
verification.json は bundle.zip のメンバーではなく、capability 保護された report artifact として扱います。
PoC として受け入れている制約
次の制約は、検証可能投票の E2E フローを明瞭に示すための意図的な PoC スコープです。実装漏れでも、本番選挙システムとしての安全性・性能の主張でも ありません。正確な定数や mode 条件は各技術章を単一の参照先とします。
| 制約 | 現行スコープ | 技術上の参照先 |
|---|---|---|
| 固定されたデモ選挙 shape | 選択肢、ユーザー/ボット構成、期待票数、Merkle tree depth を固定して E2E を再現する | ホストと証明生成 > 現行 PoC の選挙 shape |
| bitmap chunk の開示 | 自票の proof が同じ chunk 内の counted / seen 状態も開示する | ビットマップ Merkle > プライバシーに関する注意 |
| 証明実行の resource/mode | target は CPU Fargate の非同期 production proof、local/test は目的別 profile による同期実行を使う | 非同期プローバー > 実行リソースと mode 境界 |
プロジェクト規模
コードベースの規模は、テストを含め約 33 万行です。内訳は次のとおりです(2026 年 8 月時点)。
| 区分 | 行数 |
|---|---|
| TypeScript / React(アプリ本体) | 約 66,000 行 |
| TypeScript(テストコード) | 約 154,000 行 |
| Rust(zkVM ゲスト + ホスト + 検証サービス) | 約 8,000 行 |
| Terraform / Shell / 補助スクリプト | 約 99,000 行 |
| 上記 4 区分の合計 | 約 327,000 行 |
各章への案内
| 部 | 内容 |
|---|---|
| 暗号プロトコル | コミットメント、掲示板 (CT Merkle)、入力コミットメント、STH ダイジェスト、ビットマップ Merkle |
| zkVM 設計 | ゲストプログラム、ホスト、証明生成、検証サービス、Image ID |
| 検証パイプライン | 4 段階モデル、チェック一覧、バンドル構造、ゲーティングロジック |
| 改ざんシナリオ | S0〜S5 シナリオ、検出メカニズム |
| 品質保証と形式手法 | 単体、結合、E2E、Property-based Testing、Lean による形式化 |
| AWS アーキテクチャ | トポロジー、非同期プローバー、可観測性、operator gate、DynamoDB recovery、Terraform |
| API リファレンス | エンドポイント一覧、セッションライフサイクル |
| 第三者検証ガイド | 検証ページで取得した bundle.zip を使う Ubuntu 向けローカル検証手順 |
| PoC として受け入れている制約 | 固定デモ shape、bitmap 開示、証明実行 mode のスコープ |
利用フロー
このページは、ブラウザでの通常利用順と、その裏で使われる API を一続きで示します。 検証モデルや zkVM の詳細に入る前に、どの画面で何が作られ、どの時点で何を検証するのかを把握するための入口です。
画面と API の対応
| 順序 | 画面 / 状態 | 主な操作 | 役割 |
|---|---|---|---|
| 1 | Home | セッション作成 | セッション、capability token、選挙設定の識別子を作る |
| 2 | Vote | 投票送信 | 投票コミットメントを送信し、投票レシートと掲示板上の位置を受け取る |
| 3 | Bot progress | ボット投票進捗 | デモ用ボット投票の進捗をポーリングする |
| 4 | Aggregate | 集計/証明生成 | 集計、zkVM 実行、監査用アーティファクト生成を要求する |
| 5 | Async status | current finalization 取得 | current finalization の pending / running / succeeded / failed / timeout を確認する |
| 6 | Result | current finalization 取得、STARK 検証実行 | finalization output を表示し、必要なら STARK verification を開始する |
| 7 | Verify | 検証 resource 取得、STARK 検証実行、cast 観測の記録 | 検証ペイロードの取得、STARK verification の開始、非 gating の観測記録を行う |
| 8 | Bundle / report | バンドル ZIP 取得、検証レポート取得 | capability 保護 API から public bundle.zip と protected report を取得する |
正確な path、request/response schema、公開エラーは エンドポイント一覧 を参照してください。
POST /api/sessions を除く session-scoped API は、path の :sessionId と X-Session-Capability ヘッダーだけで現在の session を特定して保護します。
finalization、verification、bundle / report の resource は path の :finalizationId にも結び付けられ、session capability と一致した場合だけ返されます。
ヘッダー要件の詳細は エンドポイント一覧 > 共通 contract を参照してください。
1. Home
Home で開始すると、ブラウザは POST /api/sessions を送ります。
サーバーは sessionId、capabilityToken、electionId、electionConfigHash、logId などを返し、ブラウザはそれらをローカル session state に保存して Vote へ進みます。
この session が以後の API 呼び出しの権限境界です。 別タブや古い session が現在の session と食い違う場合、後続画面は fail-closed に扱います。
2. Vote と bot progress
Vote では、ブラウザが投票選択と乱数からコミットメントを作り、POST /api/sessions/:sessionId/votes に送信します。
成功すると、投票者は voteId、bulletinIndex、bulletinRootAtCast を含む投票レシートを受け取ります。
その後、デモ用のボット投票が進むあいだ GET /api/sessions/:sessionId/progress をポーリングします。
これは最終的な集計に十分な投票が揃うまでの進捗表示であり、検証の成功判定ではありません。
3. Aggregate と async status
Aggregate では、ユーザーが通常シナリオまたは教育用 tamper scenario を選び、POST /api/sessions/:sessionId/finalizations を送ります。
この段階で、集計結果、zkVM の公開出力、receipt、bundle.zip 用の公開可能アーティファクトなどが作られます。
finalization 作成要求は同期結果を返す場合も、202 Accepted で非同期 queue に入る場合もあります。
非同期の場合、ブラウザは GET /api/sessions/:sessionId/finalizations/current をポーリングします。
current finalization resource は pending、running、succeeded、failed、timeout のいずれかであり(status ごとのフィールドは エンドポイント一覧 > Finalization resource)、ブラウザは succeeded の result だけを Result へ引き継ぎ、failed と timeout は失敗として扱います。
finalization と STARK receipt verification は同一ではありません。 finalization は proof material と結果を作る段階ですが、STARK verification が常にこの時点で完了しているとは限りません。
4. Result から Verify へ
Result は current finalization resource の succeeded result から、tally、scenario、journal / receipt 由来の表示材料などを表示します。
finalization response のアーティファクト情報は available / unavailable の bounded availability であり、raw locator やダウンロード URL ではありません。
ユーザーが検証へ進むと、ブラウザは STARK verification が未開始なら POST /api/sessions/:sessionId/finalizations/:finalizationId/verification を送ってから /verify へ遷移します。
この要求は terminal status まで待たず、/verify 側で同じ finalization-scoped resource を GET でポーリングしながら状態を解決します。
verification POST 応答の verificationStatus と、available な verification GET resource の
verifierResult.status は STARK receipt verification の状態を示す信号であり、Cast-as-Intended、
Recorded-as-Cast、Counted-as-Recorded を含む overall verdict そのものではありません。
UI が “Verified” を表示できるかは、全 required checks の評価と hard-failure 条件から別途導かれます。
5. Verify
Verify は GET /api/sessions/:sessionId/finalizations/:finalizationId/verification で検証ペイロードを取得します。
resource は available、unavailable、corrupt を明示します。available の場合は
proofAuthority、presentation、verifierResult、verification.checks / verification.steps、
voterEvidence、journal の included / omitted discriminator を返します。投票レシートと自票 proof は
voterEvidence.availability = "available" の場合だけ存在し、それ以外は理由を伴って unavailable になります。
STARK verification が not_run の場合、Verify は同じ finalization-scoped verification resource への
POST を一度起動します。not_run または running の間は、terminal status に到達するまで GET をポーリングします。
その後、4 段階のチェック結果から最終表示を解決します。
required check が未実行、実行中、失敗、または hard-failure に該当する場合、成功した overall verification にはなりません。
server-validated な検証シーケンスが terminal status まで完了すると、ブラウザは POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations で Cast-as-Intended の観測結果を best-effort に記録します。
これは telemetry であり、送信の成否が画面の verdict を変更することはありません。
検証の詳しい評価順序は 検証パイプライン と ゲーティングロジック を参照してください。
6. Bundle と report
検証画面から取得できる bundle.zip は、秘密を含まない公開可能アーティファクトだけを含む監査用 ZIP です。
一方、verification.json は検証サービスの protected report artifact であり、bundle.zip のメンバーではありません。
ブラウザは、検証レスポンスで admit した finalizationId と現在の session identity から authenticated artifact URL を組み立てます。
通常の取得経路はどちらも capability 保護 API であり、レスポンス内の raw locator をたどる方式ではありません。
| API | 返すもの |
|---|---|
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/bundle | public bundle.zip |
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/report | protected verification.json report artifact |
ここでいう public は「秘密を含まず第三者検証に使える」という機密性の分類であり、無認証公開を意味しません。
ファイル構成と除外対象は 公開境界 と バンドル構造 を参照してください。
Receipt と proof mode の読み分け
ローカル UI 開発では local-demo の mock zkVM、実装連携確認では local-proof-development の development receipt、本番相当の検証では local-proof-production または target runtime の production STARK proof を使います。
mock や dev-mode receipt はデモや開発速度のための経路であり、production STARK proof と同じ保証を持ちません。
このページでは利用順だけを扱います。 receipt の意味、Image ID、dev-mode の fail-closed ルールは zkVM 設計 と 4 段階検証モデル を参照してください。
暗号プロトコル
この部は、投票の検証可能性を支える暗号プリミティブを説明します。
公開データに対する投票の秘匿性(hiding)と束縛性(binding)は、コミットメントスキームで扱います。 追記専用の掲示板は、RFC 6962 に基づく CT スタイルの Merkle ツリーで扱います。 zkVM 入力のうち対象となる公開フィールドの束縛は、正準エンコーディングと入力コミットメントで扱います。
本書で「コミットメント」は、投票コミットメント(個々の投票の束縛)と 入力コミットメント(zkVM 公開入力のうち正準エンコーディング対象フィールドの束縛)の 2 種を指します。 両者は対象とドメイン分離タグが異なるため、それぞれ独立した章で説明します。
プロトコル章の構成
- コミットメントスキーム:投票コミットメントの構成と安全性
- CT Merkle ツリー:CT スタイルの追記専用掲示板
- 入力コミットメント:zkVM 公開入力の対象フィールドに対するコミットメントの正準エンコーディング
- STH ダイジェスト:スプリットビュー緩和のためのツリーヘッドダイジェスト
- ビットマップ Merkle:投票カウント証明のためのビットマップツリー
想定読者と前提
- 想定読者:暗号プリミティブの仕様を実装または監査する技術者
- 前提:SHA-256 ハッシュ計算と Merkle ツリーの基本概念を把握していること
扱わない範囲
- SHA-256 や Pedersen コミットメントなど暗号プリミティブの数学的安全性証明
- RFC 6962 / CT エコシステムの運用詳細(証明書ログ、Monitor 役割など)
- 暗号ライブラリ実装の詳細(定数時間実装、サイドチャネル対策)
関連する章
コミットメントスキーム
この章では、投票者の選択を公開データから隠しつつ、後から変更できないようにするコミットメントスキームを説明します。
本システムの投票コミットメントは SHA-256 を使い、投票内容に対する hiding(秘匿性)と binding(束縛性)を支えます。 ドメイン分離タグは、他のハッシュ用途との入力解釈の混同を避けるために使います。
概要
投票コミットメントは、投票者が選んだ選択肢を公開データから隠したまま、その選択に束縛されることを可能にする暗号プリミティブです。
掲示板に記録されるのはコミットメント値のみです。
選択肢と乱数(opening)は、掲示板や bundle.zip 内の public-input.json などの公開配布物には現れません。
投票者は Cast-as-Intended 検証のために opening をローカルに保持します。
flowchart LR
subgraph 入力
E[選挙 ID<br/>16 バイト UUID]
C[選択肢<br/>1 バイト]
R[乱数<br/>32 バイト]
end
E --> H[SHA-256]
C --> H
R --> H
T["ドメインタグ<br/>"stark-ballot:commit|v1.0""] --> H
H --> CM[コミットメント<br/>32 バイト]
コミットメントの正準フォーマット
コミットメント値は、次の入力を連結して SHA-256 に渡すことで生成されます。
commitment = SHA-256(
domain_tag || ← "stark-ballot:commit|v1.0" (24 バイト, UTF-8)
election_id || ← UUID v4 のバイナリ表現 (16 バイト)
choice || ← 選択肢の値 (1 バイト, 0〜4)
random ← 一様乱数 (32 バイト)
)
各フィールドの仕様
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ドメインタグ | 24 バイト | UTF-8 固定文字列 | "stark-ballot:commit|v1.0" |
| 選挙 ID | 16 バイト | UUID v4 からハイフンを除去し、16 進数をバイト列に変換 | 選挙スコープの識別子 |
| 選択肢 | 1 バイト | 符号なし整数 (0 = A, 1 = B, 2 = C, 3 = D, 4 = E) | 投票者の選択 |
| 乱数 | 32 バイト | 暗号学的に安全な一様乱数 | hiding 性の根拠 |
SHA-256 への入力は合計 73 バイト、出力は 32 バイト(16 進数表記で 64 文字、0x プレフィックス付きでは 66 文字)です。
ドメイン分離
ドメインタグ "stark-ballot:commit|v1.0" は、このコミットメントを他のハッシュ用途から分離するための仕組みです。
本システムの主要なハッシュ用途では、用途ごとに次のドメインタグまたは構造的プレフィックスを使います。 これにより、ハッシュ入力の意味を分離します。
| プリミティブ | タグ / プレフィックス |
|---|---|
| コミットメント | "stark-ballot:commit|v1.0" |
| 入力コミットメント | "stark-ballot:input|v1.0" |
| Merkle リーフ | 0x00 || "stark-ballot:leaf|v1" |
| Merkle ノード | 0x01 |
| ログ ID | "stark-ballot:bulletin-log|v1.0" |
STH ダイジェストは専用のドメインタグを持ちません。 ログ ID を含む正準フォーマットで束縛されます(STH ダイジェスト を参照)。
安全性
Hiding(秘匿性)
乱数フィールドが 32 バイト(256 ビット)のエントロピーを持つため、公開されたコミットメント値から選択肢を推測することは計算量的に不可能です。
前提条件:
- 乱数は暗号学的に安全な乱数生成器(CSPRNG)から生成される
- 同じ乱数は決して再利用しない(再利用すると、同じ選択肢で同じコミットメント値が現れて情報が漏えいする)
PoC の制約: 本 PoC は operator に対する完全な秘匿性を目的としていません。 投票 API に送信された opening(選択肢と乱数)はサーバー側ストアに保持されうるため、ここでいう hiding は公開観測者と公開配布物に対する性質を指します。
Binding(束縛性)
SHA-256 の原像耐性(preimage resistance)と第二原像耐性(second-preimage resistance)により、一度コミットした値と異なる選択肢に対して同じコミットメント値を生成することは計算量的に不可能です。
つまり、投票者はコミットメント公開後に「別の選択肢に投票した」と主張を変えることができません。
TypeScript と Rust の実装同期
コミットメントは、TypeScript(クライアントとサーバー)と Rust 実装(zkVM guest/host から共有される zkvm/contract-core)の双方で計算されます。
この 2 系統の実装は、バイトレベルで完全に同一の出力を生成する必要があります。
同期が必要な要素:
- ドメインタグの文字列とエンコーディング(UTF-8)
- UUID からバイト列への変換規則(ハイフン除去 → 16 進数デコード)
- 選択肢の整数エンコーディング(1 バイト、符号なし)
- 乱数の 16 進数デコード規則
ドメインタグやエンコーディング規則を変更する場合は、TypeScript と Rust の両実装を同時に更新する必要があります。 どちらか一方のみの変更は、コミットメント照合の失敗を引き起こします。
検証パイプラインにおける役割
コミットメントは、4 段階検証モデルの最初の 3 段階で使われます。
| 検証段階 | コミットメントの役割 |
|---|---|
| Cast-as-Intended | 投票者がローカルに保持する(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、投票レシートと照合する |
| Recorded-as-Cast | 掲示板上でコミットメントの包含証明を検証し、投票時点のツリー状態に対して正しく記録されたことを確認する |
| Counted-as-Recorded | zkVM ゲストが prover から渡された各 vote opening でコミットメントを再計算し、掲示板上の値と整合する票だけを tally に含める |
注意: Cast-as-Intended と Counted-as-Recorded は同じコミットメント計算式を使いますが、opening の出所が異なります(投票者ローカル / prover に渡された値)。
Recorded-as-Cast は、投票時点(cast-time)のツリー状態に対する包含証明を使います。
この包含証明を、レシートの bulletinRootAtCast と整合させます。
rootAtCast の保存と再導出の詳細は CT Merkle ツリー を参照してください。
各チェックの判定ロジックは チェック一覧 > Cast-as-Intended を参照してください。
sequenceDiagram
participant V as 投票者
participant S as サーバー
participant B as 掲示板
participant Z as zkVM
V->>V: (選択肢, 乱数) を選び<br/>コミットメントを計算
V->>S: コミットメント, 選択肢, 乱数を送信
S->>S: opening から<br/>コミットメントを再計算して照合
S->>B: コミットメントを掲示板に追記
S-->>V: 投票レシート(インデックス,<br/>bulletinRootAtCast)
Note over V: ローカルに (選択肢, 乱数) を保存
Note over Z: ゲストプログラムが<br/>コミットメントを再計算し<br/>掲示板の値と照合
CT Merkle ツリー
この章は、RFC 6962 を参照した追記専用 Merkle ツリーによって、掲示板の透明性をどう作るかを説明します。
リーフハッシュ(0x00 プレフィックス)とノードハッシュ(0x01 プレフィックス)の区別により、second-preimage 攻撃を防止します。
包含証明と整合性証明により、投票の記録と掲示板の追記専用性を検証可能にします。
なぜ RFC 6962 CT スタイルか
- 整合性証明で追記専用性を示せるため、Recorded-as-Cast に必要な「後から削除や改変がされていない」保証を与えられる。
- 包含証明と整合性証明の両方を同一モデルで扱える。
- STH ダイジェストと組み合わせてスプリットビュー攻撃の検出力を上げられる。
概要
本システムの掲示板(Bulletin Board)は、Certificate Transparency (CT) で実績のある追記専用ログの設計を投票に応用しています。 各投票コミットメントは Merkle ツリーのリーフとして追記され、一度記録されたエントリは削除や改変ができません。
graph TD
subgraph "8 リーフのツリー(N=8)"
R["ルート<br/>SHA-256(0x01 || L || R)"]
N1["ノード"]
N2["ノード"]
N3["ノード"]
N4["ノード"]
N5["ノード"]
N6["ノード"]
L0["リーフ 0"]
L1["リーフ 1"]
L2["リーフ 2"]
L3["リーフ 3"]
L4["リーフ 4"]
L5["リーフ 5"]
L6["リーフ 6"]
L7["リーフ 7"]
R --> N1
R --> N2
N1 --> N3
N1 --> N4
N2 --> N5
N2 --> N6
N3 --> L0
N3 --> L1
N4 --> L2
N4 --> L3
N5 --> L4
N5 --> L5
N6 --> L6
N6 --> L7
end
RFC 6962 参照ハッシュ規則
RFC 6962 Section 2 に従い、リーフノードと内部ノードに異なるドメイン分離プレフィックスを適用します。
リーフハッシュ
LeafHash = SHA-256(0x00 || "stark-ballot:leaf|v1" || leaf_data)
| 要素 | サイズ | 説明 |
|---|---|---|
| プレフィックス | 1 バイト | 0x00(リーフ識別子) |
| 使用タグ | 20 バイト | "stark-ballot:leaf|v1"(UTF-8) |
| リーフデータ | 可変 | コミットメント hex をデコードした生バイト列(32 バイト) |
内部ノードハッシュ
NodeHash = SHA-256(0x01 || left_hash || right_hash)
| 要素 | サイズ | 説明 |
|---|---|---|
| プレフィックス | 1 バイト | 0x01(内部ノード識別子) |
| 左子ハッシュ | 32 バイト | 左部分木のハッシュ |
| 右子ハッシュ | 32 バイト | 右部分木のハッシュ |
ドメイン分離の安全性
0x00(リーフ)と 0x01(内部ノード)のプレフィックス区別は、second-preimage 攻撃を防止するために必要です。
この区別がなければ、攻撃者はリーフノードを内部ノードとして解釈させる(またはその逆の)偽造データを構築できる可能性があります。
使用タグ "stark-ballot:leaf|v1" は、他システムのリーフハッシュとの偶発的な衝突を防止する追加の防御層です。
Merkle Tree Hash(MTH)アルゴリズム
RFC 6962 で定義される MTH アルゴリズムは、任意のサイズのデータセットからルートハッシュを計算します。
アルゴリズムの定義
- 空のツリー:
MTH({}) = SHA-256()(空入力のハッシュ) - 単一リーフ:
MTH({d₀}) = LeafHash(d₀) - 複数リーフ: サイズ n のツリーに対し、k を n 未満の最大の 2 のべき乗とする
MTH({d₀, ..., dₙ₋₁}) = SHA-256(0x01 || MTH({d₀, ..., dₖ₋₁}) || MTH({dₖ, ..., dₙ₋₁}))
非 2 のべき乗サイズへの対応
ツリーサイズが 2 のべき乗でない場合(例: 5, 6, 7 リーフ)、MTH アルゴリズムは「n 未満の最大の 2 のべき乗」で分割を行います。 これにより、左部分木は常に完全二分木(2 のべき乗サイズ)となり、右部分木にのみ不完全さが集中します。
graph TD
subgraph "5 リーフのツリー(k=4 で分割)"
Root["ルート"]
Left["左部分木<br/>MTH(d₀..d₃)<br/>完全二分木"]
Right["右部分木<br/>MTH(d₄)<br/>単一リーフ"]
Root --> Left
Root --> Right
end
デフォルトのデモ構成では 64 票(2⁶)を扱うため、最終的なツリーは完全二分木になります。
ただし投票の追加途中や小規模な検証ケースでは非 2 のべき乗サイズが現れるため、実装は任意の treeSize に対する一般対応を備えます。
掲示板のリーフデータ形式
掲示板に追記される各投票のリーフデータは、コミットメントの正規化された 16 進数表現(0x なし、小文字、64 文字)を 32 バイトにデコードした生バイト列です。
leaf_data = hex_decode(normalized_commitment_hex) (32 バイト)
掲示板は以下の不変条件を維持します:
- 単調増加インデックス: 各投票に 0 から始まる連番が割り当てられる
- 重複排除: 同一の投票 ID やコミットメントの二重追記を拒否する
- ルート履歴: 各追記時点のルートハッシュをタイムスタンプとともに保存する
包含証明(Inclusion Proof)
包含証明は、特定のコミットメントがツリーの特定位置に含まれていることを、ルートハッシュに対して暗号学的に証明するものです。
構造
包含証明は以下の要素で構成されます:
| フィールド | 説明 |
|---|---|
| leafIndex | リーフの 0 始まりインデックス |
| proofNodes | 兄弟ハッシュの配列(Merkle パス) |
| treeSize | 証明時点のツリーサイズ |
| rootHash | 検証対象のルートハッシュ |
実装での名称
本章では RFC 6962 の抽象名を使いますが、API では異なるフィールド名で返します。
| 抽象名 | API フィールド名 |
|---|---|
proofNodes | merklePath |
rootHash | bulletinRootAtCast |
finalization-scoped verification GET の voterEvidence.userVote.proof や
/api/sessions/:sessionId/votes/:voteId/inclusion-proof がこの構造に対応します。
PATH アルゴリズム
RFC 6962 の PATH 関数に従い、Merkle パス(audit path、監査パス)を再帰的に生成します。
- ツリーをサイズ k(n 未満の最大の 2 のべき乗)で左右に分割
- 対象リーフが左部分木にある場合(
index < k):- 左部分木の PATH を再帰計算
- 右部分木のハッシュをMerkle パスに追加
- 対象リーフが右部分木にある場合(
index >= k):- 右部分木の PATH を再帰計算(インデックスを
index - kに調整) - 左部分木のハッシュをMerkle パスに追加
- 右部分木の PATH を再帰計算(インデックスを
検証手順
検証者は以下の手順で包含を確認します:
- コミットメントのリーフハッシュを計算:
LeafHash(commitment) - PATH アルゴリズムと同じ木構造に従い、Merkle パスのノードを順に結合
- 計算されたルートが期待するルートハッシュと一致するか確認
Recorded-as-Cast では、包含証明に加えて以下の cast-time 一貫性も確認します:
leafIndexがレシートのbulletinIndexと一致することtreeSizeがbulletinIndex + 1と一致すること(投票時点のツリーサイズ)
Merkle パスのサイズは O(log n) であり、64 票のツリーでは最大 6 ノードです。
graph TD
subgraph "インデックス 5 の包含証明"
Root["ルート ✓"]
N1["H(N3, N4)<br/>Merkle パス"]
N2["H(N5, N6) ← 計算対象"]
N3["H(L0,L1)"]
N4["H(L2,L3)"]
N5["H(L4,L5) ← 計算対象"]
N6["H(L6,L7)<br/>Merkle パス"]
L4["L4<br/>Merkle パス"]
L5["L5 ← 対象リーフ"]
Root --> N1
Root --> N2
N1 --> N3
N1 --> N4
N2 --> N5
N2 --> N6
N5 --> L4
N5 --> L5
end
style L5 fill:#4CAF50,color:#fff
style N1 fill:#2196F3,color:#fff
style N6 fill:#2196F3,color:#fff
style L4 fill:#2196F3,color:#fff
整合性証明(Consistency Proof)
整合性証明は、古いツリー状態(サイズ m)が新しいツリー状態(サイズ n)の前方互換的なプレフィックスであることを暗号学的に証明するものです。 これは追記専用性の確認に使われます。
構造
| API フィールド | 説明 |
|---|---|
| fromTreeSize | 古いツリーのサイズ(m) |
| toTreeSize | 新しいツリーのサイズ(n) |
| proofNodes | 整合性を証明するハッシュの配列 |
補足:
/api/sessions/:sessionId/bulletin/consistency-proofはproofNodesに加えてrootAtFromTreeSizeとrootAtToTreeSizeも返します。verification GET のrecorded_consistency_proof判定では、サーバーが取得・検査済みの root と整合性証明だけを 判定に使います。判定では from root を レシートのbulletinRootAtCast、to root を最終bulletinRootと照合します。
SUBPROOF アルゴリズム
RFC 6962 の SUBPROOF 関数に基づき、整合性証明を再帰的に生成します。
m = nかつ古いツリーが完全部分木: 空の証明を返すm = nかつ完全部分木でない: 部分木のルートハッシュを返すm = 1、n = 2、かつ古いツリーが完全部分木: 2 番目のリーフの生データを返す- k を n 未満の最大の 2 のべき乗とし:
m <= kの場合: 左部分木の SUBPROOF + 右部分木のハッシュm > kの場合: 右部分木の SUBPROOF + 左部分木のハッシュ
1→2 の特殊ケースでは、証明ノードは 2 番目のリーフの MTH ではなく生の 32 バイトリーフです。
検証時にそのリーフへ通常の LeafHash を適用し、SHA-256(0x01 || oldRoot || LeafHash(second_leaf)) が新しいルートと一致するかを確認します。
検証の意味
整合性証明の検証が成功することは、以下を意味します:
- 古いツリーのすべてのリーフが、新しいツリーにも同じ位置に同じ値で存在する
- 新しいツリーは古いツリーの末尾にリーフを追加しただけで構成されている
- 古いツリーのルートハッシュと新しいツリーのルートハッシュの両方が、提供された証明ノードから独立に再構成できる
この性質により、サーバーが過去に記録した投票を密かに削除したり順序を変更したりする攻撃を検出できます。
検証パイプラインにおける役割
CT Merkle ツリーは、4 段階検証モデルの主に 2 段階で使用されます。
| 検証段階 | CT Merkle の役割 |
|---|---|
| Recorded-as-Cast | 包含証明でコミットメントの記録を確認し、整合性証明で追記専用性を確認する |
| Counted-as-Recorded | zkVM ゲストが同じハッシュ規則で各 vote の包含を内部検証する |
Recorded-as-Cast では recorded_inclusion_proof と recorded_consistency_proof が表の役割を担当し、他の派生チェックもこれらの結果に基づきます。
ハッシュ規則の不一致は zkVM ゲスト内での検証失敗として即座に検出されます。
各チェックの判定ロジックは チェック一覧 > Recorded-as-Cast を参照してください。
RFC 6962 参照範囲
| 要件 | 対応状況 | 備考 |
|---|---|---|
| リーフとノードのドメイン分離 | 実装済 | 0x00 / 0x01 プレフィックス |
| MTH アルゴリズム | 実装済 | 再帰的分割 + キャッシュ最適化 |
| PATH 関数(包含証明) | 実装済 | O(log n) サイズのMerkle パス |
| SUBPROOF 関数(整合性証明) | 実装済 | 再帰的生成 + 1→2 特殊ケース対応 |
| 非 2 のべき乗サイズ | 実装済 | 最大 2 のべき乗分割 |
| 証明の独立検証 | 実装済 | ツリーの完全な再構築なしに検証可能 |
リーフハッシュへの使用タグ "stark-ballot:leaf|v1" の追加は RFC 6962 にない拡張であり、標準の CT 実装との直接的な相互運用は意図していません。
入力コミットメント
この章では、zkVM 入力のうち公開検証に使うフィールドを正準エンコーディングで束縛し、ジャーナルから再計算できる入力コミットメントを定義します。
入力コミットメントは、「証明されたデータセット」と「主張されたデータセット」の一致を検証可能にします。 バイトレベルの正準化は、TypeScript と Rust が同じ入力から決定的に同じ値を計算するための前提です。
概要
入力コミットメントは、公開可能な検証フィールドにドメインタグとバージョンを加えて正準連結し、SHA-256 で集約したハッシュ値です。
このハッシュ値は zkVM のジャーナル(公開出力)にコミットされます。
第三者は、ジャーナルに記録された入力コミットメントと、public-input.json などの公開可能な検証データから再計算した値を照合できます。
この照合により、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 証明は、「ゲストプログラムが正しく実行された」ことを証明します。 一方で、「どの入力に対して実行されたか」は 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 バイト | ハッシュ値 | 包含証明の兄弟ハッシュ |
public-input.json と公開監査アーティファクトとの関係
要点: public-input.json の全フィールドが入力コミットメントに束縛されるわけではありません。
残りのフィールドは、別のチェックで補完的に検証されます。
public-input.json は、zkVM 検証に使う秘密データを含まない検証用レコードです。
現行実装では schema、version、electionId、electionConfigHash、bulletinRoot、treeSize、totalExpected、logId、timestamp、methodVersion を含みます。
各投票については、index、コミットメント値、Merkle パスを含みます。
入力コミットメントが直接束縛するのは、フィールド一覧に示した対象のみです。
schema、version、electionConfigHash、logId、timestamp、methodVersion は対象外です。
対象外のフィールドは、proof bundle 内の election-manifest.json や close-statement.json を組み合わせて、次のように照合されます。
electionConfigHash→counted_election_manifest_consistent(manifest と journal 等を照合)logIdとtimestamp→counted_close_statement_consistent(close statement と journal 等を照合)schema、version→public-input.jsonの管理対象 artifact format として artifact 採用時に検証methodVersion→public-input.json採用時に journal と照合 Image ID 解決では正規化済み journal 値を使用
正準化規則
エンコーディングの決定性を保つために、次の規則を使います。
ソート規則
投票はエンコーディング前にインデックスの昇順にソートします。
各投票の index は一意であることが前提です。
この規則により、同じ投票集合から常に同一のバイト列が生成されます。
この規則に違反すると、TypeScript と Rust で異なるハッシュ値が計算され、検証が失敗します。
異常系の補助: 重複インデックスはプロトコル違反です。 TS/Rust 双方は決定性のために
commitment/merklePathで tie-break しますが、正常系仕様はindex昇順のままです。
エンディアン規則
すべての整数フィールドはリトルエンディアンでエンコードされます。
| 型 | バイト数 | エンコーディング |
|---|---|---|
| u16 | 2 | リトルエンディアン |
| u32 | 4 | リトルエンディアン |
16 進数正規化
コミットメント値やパスノードなどの 16 進数表現は、0x プレフィックスを除去した上でバイト列にデコードされます。
16 進数文字列のまま連結するのではなく、常にバイナリ表現を使用します。
TypeScript と Rust の同期
入力コミットメントは TypeScript(サーバー側)と Rust(zkVM ゲスト内)の双方で独立に計算されます。 両者の結果は一致する必要があります。
flowchart LR
subgraph TypeScript
TS_IN[public-input.json から抽出した<br/>入力コミットメント対象フィールド] --> 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 が処理した入力データと、公開可能な検証データから再構成される対象フィールドが食い違うことを意味します。 その場合、結果の信頼性は根本的に損なわれます。 対象外フィールドの補完検証は上記のとおりです。
各チェックの判定ロジックは チェック一覧 > Counted-as-Recorded を参照してください。
注意事項
入力コミットメントには、投票者の秘密データ(選択肢や乱数)は含まれません。 したがって、入力コミットメントの公開は投票の秘密性を損ないません。
入力順序に依存しない正準エンコーディングは、Property-based Testing の permutation invariance と、Lean による形式化 の input-commitment vectors で検査します。
STH ダイジェスト
このページでは、Signed Tree Head ダイジェストを第三者と照合し、スプリットビュー攻撃を緩和する仕組みを説明します。
ログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートを束縛するダイジェストにより、サーバーが異なるクライアントに異なるツリー状態を提示する攻撃を検出可能にします。
スプリットビュー攻撃に対する位置づけ
スプリットビュー攻撃(split-view attack)とは、悪意あるサーバーが異なる検証者に対して異なる掲示板の状態を提示する攻撃です。 STH ダイジェストは、掲示板の状態を(ログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュ)の組として束縛し、独立した第三者ソースとの合意確認を通じてこの攻撃を検出します。 具体的な攻撃と検出の流れは後述します。
flowchart TD
subgraph "STH ダイジェストの構成"
LID[ログ ID<br/>32 バイト]
TSZ[ツリーサイズ<br/>4 バイト]
TS[タイムスタンプ<br/>8 バイト]
BR[掲示板ルート<br/>32 バイト]
end
LID --> H[SHA-256]
TSZ --> H
TS --> H
BR --> H
H --> STH[STH ダイジェスト<br/>32 バイト]
STH --> J[zkVM ジャーナルに記録]
STH --> CS[close-statement.json に記録]
STH --> TP[第三者ソースの STH と照合]
本実装での検証対象: このページで言う「STH ダイジェスト」は
sthDigest自体です。 本実装は STH の署名検証は行いません(スコープの詳細は 合意ロジック を参照)。
ダイジェストフォーマット
sth_digest = SHA-256(
log_id ← 32 バイト
|| tree_size ← u32 リトルエンディアン (4 バイト)
|| timestamp ← u64 リトルエンディアン (8 バイト, Unix 時刻ミリ秒)
|| bulletin_root ← 32 バイト
)
SHA-256 への入力は合計 76 バイトです。
各フィールドの仕様
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ログ ID | 32 バイト | ハッシュ値 | 掲示板インスタンスの識別子 |
| ツリーサイズ | 4 バイト | u32 LE | 掲示板のリーフ数 |
| タイムスタンプ | 8 バイト | u64 LE | Unix 時刻(ミリ秒) |
| 掲示板ルート | 32 バイト | ハッシュ値 | Merkle ツリーのルート |
ログ ID
ログ ID は掲示板インスタンスを一意に識別する値です。 次の式で生成されます。
log_id = SHA-256("stark-ballot:bulletin-log|v1.0" || seed)
ドメインタグ "stark-ballot:bulletin-log|v1.0" と任意のシード値を連結し、SHA-256 でハッシュします。
ログ ID は掲示板のライフタイム中に変化しない固定値です。
ログ ID を STH ダイジェストに含めることで、異なる掲示板インスタンスの STH が偶然に衝突することを防止します。
スプリットビュー攻撃と検出メカニズム
攻撃シナリオ
sequenceDiagram
participant A as 投票者 A
participant S as 悪意あるサーバー
participant B as 投票者 B
S->>A: ツリー状態 X<br/>(64 票、ルート R₁)
S->>B: ツリー状態 Y<br/>(63 票、ルート R₂)
Note over A,B: A と B は互いに異なる<br/>ツリー状態を見ている
この攻撃では、サーバーは投票者 B に対して特定の票を除外したツリーを見せています。 投票者 B は自身に提示されたツリーに対する包含証明や整合性証明を検証できますが、投票者 A とは異なるツリーを見ていることに気づけません。
第三者合意による検出
検証 API は、VITE_STH_SOURCES に設定されたソースから STH evidence を取得し、ジャーナル内の STH ダイジェストと照合することでスプリットビューを検出できます。
sequenceDiagram
participant V as 検証者
participant S as 検証 API
participant T1 as 第三者ソース 1
participant T2 as 第三者ソース 2
V->>S: 検証リソースを要求
S->>S: ジャーナルから STH ダイジェスト D' を取得
S->>T1: STH を問い合わせ → D₁
S->>T2: STH を問い合わせ → D₂
S->>S: D' = D₁ = D₂ ?
alt 全て一致
S->>V: 合意成立を含む検証結果
else 不一致あり
S->>V: スプリットビューの疑い → 検証失敗
end
合意ロジック
合意判定の条件(最小一致数と全会一致の要求)は チェック一覧 > recorded_sth_third_party を正とします。
各ソースに対して次のフィールドを照合します。
| 照合フィールド | 条件 |
|---|---|
| STH ダイジェスト | 必須一致 |
| 掲示板ルート | 提供されている場合は一致 |
| ツリーサイズ | 提供されている場合は一致 |
外部 HTTPS ソースの応答は、sthDigest、bulletinRoot、treeSize、timestamp、logId
だけを許可する flat な canonical record として fail-closed に受理します。sthDigest は必須で、
その他のフィールドは任意ですが、指定された場合は型と形式を検証します。wrapper、未知のキー、
digest / root / log_id などの alias は受理しません。
設定済みソースの取得失敗、利用不能、または malformed response は、ほかの受理済みソースが
最小一致数を満たしていても合意失敗になります。
冒頭の注記のとおり応答署名は検証しません。外部アンカリングと STH の自動公開も本実装のスコープ外です。
zkVM との連携
zkVM ゲストプログラムは、入力として受け取ったログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートから STH ダイジェストを再計算し、ジャーナルにコミットします。
finalize 時には、同じツリー状態から close-statement.json も構築されます。
sthDigest は、配布対象アーカイブ bundle.zip に含まれる公開監査アーティファクトへ反映されます。
この仕組みにより、STARK 証明と bundle.zip 内の close-statement.json がともに特定のツリー状態へ束縛されます。
第三者はジャーナルの STH ダイジェストを独立ソースの値と照合することで、サーバーが証明と異なるツリー状態を提示していないかを確認できます。
検証パイプラインにおける役割
STH ダイジェストは 2 つの段階で利用されます。
| 段階 | チェック ID | 検証内容 |
|---|---|---|
| Recorded-as-Cast | recorded_sth_third_party | 独立ソースから取得した STH ダイジェストがジャーナルの値と一致するか |
| Counted-as-Recorded | counted_close_statement_consistent | close-statement.json の sthDigest が公開入力およびジャーナルの値と整合するか |
recorded_sth_third_party は既定では任意チェック(optional)ですが、STH ソースが設定されている場合は required に昇格します(昇格と最終判定の規則は ゲーティングロジック を参照)。
counted_close_statement_consistent は常に必須チェック(required)です。
close-statement.json がジャーナルと整合しない場合、検証は失敗します。
各チェックの判定ロジックは チェック一覧 > Recorded-as-Cast と チェック一覧 > Counted-as-Recorded を参照してください。
設定
第三者 STH 検証は環境変数で制御されます。
| 環境変数 | 説明 | コードフォールバック値 |
|---|---|---|
VITE_STH_SOURCES | カンマ区切りの same-origin template / 外部 HTTPS URL | 未設定(第三者照合を実行しない) |
VITE_STH_MIN_MATCHES | 必要な最小一致ソース数 | 2 |
STH ソースが未設定の場合、recorded_sth_third_party は not_run(未実行)となり、第三者照合は行いません。
開発用の .env.local.example では、必要な場合に有効化する例として次の値がコメントで示されています。
VITE_STH_SOURCES=/api/sessions/:sessionId/bulletin/sthVITE_STH_MIN_MATCHES=1
same-origin template と外部 HTTPS source の取り扱い
same-origin template: 相対 source として受理するのは exact template
/api/sessions/:sessionId/bulletin/sth だけです。任意で auditorId query を 1 つ指定できます。
検証 API は :sessionId を現在の session ID で解決し、認証済み session context から STH を
直接 project します。この内部取得では HTTP request や session capability header の転送を行いません。
同じ path を通常の HTTP endpoint として直接取得する場合は、same-origin request と path の
session ID に対応する X-Session-Capability が必要です。finalization が成功している session に限り、
{ data: { sth: { ... } }, meta } envelope を返します。
外部 HTTPS source: absolute HTTPS URL は、app と同じ origin を指す場合でも外部 source として 取得します。repository の session capability や同等の認証情報は送信しません。そのため、独立した 第三者 source は session 認証に依存しない公開 STH endpoint である必要があります。
same-origin projection の timestamp: same-origin projection が返す timestamp はジャーナル内の
canonical な時刻ではなく session.voting.lastActivityMs です。
そのため、第三者合意の一致判定で実際に照合するのは必須の sthDigest と、ソースが返した場合の bulletinRoot / treeSize です。
PoC における制約
本 PoC の開発用テンプレート(.env.local.example)では、STH ソースとして同一サーバー上の
session-scoped template(/api/sessions/:sessionId/bulletin/sth)を使う例を示しています。
同一サーバー上のソースのみでは防御力が限定的であるため、独立した組織が運営する複数ソースを VITE_STH_MIN_MATCHES >= 2 で構成することを推奨します。
ビットマップ Merkle
自票が集計に含まれたかを Merkle 証明で開示するためのビットマップツリーを扱う章です。
zkVM ゲスト内で計算されるビットマップにより、各投票インデックスが集計に含まれたかどうかを個別に検証可能にします。 Merkle 証明により、自票が含まれていることをサーバーを信頼せずに確認できます。
概要
Counted-as-Recorded 段階の検証では、zkVM に提示された入力に対する集計の正しさは STARK 証明で保証されますが、個々の投票者にとって「自票が集計に含まれたか」を直接確認する手段が別途必要です。
ビットマップ Merkle ツリーは、この「個別のカウント証明」を提供します。 zkVM ゲストは投票ごとの状態をビットマップとしてエンコードし、その Merkle ルートをジャーナルにコミットします。 投票者は自分のインデックスに対応するビットの Merkle 証明を取得し、「自票がカウントされたか」「そもそも prover に提示されたか」を独立に検証できます。
flowchart TD
subgraph "zkVM ゲスト内"
BM["ビットマップ<br/>[true, true, false, true, ...]"] --> PK[ビットパッキング<br/>LSB-first]
PK --> CH[32 バイトチャンク分割]
CH --> LH["リーフハッシュ<br/>SHA-256(0x00 || tag || chunk)"]
LH --> MT[Merkle ツリー構築]
MT --> ROOT["includedBitmapRoot / seenBitmapRoot"]
end
ROOT --> JNL[ジャーナルにコミット]
ビットマップの構造
ビットマップの定義
ビットマップは、ツリーサイズ(投票数)と同じ長さのブール配列です。 現行実装では同じエンコーディング規則を持つ 2 種類のビットマップを扱います。
includedBitmap[i] = true: インデックス i の投票が正常に検証され、集計に含まれたincludedBitmap[i] = false: インデックス i の投票が除外されたseenBitmap[i] = true: インデックス i の投票が prover に提示されたseenBitmap[i] = false: インデックス i の投票が prover に提示されなかった
本 PoC では 64 票を扱うため、ビットマップは 64 ビット(= 8 バイト)です。
LSB-first ビットパッキング
ブール配列は LSB-first(Least Significant Bit first)方式でバイト列にパッキングされます。
ビット配列: [b₀, b₁, b₂, b₃, b₄, b₅, b₆, b₇, b₈, ...]
バイト 0 = b₀ | (b₁ << 1) | (b₂ << 2) | ... | (b₇ << 7)
バイト 1 = b₈ | (b₉ << 1) | ...
| ビット位置 | バイトインデックス | バイト内ビット位置 |
|---|---|---|
| 0 | 0 | 0 (LSB) |
| 1 | 0 | 1 |
| 7 | 0 | 7 (MSB) |
| 8 | 1 | 0 (LSB) |
| 63 | 7 | 7 (MSB) |
64 ビットのビットマップは 8 バイトにパッキングされます。
32 バイトチャンク分割
パッキングされたバイト列は 32 バイト(256 ビット)単位のチャンクに分割されます。 各チャンクが Merkle ツリーの 1 つのリーフとなります。
- 1 チャンク = 32 バイト = 256 ビット分の投票カウント状態
- 最後のチャンクが 32 バイトに満たない場合はゼロパディング
本 PoC の 64 票は 8 バイトであるため、1 つのチャンク(24 バイトのゼロパディング付き)に収まります。
Merkle ツリーの構築
ハッシュ規則
ビットマップ Merkle ツリーは、CT Merkle ツリーと同一のハッシュ規則を使用します。
リーフハッシュ:
LeafHash = SHA-256(0x00 || "stark-ballot:leaf|v1" || chunk)
内部ノードハッシュ:
NodeHash = SHA-256(0x01 || left_hash || right_hash)
ドメイン分離プレフィックス(0x00 / 0x01)と使用タグ("stark-ballot:leaf|v1")は、CT Merkle ツリーの章で解説したものと同一です。
ツリー構築アルゴリズム
- 各 32 バイトチャンクにリーフハッシュを適用
- ボトムアップでペアを結合し、内部ノードハッシュを計算
- 奇数ノードがある場合は、そのまま次のレベルに昇格(ハッシュなし)
- ルートに到達するまで繰り返す
graph TD
subgraph "3 チャンクの場合"
R["ルート<br/>SHA-256(0x01 || N1 || C2)"]
N1["ノード<br/>SHA-256(0x01 || C0 || C1)"]
C2["リーフ 2<br/>SHA-256(0x00 || tag || chunk₂)"]
C0["リーフ 0<br/>SHA-256(0x00 || tag || chunk₀)"]
C1["リーフ 1<br/>SHA-256(0x00 || tag || chunk₁)"]
R --> N1
R --> C2
N1 --> C0
N1 --> C1
end
Merkle 証明の生成と検証
証明の構造
GET /api/sessions/:sessionId/finalizations/current/bitmap-proofs/:kind/:voteIndex
は、現在の finalization に対する証明材料を返します。kind と voteIndex はどちらも必須の
path parameter です。成功時の標準 JSON envelope は次の形です。
{
"data": {
"leafChunk": "...",
"auditPath": []
},
"meta": {
"requestId": "..."
}
}
data は以下の要素で構成されます:
| フィールド | 説明 |
|---|---|
| leafChunk | 対象ビットを含む 32 バイトチャンク(16 進数) |
| auditPath | ルートまでの兄弟ハッシュ配列(各要素にハッシュ値と位置) |
leafIndex(floor(voteIndex / 256))と bitOffset(voteIndex mod 256)は、クライアント側で path の voteIndex から導出します。
サーバーは返しません。
kind=included:includedBitmapRootに対する証明を返すkind=seen:seenBitmapRootに対する証明を返す
このエンドポイントは path のセッション ID と X-Session-Capability を必須とし、
voteIndex はそのセッションで cast した自票の index に限られます
(任意の近傍 index に対する proof oracle としては利用できません)。
ビット抽出
投票者は受け取ったチャンクから、path で指定した voteIndex に対応する bitIndex のビットを以下の手順で抽出します:
bit_offset = bit_index mod 256
byte_index = bit_offset / 8 (整数除算)
bit_in_byte = bit_offset mod 8
included = (chunk[byte_index] AND (1 << bit_in_byte)) != 0
kind=included で included = true なら、自票がカウントされたことを意味します。
kind=seen で included = true なら、自票が prover に提示されたことを意味します。
検証手順
- ジャーナルの正の整数
treeSizeと 32 バイトの対象 root を認証済み context として使い、bitIndexが0 <= bitIndex < treeSizeを満たすことを確認 leafIndex = floor(bitIndex / 256)、bitOffset = bitIndex mod 256、leafCount = ceil(treeSize / 256)をクライアント側で導出leafChunkが 32 バイト、各 sibling hash が 32 バイトであることに加え、auditPathの長さとleft/rightの並びがleafIndexとleafCountから 一意に決まる tree shape と一致することを確認- 最終 chunk の
treeSizeより後ろにある未使用 bit がすべて 0 であることを確認 - チャンクのリーフハッシュを計算:
SHA-256(0x00 || "stark-ballot:leaf|v1" || chunk) - Merkle パス(
auditPath)に沿ってルートまで再計算:- 兄弟の位置が
left→SHA-256(0x01 || sibling || current) - 兄弟の位置が
right→SHA-256(0x01 || current || sibling)
- 兄弟の位置が
- 計算されたルートが、
kindに対応するジャーナル上のルートと一致するか確認kind=included→includedBitmapRootkind=seen→seenBitmapRoot
- 以上がすべて成功した場合にのみ、チャンクから抽出したビット値を個票の状態として解釈
flowchart TD
CK[チャンク受信] --> SH{treeSize / index / path shape<br/>unused padding は正しい?}
SH -->|正しい| LH["リーフハッシュ計算"]
LH --> AP[Merkle パスに沿って<br/>ルートを再計算]
AP --> CMP{計算ルート =<br/>bitmap root ?}
CMP -->|一致| EX[ビット抽出結果を解釈<br/>included = true/false]
EX --> V[証明有効]
CMP -->|不一致| IV[証明無効]
SH -->|不正| IV
zkVM ゲストとの連携
ビットマップルートは zkVM ゲストプログラム内で計算され、ジャーナルにコミットされます。
ゲストプログラムは以下の手順を実行します:
- 各投票に対してコミットメントの再計算と包含証明の検証を実施
- prover に提示された投票インデックスを
seenBitmapに記録 - 検証に成功して集計対象になった投票インデックスを
includedBitmapに記録 - それぞれのビットマップを LSB-first でパッキングし、32 バイトチャンクに分割
- CT スタイルのハッシュ規則で Merkle ルートを計算
seenBitmapRootとincludedBitmapRootをジャーナルにコミット
この計算はゲスト内で行われるため、STARK 証明がビットマップの正しさも保証します。 サーバーが事後的にビットマップを改ざんしても、ジャーナルのルート値と一致しなくなるため検出されます。
サーバーのビットマップデータ管理
現行 method 14 のジャーナルは includedBitmapRoot と seenBitmapRoot の両方を必須で
コミットします。full bitmap 自体はジャーナルの公開出力ではありません。production host は
admitted prover input から includedBitmap と seenBitmap を再構成し、guest output の root と
一致した場合に sidecar を出力します。finalization 時の保存・復元と bitmap-proof endpoint はさらに、
bitmap の長さが treeSize と一致し、再計算した root が保存 root とジャーナル root の両方に
一致した場合にのみ、その非公開 sidecar を採用します。
これらは配布対象アーカイブ bundle.zip には含まれません。
async finalize 経路では、必要に応じて S3 の sibling object から復元されます。
安全性ゲートと検証結果の分岐
採用前と採用後で、counted_my_vote_included の判定が次の 2 つに分岐します。
- 採用前に弾かれる、または証明材料が取得できない →
counted_my_vote_includedはnot_run(証拠不足による fail-closed)。 例:- private bitmap sidecar が無い
- 保存や復元時の root 一致ゲートで採用されなかった
- cast-time 証跡(
voteReceipt/userVote.proof)が store から再構成できずvoteReceipt.bulletinIndexが確定しない
- 採用後にクライアント側の root 照合が失敗 →
counted_my_vote_includedはfailed。 サーバーが返した chunk と Merkle パスから再計算したルートが、ジャーナルのincludedBitmapRoot(またはseenBitmapRoot)と一致しない。
採用前ゲートおよび counted_my_vote_included の評価詳細は チェック一覧 > counted_my_vote_included を参照してください。
検証パイプラインにおける役割
ビットマップ Merkle 証明は、Counted-as-Recorded 段階のチェックとして使用されます。
| チェック ID | 検証内容 |
|---|---|
counted_my_vote_included | ビットマップ Merkle 証明により、自票のインデックスがカウントされたことを確認する |
対応する seenBitmap sidecar から生成した proof も取得・採用できた場合
(seenBitmapRoot はジャーナル必須。前述)、
このチェックは「prover に提示されたが無効化された」
と「そもそも提示されなかった」を区別して説明します。seen proof が取得または検証できない場合は、
included bit が false である理由を unknown_excluded として fail-closed に扱い、推測で分類しません。
各チェックの判定ロジックは チェック一覧 > Counted-as-Recorded を参照してください。
プライバシーに関する注意
ビットマップ Merkle 証明では、対象ビットを含む 32 バイトチャンク全体がクライアントに提供されます。 1 チャンクは 256 ビット分のカウント状態を含むため、近傍のインデックスのカウント状態が同時に開示されます。
本 PoC では 64 票が 1 チャンクに収まるため、チャンクを受け取った投票者は全 64 票のカウント状態を知ることができます。 この設計は、63 票がボットでありチャンク漏洩の情報価値が限定的である、という割り切りに基づきます。
開示範囲を狭めるには、chunk を小さくして Merkle tree を深くする方法があります。 一方で leaf 数、proof path、guest 内の hash 計算が増えるため、証明時間と artifact サイズを再評価する必要があります。現行 PoC は 32 バイト chunk を維持し、この 緩和は実装していません。
LSB-first packing とビット境界の扱いは、Property-based Testing で生成入力を使って境界条件を探索し、Lean による形式化 の bitmap vectors で抽象モデルとの対応を検査します。
zkVM 設計
この章群では、投票集計の正当性を RISC Zero zkVM で検証し、レシートとジャーナルを検証パイプラインへ渡す流れを説明します。
mock、RISC0_DEV_MODE=1、production STARK proof は、それぞれ異なる検証前提として区別します。
この部の章
- zkVM の基礎 では、zkVM の概念、RISC Zero の選択理由、データフロー、保証境界を扱います。
- ゲストプログラム では、zkVM 内で実行される検証と集計ロジックを扱います。
- ホストと証明生成 では、ホストプログラムと同期または非同期の証明パスを扱います。
- 検証サービス では、Rust ベースのレシート検証を扱います。
- Image ID では、ゲストバイナリの暗号的識別子と管理を扱います。
想定読者と前提
- 想定読者は、集計の正当性を STARK で証明したい実装者と運用者です。
- 前提は、暗号プロトコル の入力コミットメントと Merkle ツリーを把握していることです。
この部で扱わないもの
- RISC Zero SDK の API リファレンスやアップグレード手順
- STARK / FRI の数学的構成証明
- ECS Fargate などインフラ側の構成
STARK / FRI の概念は zkVM の基礎 で扱います。 インフラ側の構成は AWS アーキテクチャ を参照してください。
関連する章
- 暗号プロトコル は、ゲストプログラムが入力として受け取るプリミティブを説明します。
- 検証パイプライン は、生成されたレシートとジャーナルの検証方法を説明します。
- AWS アーキテクチャ は、非同期プローバーの実行環境を説明します。
- 第三者検証ガイド は、
bundle.zipでレシートをローカル監査する手順を説明します。
zkVM の基礎
RISC Zero zkVM を採用した理由、データフロー、暗号学的保証の境界を整理する章です。
RISC Zero を採用する理由
- Trusted setup 不要で扱いやすい。
- RISC-V 上で通常の Rust コードを使えるため、実装と監査の往復がしやすい。
- Image ID による実行バイナリ照合と組み合わせやすい。
アーキテクチャ概要
zkVM パイプラインは 3 つのコンポーネントで構成されます。
flowchart TB
subgraph C1["第1フェーズ: 入力構築"]
SES[セッションデータ] --> IB[入力ビルダー]
IB --> INP[ZkVMInput]
end
subgraph C2["第2フェーズ: 証明生成"]
INP --> HOST[ホストプログラム]
HOST --> GUEST["ゲストプログラム<br/>(zkVM 内実行)"]
GUEST --> JNL[ジャーナル]
HOST --> RCP[レシート<br/>STARK 証明]
end
subgraph C3["第3フェーズ: 検証"]
RCP --> VS[検証サービス]
VS --> VR[検証レポート]
end
| コンポーネント | 言語 | 責務 |
|---|---|---|
| ゲストプログラム | Rust | zkVM 内で投票を検証して集計し、結果をジャーナルにコミットする |
| ホストプログラム | Rust | 入力を受け取り、zkVM を起動して STARK 証明(レシート)を生成する |
| 検証サービス | Rust | レシートの STARK 検証を実行し、期待される Image ID との一致を確認する |
zkVM とは
RISC Zero zkVM は、RISC-V(RV32IM)命令列として実行されるゲストプログラムについて、その実行が正しいことを STARK 証明で示すゼロ知識 VM です。
本システムでは「投票検証と集計」をゲストとして実行し、ホストがレシート(seal + journal)を生成します。
第三者は Receipt::verify(image_id) により、指定したゲストバイナリ(Image ID)で実行された結果であることを検証できます。
30 秒で要点
| 観点 | 要点 |
|---|---|
| 何を証明するか | 「指定した Image ID のゲストが、ある非公開入力で正常終了し、このジャーナルを出力した」こと |
| 何を公開するか | ジャーナル(公開出力)とレシート(証明本体) |
| どこが信頼アンカーか | Image ID(どのゲストを検証対象にするか) |
| この PoC での意味 | 集計値、除外件数、入力整合性を暗号学的に検証可能にする |
ジャーナルは証明に束縛される公開出力であり、検証成功後に改ざんできません。
Receipt::verify(image_id) 単体は、ゲストに渡された非公開入力を特定せず、公開された public-input.json との一致も保証しません。
本 PoC は、公開入力のうちコミットメント対象フィールドから値を再計算し、ジャーナルの inputCommitment と照合する必須チェック counted_input_commitment_match によって、その対象フィールドを証明済み実行へ束縛します。
対象外フィールドは別の必須チェックで補完します。詳細は 入力コミットメント を参照してください。
ジャーナル項目の一覧と各フィールドが何を保証するかは ゲストプログラム > ジャーナル出力 を参照してください。
配布対象ファイル(bundle.zip 等)との違いは バンドル構造 を参照してください。
RISC Zero zkVM 実装の要点
RISC Zero 公式ドキュメントに基づく実装上の要点です。
- 実行モデル: ゲストは RV32IM として決定論的に実行され、外部との境界は syscall (
ecall) で扱う - 証明の分割: 長い実行はセグメント証明に分割される(大規模実行を扱うため)
- 再帰合成と圧縮: SDK は再帰合成や
succinct/Groth16への圧縮をサポートするが、本 PoC はCompositereceipt のまま配布する - 検証 API:
Receipt::verify(image_id)が、証明本体と Image ID の束縛を同時に検証する - 公開出力の束縛:
journalは証明に束縛されるため、検証成功後に改ざんできない
ジャーナルとレシート
- ジャーナル: ゲストが公開出力としてコミットするデータ(検証済み集計、除外情報、入力コミットメントなど)
- レシート: ジャーナルと STARK 証明(seal)のペア。 検証成功時、ジャーナルは正しいゲスト実行結果として受理できる
flowchart TB R[レシート] R --> S[Seal<br/>STARK 証明] R --> J[ジャーナル<br/>公開出力]
ジャーナル各フィールドの定義と検証チェックとの対応は ゲストプログラム > ジャーナル出力 を参照してください。
excludedSlots と rejectedRecords の使い分けは スロット / レコード分離モデル を参照してください。
数学ミニ補足(読み飛ばし可)
STARK の直感的理解に必要な最小限の説明だけを記します。
1. 巡回ドメイン(cyclic domain)
有限体 F の乗法群の部分群 H = {1, w, w^2, ..., w^(n-1)} を評価点集合(ドメイン)として使います。
RISC Zero はこれを cyclic domain と呼びます。
実行トレース(レジスタ値や遷移)は、この H 上で評価される多項式として扱われます。
2. 有限体上の多項式除算
制約違反の有無は、概念的には次の形で整理できます。
- 制約多項式を
C(x)とする - 評価領域
Hの零化多項式をZ_H(x)とする(典型例:Z_H(x) = x^n - 1) - すべての点で制約を満たすなら
C(h)=0(h in H)となり、C(x)はZ_H(x)で割り切れる - したがって
Q(x) = C(x) / Z_H(x)が多項式として成立するかを検査すれば、制約満足性をチェックできる
実際の STARK では、FRI(Fast Reed-Solomon IOP)と Merkle コミットメントを組み合わせて、この条件を効率的に検証します。
この PoC での保証境界
Receipt::verify(image_id) の成功は強い保証ですが、それ単体では「全票が提示された」ことまでは保証せず、Verified 表示には不十分です。
Verified は、STARK 検証成功に加えて、excludedSlots == 0、totalExpected == treeSize、追記専用性と締切 STH を含む必須チェックがすべて成功した場合にのみ成立します。
詳細なゲーティング規範と判定ロジックは ゲーティングロジック を参照してください。
参考資料(RISC Zero 公式)
データフロー
投票セッションの開始からレシート検証までの全体的なデータフローを示します。
sequenceDiagram participant V as 投票者 participant S as サーバー participant B as 掲示板 participant H as ホスト participant G as ゲスト (zkVM) participant VS as 検証サービス Note over V,B: 投票フェーズ V->>S: 投票意図(選択肢・乱数)+ コミットメント S->>B: 掲示板に追記 S-->>V: 投票レシート (インデックス, ルート) Note over S: ボット投票を自動追加 Note over S,G: 集計フェーズ S->>S: 入力ビルダーで ZkVMInput を構築 S->>H: ZkVMInput を渡す H->>G: ゲスト実行を開始 G->>G: 各投票のコミットメント検証 G->>G: 各投票の包含証明検証 G->>G: 集計 + ビットマップ計算 G-->>H: ジャーナル出力 H-->>S: STARK レシート + ジャーナル Note over S,VS: 検証フェーズ S->>VS: 検証 bundle 参照 + 期待 Image ID VS->>VS: bundle 内の receipt.json を解決 VS->>VS: Receipt::verify(image_id) VS-->>S: 検証レポート S-->>V: 検証結果を提供
ゲストプログラム
zkVM 内のゲストは、入力検証から集計、ビットマップ計算までを実行します。
ゲストプログラムは、投票コミットメントの再計算、RFC 6962 包含証明の検証、集計の実行、ビットマップルートの計算を行い、結果をジャーナルにコミットします。
契約上重要なヘルパー(コミットメント計算、正準エンコーディング、RFC 6962 包含証明、ビットマップルートなど)は zkvm/contract-core/ に集約されており、ゲストとホストが同じ実装を参照します。
概要
ゲストプログラムは RISC Zero zkVM 上で動作する Rust プログラムです。
現行の guest crate は #![no_std] で構築され、risc0-zkvm の default features を無効化した上で heap-embedded-alloc を使用します。
ホストから投票データ(選択肢、乱数、コミットメント、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| I5{guest 境界と<br/>Merkle パス長が有効?}
I4 -->|No| FAIL
I5 -->|Yes| NEXT[フェーズ 2 へ]
I5 -->|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
入力の境界条件
次のいずれかに該当する入力は、ジャーナル生成前に fail-closed で拒否されます。
bulletin_rootがゼロtree_sizeが 0timestampが 0tree_size > 1,000,000total_expected > 1,000,000votes.length > 1,000,000- 候補別 tally bucket が 1,000,000 を超える
- Merkle パス長が
u16::MAXを超える - Merkle パスノード総数が 1,000,000 を超える
- 推定構造化入力サイズが 64 MiB を超える
votes.length > tree_size のような入力は、上記境界内であれば事前 reject されません。
重複や範囲外は record 単位で rejectedRecords に反映されます。
投票の 6 段階検証
各投票に対して、以下の 6 つの検証が順に実行されます。 いずれかが失敗した投票は即座に「無効」として除外され、以降の検証はスキップされます。
各検証の失敗条件と、失敗したレコードがどのカウンタに反映されるかを示します。
| # | 検証 | 失敗条件 | 反映先 |
|---|---|---|---|
| 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 で無効化された範囲内かつ初出のスロットは invalidPresentedSlots として観測され、#1〜#2 の却下は rejectedRecords のみに反映されます。
ビットマップへの反映を含む各カウンタの意味論は スロット / レコード分離モデル を正とします。
コミットメント再計算と照合(#4)
ゲスト内で投票者の(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、入力として渡された値と照合します。 これにより、投票者が主張する選択肢が掲示板上のコミットメントと一致することが保証されます。 計算規則は コミットメントスキーム を参照してください。
RFC 6962 包含証明検証(#6)
投票のコミットメントが掲示板 Merkle ツリーに含まれることを、RFC 6962 PATH 関数ベースの CT スタイル包含証明で検証します。
投票のインデックスと Merkle パスから掲示板ルートを再計算し、入力の bulletin_root と一致するかを確認します。
リーフとノードハッシュの規則とドメインタグは CT Merkle ツリー を参照してください。
集計ロジック
6 段階検証をすべて通過した投票は「有効」として集計に加算されます。
集計は選択肢ごとの配列(5 要素)で管理し、有効投票のインデックスに対応する includedBitmap のビットを true に設定します。
無効票の seenBitmap / includedBitmap への反映は スロット / レコード分離モデル を参照してください。
スロット / レコード分離モデル
現行のゲストプログラムは、掲示板スロットに対する完全性と入力レコードの異常を別々に記録します。
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 | 除外されたスロットの総数(定義は スロット / レコード分離モデル の式) |
| 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 は、抽象モデルと実装の対応付けを検査します。
ホストと証明生成
この章では、ホストプログラムが zkVM 入力を組み立て、同期モードまたは非同期モードで STARK 証明を生成する流れを扱います。
| モード | 実行形態 |
|---|---|
| 同期 | ローカルにビルドされた zkvm/target/release/host をローカルプロセスとして起動 |
| 非同期 | production feature 有効のホストバイナリを組み込んだ Fargate 用コンテナを ECS Fargate タスクとして実行 |
どちらのモードも同じ Rust ホスト CLI / 入出力契約を使用します。
このページの構成:
パイプライン全体像
証明生成パイプラインは、入力構築、ホスト実行、出力処理の 3 フェーズで構成されます。
flowchart TD
subgraph "第1フェーズ: 入力構築 (TypeScript)"
SD[セッションデータ] --> IB[入力ビルダー]
IB --> ZI[ZkVMInput]
ZI --> SER[シリアライズ<br/>JSON ファイル]
end
subgraph "第2フェーズ: ホスト実行 (Rust)"
SER --> HOST[ホストバイナリ]
HOST --> ENV[ExecutorEnv 構築]
ENV --> PROVER[デフォルトプローバー]
PROVER --> GUEST[ゲスト実行]
GUEST --> PROOF[STARK 証明生成]
end
subgraph "第3フェーズ: 出力処理"
PROOF --> RCP["レシートラッパー JSON<br/>(seal + journal)"]
PROOF --> OUT["出力 JSON<br/>(デコード済みジャーナル)"]
end
入力構築
現行 PoC の選挙 shape
デモの session orchestration は、選択肢 A〜E、ユーザー 1 票、
ボット 63 票、合計 64 slot、Merkle tree depth 6 に固定されています。
default config の authority は
packages/zkvm-contract/src/zkvm/contract-constants.ts と
packages/zkvm-contract/src/zkvm/election-config.ts です。domain と verification
package の公開定数は repository boundary test で同値性を確認します。
これは PoC の再現可能なデモ構成であり、zkVM guest や RFC 6962 Merkle
実装を任意の session で常に 64 票に制限するプロトコル主張ではありません。
host input は admitted election configuration の totalExpected と bulletin の
treeSize を保持し、その一致は検証パイプラインで fail-closed に確認します。
セッションデータからの抽出
入力ビルダーは、投票セッションに蓄積されたデータから zkVM 入力を構築します。
flowchart LR
subgraph セッションデータ
EID[選挙 ID]
ECFG[選挙設定<br/>+ 設定ハッシュ]
VOTES["投票データ<br/>(選択肢, 乱数, コミットメント)"]
BULL["掲示板<br/>(ルート履歴, 包含証明)"]
LID[ログ ID]
end
subgraph ZkVMInput
EID2[election_id]
ECFG2[election_config_hash]
BR[bulletin_root]
TS[tree_size]
TE[total_expected]
VWP["votes[]<br/>(VoteWithProof)"]
LID2[log_id]
TSTAMP[timestamp]
end
EID --> EID2
ECFG --> ECFG2
ECFG --> TE
BULL --> BR
BULL --> TS
VOTES --> VWP
LID --> LID2
入力構築の主な処理は次のとおりです。
- 掲示板の最新 STH スナップショット取得: ルートハッシュ、ツリーサイズ、タイムスタンプを取得します。
- 選挙設定の整合性確認:
electionConfigとelectionConfigHashが一致することを確認します。 - 投票データの変換: 各投票の選択肢を整数に変換します(A=0, B=1, C=2, D=3, E=4)。
- Merkle パスの解決: 各投票について、掲示板から最新の包含証明を取得します。
- 総投票数の設定: 選挙設定の
totalExpectedを設定します(デモ定数は 現行 PoC の選挙 shape を参照)。
通常のセッション入力では、投票インデックスが 0 から連続する canonical CT index であることを要求します。
教育的な除外シナリオでは、元の掲示板インデックスを保つために sparse index を許可します。
これにより、ゲスト側で missingSlots として観測できます。
Merkle パスの解決戦略
各投票の Merkle パスは、session の canonical bulletin authority から解決します。 保存済みの投票レコードは proof path を保持しないため、別の事前計算パスへの fallback はありません。
flowchart TD
START[Merkle パス解決] --> P1{掲示板から<br/>包含証明を取得可能?}
P1 -->|Yes| CHK{leaf index と<br/>treeSize が一致?}
CHK -->|Yes| USE1[掲示板の証明を使用]
CHK -->|No| ERR[エラー]
P1 -->|No| ERR
ホストプログラムの実行
ホストバイナリの役割
ホストバイナリは Rust で記述された CLI プログラムです。 証明モードでは次の処理を行います。
- JSON 形式の入力ファイルを読み込みます。
- JSON のバイト配列表現を Rust の固定長配列型へ変換します(
Vec<u8>→[u8; 16/32])。 ExecutorEnvに入力をシリアライズして設定します。- デフォルトプローバーを使用して zkVM ゲストを実行します。
- レシート(STARK 証明 + ジャーナル)を取得します。
- ジャーナルをデコードし、出力ファイルに書き出します。
入力 JSON は TypeScript 側のエグゼキューターが事前に正規化して生成します(UUID/ハッシュ文字列をバイト配列へ変換)。
Image ID の確認だけを行う場合は host --print-image-id [--json] を使います。
このモードでは入力ファイルを読まず、証明生成やアーティファクト出力も行いません。
--json 付きでは imageId と methodVersion を含む JSON を stdout に出力します。
境界に違反する入力が渡された場合、host run は fail-closed で停止し、receipt と journal を出力しません。 境界条件の詳細は 処理パイプライン を参照してください。
非同期モードで S3 に置かれる work input には、host CLI の証明入力に加えて
election_config / electionConfig も含まれます。コンテナ entrypoint はこの設定を
election-manifest.json の生成と入力の整合性検査に使います。退役済みの
contractGeneration / contract_generation は work input に含めません。
出力ファイル
証明モードのホストバイナリは 2 つの JSON ファイルを出力します。 ビットマップ整合性検査を通過した場合は、private bitmap artifact も追加で出力します。
| ファイル | 内容 |
|---|---|
| レシートラッパー JSON | { "receipt": ..., "image_id": "0x..." } 形式のラッパー JSON |
| 出力 JSON | デコード済みのジャーナル(集計結果、除外情報、各種ハッシュ値) |
レシートラッパー JSON には top-level image_id フィールドも含まれます。
検証サービスでの使われ方は 検証サービス を参照してください。
ホストはビットマップの整合性を確認し、一致した場合のみ次の非公開アーティファクトを出力します。
| ファイル | 内容 |
|---|---|
*-bitmap.json | counted bitmap の厳密 artifact(includedBitmapRoot と対応) |
*-seen-bitmap.json | presented bitmap の厳密 artifact(seenBitmapRoot と対応) |
非同期モードでの配置と非同梱の扱いは 配布対象アーカイブの構築 を参照してください。
同期モード
同期モードでは、TypeScript のサーバーサイドプロセスからホストバイナリを直接起動します。
sequenceDiagram participant S as サーバー (TypeScript) participant E as エグゼキューター participant H as ホストバイナリ (Rust) participant FS as ファイルシステム S->>E: executeZkVM(input) E->>FS: 入力 JSON を一時ファイルに書き出し E->>H: 子プロセスとして起動 Note over H: zkVM ゲスト実行<br/>+ STARK 証明生成 H->>FS: レシートラッパー JSON + 出力 JSON を書き出し H-->>E: プロセス終了 E->>FS: 出力ファイルを読み取り E->>FS: 一時ファイルを削除 E-->>S: ZkVMExecutionResult
同期モードの特性
| 項目 | 値 |
|---|---|
| 起動方式 | Node.js child_process.exec |
| タイムアウト | 10 分(600 秒) |
| 一時ファイル | リポジトリ直下の .zkvm-temp/ 配下 |
| 環境変数 | 基本は Node.js の process.env を引き継ぐ。RUST_LOG はログに影響するが、証明モードは resolved runtime profile が決定し、adapter が child process の RISC0_DEV_MODE を設定または除去する |
| エラー処理 | 終了コード非ゼロ、タイムアウト、ファイル不在で失敗 |
結果の変換
エグゼキューターは出力 JSON のフィールドを TypeScript の命名規則へ正規化し、文字列だけでなくバイト配列形式の値も受理します。
ハッシュ系フィールドは 0x 付き 16 進文字列に、election_id は UUID 文字列に変換して ZkVMExecutionResult を構築します。
非同期モード
AWS 環境では、証明生成を ECS Fargate タスクとして非同期に実行します。 STARK 証明の生成に数分を要するため、Lambda のタイムアウト制限を回避し、専用のコンピューティングリソースを割り当てます。
sequenceDiagram participant S as サーバー participant SQS as SQS participant D as ディスパッチ Lambda participant SFN as Step Functions participant ECS as ECS Fargate participant S3 as S3 participant CB as コールバック Lambda S->>S3: 入力 JSON アップロード S->>SQS: finalization リクエスト<br/>(inputS3Key を含む) SQS->>D: work message D->>SFN: SFN 実行開始<br/>(inputS3Key を渡す) Note over SFN: イメージ署名チェック SFN->>ECS: プローバータスク起動 ECS->>S3: 入力 JSON ダウンロード Note over ECS: ホストバイナリ実行<br/>+ STARK 証明生成 ECS->>S3: レシート・出力・公開アーティファクト・<br/>バンドルをアップロード ECS-->>SFN: タスク完了 SFN->>CB: 成功コールバック CB->>CB: セッションデータ更新
イメージ署名チェック
Step Functions はプローバータスク起動前にコンテナイメージの署名を検証し、承認されたイメージ以外の実行を拒否します。 署名と digest pin の運用は イメージ署名 を参照してください。
配布対象アーカイブの構築
非同期モードでは、ホストバイナリの出力のうち秘密データを含まないファイルだけを bundle.zip に同梱し、input.json などの秘密入力は含めません。
同梱対象の一覧、整合性検査ルール、取得経路は バンドル構造 を、public-input.json の項目と inputCommitment の関係は 入力コミットメント を参照してください。
async Docker entrypoint は、journal.json、public-input.json、election-manifest.json、close-statement.json を生成する際に methodVersion(現行 14)と inputCommitment の整合性を検査し、契約と一致しない host artifact は fail-closed で停止します。
非同期 S3 出力では、通常は次のオブジェクトが保存されます。
- ホスト生出力の
*-output.jsonと*-receipt.json - 生成済みの公開アーティファクト
- private bitmap sibling(
included-bitmap.json/seen-bitmap.json。非公開のままbundle.zipには同梱しない) bundle.zip
journal.json は bundle.zip の中に生成される公開アーティファクトであり、固定名の standalone journal.json は sibling object として保存しません。
entrypoint が非ゼロ終了した場合は、失敗分類だけを持つ private sibling artifact
failure-marker.json を同じ finalization scope に best-effort で保存します。S3 object は
sessions/{sessionId}/{finalizationId}/failure-marker.json という finalization identity に
従います(分類の制限、create-only 保存、finalization writer による検証などの契約は
非同期プローバー を参照)。
非同期モードの特性
| 項目 | 値 |
|---|---|
| ホスト実行タイムアウト | 15 分(900 秒)がデフォルト。entrypoint の ZKVM_TIMEOUT_SECONDS で変更可能 |
| ECS タスクタイムアウト | 30 分(1800 秒)がデフォルト。Terraform の prover_task_timeout_seconds が Step Functions の RunProver に設定 |
| リトライ | S3 アップロードはデフォルトで最大 3 回の合計試行(初回 + 最大 2 回のリトライ)。再試行間隔は指数バックオフ |
| エラー処理 | entrypoint が private failure marker を best-effort で保存し、Step Functions がタスク失敗を検出して failure callback を実行 |
| ステータス確認 | クライアントは /api/sessions/:sessionId/finalizations/current で current finalization resource をポーリング |
開発モードの動作
dev-mode build のホストで RISC0_DEV_MODE=1 を設定すると、RISC Zero は STARK 証明を生成せず、フェイクレシートを返します。
フェイクレシートを作れるのは pnpm build:zkvm:dev-mode など dev mode を許可するホストビルドだけです。
production feature 有効のホスト(pnpm build:zkvm や Fargate prover image)は
RISC0_DEV_MODE=1 を受理しません。非同期 prover の entrypoint は production proof profile
(S3-backed 実行では RUNTIME_PROFILE=target-aws-async)を要求し、退役済みの
RISC0_DEV_MODE / FORCE_DEV_MODE を host process の環境から除去します。
| 項目 | 開発モード (RISC0_DEV_MODE=1) | 本番モード |
|---|---|---|
| 証明の種類 | フェイクレシート | 本物の STARK 証明 |
| 実行時間 | 約 100 ミリ秒 | 約 370 秒(64 票の場合) |
| 保証 | 暗号学的保証なし | guest 実行に対する暗号学的証明 |
| 検証サービス | Fake として検出 | 完全な STARK 検証を実行 |
production receipt が証明するのは、対応する Image ID の guest が公開 journal を生成したことです。 これだけで投票システム全体の安全性、運用者に対する ballot secrecy、または選挙運用の妥当性まで 証明するものではありません。
開発モードのレシートは内部的には InnerReceipt::Fake 型です。
検証サービス では通常 dev_mode 扱いになります(image_id 不一致などの事前条件違反時のみ Failed)。
dev_mode は診断ステータスであり、本番モードの成功検証としては採用されません。
開発モードは次の用途に限定されます。
- host CLI や verifier 連携を含むローカルの高速フィードバック
- TypeScript と Rust の契約を短時間で確認する smoke test
- dev-mode receipt 分岐を明示的に通す CLI と E2E 検証
アプリケーションの proof executor は immutable な runtime profile が選択します。
mock 系 profile(local-demo / static-mock-e2e)の mock executor は、ホストバイナリも
RISC Zero SDK も呼びません。profile ごとの proof evidence の対応は
AWS runtime 境界 を参照してください。
USE_MOCK_ZKVM と server 設定としての RISC0_DEV_MODE は置換済み selector であり、
runtime profile の入力に混在させると fail-closed で拒否されます。なお、dev-mode 対応 host
CLI を直接診断するときの RISC0_DEV_MODE=1 は、この application runtime selector とは
異なる低レベルの host 設定です。
検証サービス
この章では、STARK レシートを検証する Rust サービスの構造と、ローカルおよび Lambda での使い方を扱います。
検証サービスは、レシートの STARK 検証をサーバー側で実行し、結果をレポートとしてクライアントに提供する Rust コンポーネントです。
概要
STARK 証明の検証は証明生成に比べれば計算コストが低い処理で、本システムではサーバー側で検証を行い、その結果をレポートとしてクライアントに提供します(委任の理由と信頼境界は サーバー側検証の信頼境界 を参照)。
flowchart LR
subgraph 入力
RCP[レシート / バンドル]
EID["期待 Image ID"]
end
RCP --> VS[検証サービス]
EID --> VS
VS --> RPT[検証レポート]
検証フロー
検証サービスは、次の手順でレシートを検証します。
flowchart TD
START[レシート / バンドル読み込み] --> FORMAT{入力形式<br/>の判定}
FORMAT -->|フラット JSON| F1[直接パース]
FORMAT -->|ネスト JSON| F2[receipt フィールドを抽出]
FORMAT -->|ディレクトリ| F3[receipt.json または<br/>*-receipt.json を探索]
FORMAT -->|ZIP アーカイブ| F4[末尾が receipt.json のファイルを探索]
F1 --> EXTRACT[Image ID 抽出]
F2 --> EXTRACT
F3 --> EXTRACT
F4 --> EXTRACT
EXTRACT --> MODE[InnerReceipt を判定<br/>Fake は dev_mode 候補として記録]
MODE --> PRESENT{メタデータ image_id<br/>が存在?}
PRESENT -->|欠落 + 非 Fake| FAIL0[failed]
PRESENT -->|欠落 + Fake| VERIFY
PRESENT -->|存在| MATCH{メタデータ Image ID<br/>= 期待 Image ID ?}
MATCH -->|不一致| FAIL1[failed:<br/>Image ID 不一致]
MATCH -->|一致| VERIFY["Receipt::verify(expected_image_id)<br/>STARK 検証実行"]
VERIFY -->|成功 かつ Fake| DEVMODE[dev_mode]
VERIFY -->|失敗 かつ Fake + InvalidProof| DEVMODE
VERIFY -->|成功 かつ 非 Fake| SUCCESS[success]
VERIFY -->|失敗(その他)| FAIL2[failed]
入力形式の解決
検証サービスは、単一のレシートファイルだけでなく、レシートを含むバンドルディレクトリや ZIP にも対応しています。
image_id はラッパーの top-level フィールドであり、レシート本体の内部フィールドではありません。
同期 finalization 経路では proof bundle ディレクトリ全体を渡し、その中の receipt.json を解決します。
| 形式 | 説明 |
|---|---|
| フラット JSON | レシートオブジェクトが直接 JSON のトップレベルにある |
| ネスト JSON | { "receipt": {...}, "image_id": "0x..." } 構造 |
| ディレクトリ | receipt.json または *-receipt.json を探索して読み込む |
| ZIP アーカイブ | エントリ名の末尾が receipt.json のファイルを探索して読み込む |
Image ID 照合と Fake receipt の扱い
ラッパーの image_id は、開発モード生成を示す Fake 型(InnerReceipt)でも期待値との一致が必須であり、不一致なら failed として即時拒否します。
image_id が欠落した場合は、非 Fake 型は failed になり、Fake 型だけが Receipt::verify まで進んで結果に応じて dev_mode または failed に振り分けられます(分岐の全体は上のフローチャートを参照)。
Image ID の管理は Image ID を参照してください。
STARK 検証の実行
top-level の Image ID 照合に成功した後、または Fake 型で image_id が欠落している場合に、RISC Zero SDK の Receipt::verify(expected_image_id) でレシートを検証します。
検証成功は、次の内容を保証します。
- レシートに含まれる seal(証明データ)が有効
- ジャーナルが指定 Image ID のゲスト実行結果である
- 証明生成後にレシートが改ざんされていない
検証レポート
検証サービスは、検証が最後まで到達した試行について JSON レポートを出力します。 exit code とレポート出力の関係は次のとおりです。
| 状況 | exit code | JSON レポート |
|---|---|---|
success | 0 | stdout または --output |
| 引数不正、bundle 不在など | 1 | 出力しない |
dev_mode | 2 | stdout または --output |
failed | 3 | stdout または --output |
呼び出し側は exit code とレポートの両方を見ます。
--quiet を指定すると stdout 出力は抑制されます。
その場合は --output も併せて指定し、レポートを保存します。
| フィールド | 型 | 説明 |
|---|---|---|
| status | 列挙型 | success / failed / dev_mode |
| verifier_version | 文字列 | verifier-service のバージョン |
| verified_at | 文字列 | RFC 3339 形式の検証完了時刻 |
| duration_ms | 数値 | 検証処理時間(ミリ秒) |
| expected_image_id | 文字列 | 検証に使用した期待 Image ID |
| receipt_image_id | 文字列? | 入力 JSON の top-level image_id から抽出した値 |
| bundle_path | 文字列 | 入力 bundle パスの basename のみ |
| receipt_path | 文字列 | 解決されたレシートファイル名の basename のみ |
| dev_mode_receipt | 真偽値 | Fake receipt なら true。status とは別に、入力 receipt の生の種別を示す診断信号として使う |
| errors | 文字列[] | 診断文字列の配列。空の場合は省略される |
errors は固定のエラーコード一覧ではなく、実装が積む自由形式の診断文字列です。
ステータスの意味づけ
| ステータス | 意味 |
|---|---|
success | STARK 検証が成功し、Image ID も一致 |
failed | Image ID 不一致または STARK 検証失敗 |
dev_mode | 開発モードのフェイクレシート |
各ステータスが最終表示に与える影響は ゲーティングロジック > STARK 検証のゲーティング を参照してください。
デプロイメントモデル
検証サービス(Rust バイナリ verifier-service)は、呼び出し経路ごとに実行場所が異なります。
呼び出しパターン
検証サービスの呼び出しは 3 分類です。 明示的検証はいずれも、サーバー側で保持している finalization result に紐付いた 権威ある bundle locator だけを使います。 クライアントが任意の S3 キーや local パスを指定し、検証サービスに検証させることはできません。
| パターン | トリガー | 説明 |
|---|---|---|
| 同期実行 | 同期 finalization (POST /api/sessions/:sessionId/finalizations) | real executor 時のみ実行。mock executor 時は verifier-service を呼ばず dev_mode 扱いとして帰る |
| target S3 実行 | クライアントが検証を要求 | POST /api/sessions/:sessionId/finalizations/:finalizationId/verification が検証 work を SQS に積み、verification-worker が後続で処理する |
| trusted local 実行 | クライアントが検証を要求 | 信頼済み local bundle を API サーバープロセス内で直接検証する |
S3 artifact backend は target profile 専用で、API が verification-worker Lambda
を同期 invoke する non-target S3 経路や backend fallback はありません。
非同期 finalization のコールバック Lambda は、結果の復元と保存を担当します。
STARK 検証は自動実行されず、POST /api/sessions/:sessionId/finalizations/:finalizationId/verification で実行します。
target AWS runtime での流れは次のとおりです。
- この POST は
verifier-serviceの完了を待たず、verificationResult.status=runningを保存して SQS work を発行する verification-workerが SQS event を受け取り、S3 bundle を検査・展開してverifier-serviceを実行する- 検証が完了した場合は、
verification.jsonの report locator と report を含む terminalverificationResultを finalization-scoped な独立 record としてセッションストアへ保存する - S3 の取得、bundle の検査、
verifier-serviceの起動、artifact の upload などの infrastructure failure は SQS で再試行する - 3 回目の受信でも失敗した場合は terminal
failedと internal-only のfailureCategoryを保存する。この終端失敗には verifier report がないため、report locator も保存しない
既存の verificationResult.status ごとの POST /api/sessions/:sessionId/finalizations/:finalizationId/verification の挙動は次のとおりです。
| 既存 status | POST の挙動 |
|---|---|
success / failed / dev_mode(終端) | 再検証せず idempotent な応答を返す |
running | 実行中として idempotent に扱う。target AWS runtime で queue marker が未保存なら同じ finalization の SQS work を補修発行する |
未設定 / not_run | 新しい検証実行または検証 work の enqueue に進む |
実行シーケンス(補足)
次のシーケンス図は、上記 3 分類の実行経路を補足するものです。
sequenceDiagram
participant C as クライアント
participant API as API サーバー
participant Q as verification-work SQS
participant RUNNER as verification-worker Lambda
participant VS as verifier-service バイナリ
participant S3 as S3
participant STORE as セッションストア
C->>API: POST /api/sessions/:sessionId/finalizations/:finalizationId/verification
API->>API: session capability と finalization result を確認
alt target AWS runtime + S3 bundle locator がある
API->>STORE: verificationResult を running に更新
API->>Q: 検証 work を送信(recordType, sessionId, finalizationId, queuedAtMs, bundleKey, expectedImageId)
API-->>C: running
Q-->>RUNNER: SQS event
RUNNER->>S3: bundle.zip を取得・展開
RUNNER->>VS: bundle ディレクトリ + 期待 Image ID + reportPath
VS->>VS: STARK 検証実行 + verification.json 書き出し
VS-->>RUNNER: 検証レポート
RUNNER->>S3: public bundle.zip / verification.json / sidecar を保存
RUNNER->>STORE: report locator と terminal verificationResult を保存
else 信頼済み local bundle がある
API->>VS: bundle ディレクトリ + 期待 Image ID + reportPath
VS->>VS: STARK 検証実行 + verification.json 書き出し
VS-->>API: 検証レポート
API->>STORE: verificationResult / execution 状態を更新
API-->>C: 検証結果
end
検証パイプラインにおける役割
検証サービスは、4 段階検証モデルの最終段階である STARK 検証を担当します。
| チェック ID | 検証内容 |
|---|---|
stark_image_id_match | レシートに記録された Image ID が期待値と一致するか |
stark_receipt_verify | STARK 証明が暗号学的に有効であるか |
stark_image_id_match は、verifier report の expected_image_id と receipt_image_id が一致することを検証します。
検証パイプラインはさらに、claimed 側および comparison 側の Image ID とも整合することを確認します。
これらのチェックが両方成功した場合に限り、「STARK Verified」のステータスが付与されます。 これは STARK 検証段階のステータスであり、全体の Verified 判定とは区別されます。 詳細は 4 段階検証モデル を参照してください。
セキュリティ上の考慮事項
サーバー側検証の信頼境界
検証サービスはサーバー側で実行されるため、クライアントはサーバーの検証結果を信頼する必要があります。 この PoC における信頼モデルは次のとおりです。
- STARK 証明自体は秘密データを含まない検証データ: レシートと Image ID があれば、第三者が独立に検証可能
- 検証サービスは利便性のための委任: ブラウザ上で RISC Zero の検証ロジックを実行することは現時点では実用的でないため、サーバー側で検証する。実用的になれば、クライアント側のみで完結させることも理論上は可能
- 配布対象アーカイブ: レシートと
public-input.jsonは ZIP ローカル検証(Ubuntu) の手順で独立検証できる
verification.json の非公開性
verification.json は bundle.zip に含めず、必要時のみ capability 保護の report エンドポイントで配布します(境界は 公開境界)。
第三者検証では、レシートファイルを直接使った独立検証が推奨されます。
Image ID
Image ID は、ゲストバイナリを一意に識別する値です。 検証時には、この値でレシートがどのゲストプログラムの実行結果かを照合します。
Image ID は、ゲストプログラムの ELF バイナリから決定的に導出される 256 ビットのハッシュ値です。 レシート検証時に期待値との一致を確認することで、レシートが正しいプログラムの実行結果であることを検証します。
RISC Zero の Image ID
RISC Zero zkVM では、ゲストプログラムの ELF バイナリが Image ID と呼ばれる 256 ビットの識別子に変換されます。 この変換は決定的であり、同一のバイナリからは常に同一の Image ID が生成されます。
Receipt::verify(image_id) は、レシートが指定した Image ID のゲスト実行結果であることを暗号学的に検証します。
期待する Image ID と一致しないレシートは拒否されます。
ラッパーメタデータとの照合手順は 検証サービス を参照してください。
flowchart LR ELF["ゲスト ELF バイナリ"] --> HASH["RISC Zero<br/>Image ID 導出"] HASH --> IID["Image ID<br/>(256 ビット)"] IID --> EMBED["ホストバイナリに埋め込み"] IID --> MAP["マッピングファイルに記録"] IID --> VS["検証サービスの期待値"]
Image ID の導出
Image ID は RISC Zero のビルドシステムによってコンパイル時に自動生成されます。
Image ID が変わる要因
同一のゲストソースコードであっても、以下の要因により異なる Image ID が生成され得ます:
| 要因 | 影響 |
|---|---|
| ゲストコードの変更 | ロジックの変更により異なるバイナリが生成される |
| コンパイラバージョン | Rust ツールチェインの差異がバイナリに反映される |
| ターゲットアーキテクチャ | 同一コードでも x86_64 と ARM64 で Image ID が違う |
| RISC Zero SDK バージョン | SDK の変更がゲストバイナリの構造に影響する |
variant とアーキテクチャ
本システムでは、同一バージョンのゲストに対して default と x86_64 の 2 つの variant authority をマッピング上で管理します。
各 authority は次の排他的な状態です。
state: "confirmed":imageIdと由来を示すprovenanceを持つstate: "unconfirmed":requiredEvidence: "accepted-prover-release"だけを持ち、Image ID として解決できない
| variant / アーキテクチャ | 取得元のビルド |
|---|---|
| default / ARM64 | target-account prover image build |
| x86_64 | local / CI production-feature build |
app deployment では、選択された prover release metadata から Image ID と methodVersion を導出し、mapping は互換性と公開台帳の証拠として照合します。
public/imageId-mapping.json は公開検証者が expected Image ID を解決するための互換性台帳であり、prover image digest の承認 authority ではありません。
ECR digest と Image ID / methodVersion の組み合わせは、選択された prover release record と app deployment record で固定します(candidate metadata から app deployment record までの chain は イメージ署名 を参照)。
現行実装では、実行環境を自動判定して x86_64 を選びません。
未指定時は default variant を選択します。
選択した variant が unconfirmed なら fail-closed で停止し、もう一方へフォールバックしません。
x86_64 用の値を使うには、アプリ側で EXPECTED_IMAGE_ID_VARIANT=x86_64 を設定するか、呼び出し側で variant を明示します。
variant 選択と EXPECTED_IMAGE_ID オーバーライドの優先順位は、下の Image ID の解決 を参照してください。
Image ID マッピング
期待される Image ID は、バージョンごとにマッピングファイルで管理されます。
マッピングファイルの構造
マッピングファイルには、各バージョンの variant authority、説明、機能リストを記録します。
| フィールド | 説明 |
|---|---|
mappingType | image-id-mapping |
current | 現在有効な method version のキー |
mappings.*.methodVersion | ゲストプログラムのバージョン番号 |
mappings.*.variants | default と x86_64 の authority |
variants.*.state | confirmed または unconfirmed |
variants.*.imageId | confirmed の場合だけ存在する Image ID |
variants.*.provenance | confirmed の場合だけ存在するビルドまたは accepted-release の由来 |
variants.*.requiredEvidence | unconfirmed の場合だけ存在する accepted-prover-release |
description / features | method version の説明と機能リスト |
deprecated / metadata | 非推奨 version とファイル全体の管理メタデータ |
current と deprecated の扱い
マッピングファイルは、現在信頼するバージョンと、必要な場合に限って非推奨バージョンを保持します。
current フィールドは現在有効なバージョンを指し、deprecated フィールドは過去のバージョンを列挙します。
全 method mapping は両 variant の authority を持ちますが、両方が confirmed とは限りません。
現行実装では current は 14 で、deprecated は空です。
明示的な trust rotation により v13 以下の mapping は削除されており、現行 mapping からは解決できません。
v14 の mapping は variants.default と variants.x86_64 を保持します。
flowchart LR CUR["current v14<br/>(default + x86_64 authority)"] DEP["deprecated<br/>(現在は空)"] CUR -. version up .-> DEP
Image ID の解決
検証時に使用する期待 Image ID の決定方法は、EXPECTED_IMAGE_ID の有無で分かれます。
EXPECTED_IMAGE_ID が設定されている場合は、その値を使用します。
設定されていない場合は、methodVersion と variant を使ってマッピングから解決します。
flowchart TD
START[Image ID 解決] --> P1{EXPECTED_IMAGE_ID?}
P1 -->|設定済み| USE1[その値を採用]
P1 -->|未設定| P2[マッピングから解決]
P2 --> P3{variant?}
P3 -->|default| F1[variants.default]
P3 -->|x86_64| F2[variants.x86_64]
F1 --> CHK[fail-closed 条件を適用]
F2 --> CHK
version 選択
EXPECTED_IMAGE_ID環境変数は最優先のオーバーライドです。 設定されている場合は mapping と variant による解決を行わず、その値を期待 Image ID として採用します。 運用者は、この値が対象 methodVersion のデプロイ済みゲスト Image ID と一致することを別途保証する必要があります。 不一致ならstark_image_id_matchまたはstark_receipt_verifyが失敗します。resolveExpectedImageId()/POST /api/sessions/:sessionId/finalizations/:finalizationId/verificationの version 選択は、省略時はマッピングのcurrentを使用し、明示時はCURRENT_METHOD_VERSIONと一致する場合のみ受理します(deprecated側は拒否)。- 低レベルのマッピング読み取り API は、mapping に保持されている version だけを明示的に解決できます。 現行 mapping には v13 以下が存在しないため、それらも fail-closed で拒否します。
- 現行の検証実行フローでは、正規化済みのジャーナルから
methodVersionを取得してresolveExpectedImageId(methodVersion)を呼びます(public-input.jsonフォールバックは現行未使用)。
variant 選択
- variant は
EXPECTED_IMAGE_ID_VARIANT(defaultまたはx86_64)または呼び出し側の明示 option で選択し、未指定時はdefaultです。 それ以外の値は受け付けません。 - app deployment flow(default / ARM64 trust material)では、variant 未指定の
defaultを使用します。 - ローカル / CI フロー(x86_64 trust material)では、
EXPECTED_IMAGE_ID_VARIANT=x86_64を明示します。verifier-service単体はEXPECTED_IMAGE_ID_VARIANTを直接読みません。 第三者検証ではread-image-id.mjs --variant x86_64または同スクリプト実行時のEXPECTED_IMAGE_ID_VARIANT=x86_64でEXPECTED_IMAGE_IDを導出し、verifier-serviceに渡します。
fail-closed 条件
未対応 methodVersion、マッピング読み込み失敗、unsupported variant、選択した variant が unconfirmed、または authority の形が不正な場合は、いずれも暗黙のフォールバックを行わず fail-closed でエラーになります。
検証パイプラインでの役割
Image ID は 4 段階検証モデルの STARK 検証段階で使用されます。
Image ID 関連チェック
現行実装では、STARK 検証段階で次の 2 つの必須チェックが Image ID に関与します。
stark_image_id_match:receipt.jsonラッパーのimage_idと期待値を照合するstark_receipt_verify: 同じ期待 Image ID を使ってReceipt::verify(expected_image_id)を実行する
詳細は 検証サービス を参照してください。
Image ID が不一致の場合は、以下のいずれかの状況が考えられます:
| 原因 | 対処 |
|---|---|
| マッピングが古い | ゲストの再ビルド後にマッピングを更新する |
| 異なるゲストで証明が生成された | レシートの出所を調査する |
| アーキテクチャの不一致 | 対象 variant の Image ID で照合する |
Image ID が一致していても、RISC0_DEV_MODE=1 の dev-mode receipt(フェイクレシート)は production STARK proof として扱いません(生成側は ホストと証明生成、判定側は ゲーティングロジック を参照)。
Image ID 更新時の手順
ゲストプログラムを変更した場合、Image ID を更新する必要があります。
- ゲスト変更と authority schema を Commit A に入れ、target
defaultをunconfirmedにする - 管理された Prover lane でイメージを 1 回ビルドし、push 前後の
host --print-image-id --jsonが一致することを確認して release を昇格する(candidate build と promotion の AWS 側の詳細は イメージ署名 を参照) - accepted Prover release の immutable key / VersionId / body SHA-256 を確認する
- Commit B で
defaultをconfirmedにし、accepted release の Image ID、image digest、candidate/release/Toolchain record checksum を provenance に記録する - Foundation と App の saved-plan lane で Commit B を反映する。App は Prover release の
zkvmContractHashと App source の再計算値も照合する
--json を付けると {"imageId":"0x...","methodVersion":14} の形で出力されます。
同一リリースで同期するもの
Image ID / methodVersion が変わる場合は、Prover release record と Commit B の imageId-mapping.json を同じ昇格系列で結びます。
Commit A の unconfirmed 期間は意図的に App deployment が停止します。
片方だけをデプロイ可能な状態にするフォールバックはありません。
一方で、entrypoint だけの prover container rebuild のように guest Image ID が変わらない変更では、ECR digest は変わっても mapping を更新する必要はありません。
- 通常の
POST /api/sessions/:sessionId/finalizations/:finalizationId/verificationフローでは、現行の journal contract のみ受け付ける - 旧成果物は Image ID 照合に進む前に、未対応の journal contract として失敗し得る
DEFAULT_POC_IMAGE_IDはテスト用定数で、期待 Image ID の解決経路には現れない(マッピングが source of truth)
信頼アンカーとしての位置づけ
Image ID は、zkVM の信頼モデルにおける信頼アンカーです。
- Image ID を知っている検証者は、対象ゲストプログラムの実行を検証できる: レシートが有効であれば、そのロジックが実行されたことを確認できる
- Image ID の管理が破綻すると、検証の信頼性が失われる: 攻撃者が独自のゲストプログラムで有効なレシートを生成し、その Image ID がマッピングに混入すると、不正な集計が「検証済み」として受理され得る
マッピングファイルは公開リポジトリにコミットされ、変更履歴を追跡できます。 AWS 構成では、イメージ署名検証と組み合わせることで、承認されたプローバーイメージだけを使用する設計です。 イメージ署名の詳細は イメージ署名 を参照してください。
検証パイプライン
この部では、投票の完全性を 4 段階に分けて検証するパイプラインを扱います。
この部の前提となる原則は次の 4 点です。
- 最終的な Verified 判定は fail-closed で、required check が失敗、未実行、保留中、実行中のまま残る場合は successful overall result になりません。
verificationStatusやstark_receipt_verifyは STARK receipt verification の信号にすぎず、単独で overall verdict を表しません(ゲーティングロジック)。 - 判定確定後に送る
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observationsは observability 専用で、送信の成否が verdict を変更することはありません(設計と実行フロー)。 - public
bundle.zipは保護対象 artifact を含まない監査用アーカイブで、publicは無認証公開を意味しません(境界の正本は 公開境界)。 RISC0_DEV_MODE=1の receipt は production STARK proof ではありません。
この部の章
読み順は次のとおりです。
- 設計と実行フロー:設計原則、パイプライン構造、実行フロー、判定確定後の observability
- 4 段階検証モデル:検証の全体設計と各段階の保証
- チェック一覧:全検証チェック ID とその判定ロジック
- ゲーティングロジック:「Verified」表示の条件と不変条件
- 公開境界:public
bundle.zipと protected report artifact の境界 - バンドル構造:証明バンドルの公開可能なアーティファクトと非公開アーティファクト
想定読者と前提
この部で扱わないもの
関連する章
- 暗号プロトコル:チェック対象となるプリミティブ
- zkVM 設計:ジャーナルとレシートの構造
- 改ざんシナリオ:どのチェックがどの改ざんを検出するか
- 可観測性設計:verdict authority と判定後 observation の分離
- 第三者検証ガイド:
bundle.zipを使ったローカル監査 - 用語集:チェック種別とゲーティング用語の定義
設計と実行フロー
この章では、検証パイプラインの設計原則と、リクエストから判定までの実行フローを説明します。
設計原則
本システムの検証パイプラインは、5 つの原則に基づいています。
原則 1: 必要な検証が未実行なら Verified を表示しない
required チェックが not_run(未実行)、pending(依存待ち)、running(実行中)のいずれかにある場合、システムは「Verified」を表示しません。
証拠の不在や未解決状態は成功として扱いません。
原則 2: 失敗した検証は即座にブロックする
いずれかの必須チェックが失敗すれば、「Verified」表示は即座にブロックされます。 代表的な失敗条件は次のとおりです。
excludedSlots > 0(除外されたスロットが存在する)- 整合性証明の失敗
- 公開監査アーティファクトとの不一致
- 第三者 STH 合意の不成立(設定時)
原則 3: チェック評価と集約の責務を分ける
チェック評価はサーバーが担い(GET /api/sessions/:sessionId/finalizations/:finalizationId/verification が Stage 2-4 を評価し、STARK 検証は同じ resource への POST で実行)、Cast-as-Intended のローカル再評価と最終判定の集約はクライアントが担います。
集約ルールと最終判定の決まり方は ゲーティングロジック を参照してください。
原則 4: evidence の取得・admission と評価を分ける
provider 選択、URL fetch、capability 付与、store read、外部 response の strict admission、transport/unavailable/malformed の分類は application/API の acquisition boundary が担います。verification evaluator は admission 済みの consistency proof、 STH result、bitmap material、session/finalization authority を受け取り、transport や credential を解決しません。
取得失敗を成功相当の空値に変換せず、閉じた unavailable/corrupt または check status へ fail-closed に写像することで、I/O と暗号・整合性評価を独立に検査できます。
原則 5: proof authority と presentation を分ける
proofAuthority、included journal、公開監査 artifact は検証根拠です。
claimed tally、scenario、tamper 表示、shared policy result は
presentation として分離し、authority から導出または照合します。
presentation の都合で proof-bound data を書き換えず、両者が不一致なら
published_tally_mismatch などの失敗として表示します。
検証パイプラインの全体構造
パイプラインは Stage 1(Cast-as-Intended)→ Stage 2(Recorded-as-Cast)→ Stage 3(Counted-as-Recorded)→ Stage 4(STARK Verification)→ 結果表示の順に評価されます。
実行責務
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification は 22 チェックのレスポンスを組み立てます。
- 評価対象: サーバーは Stage 2-4 の 18 チェックを評価し、Cast-as-Intended の 4 チェックは
not_runで返します。クライアントがローカル再評価で上書きします。summary の導出(deriveVerificationSummary)はサーバー側の verification resource とクライアント側の/verifyの両方で使われます。 200+ fail-closed のケース:- Recorded-as-Cast は cast-time 証跡(
voteReceiptとuserVote.proof)を前提とします。exact proof を store から取得できない場合でも verification resource は200を返し、voterEvidence.availability = "unavailable"と関連チェックのnot_runによって全体判定をmissing_evidence側へ倒します。flat field や不完全な proof への fallback はありません。 userVote.proof.treeSizeなど exact voter evidence 側の必要データが不在のときは、関連チェックをnot_runに補正します。- サーバーは
verificationStatusを fail-closed に補正します。unsupported な verifier status でもverificationSteps/verificationChecksを含む200応答を返します。
- Recorded-as-Cast は cast-time 証跡(
- corrupt のケース:
journalは成功済み finalization の必須 authority です。不在または malformed なら check 単位に修復せず、corrupt として fail-closed に扱います。
flowchart TD
subgraph SERVER["サーバー側"]
VFY["GET .../finalizations/:finalizationId/verification<br/>Stage 2-4 を評価<br/>Cast は not_run(クライアント再評価)"]
RUN["POST .../finalizations/:finalizationId/verification<br/>bundle 参照と expected Image ID で<br/>Stage 4 を検証"]
OBS["POST .../verification/observations<br/>(非ゲート観測)"]
end
subgraph CLIENT["クライアント(UI)"]
UI["Cast のローカル再評価<br/>STARK 解決後に step 表示を開始<br/>明示的 failure / hard-failure override / summary / pending から最終判定"]
end
VFY --> UI
RUN --> UI
UI -. "判定確定後に送信(非ゲート)" .-> OBS
検証の実行フロー
検証導線では通常、/result から /verify へ進みます。
/resultは正準な finalization snapshot をクライアント状態に保存します。 「検証へ進む」を押すとverificationRequestedAtを保存し、必要ならPOST /api/sessions/:sessionId/finalizations/:finalizationId/verificationを非同期に先行起動します(完了を待たずに/verifyへ遷移します)。/verifyはその継続状態がある場合に検証シーケンスを続行します。 STARK が未開始ならシーケンス内で起動できます。- 継続状態がなく STARK が
not_runのまま直接/verifyへアクセスした場合は、自動続行せずブロックします /verifyの UI シーケンスは、step を順に見せる前に STARK が terminal status に到達するまでポーリングします。 timeout や transport failure は STARK failure として扱われます。- UI の最終判定が確定し、pending check がなく、server response が検証済みで、STARK status が terminal なら、
/verifyはPOST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observationsを best-effort で送信します。 currentfinalizationIdは path authority であり、body に identity を重複させません。strict body は 4 個の browser-local Cast check status だけを持ちます。この事後観測は表示済みの verdict を gate または変更せず、送信失敗も検証失敗として扱いません。 リクエスト内容と authority 規則は API エンドポイント を参照してください。
継続判定に使うクライアント状態キーの視点は セッションライフサイクル を参照してください。
sequenceDiagram
participant U as ブラウザ
participant R as /result
participant V as /verify
participant A as API サーバー
participant VS as 検証サービス
Note over R: finalization snapshot は /result 表示時に保存済み
U->>R: 「検証へ進む」をクリック
R->>R: verificationRequestedAt を保存
R->>A: POST .../finalizations/:finalizationId/verification(fire-and-forget)
R-->>V: /verify へ遷移(run 完了は待たない)
Note over V: /verify 到達時に未開始なら<br/>同じ finalization-scoped POST を起動
A->>VS: bundle 参照 + expected Image ID
VS->>VS: Receipt::verify(image_id)
VS-->>A: 検証レポート保存
loop STARK 完了までポーリング
V->>A: GET .../finalizations/:finalizationId/verification
A-->>V: 検証ペイロード<br/>(ステップ, チェック, 証明材料)
end
Note over V: 継続には verificationRequestedAt と finalization snapshot が必要<br/>not_run の direct access はブロック<br/>step 表示は STARK 解決後に開始
V-->>U: ローカル Cast check を含む最終判定を表示
opt 判定確定後の observability
V->>A: POST .../verification/observations(非ゲート)
A-->>V: recorded または idempotent
end
4 段階の概要
各段階が何を証明するかは 4 段階検証モデル を参照してください。 この章の観点で重要なのは、検証の実行場所が段階ごとに異なることです。
| 段階 | 名称 | 検証の実行場所 |
|---|---|---|
| Stage 1 | Cast-as-Intended | クライアント(/verify 画面でローカル再計算) |
| Stage 2 | Recorded-as-Cast | サーバー(finalization-scoped verification GET) |
| Stage 3 | Counted-as-Recorded | サーバー(finalization-scoped verification GET) |
| Stage 4 | STARK Verification | サーバー(同じ resource への POST) |
各ステージの導出ルール(required チェック群からの集約、STH source 設定時の昇格、ガード条件)は ゲーティングロジック を参照してください。
検証チェック数
パイプライン全体で 22 個の検証チェックが定義されており、各チェックには一意の ID が割り当てられています。
チェック ID の単一ソースは packages/verification/src/verification/verification-checks.ts です。
| 段階 | チェック数 |
|---|---|
| Cast-as-Intended | 4 |
| Recorded-as-Cast | 6 |
| Counted-as-Recorded | 10 |
| STARK Verification | 2 |
| 合計 | 22 |
各チェックの重要度(required / optional)と判定ロジックは チェック一覧 を参照してください。
4 段階検証モデル
この章では、E2E 検証可能投票の各段階(Cast-as-Intended / Recorded-as-Cast / Counted-as-Recorded / STARK Verification)で成立する保証を説明します。
段階間の依存関係
概念モデルでは 4 段階を順に評価します。 ただし、実行場所は段階ごとに異なります。 Stage 1 はクライアントで実行し、Stage 2 から Stage 4 はサーバー中心で実行します。 実行責務の詳細は 設計と実行フロー を参照してください。
flowchart LR
subgraph "証拠の生成"
V["投票時<br/>投票レシート発行"]
F["集計時<br/>zkVM 実行"]
end
subgraph "4 段階検証"
S1["Stage 1<br/>Cast-as-Intended"]
S2["Stage 2<br/>Recorded-as-Cast"]
S3["Stage 3<br/>Counted-as-Recorded"]
S4["Stage 4<br/>STARK Verification"]
end
V --> S1
V --> S2
F --> S3
F --> S4
S1 ~~~ S2
S2 ~~~ S3
S3 ~~~ S4
Stage 1: Cast-as-Intended
目的
投票者が意図した選択肢のコミットメントが、サーバーから返却された投票レシートと一致することを確認します。 この確認により、サーバーが投票者の選択を差し替える攻撃を検出します。
検証する内容
/verify 画面のクライアントコードがローカルに保持された 3 つの値(選挙 ID、選択肢、乱数)からコミットメントを再計算し、投票レシートのコミットメント値と照合します。
再計算の規則(ドメインタグ、正準フォーマット)は コミットメントスキーム を参照してください。
flowchart LR
subgraph "投票時に確定したデータ"
EID[選挙 ID]
CH[選択肢]
RND[乱数]
end
subgraph "再計算"
HASH["SHA-256<br/>(ドメインタグ || 選挙ID || 選択肢 || 乱数)"]
end
subgraph "照合"
CMP{"一致?"}
REC[投票レシートの<br/>コミットメント]
end
EID --> HASH
CH --> HASH
RND --> HASH
HASH --> CMP
REC --> CMP
必要な証拠
選挙 ID、選択肢、乱数はクライアントセッション(localStorage.starkBallotSession)に保持されます。
投票レシートは capability で保護された
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification 応答の
data.voterEvidence が availability: "available" のときに、その voteReceipt から取得します。
| 証拠 | 説明 |
|---|---|
| 選挙 ID | セッション作成時に確定した UUID |
| 選択肢 | 投票者が選択した値(A〜E) |
| 乱数 | 投票時にクライアントが生成した 32 バイト乱数 |
| 投票レシート | commitment, voteId, bulletinIndex, bulletinRootAtCast を含む |
cast-time 証跡を再構成できない場合、data.voterEvidence は availability: "unavailable" と reason を返し、投票レシートは得られません(取得と fail-closed 挙動の正本は 設計と実行フロー)。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
| ローカル証拠の欠落 | localStorage 消去や別端末アクセスで投票時データを復元できない | 検証不能 |
| 投票レシート証跡の欠落 | store から cast-time 証跡を再構成できず、voterEvidence が unavailable | 検証不能 |
| コミットメント不一致 | 投票時データと投票レシートの不整合、またはエンコーディングの不整合 | 重大 |
| 選択肢の範囲外 | 不正な入力(A〜E の範囲外) | 重大 |
| 乱数フォーマット不正 | 32 バイト hex でない | 重大 |
限界
この段階は投票者の手元データに依存します。
そのため、localStorage 消去後や別端末からは検証できません。
Stage 2: Recorded-as-Cast
目的
投票者のコミットメントが、追記専用の掲示板(CT Merkle ツリー)に正しく記録されていることを確認します。 さらに、掲示板が追記専用性を維持していることを検証します。 つまり、過去のエントリが削除や改変を受けていないことを確認します。 掲示板のツリー構造とハッシュ規則は CT Merkle ツリー を参照してください。
検証する内容
この段階には 3 系統の検証があります。
flowchart TB
subgraph "2a: 包含証明"
IP["包含証明の検証<br/>自分のコミットメントが<br/>ツリーに存在するか"]
end
subgraph "2b: 整合性証明"
CP["整合性証明の検証<br/>投票時のルートから<br/>最終ルートへの追記専用性"]
end
subgraph "2c: 第三者 STH 検証"
STH["STH 合意の検証<br/>独立したソース間で<br/>ツリー状態が一致するか"]
end
IP --> RESULT{"Recorded 系の証拠を総合評価"}
CP --> RESULT
STH --> RESULT
2a: 包含証明(Inclusion Proof)
RFC 6962 の PATH 関数に基づく CT スタイルの Merkle 包含証明を検証し、投票者のコミットメントが掲示板のツリーに含まれていることを確認します。 検証者はリーフハッシュと Merkle パスからルートハッシュを再計算し、期待されるルートと照合します。
2b: 整合性証明(Consistency Proof)
投票時点のツリー状態(ルートとサイズ)から、最終的なツリー状態への遷移が追記のみで行われたことを検証します。 RFC 6962 の SUBPROOF アルゴリズムに基づく整合性証明により、サーバーが過去のエントリを密かに削除したり順序を変更したりするスプリットビュー攻撃を検出します。
2c: 第三者 STH 検証(オプション)
複数の独立した STH(Signed Tree Head)ソースに問い合わせ、合意が成立しているかを確認します。 この確認により、サーバーが検証者ごとに異なるツリー状態を提示するスプリットビュー攻撃を検出します。 照合対象となるダイジェストの構成は STH ダイジェスト を、照合条件の詳細は チェック一覧 を参照してください。
必要な証拠
Recorded の検証リソースは
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification の data です。
| 証拠 | 取得元 | 説明 |
|---|---|---|
| 包含証明の検証結果 | data.verification.checks | サーバー側で RFC 6962 包含証明を評価したチェック結果 |
| 整合性証明の検証結果 | data.verification.checks | サーバー側で RFC 6962 整合性証明を評価したチェック結果 |
| 投票時のルートハッシュ | data.voterEvidence.voteReceipt.bulletinRootAtCast | voterEvidence.availability="available" のときの投票受理時ツリールート |
| 投票時のツリーサイズ(oldSize) | data.voterEvidence.userVote.proof.treeSize | voterEvidence.availability="available" のときの整合性証明の oldSize |
| 最終ルートハッシュ/最終ツリーサイズ | data.proofAuthority.bulletinRoot / data.proofAuthority.treeSize | 集計時の proof-bound な最終状態 |
| 独立検証用の包含証明材料(任意) | /api/sessions/:sessionId/votes/:voteId/inclusion-proof | capability で保護された、個別に包含証明を再検証するための材料 |
| 補助 tooling の整合性証明材料(任意) | /api/sessions/:sessionId/bulletin/consistency-proof?fromTreeSize=...&toTreeSize=... | capability で保護された補助 tooling 用。verification handler の最終判定はこの route を呼ばない |
| STH スナップショット | 設定済み STH ソース(same-origin template と外部 HTTPS) | same-origin template は /api/sessions/:sessionId/bulletin/sth。外部応答の必須値は digest |
cast-time 証跡(voteReceipt と、oldSize を与える userVote.proof.treeSize)を再構成できない場合、Recorded の必須チェックは not_run となり、全体判定は fail-closed で missing_evidence 側へ倒れます。
取得と admission の分離は 設計原則 4 を、check 単位の判定は チェック一覧 を参照してください。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
| 包含証明の検証失敗 | ツリーサイズ/インデックスの不一致、掲示板のリセット | 重大 |
| 整合性証明の検証失敗 | 追記専用性の違反(スプリットビュー攻撃の可能性) | 重大 |
| cast-time 証跡の欠落 | store から voteReceipt / userVote.proof を再構成できず、Recorded が未実行 | 検証不能 |
| 第三者 STH 合意の不成立 | サーバーが検証者ごとに異なるツリーを提示 | 重大(有効時) |
| ルートが履歴に存在しない | ルート履歴の不整合 | 重大 |
Stage 3: Counted-as-Recorded
目的
掲示板に記録された全投票が、zkVM の集計処理に正しく含まれたことを確認します。
投票の除外、欠落、重複がないことも確認します。
さらに、公開された claimed tally(表示用集計値)が zkVM の verifiedTally と一致することを検証し、集計結果の完全性と整合性を保証します。
検証する内容
この段階では 10 個の required チェックが全て success であることを要求します。
導出ルールと journal 省略時のガード補正は ゲーティングロジック を参照してください。
| 系統 | required チェック |
|---|---|
| public 系(6) | counted_input_sanity, counted_unique_indices, counted_unique_commitments, counted_election_manifest_consistent, counted_close_statement_consistent, counted_input_commitment_match |
| zk 系(4) | counted_tally_consistent, counted_missing_indices_zero, counted_expected_vs_tree_size, counted_my_vote_included |
1 つでも success でないチェックが残る場合、Counted は成功にならず、Verified をブロックします。
必要な証拠
| 証拠 | 取得元 | 説明 |
|---|---|---|
| zkVM ジャーナル(詳細) | /api/sessions/:sessionId/finalizations/:finalizationId/verification?include=journal の data.journal.value | data.journal.representation="included" のときの集計結果、除外情報、bitmap root など |
| 集計サマリー(通常応答) | 同じ verification resource の data.proofAuthority | missingSlots / invalidPresentedSlots / rejectedRecords / excludedSlots / totalExpected / treeSize など |
| 公開入力サマリー | サーバー内部評価用 | public-input.json 相当から組み立てた、秘密データを含まない入力要約 |
| 選挙マニフェスト | 公開監査アーティファクト(election-manifest.json) | electionId と electionConfigHash を束縛する公開可能アーティファクト |
| 締切ステートメント | 公開監査アーティファクト(close-statement.json) | logId / treeSize / bulletinRoot / timestamp / sthDigest を束縛する公開可能アーティファクト |
| ビットマップ証明材料 | /api/sessions/:sessionId/finalizations/current/bitmap-proofs/:kind/:voteIndex | kind の included と seen を使い分け、自分の index が counted されたことや prover に提示されたかを説明する材料 |
| ビットマップルート | ジャーナル / data.proofAuthority | zkVM ゲストが計算した includedBitmapRoot と seenBitmapRoot |
公開入力サマリーはサーバー内部表現であり、レスポンスにそのまま含まれません。
inputCommitment が束縛するのは public-input.json の部分集合です。
通常応答で proof-bound な値は data.proofAuthority、claimed tally は
data.presentation.tally に分離され、旧 flat field への fallback はありません。
各チェックの判定ロジックは チェック一覧 を参照してください。
重要な判定: 除外数(excludedSlots)
excludedSlots > 0 はハード失敗であり、全体の検証成功を阻止します。
除外数の解決順序と legacy aliases の扱いは チェック一覧の counted_missing_indices_zero を、全体判定への影響は ゲーティングロジック を参照してください。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
excludedSlots > 0 | 欠落スロットまたは計上失敗スロットが存在する | 重大(即座にブロック) |
| 欠落スロット / invalid presented slot | 一部の bulletin slot が prover に提示されなかった、または提示後に計上されなかった | 重大 |
| 公開集計値の不一致 | 公開表示された tally.counts が zkVM の verifiedTally と一致しない | 重大(claimed tally 改ざんシナリオ S2/S4 で発火) |
| 集計合計の不一致 | verifiedTally の合計が validVotes または tally.totalVotes と一致しない | 重大 |
| 選挙マニフェスト不整合 | electionId または electionConfigHash が verification inputs と一致しない | 重大(必須チェック失敗) |
| 締切ステートメント不整合 | logId / timestamp / sthDigest / bulletinRoot / treeSize が一致しない | 重大(必須チェック失敗) |
| 入力コミットメント不一致 | 公開入力のうち inputCommitment 対象フィールドと zkVM 実行で使用された入力が異なる | 重大 |
| 自票のビットマップ証明が失敗または欠落 | bit が 0、proof source 不可、またはルート不一致 | 重大(required check が failed/not_run) |
| ツリーサイズの不一致 | totalExpected と treeSize が異なる(暗黙の除外、または close/input side の不整合を示す) | 重大(必須チェック失敗) |
Stage 4: STARK Verification
目的
zkVM の実行が正しく行われたことを STARK 証明(レシート)の暗号学的検証により確認します。 レシートの検証に成功すれば、ジャーナルの内容(集計結果、除外情報、入力コミットメント等)がゲストプログラムの正しい実行の結果であることが保証されます。
検証する内容
flowchart LR
subgraph "入力"
RCP[レシート<br/>Seal + Journal]
EID[期待 Image ID]
end
subgraph "Rust 検証サービス"
VF["Receipt::verify(image_id)"]
end
subgraph "結果"
OK["success<br/>暗号学的に検証済み"]
NG["failed<br/>証明が無効"]
DM["dev_mode<br/>フェイクレシート"]
end
RCP --> VF
EID --> VF
VF --> OK
VF --> NG
VF --> DM
検証の 2 段階
STARK Verification では 2 つのチェックを実行します。
Image ID 照合: verifier-confirmed な receipt_image_id が期待 Image ID と一致し、ホスト主張値(imageId)や comparison-only の journal.imageId とも矛盾しないことを確認します。
Image ID はゲストプログラムから導出される暗号的識別子であり、プログラムの改変やホスト主張値の食い違いを検出します。
解決順の詳細は チェック一覧 を参照してください。
レシート検証: RISC Zero の Receipt::verify(image_id) を呼び出し、Seal(STARK 証明)がジャーナルと Image ID に対して暗号学的に正当であることを検証します。
この検証は計算量が多いため、サーバー側の Rust 検証サービスで実行されます。
必要な証拠
| 証拠 | 取得元 | 説明 |
|---|---|---|
| レシート(Seal + Journal) | 証明バンドル | zkVM ホストが生成した STARK 証明 |
| 期待 Image ID | サーバー側で解決 | ゲストプログラムの暗号的識別子(解決順は チェック一覧 参照) |
| ホスト主張値と比較用メタデータ | 検証コンテキストと report | imageId, journal.imageId, verificationReport.receipt_image_id を相互照合し、主張の食い違いを検出 |
開発モードの検出
RISC0_DEV_MODE=1 で生成された dev-mode receipt はフェイクレシートであり、本物の STARK 証明ではありません。
dev_mode ステータスの検出、正規化、Demo Only(demo_only)表示のルールは ゲーティングロジック を参照してください。
profile ごとの帰結は次節の失敗モード表にまとめています。
失敗モードと非 production evidence
| 症状 | 原因 | 影響 |
|---|---|---|
| Image ID 不一致 | マッピングが古い、ホスト主張値が誤っている、またはプローバーイメージが異なる | 重大。Verification Failed |
| レシート検証失敗 | 証明が暗号学的に無効 | 重大。Verification Failed |
| 開発モード検出 | 非 production profile でフェイクレシートを検出 | Demo Only。Verified をブロック |
| 未許可の開発モード | production または dev-mode を許可しない profile でフェイクレシートを検出 | fail-closed。Verified をブロック |
UI ステップの対応チェック、集約ルール、最終判定の決定方法は ゲーティングロジック を参照してください。
チェック一覧
全検証チェック ID の定義、判定ロジック、失敗時の影響を一覧で示します。
各チェックは一意の ID を持ち、success / failed / not_run / running / pending のステータスで管理されます。
required チェックが not_run / running / pending のまま残っている場合、「Verified」は表示されません。
optional チェックの取り扱いは ゲーティングロジック を参照してください。
チェックの属性
各チェックには以下の属性が定義されています。
| 属性 | 説明 |
|---|---|
| ID | チェックの一意な識別子(スネークケース) |
| カテゴリ | 所属する検証段階 |
| 証拠種別 | チェックに使用するデータの出所 |
| 重要度 | required(必須)または optional(任意) |
| 派生元 | 他のチェックから結果を導出する場合のソースチェック ID |
証拠種別
現行 22 チェックで使う証拠種別は次のとおりです。
| 種別 | 説明 |
|---|---|
local | 投票者の端末に保持されたユーザー固有データ(localStorage の投票意図など) |
public | 掲示板や capability 保護 API から取得する、秘密データを含まない検証用データ |
zk | zkVM が束縛した公開証拠(ジャーナル、receipt 検証結果、bitmap root に基づく証明など) |
demo | dev-mode receipt を許可した場合に、zk 証拠種別の表示用結果として使うデモ証拠 |
定義上のチェックは local / public / zk のいずれかを持ちます。
RISC0_DEV_MODE=1 の receipt が明示的に許可された場合だけ、表示用のチェック結果で zk 証拠が demo に置き換えられます(正規化と表示のルールは ゲーティングロジック)。
demo 証拠は demo_only 扱いであり、production STARK proof としての Verified にはなりません。
重要度
| 重要度 | 説明 |
|---|---|
required | 「Verified」表示に必須で、失敗すれば即座にブロック |
optional | 補助的な検証で、通常は単独で失敗扱いにしないが、実行時条件により blocking に昇格する場合がある |
代表例は recorded_sth_third_party です。
注意: verificationChecks と UI 表示用の verificationSteps は 1 対 1 ではありません。
対応関係は ゲーティングロジック を参照してください。
全チェック早見表
| # | チェック ID | カテゴリ | 証拠 | 重要度 | 派生元 |
|---|---|---|---|---|---|
| 1 | cast_receipt_present | Cast | local | required | — |
| 2 | cast_choice_range | Cast | local | required | — |
| 3 | cast_random_format | Cast | local | required | — |
| 4 | cast_commitment_match | Cast | local | required | — |
| 5 | recorded_commitment_in_bulletin | Recorded | public | optional | recorded_inclusion_proof |
| 6 | recorded_index_in_range | Recorded | public | required | — |
| 7 | recorded_root_at_cast_consistent | Recorded | public | optional | recorded_consistency_proof |
| 8 | recorded_inclusion_proof | Recorded | public | required | — |
| 9 | recorded_consistency_proof | Recorded | public | required | — |
| 10 | recorded_sth_third_party | Recorded | public | optional | — |
| 11 | counted_input_sanity | Counted | public | required | — |
| 12 | counted_unique_indices | Counted | public | required | — |
| 13 | counted_unique_commitments | Counted | public | required | — |
| 14 | counted_tally_consistent | Counted | zk | required | — |
| 15 | counted_missing_indices_zero | Counted | zk | required | — |
| 16 | counted_expected_vs_tree_size | Counted | zk | required | — |
| 17 | counted_election_manifest_consistent | Counted | public | required | — |
| 18 | counted_close_statement_consistent | Counted | public | required | — |
| 19 | counted_my_vote_included | Counted | zk | required | — |
| 20 | counted_input_commitment_match | Counted | public | required | — |
| 21 | stark_image_id_match | STARK | zk | required | — |
| 22 | stark_receipt_verify | STARK | zk | required | — |
Cast-as-Intended(4 チェック)
投票者の意図通りにコミットメントが生成されたかを検証するチェック群です。
ブラウザはこのカテゴリを、capability 保護された
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification の voterEvidence
が available の場合に得られる voteReceipt と、保存済みのローカル投票意図
(electionId / myVote / myRand)から評価します。API 応答上の Cast チェックは
いったん not_run とし、ブラウザがローカルで再計算した結果を overlay します。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
cast_receipt_present | 投票レシートが存在し、voteId とコミットメントを含む | local | required |
cast_choice_range | 選択肢が有効範囲内(A〜E) | local | required |
cast_random_format | 乱数が 32 バイトの 16 進数文字列 | local | required |
cast_commitment_match | 投票時データから再計算したコミットメントが投票レシートと一致 | local | required |
判定ロジックの詳細
cast_receipt_present
voteReceipt が存在し、voteId と commitment フィールドが存在することを確認します。
このチェック単体では UUID/hex 形式までは検証しません。
cast_choice_range
投票時データの選択肢が A〜E のいずれかであることを確認します。
範囲外の値は不正な入力として failed となります。
cast_random_format
投票時データの乱数が 32 バイト(64 文字)の 16 進数文字列であることを確認します。
0x プレフィックスの有無は正規化により吸収されます。
cast_commitment_match
投票時データから再計算した投票コミットメントが投票レシートの commitment 値と一致することを確認します。
再計算規則(ドメインタグ、正準フォーマット)は コミットメントスキーム を参照してください。
Recorded-as-Cast(6 チェック)
コミットメントが掲示板に正しく記録されたかを検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
recorded_commitment_in_bulletin | コミットメントが掲示板ツリーに存在する | public | optional |
recorded_index_in_range | 掲示板インデックスが 0 以上かつツリーサイズ未満 | public | required |
recorded_root_at_cast_consistent | 投票時のルートが最終ツリーの正当なプレフィックスである | public | optional |
recorded_inclusion_proof | RFC 6962 包含証明が暗号学的に検証成功 | public | required |
recorded_consistency_proof | RFC 6962 整合性証明が暗号学的に検証成功 | public | required |
recorded_sth_third_party | 第三者 STH ソース間で合意が成立(比較可能応答間) | public | optional |
判定ロジックの詳細
recorded_inclusion_proof と recorded_consistency_proof は cast-time 証跡(voteReceipt と userVote.proof)の存在を前提とします。
両チェックは、まず cast snapshot の一貫性(leafIndex、treeSize、bulletinRootAtCast が receipt と矛盾しないこと)を確認してから個別の検証に進みます。
証跡が揃わない場合はどちらも not_run となり、全体判定は fail-closed で missing_evidence 側へ倒れます。
recorded_commitment_in_bulletin
包含証明(recorded_inclusion_proof)の結果から派生します。
包含証明が成功すれば、コミットメントがツリーに存在することが暗号学的に証明されています。
recorded_index_in_range
掲示板インデックスが 0 <= index < treeSize の範囲内であることを確認します。
範囲外のインデックスは、データの不整合を示します。
recorded_root_at_cast_consistent
整合性証明(recorded_consistency_proof)の結果から派生します。
整合性証明が成功すれば、投票時のルートが最終ツリーの有効な追記専用プレフィックスであることが証明されています。
recorded_inclusion_proof
投票者のコミットメントに対する RFC 6962 包含証明(Merkle パス)を検証します。
リーフハッシュと Merkle パスから cast 時点のルートを再計算し、receipt の bulletinRootAtCast と一致することを確認します。
proof の treeSize が voteReceipt.bulletinIndex + 1 と一致しない場合は failed です。
recorded_consistency_proof
投票時のツリー(oldSize, oldRoot)から最終ツリー(newSize, newRoot)への RFC 6962 整合性証明を検証します。 evidence の取得と admission は acquisition boundary が担います(設計原則 4)。判定条件は次のとおりです。
- evidence の old/new 両時点の tree size と root が、receipt / 最終 bulletin authority の期待値と一致すること
- 投票時ルートが最終ツリーの追記専用プレフィックスであること
treeSizeチェックは包含証明と同条件- evidence が
unavailableの場合はnot_run、malformedまたはtransport_failedの場合はfailed
recorded_sth_third_party
設定された STH ソースからスナップショットを取得し、比較可能な応答同士で合意を確認します。
判定は matchingSources >= minMatches(デフォルト: 2)に加えて、比較対象になった応答間の全会一致(consensus)が必要です。
照合対象は STH ダイジェストが必須で、bulletinRoot / treeSize は各ソースが返した場合に追加で照合されます。
STH ソースが未設定の場合は not_run になります。
早見表では optional ですが、STH ソース設定時は required 相当に昇格します(昇格ルールはゲーティングロジックが定めます)。
Counted-as-Recorded(10 チェック)
記録された全投票が正しく集計に含まれたかを検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
counted_input_sanity | 公開入力サマリーが有効 | public | required |
counted_unique_indices | 入力中の全インデックスが一意 | public | required |
counted_unique_commitments | 入力中の全コミットメントが一意 | public | required |
counted_tally_consistent | claimed tally と zkVM の検証済み集計が一致(fallback: 合計整合) | zk | required |
counted_missing_indices_zero | 解決済み fail-closed exclusion count(excludedSlots 優先)が 0 | zk | required |
counted_expected_vs_tree_size | totalExpected がツリーサイズと一致 | zk | required |
counted_election_manifest_consistent | election-manifest.json が自己整合し、選挙 ID / electionConfigHash と一致 | public | required |
counted_close_statement_consistent | close-statement.json が自己整合し、log/tree/timestamp/root/sthDigest と一致 | public | required |
counted_my_vote_included | ビットマップ証明により自分のインデックスが counted 側に含まれたことを確認 | zk | required |
counted_input_commitment_match | 公開入力から計算した入力コミットメントがジャーナルの値と一致 | public | required |
判定ロジックの詳細
counted_input_sanity
public-input.json 相当から組み立てた公開入力サマリーが存在し、スキーマ検証に成功していることを確認します。
さらに、treeSize が正の整数、votesCount <= treeSize、bulletinRoot が 32 バイト hex かつゼロ値でないことを要求します。
counted_unique_indices
zkVM に渡された全投票のインデックスが重複なく一意であることを確認します。 重複インデックスは、同一投票の二重カウントを示す可能性があります。
counted_unique_commitments
zkVM に渡された全投票のコミットメントが重複なく一意であることを確認します。 重複コミットメントは、同一コミットメントに対する複数投票を示す可能性があります。
counted_tally_consistent
ジャーナルが存在し、journal.totalVotes が 0 の場合は、zkVM が投票を 1 件も処理していないため failed とします。
主経路では、公開される claimed tally(tally.counts)が zkVM の verifiedTally と選択肢ごとに一致し、かつ verifiedTally の合計が tally.totalVotes と一致することを確認します。
これにより「公開表示された集計値」と「zkVM が証明した集計値」の不一致を検出します。
claimed tally が利用できない場合は、フォールバックとして journal.verifiedTally の合計と journal.validVotes の一致を検証します。
counted_missing_indices_zero
fail-closed(安全側に倒す)な除外数が 0 であることを確認します。
除外数は次の優先順で解決し、0 でなければ即座に failed とします。
journalがある場合:journal.excludedSlotsを proof-bound な除外数として使用し、excludedSlots/missingSlots/invalidPresentedSlots/validVotesが非負整数であることも先に確認するjournalがない場合:excludedSlots→missingSlots + invalidPresentedSlotsの順で探索
rejectedRecords は説明用の補助値で、この判定には使いません。
旧フィールド (excludedCount / missingIndices / invalidIndices) は fail-closed 補正経路でのみ参照され、本判定では使用しません。
counted_expected_vs_tree_size
totalExpected(期待される投票数)が掲示板のツリーサイズと一致することを確認します。
不一致は、暗黙の投票除外を示す可能性があります。
counted_election_manifest_consistent
election-manifest.json の electionConfigHash を再計算し、manifest 自身の宣言値と一致することを確認します。
そのうえで electionId と electionConfigHash を、セッション値、publicInputArtifact
を厳密に admit して得た内部 publicInputAuthority、ジャーナルに含まれる対応値と相互照合します。
counted_close_statement_consistent
close-statement.json から sthDigest を再計算し、宣言された snapshot と一致することを確認します。
そのうえで timestamp が、publicInputArtifact を厳密に admit して得た内部
publicInputAuthority.timestamp と一致することを確認します。
さらに logId / treeSize / bulletinRoot / sthDigest が、検証入力やジャーナルと矛盾しないことを確認します。
counted_my_vote_included
includedBitmapRoot に対する bitmap Merkle 証明を検証し、投票者のインデックスに対応するビットが 1(counted に含まれた)であることを確認します。
verification handler は対象 finalization の included proof evidence と、seenBitmapRoot がある場合の
seen proof evidence を取得・admit してから evaluator に渡します。両方の evidence がある場合は、
presented but invalid / not presented to the prover / unknown excluded を説明可能にします。
owner-scoped な proof building block の現行 endpoint は
GET /api/sessions/:sessionId/finalizations/current/bitmap-proofs/:kind/:voteIndex です。
証明材料が取得できない場合は not_run になり、補助 note が付くことがあります。
counted_input_commitment_match
公開入力から再構成した入力コミットメントが、zkVM ジャーナルの値と一致することを確認します。
対象フィールドの集合と正準エンコーディングは 入力コミットメント を参照してください。
public-input.json 全体の単純なハッシュではない点に注意してください。
STARK Verification(2 チェック)
STARK 証明の暗号学的正当性を検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
stark_image_id_match | verifier-confirmed な Image ID と expected / host-side metadata が整合 | zk | required |
stark_receipt_verify | STARK レシートが暗号学的に検証成功 | zk | required |
判定ロジックの詳細
stark_image_id_match
STARK status が success に解決された後で、期待 Image ID、verifier-confirmed Image ID、ホスト主張値が相互に矛盾しないことを確認します。
期待 Image ID を解決できない場合は、暗黙にフォールバックせず fail-closed で失敗します。
解決順と variant の指定規則、整合条件(report に Image ID フィールドが揃わない場合の挙動を含む)は Image ID を参照してください。
stark_receipt_verify
サーバー側の Rust 検証サービスが Receipt::verify(image_id) を実行し、Seal(STARK 証明)が暗号学的に正当であることを確認します。
チェック結果は success / failed / not_run / running で表現され、dev_mode は ゲーティングロジック に従って正規化されます(証拠種別 を参照)。
チェックステータスの解決
チェックごとの永続的な状態機械は持ちません。evaluator はその時点で admit 済みの入力と
依存する STARK status から各チェックの snapshot status を再計算します。そのため、証拠の取得や
検証結果の更新により、not_run または pending から success / failed へ直接解決する
ことがあります。
| ステータス | 説明 | required の場合の影響 |
|---|---|---|
not_run | 関連データが未取得、または検証未開始 | ブロック |
pending | 依存する検証の完了を待機中 | ブロック |
running | 検証を実行中 | ブロック |
success | 検証成功 | 通過 |
failed | 検証失敗 | ブロック |
全体判定(集約結果)の決まり方は ゲーティングロジック を参照してください。
ゲーティングロジック
この章は、「Verified」を表示してよい条件と、表示を必ず阻止する不変条件を定義します。
「必要な検証が未実行または失敗なら Verified を表示しない」という原則のもと、各チェックの結果がどう最終判定に集約されるかを定式化します。
この章での Verified は、UI の最終表示が緑色の Verified になってよいかだけを指します。
verificationStatus='success' や stark_receipt_verify=success は STARK receipt 検証成功の信号にすぎず、単独で overall verification の成功を意味しません。
最終判定の種類
検証パイプラインの最終表示は shared
deriveVerificationPresentationPolicy が一元的に決定します。この policy は
deriveVerificationSummary の集約結果に、evidence availability、hard failure、
明示的な server failure、表示 sequence の readiness を合わせ、UI 上の次の
ステータスと「Verified」表示資格を返します。API と browser は同じ policy を使い、
UI は policy が許可した最終表示だけを描画します。
| 表示ステータス | 主な条件 | UI 表示 |
|---|---|---|
| Verified | required 条件が満たされ、optional チェックの劣化もない(fully_verified) | 緑色 |
| Verification Failed | 証明失敗、票除外、Recorded/Counted/Cast の必須失敗、または公開集計値と検証済み tally の不一致が確定した場合 | 赤色 |
| Warning | required チェックが進行中、証拠不足がある、または optional チェックのみ劣化している場合 | 黄色 |
| Demo Only | required 条件は満たしたが dev-mode receipt を含む場合(demo_only) | 黄色 |
Verified の必要条件は 不変条件のまとめ に集約しています。
要点は、既知の required check がすべて存在してすべて success であること、票除外(excludedSlots > 0)がないこと、dev-mode receipt を production STARK proof として扱っていないことです。
ステータスの判定順序
| 優先順 | 判定条件 | 最終ステータス |
|---|---|---|
| 1 | required チェックに pending / running がある | Warning (in_progress) |
| 2 | STARK 証明系ロールが failed | Verification Failed |
| 3 | completeness ロールが failed(user_vote_excluded / votes_excluded / votes_excluded_unknown) | Verification Failed |
| 4 | Recorded-as-Cast の required チェックが failed | Verification Failed |
| 5 | tally consistency だけが失敗し、proof / completeness / user inclusion / input integrity / Recorded required が成功 | Verification Failed (published_tally_mismatch) |
| 6 | Counted-as-Recorded または Cast-as-Intended の required チェックが failed | Verification Failed |
| 7 | (a) required に not_run がある (b) 必須ロール不足 (c) 既知チェックと混在する未知チェック (d) required 定義の未解決のいずれか | Warning (missing_evidence) |
| 8 | dev-mode evidence がある、または check evidence に demo がある | Demo Only (demo_only) |
| 9 | optional チェックに failed / not_run がある | Warning (verified_with_limitations) |
| 10 | 上記いずれにも該当しない | Verified |
この表は deriveVerificationSummary の集約結果です。
チェックが空、または未知チェックだけで既知チェックが 1 件も解決できない場合、summary は null になり、Verified ではなく最終サマリー未表示として扱われます。
未知チェックが既知チェックと混在する場合も、summary は missing_evidence になります。
これは将来のチェック追加や API drift を成功として解釈しないための fail-closed ルールです。
shared policy の readiness は、検証開始済み、表示 sequence 完了、check の
pending / running 解消がそろうまで renderedStatus を返しません。表示 source
は (1) 明示的な server failure、(2) hard-failure fallback、(3) summary、
(4) pending warning の順で policy 内で解決されます。/verify ページは timeout
や sequence failure を明示的な server failure input として渡し、独自の
「Verified」override は持ちません。
STARK 検証のゲーティング
STARK 検証は整合性検証とは独立に評価されます。
| STARK ステータス | 説明 | 最終判定への影響 |
|---|---|---|
success | 暗号学的に検証成功 | 他の必須チェックも success なら Verified 可能 |
failed | 検証失敗 | Verified をブロック |
dev_mode | 開発モードのフェイクレシート | core evaluator では allowDevModeVerification=true なら success、それ以外は not_run |
not_run | 未実行 | missing_evidence(Warning)扱い。Verified をブロック |
running | 実行中 | in_progress(Warning)扱い。Verified をブロック |
stark_receipt_verify=success でも、他の required checks のいずれかが失敗、未実行、または進行中なら fully verified にはなりません(不変条件のまとめを参照)。
zkGate: STARK 結果に基づく Counted チェックの制御
STARK 検証の結果は、Counted-as-Recorded 段階のチェック評価にも影響します。 これを zkGate と呼びます。
| STARK 解決後ステータス | Counted チェックへの反映 |
|---|---|
running | pending |
not_run | not_run |
failed | failed |
success | ゲートなしで通常評価 |
core evaluator では、dev_mode は事前に success または not_run に正規化されてから zkGate に入力されます。
一方、現行の GET /api/sessions/:sessionId/finalizations/:finalizationId/verification の表示用ステータス組み立てでは、dev mode が許可されていない場合は fail-closed の failed として反映されます。
ステップとチェックの対応関係
UI に表示される 4 つのステップは、現行実装では 22 個のチェック定義から派生します。
ただし、verificationSteps[].status は「その stage で required 扱いになるチェック群」から導出されます。
verificationSteps[].inputs は stage 内の 全チェック定義 から集約されます。
| ステップ | required として集約されるチェック ID |
|---|---|
| Cast-as-Intended | cast_receipt_present, cast_choice_range, cast_random_format, cast_commitment_match |
| Recorded-as-Cast | recorded_index_in_range, recorded_inclusion_proof, recorded_consistency_proof、および STH source 設定時の recorded_sth_third_party |
| Counted-as-Recorded | counted_input_sanity, counted_unique_indices, counted_unique_commitments, counted_tally_consistent, counted_missing_indices_zero, counted_expected_vs_tree_size, counted_election_manifest_consistent, counted_close_statement_consistent, counted_my_vote_included, counted_input_commitment_match |
| STARK Verification | stark_image_id_match, stark_receipt_verify |
補足:
recorded_commitment_in_bulletinはrecorded_inclusion_proofから、recorded_root_at_cast_consistentはrecorded_consistency_proofから導出される表示用チェックです。 チェック一覧には現れますが、単独では step status を決定しません。recorded_sth_third_partyは通常は optional ですが、STH source が設定されている場合だけ required に昇格し、Recorded-as-Cast の step status と最終判定をブロックし得ます。
ステップのステータスは、required 扱いになったチェックのステータスから次の順序で集約されます。
| 集約ルール | 条件 |
|---|---|
failed | required チェックのいずれかが failed |
running | failed がなく、required チェックのいずれかが running |
pending | failed/running がなく、いずれかが pending |
success | required チェックがすべて success |
not_run | 上記のいずれにも該当しない |
さらに現行実装には、単純集約だけではない 3 つの補正があります。
counted_as_recordedはjournalが存在しない場合、required チェックにfailedがない限りnot_runに補正されます。recorded_as_castはuserVote.proof.treeSizeがない場合、not_runに補正されます。GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationはcastSource='client'でverificationSteps/verificationChecksを組み立てるため、API 応答上の Cast-as-Intended はいったんnot_runです。 その後ブラウザ側で保存済みセッション情報から Cast チェックを再評価して上書きし、最終的な UI 表示と summary にはそのローカル結果が反映されます。
検証実行と UI 表示の時系列(ポーリングとステップ表示の順序)は 設計と実行フロー を参照してください。
不変条件のまとめ
以下の不変条件は、コードの変更によっても決して緩和してはなりません。
| 不変条件 | 根拠 |
|---|---|
required check の failed / not_run / pending / running → Verified を表示しない | 必須証拠の失敗、不在、未完了を成功として扱わない |
| 空 check set、未知チェックだけ、未知チェック混在、required 定義の欠落 → Verified を表示しない | API drift や未対応チェックを fail-closed にする |
excludedSlots > 0 → successful overall verification を許さない | 投票除外は最も深刻な不正 |
legacy count alias(excludedCount など)→ fail-closed compatibility signal としてのみ扱う | 旧フィールドを公開成功契約として復活させない |
recorded_consistency_proof の失敗 → Verified を表示しない | 追記専用性が保証されない |
| STH 合意の不成立(有効時) → Verified を表示しない | スプリットビュー攻撃の可能性 |
counted_missing_indices_zero / counted_expected_vs_tree_size の失敗 → Verified を表示しない | tally completeness と入力境界が保証されない |
counted_election_manifest_consistent / counted_close_statement_consistent の失敗 → Verified を表示しない | 公開 manifest / close statement との binding が崩れる |
counted_my_vote_included / counted_input_commitment_match の失敗 → fully verified をブロックする | ユーザー inclusion と proof input binding が崩れる |
stark_image_id_match / stark_receipt_verify の失敗 → fully verified をブロックする | 期待 guest image と receipt の正当性が保証されない |
stark_receipt_verify=success だけでは Verified にしない | STARK receipt は必要条件であり十分条件ではない |
| dev-mode receipt → production STARK proof として扱わず、緑色の Verified を表示しない | RISC0_DEV_MODE=1 は実証用の fake receipt |
| 非公開アーティファクトをバンドルに含めない | 投票の秘匿性を維持 |
これらの不変条件は、改ざんシナリオ(S0〜S5)の検出を保証する基盤です。 各シナリオがどのチェックで検出されるかは、検出メカニズム を参照してください。
この fail-closed モデルは、単体、結合、E2E テスト で「Verified を誤表示しない」ケースを継続的に検査しています。 形式化側では Lean による形式化 の verification summary / display vectors を通じて、モデルと実装の対応を確認します。
公開境界
検証アーティファクトは、bundle.zip に入れる公開可能データと、保護対象として残すデータに分かれます。
ここでいう public は、「秘密を含まず、第三者検証に使える公開可能データ」という機密性の分類です。
「無認証で誰でも取得できること」は意味しません。
通常のブラウザと CLI では、bundle.zip も検証レポートも session capability で保護された API から取得します。
境界の原則
bundle.zip は、公開可能アーティファクトだけを含む監査用アーカイブです。
zkVM の witness、投票内容を復元し得る入力、個票状態を細かく説明する private bitmap、検証サービスの protected report artifact は含めません。
この境界は利便性より優先します。
bundle.zip 単体で /verify 画面の最終判定を完全再現できない場合でも、private artifact を公開アーカイブへ混ぜません。
UI の最終判定には、session-scoped な自票 proof、bitmap proof、設定時の third-party STH evidence、サーバー側の STARK receipt verification 結果も関わります。
public bundle の default-deny manifest
同期生成の bundle.zip は次の member に限定されます。async bundle は共通の必須 5 member
だけを含みます。
| ファイル | 扱い | 役割 |
|---|---|---|
public-input.json | 必須 | 秘密値を除いた zkVM 入力側の公開監査レコード |
election-manifest.json | 必須 | 選挙設定と electionConfigHash を照合するための公開レコード |
close-statement.json | 必須 | 集計締切時点の logId / treeSize / bulletinRoot 境界 |
receipt.json | 必須 | production 検証で実証明として検証する STARK receipt payload |
journal.json | 必須 | tally、root、count、commitment などを含む zkVM の公開出力 |
metadata.json | 必須 | sync bundle の作成時刻、session ID、method version など |
sth.json | 任意 | STH snapshot を bundle に保存する場合の公開可能 evidence |
consistency-proof.json | 任意 | CT-style append-only consistency proof を保存する場合の公開証拠 |
receipt.json が含まれていても、それだけで production STARK proof として成功扱いにはなりません。
開発モードの receipt は production STARK proof ではなく、production 検証では fail-closed に扱います。
新しい artifact は、生成された時点では private です。公開可能かどうかを各 builder
や配信 handler が個別に推測せず、canonical manifest に明示的に追加され、sync/async
member set と completed-archive admission の review を通過した場合にだけ
bundle.zip へ入れられます。この default-private / allowlist-driven 方針により、
新しい内部 artifact の追加が意図しない公開へ直結することを防ぎます。
manifest にない member は private が既定です。required member の欠落、private/unknown member、 duplicate/case conflict、traversal・absolute・非 canonical path を含む completed ZIP は publication/delivery より前に拒否されます。
bundle.zip に入れないファイル
次のファイルは bundle.zip のメンバーではありません。
| ファイル | 理由 |
|---|---|
input.json | 投票選択や乱数を含み得る zkVM witness 付き private input |
verification.json | 検証サービスの protected report artifact |
included-bitmap.json | counted された index を説明する private bitmap artifact |
seen-bitmap.json | prover に提示された index を説明する private bitmap artifact |
failure-marker.json | async prover の bounded category だけを持つ private diagnostic |
input.json、verification.json、included-bitmap.json、seen-bitmap.json の 4 つは、モードに依存しない保護対象です。
verification.json は必要な場合に bundle.zip とは別の report endpoint から取得します。
failure-marker.json は非同期モードのみで生成され得る private diagnostic で、report endpoint からも配布しません。ライフサイクル契約の詳細は 非同期プローバー を参照してください。
bundle と report の取得経路
通常のブラウザと CLI の contract は、次の capability 保護 API です。
| エンドポイント | 返すもの |
|---|---|
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/bundle | public bundle.zip |
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/report | protected verification.json report artifact |
どちらも session capability によって保護されます。
finalizationId は、その session の現在の succeeded finalization record が持つ
finalizationId と一致している必要があります。
S3-backed 実行でも、ブラウザと CLI は raw S3 URL や presigned URL を通常契約として使いません。 API handler が権限と finalization scope を確認したうえで、ローカル保存または S3 から artifact を読み出して返します。 大きい S3 bundle は、同じ API 経由の range response で配信されます。
sync mode と async mode の差分
公開境界(何を入れないか)は sync mode と async mode で同じです。 モードごとの実際のメンバー構成、生成環境、生成フローの差分は バンドル構造 を参照してください。
実装上の照合点
この境界を確認するときは、次の実装を合わせて参照します。
- mode-aware manifest and archive admission:
packages/verification/src/verification/public-bundle-manifest.{json,ts} - sync/async builders:
packages/verification/src/verification/verification-bundle.ts,infra/docker/entrypoint.sh - async prover failure marker:
infra/docker/prover-failure-marker-contract.json,packages/aws-adapters/src/finalize/async-prover-artifacts.ts - authenticated bundle/report delivery:
apps/api/src/server/api/handlers/verificationBundles.ts - 現行 verification flow notes:
docs/verification/README.md
詳細なファイル構造とローカル監査手順は、バンドル構造 と 第三者検証ガイド を参照してください。
バンドル構造
この章は、証明バンドルのファイル構造と、同期モードと非同期モードの差分を説明します。 公開可能アーティファクトと保護対象アーティファクトの境界は 公開境界 に集約しています。
概要
証明バンドル は、zkVM の実行結果を検証可能な形で保存し、配布するためのアーティファクト群です。
実際に配布される bundle.zip は、mode-aware
な default-deny manifest によって公開可能アーティファクトだけを含める配布対象アーカイブです。
ここでいう public / 「公開可能」は「秘密を含まず第三者検証に使える」という機密性の分類です(public ≠ 無認証公開の正本は 公開境界)。
bundle.zip は監査用成果物であり、/verify 画面の最終判定を単体で完全再現することは目的としていません。
UI 最終判定に必要な追加材料は 第三者検証ガイド を参照してください。
flowchart TB
subgraph "バンドルディレクトリ"
subgraph "公開可能アーティファクト"
PI["public-input.json<br/>公開入力"]
EM["election-manifest.json<br/>選挙マニフェスト"]
CS["close-statement.json<br/>締切ステートメント"]
RC["receipt.json<br/>STARK レシート"]
JN["journal.json<br/>zkVM ジャーナル"]
MT["metadata.json<br/>メタデータ(syncのみ)"]
STH["sth.json<br/>STH スナップショット"]
CP["consistency-proof.json<br/>整合性証明"]
end
subgraph "非公開アーティファクト"
IN["input.json<br/>秘匿入力(ウィットネス)"]
VR["verification.json<br/>検証レポート"]
IB["included-bitmap.json<br/>厳密 counted bitmap"]
SB["seen-bitmap.json<br/>厳密 presented bitmap"]
FM["failure-marker.json<br/>非同期失敗分類"]
end
BZ["bundle.zip<br/>配布対象アーカイブ"]
end
PI --> BZ
EM --> BZ
CS --> BZ
RC --> BZ
JN --> BZ
MT --> BZ
STH -.-> BZ
CP -.-> BZ
IN -.-x BZ
VR -.-x BZ
IB -.-x BZ
SB -.-x BZ
FM -.-x BZ
公開境界の要約
配布対象アーカイブ(bundle.zip)に含められるファイルは、default-deny manifest によって厳格に制限されています。
この節は構造を読むための要約です。境界の規則そのものは 公開境界 が正です。
bundle.zip に入る公開可能アーティファクト
| ファイル | 内容 | 用途 |
|---|---|---|
public-input.json | zkVM 検証に使う秘密データを含まない検証用レコード | 第三者が入力コミットメントを再計算するため |
election-manifest.json | 選挙設定の公開監査用スナップショット | 選挙設定と electionConfigHash の照合 |
close-statement.json | 集計締切時点のログ境界を表す公開監査レコード | logId、treeSize、bulletinRoot の照合 |
receipt.json | ホストが出力する { receipt, image_id } ラッパー JSON | 第三者がレシートを独立に検証するため |
journal.json | zkVM ゲストの公開出力(集計結果、除外情報、ビットマップルート等) | 集計結果と整合性データの確認 |
metadata.json | バンドルの作成日時、セッション ID、メソッドバージョン(sync のみ) | バンドルの来歴追跡 |
sth.json | 第三者 STH 検証のスナップショット(任意) | STH 合意の再現可能な証拠 |
consistency-proof.json | RFC 6962 整合性証明(任意) | 追記専用性の独立検証 |
receipt.json の存在だけでは成功扱いになりません。dev-mode(RISC0_DEV_MODE=1)receipt の扱いは 公開境界 と同じ規則です。
metadata.json は sync bundle で必須です。sth.json と consistency-proof.json は sync
bundle の optional member ですが、現行フローでは通常生成されません。
bundle.zip に入れない保護対象アーティファクト
冒頭図の非公開アーティファクト 5 ファイルは bundle.zip のメンバーではありません。
対象ファイルの一覧と除外理由は 公開境界 を参照してください。
公開入力の構造
public-input.json は、第三者検証に必要で、かつ選択肢と乱数を含まない入力側レコードです。
input.json の単純なサブセットではありません。
| フィールド | 説明 |
|---|---|
schema | スキーマ識別子("stark-ballot.public_input") |
version | スキーマバージョン("1.1") |
electionId | 選挙 ID(UUID) |
electionConfigHash | 選挙設定のハッシュ |
bulletinRoot | 掲示板の最終ルートハッシュ |
treeSize | 掲示板のツリーサイズ |
totalExpected | 期待される投票数 |
logId | 掲示板ログ ID |
timestamp | 集計時のタイムスタンプ |
methodVersion | zkVM メソッドバージョン |
votes | 各投票のインデックス、コミットメント、Merkle パスの配列 |
votes 配列の内容から、第三者は入力コミットメントを再計算できます。選択肢と乱数はこのファイルに含まれません。
public-input.json と inputCommitment は同一ではありません。
後者が直接束縛するのはこのレコードの一部です。
計算対象フィールドと非対象フィールドの整理は 入力コミットメント を参照してください。
同期バンドルと非同期バンドルの違い
証明生成には、同期モード(local profile の TypeScript API プロセスが生成)と 非同期モード(ECS Fargate コンテナの entrypoint.sh が生成し、S3 経由でコールバック Lambda がセッションに反映)の 2 つのパスがあります。
両モードとも、public-input.json、election-manifest.json、close-statement.json は journal.json と正準な proof-bound data との整合性検査を通過した場合にのみ bundle 化されます。
不一致があれば fail-closed で処理を中断します。
| 項目 | 同期モード | 非同期モード |
|---|---|---|
| 実行環境 | local profile の TypeScript API プロセス | ECS Fargate コンテナ |
input.json | 生成される(非公開保存) | ワーク入力として S3 に置かれる(配布対象外) |
public-input.json | TypeScript で生成 | entrypoint.sh 内で生成 |
election-manifest.json | TypeScript で生成 | entrypoint.sh 内で生成 |
close-statement.json | TypeScript で生成 | entrypoint.sh 内で生成 |
journal.json | TypeScript で生成 | *-output.json から bundle.zip 用に生成 |
receipt.json | ホストの { receipt, image_id } 出力を保存 | *-receipt.json を receipt.json としてコピーして同梱 |
included-bitmap.json | 生成される場合は private に保持 | 生成される場合は隣接オブジェクト(sibling object)として保持 |
seen-bitmap.json | 生成される場合は private に保持 | 生成される場合は隣接オブジェクトとして保持 |
metadata.json | 生成される | 生成されない |
verification.json | 検証サービス呼び出し後に保存 | finalize コールバック時点では生成されず、後続の scoped verification POST を worker が処理した場合に保存 |
failure-marker.json | 生成されない | 非ゼロ終了時だけ、bounded な失敗分類を持つ private sibling object として best-effort で保存 |
bundle.zip | shared manifest に基づき作成し、completed ZIP を admission | shared manifest の async member set に基づき作成し、completed ZIP を admission |
| 保存先 | ローカルファイルシステム | S3 |
| 配信方法 | capability 保護 API がローカルから読み出して返す | capability 保護 API が S3 から読み出して返す。大きい bundle は range response で配信 |
非同期バンドルの生成フロー
非同期モードでは、ECS Fargate コンテナの entrypoint.sh が次の手順でバンドルを構築します。
- S3 から入力 JSON をダウンロード
- ホストバイナリを実行し、レシートと出力を生成
- 出力から
journal.jsonを変換生成 - 入力と出力から
public-input.json、election-manifest.json、close-statement.jsonを構築 - 整合性検査(本節冒頭参照)を通過したものだけを bundle に含める
- shared manifest の async member set から
bundle.zipを作る - completed ZIP の member 構成と path を admission する
- 次を S3 にアップロード
- ホストの生出力:
*-receipt.json、*-output.json - 公開可能アーティファクト:
public-input.json、election-manifest.json、close-statement.json - 非公開の隣接 artifact:
included-bitmap.json、seen-bitmap.json - 配布対象アーカイブ:
bundle.zip
- ホストの生出力:
上記は成功時の生成フローです。
非ゼロ終了時、entrypoint は failure-marker.json(bounded な失敗カテゴリだけを持つ private sibling object)を同じ execution scope の private S3 prefix へ best-effort で保存します。
bundle.zip にも protected verification.json report にも取り込まれません。カテゴリ集合や配信されない規則を含む契約の詳細は 非同期プローバー を参照してください。
コールバック Lambda は S3 の bundle.zip からジャーナル、レシート、public-input.json、election-manifest.json、close-statement.json を復元します。
利用可能な場合は隣接オブジェクトの bitmap artifact も取り込み、bundle locator と監査用データを保存します。
report locator は後続の
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification が verification work を起動し、
worker が report を保存した場合に記録されます。
ブラウザと CLI への通常配信は capability 保護 API が担当します。
infra/docker/entrypoint.sh は methodVersion 14 のホスト出力を検証し、現行契約と一致しない出力は fail-closed で停止します。
journal.json と public-input.json の methodVersion および inputCommitment が一致し、election-manifest.json と close-statement.json が各自の公開監査フィールドと整合することを確認してから bundle.zip を生成します。
バンドルディレクトリ構造
同期モード(ローカルファイルシステム)
{VERIFIER_WORK_DIR}/
{sessionId}/
{finalizationId}/
input.json ← 非公開: ウィットネス
public-input.json ← 公開可能
election-manifest.json ← 公開可能
close-statement.json ← 公開可能
journal.json ← 公開可能
receipt.json ← 公開可能
metadata.json ← 公開可能
included-bitmap.json ← 非公開: 厳密 counted bitmap artifact
seen-bitmap.json ← 非公開: 厳密 presented bitmap artifact
verification.json ← 非公開: 検証レポート
bundle.zip ← 配布対象: manifest member の admitted archive
非同期モード(S3)
s3://{BUCKET}/sessions/{sessionId}/{finalizationId}/
input.json ← 非公開: ワーク入力
{inputBase}-receipt.json ← ホストの生出力
{inputBase}-output.json ← ホストの生出力
{inputBase}-journal.json ← ホストが生成した場合のみ
public-input.json ← 公開可能
election-manifest.json ← 公開可能
close-statement.json ← 公開可能
included-bitmap.json ← 非公開: 厳密 counted bitmap artifact
seen-bitmap.json ← 非公開: 厳密 presented bitmap artifact
bundle.zip ← 配布対象: 内部は receipt.json / journal.json / public-input.json / election-manifest.json / close-statement.json
verification.json ← scoped verification POST の worker 完了後に参照可能になる場合あり(非公開)
failure-marker.json ← 非ゼロ終了時のみ: bounded category-only diagnostic(非公開、best-effort)
補足
- 上記の S3 構造は成功時と失敗時の任意 artifact をまとめたものです。
bundle.zipとfailure-marker.jsonが同時に生成されることを意味しません。 - 非同期モードの S3 オブジェクト名は、コンテナ実行時に生成される一時入力ファイル名
inputBaseに依存するため、固定のreceipt.json/journal.jsonになりません。 - target AWS profile の capability 保護 API は、
sessionIdとfinalizationIdから導出した canonical S3 key の report だけを読み出します。 - local profile の API は同じ identity から導出したローカル report だけを読み出します。profile をまたぐ fallback はなく、選択された backend に artifact がなければ
404を返します。
バンドルのアクセス方法
ダウンロードエンドポイント
取得経路とアクセスポリシーは 公開境界 > bundle と report の取得経路 を、wire 契約(status、Range、エラーコード)は API エンドポイント を参照してください。
アーカイブの再現性
同期モード(verification-bundle.ts)の bundle.zip は再現性を確保するため、次の措置を講じています。
- エントリのタイムスタンプをゼロに固定
- shared manifest に一致するファイルのみを含める
- ファイル名のアルファベット順でエントリを追加
非同期モード(infra/docker/entrypoint.sh)は zip -r で作成されます。
そのため、上記の再現性制御とは実装が異なります。
セキュリティ上の制約
パストラバーサル防止
バンドルのパスセグメント(セッション ID、finalization ID)は英数字とハイフンのみに制限されています。
.. を含むパスや許可されていない文字を含むパスは拒否されます。
completed ZIP も required member の欠落、private/unknown member、duplicate/case conflict、
traversal・absolute・非 canonical member name を含む場合は拒否されます。
改ざんシナリオ
STARK Ballot Simulator は、E2E 検証可能投票の教育的デモとして、正常系 S0 と改ざんシナリオ S1 から S5 を提供します。
S1 から S5 は特定の異常ケースを模擬し、検証パイプラインがどのチェックで異常を検出するかを示します。
この部の章
想定読者と前提
想定読者は、検証パイプラインの教育的デモを試したい技術者です。 読む前に、検証パイプライン の 4 段階モデルを把握していることを前提にします。
この部で扱わないもの
- 実世界の投票システムに対する攻撃手法の一般論
- 本番投票システム向けの脅威モデリングや対策ガイド
S2とS4を proof-tampering とみなすなど、PoC スコープを超える攻撃シナリオ
関連する章
シナリオ一覧
改ざんシナリオ S0〜S5 の定義と、実装上どこを改変するかを整理します。 説明の中心は、zkVM 入力、主張集計(claimed tally)、ジャーナル統計(missing/invalid/excluded)の関係です。
教育モードの目的
改ざんシナリオは、暗号的検証が実際に機能することを確認するために設計されています。
- 正常ケース(S0)を基準として、検証パイプラインが通過する状態を確認する
- 攻撃シナリオ(S1〜S5)を適用して、どの不変条件が破れると検証が失敗するかを確認する
攻撃の類型
- 入力改ざん (
tamperMode=input): S1 / S3 / S5 - 主張改ざん (
tamperMode=claim): S2 / S4
この分類は「改ざんがどこに入るか」のみを示します。 どのチェックで失敗するかの詳細は 検出メカニズム を参照してください。
実装上の共通前提
- 1 回の finalize で選択されるシナリオは 1 つ(S0〜S5)
- UI:
/aggregateは single-select(S0〜S5 のラジオボタン) - API:
POST /api/sessions/:sessionId/finalizationsはscenarioIdを 1 つ受け取る
- UI:
totalExpectedは 64(ユーザー 1 + ボット 63)- 掲示板(CT Merkle)は追記専用で、シナリオ適用で既存エントリは削除しない
tamperModeはnone/input/claimの 3 種
実行モードと検証の前提
- 本章は実 API 経路(
POST /api/sessions/:sessionId/finalizations→finalizeSessionHandler→finalizeSessionUsecase→finalizeSync|finalizeAsync)を基準に説明する - finalize 実行モード(同期 / 非同期)は
RUNTIME_PROFILEが一意に決める。profile ごとの意味論は AWS runtime 境界 を参照 - mock mode の差分: browser mock(
VITE_RUNTIME_PROFILE=local-demo)の API fixture は本章と異なるチェック結果を返すことがあり、local-demo/static-mock-e2eの mock zkVM executor は production STARK proof を生成しない local-proof-developmentの dev-mode receipt も production STARK proof ではない- 本章の「主な失敗点」は STARK 検証が
successの局面を前提とする(zkGate の詳細は 検出メカニズム を参照)
tamperMode は、シナリオ変更を zkVM 入力へ反映するかどうかを決めます。
flowchart TD
A[シナリオ選択] --> B{tamperMode}
B -->|none / claim| C[元の votes を zkVM 入力へ]
B -->|input| D[modifiedVotes を zkVM 入力へ]
C --> E[zkVM 実行]
D --> E
シナリオ一覧表
| シナリオ | 類型 | tamperMode | zkVM 入力 |
|---|---|---|---|
| S0 | 正常 | none | 元の 64 票 |
| S1 | 除外 | input | 63 票(ユーザー除外) |
| S2 | 主張改ざん | claim | 元の 64 票 |
| S3 | 除外 | input | 63 票(ボット除外) |
| S4 | 主張改ざん | claim | 元の 64 票 |
| S5 | ランダム除外 | input | 63 票(通常) |
シナリオ別に失敗するチェックは 検出メカニズム > シナリオ別の主な失敗チェック を参照してください。
S0: 正常(改ざんなし)
改ざんを適用しない基準シナリオです。
| 項目 | 値 |
|---|---|
| tamperMode | none |
| zkVM 入力票数 | 64 |
| claimed と verified | 一致 |
excludedSlots | 0 |
S1: ユーザー票の除外
ユーザー票(インデックス 0)を modifiedVotes から削除し、63 票を zkVM に渡します。
| 項目 | 値 |
|---|---|
| tamperMode | input |
| zkVM 入力票数 | 63 |
| claimed と verified | 一致(どちらも 63 票入力ベース) |
| ジャーナル統計 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
失敗するチェックは 検出メカニズム を参照してください。
S2: ユーザー票に関する主張集計の改ざん
ユーザー票に対する「主張集計(表示する tally)」のみ改ざんします。 zkVM には元の 64 票を渡します。
| 項目 | 値 |
|---|---|
| tamperMode | claim |
| zkVM 入力票数 | 64(元データ) |
| claimed と verified | 不一致(ユーザー選択肢が -1、別候補が +1) |
excludedSlots | 0(通常) |
inputCommitment | zkVM 入力由来のため通常は一致 |
失敗するチェックは 検出メカニズム を参照してください。
S3: ボット票の除外
現行実装ではボット票インデックス 1(targetBotId 初期値)を削除し、63 票を zkVM に渡します。
| 項目 | 値 |
|---|---|
| tamperMode | input |
| zkVM 入力票数 | 63 |
| claimed と verified | 一致(どちらも 63 票入力ベース) |
| ジャーナル統計 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
S1 との違い
- S1: ユーザー自身の未集計をビットマップで直接示せる
- S3: ユーザー票は含まれるが、集計全体の完全性違反で検出される
S4: ボット票に関する主張集計の改ざん
1 票のボット票に関する「主張集計」だけを改ざんします。 zkVM 入力は元の 64 票のままです。
| 項目 | 値 |
|---|---|
| tamperMode | claim |
| zkVM 入力票数 | 64(元データ) |
| claimed と verified | 不一致(対象ボットの元候補が -1、別候補が +1) |
excludedSlots | 0(通常) |
inputCommitment | zkVM 入力由来のため通常は一致 |
S2 と同様に、改ざん対象は tally.counts 側です。失敗するチェックは 検出メカニズム を参照してください。
S5: ランダムな票の除外
現行実装では、64 票からランダムに 1 票を選び、modifiedVotes から削除します。
選んだ票の候補を別候補へ変える処理はありません。
S5 の処理
tamperModeは常にinputのため、zkVM 入力は常にmodifiedVotesが使われる- シナリオ変更は
IGNOREDとして記録され、ignoredCount=1,recountedCount=0になる - 元の掲示板インデックスは保存される(失敗するチェックは 検出メカニズム を参照)
| 項目 | 値 |
|---|---|
| tamperMode | input |
| zkVM 入力票数 | 63(通常) |
| シナリオ変更 | ランダムに選ばれた 1 票を IGNORED |
| ジャーナル統計 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
ジャーナル統計の扱い(sync / async 共通)
現行実装では、missingSlots / invalidPresentedSlots / excludedSlots / validVotes などのジャーナル統計は、sync / async いずれも zkVM が返した proof-derived な値をそのまま使います。
- sync / async いずれも、finalize 後にジャーナル統計を上書きしない
- target async finalize は finalization writer が S3 の
bundle.zipから復元したjournalをそのまま使う
シナリオ由来の ignoredCount / recountedCount / claimedCounts は presentation 用であり、journal の統計値を書き換えません。
ignoredCount/recountedCount→tamperSummaryやtamperedCountに反映claimedCounts→ S2/S4 の表示用 tally
tamperMode=claim(S2/S4)でも同様に、ジャーナル統計は zkVM の値のままです。
集計フローへの挿入点
flowchart TB
A[セッション votes 読み込み] --> B[シナリオ適用]
B --> C{tamperMode}
C -- input --> D[modifiedVotes で zkVM 入力生成]
C -- claim --> E[元の votes で zkVM 入力生成]
D --> F[zkVM 実行]
E --> F
F --> G[finalizationResult 保存]
B --> H[claimedCounts 計算]
H --> G
検出メカニズム
各改ざんシナリオに対して、検証パイプラインがどのチェックで失敗するかを整理します。
このページは実 API の判定ロジックを基準にしており、STARK 検証が success になった後の挙動を前提とします。
前提
- 実行モードと検証の前提は シナリオ一覧 > 実行モードと検証の前提 と同じ。
- 現行の verification resource は
GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationで取得する。 - サーバー応答は
castSource=clientのため、raw なverification.checksのcast_*は シナリオに関係なくnot_run。ブラウザは保持している投票内容と乱数、サーバーから取得した exact receipt を使って local cast checks を評価し、この 4 チェックを overlay してから表示用の stage と最終判定を導出する。
Counted 系チェックの zkGate
verification resource の Counted 系チェックには、STARK 前に評価できる項目と、STARK 状態でゲートされる項目が混在します。
counted_input_sanity/counted_unique_indices/counted_unique_commitmentsは、publicInputArtifactから導出した内部publicInputAuthorityがあれば STARK 未解決でも評価されます。counted_tally_consistent/counted_missing_indices_zero/counted_expected_vs_tree_size/counted_election_manifest_consistent/counted_close_statement_consistent/counted_my_vote_included/counted_input_commitment_matchは zkGate の対象です。- STARK 未解決(
not_run/running)の間、zkGate 対象チェックは原則としてnot_runまたはpendingになります。 - 例外として、
counted_missing_indices_zeroは journal count の異常やexcludedSlots > 0が既に解決できる場合、STARK 未解決でも fail-closed にfailedになり得ます。 verifierResult.status=failedでは、zkGate 対象チェックもfailedになり得ます。
zkGate の一般規則は ゲーティングロジック > zkGate を参照してください。
検出の 2 つの原理
- 原理1: 完全性違反 (
excludedSlots > 0) →counted_missing_indices_zeroが失敗(主に S1/S3/S5)。 - 原理2: 主張集計の不整合 (claimed ≠ verified) →
counted_tally_consistentが失敗(主に S2/S4)。
シナリオ別の主な失敗チェック(STARK 解決後)
| シナリオ | 主に失敗するチェック | 説明 |
|---|---|---|
| S0 | なし | 正常系 |
| S1 | counted_missing_indices_zero | ユーザー票除外により excludedSlots=1 |
| S2 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S3 | counted_missing_indices_zero | 現行実装では botId=1 のボット票除外により excludedSlots=1 |
| S4 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S5 | counted_missing_indices_zero | ランダムに 1 票を除外するため excludedSlots>0 が発生し、完全性違反として検出される |
補足:
- S1 では、ビットマップ証明が利用可能な場合
counted_my_vote_includedも失敗し得ます。 - S2/S4 では、zkVM 入力を改変していないため、
counted_input_commitment_matchは通常成功します。 - S5 ではランダム対象がユーザー票の場合、ビットマップ証明が利用可能なら
counted_my_vote_includedも失敗し得ます。
4 段階検証モデルとの対応(raw サーバー応答)
| 検証段階 | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
| Cast-as-Intended | not_run | not_run | not_run | not_run | not_run | not_run |
| Recorded-as-Cast | success | success | success | success | success | success |
| Counted-as-Recorded | success | failed | failed | failed | failed | failed |
| STARK Verification | success | success | success | success | success | success |
この表は「シナリオ適用による典型挙動」を示します。
running や追加の not_run は、運用状態や証拠不足により別途発生します。
ここでの値はサーバーが返す verification.steps の状態です。
verifierResult.status は STARK receipt 検証の状態であり、overall verdict そのものではありません。
ブラウザ表示では 前提 の overlay を適用します。
典型フローでは overlay 後の Cast-as-Intended は success になり、ローカル証拠が欠けるか不整合なら not_run または failed のままで、最終表示は Verified になりません。
主要チェック ID マトリクス(STARK 解決後の raw サーバーチェック)
| チェック ID | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
cast_commitment_match | not_run | not_run | not_run | not_run | not_run | not_run |
counted_tally_consistent | success | success | failed | success | failed | success |
counted_missing_indices_zero | success | failed | success | failed | success | failed |
counted_my_vote_included | success | failed または not_run | success | success | success | 対象依存 |
counted_input_commitment_match | success | success | success | success | success | success |
- 証拠不足時の
counted_my_vote_includedはnot_runになります。 - S5 の
counted_tally_consistentが通常成功するのは、claimed tally と verified tally がどちらも除外後の 63 票入力ベースだからです。 cast_commitment_matchを含む 4 つのcast_*のnot_runは raw サーバー値であり、前提 の overlay 後は通常successになります。
S2/S4 の主張集計改ざん
S2/S4 は「入力改ざん」ではなく「主張集計改ざん」です。
flowchart TD
A["tamperMode=claim (S2/S4)"]
A --> V1["元の votes を zkVM 入力へ"]
V1 --> V2["verifiedTally"]
A --> C1["claimedCounts を改変"]
C1 --> C2["API の presentation.tally.counts"]
V2 --> X{"claimed と verified は一致?"}
C2 --> X
X -->|不一致| F["counted_tally_consistent = failed"]
このため、counted_input_commitment_match の失敗は通常発生しません。
zkVM 入力は元票であり、投票レシートと STARK 証明も有効なままです。
S5 の除外処理
S5 の定義と処理内容(tamperMode=input、modifiedVotes、ジャーナル統計の扱い)はシナリオ一覧 > S5を参照してください。
検出の主因は、missingSlots=1 / excludedSlots=1 による counted_missing_indices_zero の失敗です。
ビットマップ証明の役割
counted_my_vote_included は、チェック定義上 required のユーザー包含チェックです。
- S1(ユーザー票除外)では、証明が利用可能なら失敗して「自票が未集計」であることを直接示せる。
- 証拠不足で
not_runになる場合でも、最終判定は Verified になりません:- 完全性違反が同時にある場合は
votes_excluded_unknownになります。 - 完全性違反がなく required evidence が欠ける場合は
missing_evidenceになります。
- 完全性違反が同時にある場合は
最終判定(Verified 表示)
最終表示は共有 verification-presentation-policy が決定します(厳密な判定条件は ゲーティングロジック を参照)。
ブラウザが policy に渡すのは、前提 の overlay 後の完全なチェック集合です。
policy は verification-summary に加えて、resource の
availability、fail-closed exclusion signal、明示的な proof failure、検証シーケンスの完了状態を統合します。
flowchart TB
A[raw サーバーチェック] --> B[browser-local cast checks を overlay]
B --> C[共有 presentation policy]
C --> D[summary + hard failure + explicit proof failure]
D --> E{表示準備完了?}
E -- no --> W[Verified を表示しない]
E -- yes --> F{fullyVerifiedLabelEligible?}
F -- no --> N[failed / warning / demo_only]
F -- yes --> V[Verified]
fullyVerifiedLabelEligible が true になるのは、summary が fully_verified、hard failure がなく、
明示的な proof failure もなく、表示 readiness が ready で rendered status が verified の場合だけです。
summary 内の分類順序は ゲーティングロジック > ステータスの判定順序 を参照してください。
代表的な失敗ステータス:
user_vote_excluded/votes_excluded/votes_excluded_unknown: 完全性違反(S1/S3/S5)published_tally_mismatch: claimed と verified の不一致(S2/S4)counted_integrity_failed: Counted 系必須チェック失敗の一般ケース
これらの検出経路は、単体、結合、E2E テスト の CLI と E2E フローで補強しています。 Merkle と journal の不変条件は、Property-based Testing でも補強しています。
品質保証と形式手法
この部では、STARK Ballot Simulator の検証ロジックをどの品質境界で支えているかを説明します。
本プロジェクトは AI コーディングエージェントと協業して実装を進めました。 そのため、実装速度だけでなく、AI 協業で混入しやすい次の乖離を検出する境界を重視します。
- 仕様と実装のドリフト
- 暗黙の fallback
- 公開してはいけないアーティファクトの混入
Verified判定ロジックの分散
この部の章
- 単体、結合、E2E テスト: example-based テストでの局所退行検出と CLI / E2E 経路
- Property-based Testing:
fast-checkと Rustproptestによる入力空間探索 - Lean による形式化: 抽象モデルでの不変条件証明と CI 連携
想定読者と前提
- 想定読者: 実装に追従するテストや形式化の設計判断を確認したい開発者、監査者
- 前提: 単体テスト、結合テスト、E2E テストの一般的な区分と、Property-based Testing の基本概念を把握していること
品質保証のレイヤー
本書では、example-based tests、property-based testing、Lean による形式化、それらを CI に接続する仕組みを次のレイヤーで使い分けます。
| レイヤー | 目的 | 主な対象 |
|---|---|---|
| 単体テスト | 純粋関数、UI component、API helper の局所退行を検出 | packages/*/src, apps/web/src/components, apps/api/src/server/api |
| 結合テスト | API route、store、finalization、bundle 境界を検査 | apps/api/src/server/api, packages/application/src/finalize, packages/aws-adapters/src/store, packages/verification/src/verification |
| CLI / E2E | session 作成から投票、集計、検証までの流れを検査 | scripts/tests/cli-e2e-voting-flow.ts, tests/e2e |
| PBT | 手書き fixture では漏れやすい入力空間を property で探索 | fast-check, Rust proptest |
| Lean | 抽象モデル上の重要な不変条件を証明 | formal/StarkBallotFormal |
| CI / audit | 成果物 freshness、proof hygiene、公開境界を検査 | formal:verify, public safety scan, docs build checks |
中心に置く不変条件
最も重要な品質目標は、ユーザーに Verified と表示してよい条件を緩めないことです。
- required check が失敗、未実行、実行中なら Verified にしない
excludedSlots > 0を成功状態にしない- STARK receipt verification だけを根拠に全体成功としない
- mode 非依存の保護 4 アーティファクト(公開境界)を公開配布対象に含めない
- mock / dev receipt / production STARK proof の違いをテスト階層で明示する
これらは ゲーティングロジック と バンドル構造 で説明した安全境界を、テストと形式化の側から支えるものです。
この部で扱わないもの
この部は、システム全体の完全な形式検証を主張するものではありません。 SHA-256 や RISC Zero の暗号学的健全性、各種ランタイムや AWS の正しさ、本番選挙システムとしての安全性は対象外です。 Lean が扱う射程と扱わない射程の詳細は Lean による形式化 > 証明していないこと を参照してください。
関連する章
- 検証パイプライン: テストと形式化が守る
Verified判定の本体 - ゲーティングロジック: 不変条件として品質保証が支える側のロジック
- バンドル構造: 公開境界の判定に関わる安全境界
単体、結合、E2E テスト
このページでは、example-based tests がどのリスクを守っているかを整理します。 焦点はテスト数ではなく、検証アプリとして失敗できない境界にどの層のテストを置いているかです。
テスト層の役割
単体テスト
単体テストは、純粋関数、小さな UI component、schema、環境変数 guard、エラー整形の退行検出に使います。
対象:
- 検証チェック定義と summary logic
- Lean 生成 vector と照合する verification summary / display / check definition
- zkVM journal / input commitment / bitmap helper
- session capability token と Turnstile bypass guard
- UI component と hooks
- i18n translation consistency
結合テスト
結合テストは、境界をまたいだ契約が崩れていないかを検査します。
対象:
- Hono 互換の API route inventory と request / response contract
- store 実装と finalization state transition
- verification bundle の public allowlist
verifier-serviceclient と STARK receipt status の扱い- Lean 生成 vector と照合する input commitment / bitmap Merkle / Rust guest model
- bitmap proof、bulletin proof、verification run などの session-scoped API
CLI と E2E テスト
CLI と Playwright は、単一関数ではなく利用者フローとしての正しさを見ます。
- CLI flow: session 作成、投票、集計、検証をブラウザなしで実行する
- Playwright mock flow: Vite の local-mock build を static Hono server で配信し、主要画面を通す
- axe smoke: 主要ページの重大な accessibility violation を検出する
- real zkVM dev flow と real zkVM prod flow: proof contract 変更時に mock だけで完了扱いにしないための重い確認経路
主なコマンド
| コマンド | 目的 |
|---|---|
pnpm test:run | Vitest による単体テストと結合テスト |
pnpm test:public | public snapshot 向けの安全なテスト subset |
pnpm test:cli:mock | mock zkVM / mock store で CLI voting flow を実行 |
pnpm test:e2e:mock | Playwright によるブラウザ E2E |
pnpm test:e2e:axe | axe accessibility smoke |
pnpm test:cli:real-dev | development evidence の real zkVM 接続 smoke |
pnpm test:cli:real-prod:s0 | S0 の production STARK proof flow |
pnpm formal:verify | Lean build / formal vector drift guard |
pnpm build:zkvm | zkVM guest / host の build |
pnpm build:verifier-service | Rust verifier-service の build |
pnpm rust:verifier:test | verifier-service の Rust tests |
pnpm rust:zkvm:test | zkVM / contract-core 側の Rust tests |
local-proof-development profile の receipt は production STARK proof ではなく、UI/API の回帰検出に有用でも proof soundness を確認したことにはなりません(判定側の扱いは ゲーティングロジック を参照)。
Vitest の ownership
root の pnpm test:run は、workspace manifest と repository tooling metadata から導出した明示的な Vitest project を実行します。
各 apps/*、packages/*、scripts/、repository tooling scope が自身の test file を所有し、重複した project ownership を許しません。
test environment も scope ごとに固定します。
browser component を持つ @stark-ballot/web だけが jsdom と browser setup を使い、それ以外の workspace と repository tooling は Node environment で実行します。
各 workspace の local test task は狭い確認用で、root の pnpm test:run が repository 全体の単体・結合テスト gate です。
public snapshot では pnpm test:public が公開対象に残した範囲だけを実行します。
mock mode の実行経路
pnpm dev と Playwright mock E2E は、どちらもローカル確認用の mock evidence を使いますが、同じ runtime path ではありません。
| 経路 | profile 選択と起動 | projection の内訳 |
|---|---|---|
browser-mock pnpm dev | VITE_RUNTIME_PROFILE=local-demo で Vite dev server を起動する。server 側も使う場合は .env.local の RUNTIME_PROFILE=local-demo を選ぶ | browser projection は browser mock API、Turnstile bypass eligibility、mock proof。server 側は memory store、同期 finalization、local artifact |
| production ビルド + 静的 Hono(Playwright) | pnpm test:e2e:mock / pnpm test:e2e:axe が Playwright の webServer.command から scripts/start-test-server.sh を使い、selector を RUNTIME_PROFILE=static-mock-e2e / VITE_RUNTIME_PROFILE=static-mock-e2e に固定して、NODE_ENV=production で pnpm build:local-mock 後に scripts/dev/static-hono-server.ts を直接起動する | browser projection は same-origin API と Turnstile bypass eligibility。server projection は file-backed store、mock proof、同期 finalization、local artifact |
static-mock-e2e はブラウザ内だけで API を完結させず static Hono server を通るため、pnpm dev より本番配信形に近い経路を確認します。
この分離により、日常の UI 反復は Vite dev server で速く回し、E2E では production-mode の静的 frontend 配信、Hono API handler、session-scoped endpoint、mock zkVM / mock store の結合を確認します。
個別の mock、store、proof、Turnstile flag は runtime selector として使いません。profile の意味論は AWS runtime 境界 を参照してください。
target / reusable な pnpm build:ci artifact は target-aws-async browser profile を選択し、artifact scan で mock API や test-only の Turnstile bypass が混入していないことを確認します。
検査観点
Verified を誤表示しない
このアプリの最優先 invariant は、必要な暗号チェックと整合性チェックが通っていない状態で Verified を表示しないことです。
テストでは次のような状態を fail-closed に扱います。
- required check が
failed - required check が
not_run/pending/running - STARK receipt verification が失敗または未解決
- unknown check や空の check set
excludedSlots > 0- public tally と verified tally の不一致
この観点は ゲーティングロジック の実装側の安全網です。
公開 artifact 境界
bundle.zip は第三者検証に必要な公開可能 artifact だけを含みます。
公開してよいものは allowlist で管理し、mode 非依存の保護 4 アーティファクトは配布対象に含めません(一覧と理由は 公開境界 を参照)。
この境界は sync 生成、async container、bundle/report delivery の複数箇所にまたがるため、テストで契約として固定します。
mock zkVM と real zkVM の境界
mock zkVM は UI や API flow の高速な退行検出に使います。 一方で、journal format、input commitment、Image ID、receipt verification contract に触れる変更では、Rust 側や real zkVM 経路を使って TypeScript と Rust の対応を確認します。
コストの違いを前提に、普段は軽い gate を使い、proof contract に触れる変更では重い gate へ進む設計です。
UI と accessibility
Playwright は、ユーザーが実際に触る投票、集計、検証の流れを production-mode の static Hono server 上で確認します。
テスト ID は翻訳文ではなく安定した data-testid を使い、i18n やレイアウト変更に引きずられにくくしています。
CI での位置づけ
CI では、TypeScript の core checks、UI mock E2E、Rust tests、formal checks、public snapshot checks が役割を分けて動きます。 変更領域に応じて必要な gate を選ぶ方針は mock zkVM と real zkVM の境界 と同じです。
Property-based Testing
Property-based Testing (PBT) は、少数の fixture では見落としやすい境界条件を、生成入力と「常に成り立つべき性質」で検査します。
本プロジェクトでは、Merkle tree、bitmap packing、input commitment、journal count のように、順序、境界、改ざん耐性が重要なロジックに PBT を置いています。
PBT を導入した理由
Example-based tests は既知のシナリオを守ります。 PBT はそれに加えて、input commitment の順序不変性、Merkle proof の改ざん拒否、LSB-first bitmap packing の境界安定性、journal count の分解式のような仕様レベルの性質を、生成器が到達できる範囲で探索します。
PBT は数学的証明ではありません(限界)。 それでも、CI で広い生成入力を継続的に試せるため、暗号周辺の encoding drift や境界条件の退行を早期に検出しやすくなります。
TypeScript 側の PBT
RFC 6962 Merkle tree
対象:
packages/merkle/src/rfc6962-merkle-tree.property.test.ts
検査する性質:
- 任意の leaf set について inclusion proof が round-trip する
- root、leaf、index、proof node を改ざんすると検証に失敗する
- append-only consistency proof が old size / new size の組み合わせで検証できる
- 奇数サイズ tree の代表ケースを固定 regression として保持する
Bitmap Merkle
対象:
packages/verification/src/merkle/bitmap-merkle-tree.property.test.ts
検査する性質:
- 生成した bitmap の任意 index について proof が round-trip する
- proof から抽出した
includedが元の bit と一致する - leaf chunk や Merkle パスの改ざんを拒否する
- 認証済みの logical tree size から外れる bit の proof を拒否する
- logical tree size より後ろの padding bit が非ゼロなら拒否する
- 0, 1, 7, 8, 255, 256, 257, 511, 512, 513, 1025 bit の境界を固定ケースで検査する
Input commitment
対象:
packages/zkvm-contract/src/zkvm/__tests__/input-commitment.property.test.ts
検査する性質:
- 同じ vote multiset の順序を入れ替えても input commitment が変わらない
- duplicate index がある異常入力でも deterministic tie-break により順序が安定する
- election ID、bulletin root、tree size、total expected を変えると commitment が変わる
- vote の index、commitment、Merkle パスを変えると commitment が変わる
Journal invariants
対象:
packages/node-adapters/src/zkvm/journal-invariants.property.test.ts
検査する性質:
totalVotes = validVotes + rejectedRecordsinvalidVotes = rejectedRecordsseenIndicesCount = validVotes + invalidPresentedSlotsvalidVotes + invalidPresentedSlots + missingSlots = treeSizeexcludedSlots = missingSlots + invalidPresentedSlots- included bitmap の
trueは seen bitmap のtrueを含意する
Rust 側の PBT
対象:
zkvm/methods/guest/src/property_tests.rs
検査する性質:
- input commitment が vote order に対して permutation invariant
- duplicate index の tie-break を含めても permutation invariant
- RFC 6962 inclusion proof が reference tree と一致する
- root / leaf / path 改ざんを拒否する
- bitmap root が reference oracle と一致する
- bit flip で bitmap root が変わる
- guest の slot / record accounting が現在の journal semantics と一致する
- duplicate index の処理順序 semantics を固定する
Rust 側の PBT は、zkVM guest / contract-core で使う低レベル実装に近い場所で動きます。 TypeScript 側と同じ性質を別実装として検査します。
Lean との関係
PBT は実装に対して広い入力空間を探索します。 Lean は同種の不変条件を抽象モデル上で証明します。 両者の役割分担と、Lean から出力した generated vectors を介して実装テストに接続する仕組みは Lean による形式化 > 実装との接続 に整理しています。
限界
- PBT は数学的証明ではない
- 生成範囲は CI 実行時間とのバランスで制限する
- SHA-256 の衝突困難性は PBT では証明しない
- RISC Zero receipt soundness も PBT の対象ではない
- 生成器に含めていない入力領域は探索されない
そのため、PBT は example-based tests や Lean formalization を置き換えるものではありません。 境界条件と実装 drift を検出する追加レイヤーとして扱います。
Lean による形式化
本プロジェクトでは Lean 4 を使い、Verified 表示の fail-closed 条件、journal count の整合性、input commitment の canonical encoding、LSB-first bitmap packing、抽象 guest tally model の不変条件を形式化しています。
Lean は実装を直接証明しません。 代わりに、抽象モデル上で不変条件を証明し、そこから生成した generated vectors、formal report、formal audit を TypeScript / Rust のテストと CI に接続します。 この接続により、モデルと実装の対応付けを継続的に検査します。 Lean が扱わない範囲は末尾の 証明していないこと を参照してください。
Lean で定義しているもの
| Lean module | 主な定義 | 役割 |
|---|---|---|
Basic.lean | CheckStatus, SummaryStatus, SummaryTone, CheckId, CheckCategory, CheckRole, Criticality | 検証チェックと summary model の基礎型 |
JournalCounts.lean | missingSlotsOf, invalidPresentedSlotsOf, excludedSlotsOf | zkVM journal の count 分解を Nat モデルで表す |
VerificationSummary.lean | checkDefinitions, isRequiredCheck, canFullyVerify, deriveSummaryModel | /verify の最終判定に関わる fail-closed model |
InputCommitment.lean | CommitmentVote, InputCommitmentCase, canonical order, u16LE, u32LE, preimage encoding | input commitment の byte layout と順序安定性をモデル化 |
Bitmap.lean | packedByteCount, packedAddress, byteValueAt, packBits | LSB-first bitmap packing と bit address をモデル化 |
GuestModel.lean | RejectReason, GuestVote, CandidateTally, GuestState, classifyVote, processVotes, guest bounds | zkVM guest の抽象 tally / rejection state machine |
GuestModel.lean は zkvm/methods/guest/src/main.rs を行単位で翻訳したものではありません。
外部的に重要な処理順序を抽象 state machine として表します。
具体的には、インデックス範囲、重複 index、選択肢、コミットメント、重複 commitment、包含証明、集計反映の順序をモデル化します。
Lean で証明していること
| 領域 | 代表 theorem | 主張 |
|---|---|---|
| Journal count | excluded_zero_implies_no_slot_loss, slot_partition_total | excludedSlots = 0 なら missing / invalid presented が 0 になり、slot loss がない |
| Verification summary | fully_verified_implies_all_required_success, fully_verified_implies_no_unknown_checks, fully_verified_implies_required_roles_success | fully_verified は required check 成功、unknown check 不在、重要 role 成功を要求する |
| Input commitment | canonical_vote_order_total, canonical_encoding_permutation_invariant | vote の入力順序に依存しない canonical encoding が定義されている |
| Bitmap | pack_bits_length, pack_bits_get_bit | LSB-first packing の byte 数と bit 取得がモデル通りになる |
| Guest model | accepted_votes_count_tally, valid_votes_count_accepted, processVotes_fold_invariant | 抽象 guest fold が tally / validVotes / seen index の不変条件を保つ |
| Guest completeness | zero_exclusion_guest_model_complete | excludedSlots = 0 が guest model 上で missing / invalid presented の不存在につながる |
| Bounded counts | no_overflow_under_guest_bounds | treeSize と vote count の上限から seen / valid / rejected / tally bucket が Rust u32 に収まり、各 tally bucket が 1,000,000 以下になることを導く |
これらは抽象モデル上の主張であり、実装との対応は次節の generated vectors とテストで検査します。
実装との接続
flowchart LR L["Lean models<br/>formal/StarkBallotFormal/*.lean"] T["Theorems<br/>形式的な不変条件"] R["formal-report.json<br/>主張と前提"] V["generated-vectors/*.json<br/>実装対応付けケース"] A["formal-audit.json<br/>theorem hash / dependency / hygiene"] TS["TypeScript tests<br/>Vitest"] RS["Rust vector tests<br/>cargo test"] FCI["formal CI<br/>pnpm formal:verify"] RCI["Rust tests workflow<br/>cargo test"] L --> T T --> R L --> V T --> A V --> TS V --> RS R --> FCI V --> FCI A --> FCI TS --> FCI RS --> RCI
| generated vector | 消費先 | 目的 |
|---|---|---|
verification-summary-cases.json | TypeScript summary tests | Lean summary model と deriveVerificationSummary の対応 |
verification-display-cases.json | shared presentation-policy test | UI が verified を誤表示しないことの drift guard |
check-definitions.json | TypeScript check-definition test | check ID / category / role / criticality / required 条件の drift guard |
input-commitment-cases.json | TypeScript / Rust tests | canonical order と pre-hash bytes の対応 |
bitmap-cases.json | TypeScript / Rust tests | LSB-first packing と bitmap behavior の対応 |
guest-model-cases.json | Rust guest tests | 抽象 guest model と Rust guest inspection surface の対応 |
pnpm formal:verify は Lean build、formal report、generated vectors、audit の freshness、TypeScript の vector-consuming tests、生成 JSON の format check をまとめて実行します。
Rust 側の vector-consuming tests は docs/formal/generated-vectors/** の変更で起動する Rust tests workflow の cargo test によって検査します。
公開 repository snapshot の release workflow でも export 前に Lean workspace を build して pnpm formal:verify を実行します。
そのため、formal artifact が古い、または formal verification が失敗する状態では公開 snapshot を生成できません。
この接続により、Lean model と実装のどちらが変わっても、追従していない drift を CI 上で検出できます。
対応付けを支える成果物
前掲の generated vectors と vector-consuming tests に加えて、次の audit artifact と検査を組み込んでいます。
- theorem statement hash と generated-vector hash
#print axiomsに基づく theorem dependency audit- proof hygiene scan
treeSize <= 1,000,000と vote count<= 1,000,000の explicit guest input bounds、およびそこから導く candidate tally bucket<= 1,000,000
実行コマンド
| コマンド | 目的 |
|---|---|
pnpm formal:build | Lean workspace を build する |
pnpm formal:report | formal-report.json を再生成する |
pnpm formal:report:check | report が最新か確認する |
pnpm formal:vectors | Lean から generated vectors を再生成する |
pnpm formal:vectors:check | generated vectors が最新か確認する |
pnpm formal:audit | theorem statement / dependency / proof hygiene audit を生成する |
pnpm formal:audit:check | audit artifact が最新か確認する |
pnpm formal:test:ts | Lean vector を消費する TypeScript tests を実行する |
pnpm formal:verify | build、report、vectors、audit、TS tests、format checks をまとめて検証する |
証明していないこと
Lean は次を証明しません。
- SHA-256 の衝突困難性
- RISC Zero receipt soundness
- Rust compiler / TypeScript runtime / browser runtime の完全な正しさ
- AWS runtime behavior
- React rendering 全体の正しさ
- 本番選挙システムとしての安全性
- zkVM guest Rust 実装全体の line-by-line verification
したがって、主張は「投票システム全体を形式検証した」ではありません。 選択した安全モデルを Lean で証明し、generated vectors と CI drift guard によって TypeScript / Rust 実装との対応を検査しています。
AWS アーキテクチャ
この部は、STARK Ballot Simulator を AWS 上で動かす場合の runtime 境界とサービス責務を説明します。 まず AWS runtime 境界 を読み、公開仕様として扱う範囲を確認してください。
AWS runtime は、Terraform 管理の静的フロントエンド、Hono/API ランタイム、DynamoDB ストア、非同期プローバー、検証 worker、private proof artifact storage で構成されます。 この部はそれぞれの責務分担を説明します。
ここでいう public は「秘密を含まず第三者検証に使える」という機密性の分類です(無認証公開ではありません。定義は 公開境界)。
bundle / report の通常取得経路は capability 保護 API です(公開境界 > bundle と report の取得経路)。
この部の章
- AWS runtime 境界:公開仕様として扱う runtime 境界、artifact 境界、扱わない情報
- 現行構成とサービス一覧:runtime 管理境界、環境分離、主要サービスの責務
- トポロジー:レイヤ別のサービス構成と通信フロー
- 非同期プローバー:SQS → Step Functions → ECS による証明パイプライン
- 可観測性設計:verdict authority と分離した構造化ログ、検出、相関、通知境界
- イメージ署名:AWS Signer managed signing と deployment preflight / ECS 実行前の暗号学的検証
- Terraform:IaC による root ownership、recovery selection、deployment / release authority、runtime wiring
想定読者と前提
- 想定読者: 公開仕様として AWS runtime の責務分担を把握したい読者
- 前提: AWS の基本サービス(S3 / SQS / Step Functions / ECS)、CloudFront / KMS / CodePipeline などの配信・デプロイ系サービス、Terraform の概念を把握していること
この部で扱わないもの
- 退役済み Amplify / AppSync 構成の詳細
- Terraform 文法・モジュール設計の一般論
- 個別 account の alarm tuning、24 時間 on-call、詳細なコスト最適化
- 個別アカウント、秘密値、アカウント固有の承認手順や詳細な証跡
- 移行作業記録と、環境切り替え、本番環境の初期構築、独自ドメインの本番 DNS 切り替え、旧 AWS アカウント削除の進捗・完了状態
関連する章
- zkVM 設計:ECS 上で実行されるホストとレシート生成
- バンドル構造:
bundle.zipと protected report artifact の境界 - 第三者検証ガイド:capability 保護 API から取得した
bundle.zipを使うローカル監査 - 用語集:インフラ用語の定義
AWS runtime 境界
このページは、AWS 章で前提にする公開仕様上の runtime 境界を定義します。 ここでいう runtime は、ブラウザから投票、集計、検証を実行するための target 構成です。 扱わない範囲は末尾の このページで扱わないもの にまとめます。
Semantic runtime profiles
server は RUNTIME_PROFILE、browser build は VITE_RUNTIME_PROFILE で実行意図を
一度だけ選びます。server resolver は adapter construction より前に profile を解決し、
store、proof evidence、finalization、artifact backend、rate limit、Turnstile posture
の混在を拒否します。
| Profile | Store | Proof evidence | Finalization | Artifact | 主な用途 |
|---|---|---|---|---|---|
local-demo | memory | mock | sync | local | Vite browser mock API と高速 UI 開発 |
static-mock-e2e | file | mock | sync | local | production-build static Hono / Playwright E2E |
local-proof-development | memory | development | sync | local | Rust host 接続を使う fake receipt smoke |
local-proof-production | memory | production | sync | local | local production STARK proof |
target-aws-async | Dynamo | production | async | S3 | Lambda/SQS/Step Functions/ECS target runtime |
target-aws-async-quiesced | Dynamo | production | async | S3 | develop recovery 時の public admission 封鎖 |
target profile は production proof、DynamoDB、S3、async orchestration、Dynamo rate limit、bypass 不可を一体として選びます。quiesced profile は develop recovery 専用で browser public projection には公開しません。secret 値と外部 resource identifier は profile 名に埋め込まず、target runtime の参照入力として別に検証します。
公開仕様として扱う境界
Frontend
ブラウザアプリケーションは Vite / React Router で生成された静的 frontend です。 ビルド成果物は CloudFront から配信され、origin は private S3 bucket です。 S3 frontend origin は直接公開せず、CloudFront が静的 asset と browser route fallback の入口になります。
ブラウザからの API 呼び出しは、通常 same-origin の /api/* として CloudFront に送られます。
CloudFront は静的 asset を S3 origin へ、/api/* を API Gateway origin へ routing します。
これにより、frontend と API の公開入口は同じ origin 上に投影されます。
Develop target では、静的 frontend と API の両方を含む CloudFront behavior を operator / tester 向けの signed-cookie gate で保護します。 署名用の秘密鍵は KMS 内で管理し、対応する公開鍵だけを CloudFront trusted key group に登録します。 この gate は環境全体への入口を制限するもので、session-scoped な capability token の代わりにはなりません。
API
API Gateway は /api/* を受け取り、Lambda 上の Hono runtime adapter に渡します。
実際の wire-level API surface は packages/api-contract/src/routes/inventory.ts が正準です。
apps/api/src/server/api/routes/definitions.ts はその contract を runtime mode 別に投影し、Hono は
contract で定義された route と handler を AWS runtime に接続します。
Target runtime では、CloudFront が API origin への転送時に origin-verification header を付与し、Hono はその値が欠落または不一致のリクエストを拒否します。 したがって、API Gateway の直接 URL は通常の browser / CLI contract ではなく、認可された CloudFront origin path が外側の admission boundary です。
その内側で、session-scoped な現行 route は path の sessionId と X-Session-Capability を照合します(対象 route と header の契約は API リファレンス > セッション capability)。
Data store
DynamoDB は session、vote、finalization state、verification run state、rate-limit state、prover semaphore state などの runtime state を保持します。 ブラウザが DynamoDB に直接アクセスする経路はありません。 API Lambda と worker Lambda が、それぞれの IAM role に基づく最小権限で DynamoDB を読み書きします。
Async prover
長時間かかる STARK 証明生成は、非同期 runtime に分離されます。
非同期モードの POST /api/sessions/:sessionId/finalizations は work item を SQS に登録します。
Step Functions は署名確認や concurrency semaphore などの gate を通過した場合だけ ECS Fargate task を起動し、zkVM host が証明と公開監査用 artifact を生成します。
生成結果は finalization-writer Lambda が finalization record に反映し、ブラウザは
GET /api/sessions/:sessionId/finalizations/current を capability 付きでポーリングします。
パイプラインの詳細は 非同期プローバー を参照してください。
Verification worker
Target runtime の POST /api/sessions/:sessionId/finalizations/:finalizationId/verification は、
path と session capability、current succeeded finalization、finalization-scoped な bundle locator、
expected Image ID を確認したうえで verification work を durable queue に登録します。
verification-worker Lambda は private S3 bucket から bundle.zip を取得して
verifier-service で STARK レシートを検証します。検証結果は
sessionId と finalizationId で scope された verification result record に、S3 の
bundle/report locator は分離された AWS delivery metadata に保存されます。
ブラウザは通常の API flow から検証状態を取得し、worker や S3 に直接アクセスしません(検証内容は 検証サービス を参照)。
Proof artifacts
Proof artifact storage は private S3 bucket です。
保護対象 artifact を public bundle.zip に含めない分類と除外リストは、公開境界 に従います。
bundle.zip と report(verification.json)の通常配信は capability 保護 API 経由で、raw S3 URL や presigned URL は通常の browser / CLI contract ではありません(取得経路とアクセスポリシーは 公開境界 > bundle と report の取得経路)。
このページで扱わないもの
この runtime boundary は公開仕様としての責務分担に限定します。 次の情報は扱いません。
- AWS account ID、具体的な ARN、bucket 名、secret 名
- secret / config handoff の手順や証跡
- post-merge Plan 02 / Plan 03 の完了条件や blocking status
- production DNS cutover の進捗
- CodePipeline / CodeBuild の詳細な承認証跡
- legacy AWS account deletion の状態
関連する章
- 現行構成とサービス一覧:AWS service ごとの責務
- トポロジー:CloudFront、API Gateway、Lambda、DynamoDB、S3、SQS、Step Functions、ECS の通信経路
- 非同期プローバー:SQS / Step Functions / ECS Fargate による証明生成
- 可観測性設計:runtime を横断する構造化ログ、検出、相関、通知境界
- バンドル構造:
bundle.zipと private artifact の境界
現行構成とサービス一覧
AWS runtime の管理境界、環境分離、主要サービスの責務を公開仕様として整理します。 前提となる公開境界と、個別デプロイの承認記録・内部運用証跡など扱わない範囲は AWS runtime 境界 を参照してください。
Terraform root ごとの管理境界
AWS runtime は Terraform の root ごとに責務を分けて管理します。 root ごとの管理対象と所有しない範囲の正本は Terraform > Root ownership です。要約は次のとおりです。
infra/terraform/app:target application runtime(CloudFront + frontend、API、DynamoDB、queue / SFN / ECS / S3、observability)。ECR repository や image publication は所有せず、digest 固定の prover image URI を消費するinfra/terraform/foundation:app 実行の前提リソース(ECR、ECR retention controller、manifest storage、secret / config container、prover networking)。app runtime resource は作らないinfra/terraform/bootstrap:Terraform state backend と deployment-control lane、report-only controls、暗号化された authority storage。runtime workload resource は所有しないinfra/terraform/legacy:retained historical context。現行 runtime の authority ではない
環境分離
develop と main は source branch / account alias / deployment environment / state scope を分けて扱います。
現在の live target app lane は stark-dev/develop/app です。
stark-prod/main/app は protected production contract の repository evidence として render されます。
これは非変更 evidence であり、live production deployment はまだ承認しません。
Target は target-aws-async profile による production STARK 証明を前提にします。Local 実行意図の authority は semantic RUNTIME_PROFILE(local-demo、static-mock-e2e、local-proof-development、local-proof-production)であり、置き換え済みの RISC0_DEV_MODE / USE_MOCK_ZKVM selector は拒否します。
| 項目 | develop | main |
|---|---|---|
| private proof artifact S3 lifecycle | 7 日 | 7 日 |
| CloudWatch log retention(runtime / Container Insights / ops-ledger) | 90〜365 日(値は トポロジー > 主な CloudWatch ログ群) | 180〜731 日(同左) |
| CloudTrail | app / foundation / bootstrap runtime root の workload resource ではない | app / foundation / bootstrap runtime root の workload resource ではない |
環境別に分かれるものは次のとおりです。
- app runtime state、proof artifact storage
- DynamoDB と選択された recovery pair
- queue / worker、Step Functions、ECS task definition
- runtime SSM refs
- CloudWatch logs / metrics / alarms / ledger
- develop operator gate の trust resources
ECR repository や manifest storage など、app 実行の前提になるリソースは foundation root の責務として扱います。
全体構成図
図: STARK Ballot Simulator の AWS 全体構成(クリックで拡大)。
サービス一覧
この runtime が使う主要な AWS サービスと役割です。
App runtime
| サービス | リソース | 役割 |
|---|---|---|
| CloudFront + S3 | static frontend / operator gate | Vite/React の静的配信、/api/* origin routing、SPA fallback、develop の signed-cookie gate |
| API Gateway (HTTP API) | /api/{proxy+} | capability 保護 API の公開入口 |
| Lambda | public-api | Hono route inventory を実行する API runtime |
| DynamoDB | sessions / votes | primary または検証済み replacement pair を選択し、セッション、投票、集計結果を永続化 |
| DynamoDB | rate-limit events / counters | API rate limit state |
| DynamoDB | prover semaphore | Step Functions が RunProver 前に取得する concurrency slot |
| Lambda | proof-dispatcher | SQS 受信 → artifact key / pending state 検証 → Step Functions 起動 |
| Lambda | finalization-writer | Step Functions 結果を session state に反映 |
| Lambda | verification-worker | S3 bundle locator を受け取り verifier-service で STARK レシート検証を実行 |
| Lambda | image-signature-verifier | 選択された digest-pinned prover image と signing profile の暗号学的署名検証 |
| Lambda | semaphore-janitor | semaphore slot cleanup と、検証済み EventBridge event の normalized operations ledger 記録 |
| SQS | prover-work + DLQ | 非同期証明リクエストのバッファリング |
| SQS | verification-work + DLQ | durable verification run のバッファリング |
| Step Functions | finalization | 暗号学的署名検証 → semaphore 取得 → ECS 実行 → slot release / finalization writer |
| ECS Fargate | prover task | zkVM host binary による STARK 証明生成 |
| S3 | proof artifacts / static frontend | private proof artifacts、public bundle.zip の読み出し元、frontend artifacts |
| SSM Parameter Store | runtime refs | non-secret runtime wiring と secret/config reference name の保持 |
| EventBridge | janitor / operations-ledger rules | terminal status / 定期 sweep と、alarm / ECS / pipeline / deployment event の正規化 |
| CloudWatch | logs / metrics / dashboard | runtime / access logs、metric filters、passive / damped actionable alarm、ledger / query |
| SNS | invariant / ops-actionable | verification verdict invariant の矛盾と、限定された持続的な運用 signal の通知 |
| KMS + CloudFront | develop operator-gate keyset | versioned asymmetric signing keys と CloudFront trusted public-key group を管理 |
サービス表の補足は次のとおりです。
- Observability:telemetry は verification verdict を変更せず、自動 remediation も起動しません。Verification invariant の直接通知と、隔離された
ops-actionable経路の routing 対象(damped alarm と exact owned-pipeline failure)は 可観測性設計 > 通知の境界 が正本です。その他の調査用 alarm は passive です。 - Artifact 境界:
publicの分類、bundle.zipの除外 artifact、verification.jsonの capability 保護配信は 公開境界 の定義に従います。 - DynamoDB recovery:4 phase の定義と quiesce を含む切替契約は Terraform を参照してください。
- Develop operator gate:追跡対象の keyset manifest を authority とし、KMS の private signing material を Terraform state、pipeline artifact、runtime role に渡しません。KMS から導出した public key だけを CloudFront trust group に登録し、署名権限は target runtime の責務外に保ちます。
Foundation resources
| サービス | リソース | 役割 |
|---|---|---|
| ECR | prover / RISC Zero toolchain | プローバーコンテナと RISC Zero toolchain image の repository 管理 |
| Lambda + EventBridge + CloudWatch | ECR retention controller | accepted / rollback / running / in-progress reference を保護する reference-aware retention を日次実行し、専用 log に記録 |
| S3 | versioned manifest storage | image / deployment manifest 系メタデータの非公開保存 |
| Secrets Manager | runtime secret containers | app runtime が参照する secret container の作成 |
| SSM Parameter Store | non-secret config records | Turnstile site key など、app runtime が参照する non-secret config |
| VPC / subnet / security group | prover networking inputs | ECS prover task が利用する networking input |
Prover image metadata、promotion evidence、release record / accepted pointer は proof artifact bucket ではなく image build / signing と runtime wiring の境界に属し、旧構成の SSM current pointer は候補 metadata lookup にとどまります(target topology の release authority ではありません)。 Image build、digest 固定、署名ステータス確認、Image ID との関係は イメージ署名 に集約します。 ECR retention は count / age ベースの native lifecycle expiry ではなく、Foundation-owned の reference-aware retention controller が担います(保護対象と動作の正本は Terraform > Retention authority)。
Deployment control
| サービス | リソース | 役割 |
|---|---|---|
| S3 + KMS | state / execution / retained authority storage | Terraform state / lockfile、lane ごとの暗号化された execution artifact、retained deployable / acceptance evidence、accepted Terraform baseline / report の保護 |
| CodePipeline | controlled lanes | Bootstrap / Foundation saved-plan、Toolchain / Prover release、App full(normal / rollback)、App static-fast の分離された実行経路 |
| CodeBuild | root / app build and deploy jobs | Bootstrap / Foundation / App の validation、saved plan / review / apply / smoke、App artifact build / deploy / acceptance |
| CodeBuild | image release jobs | Toolchain / Prover candidate build、Image ID / signing / scan evidence、accepted pointer promotion |
| CodeBuild | report-only jobs | weekly / on-demand drift coordinator、Bootstrap / Foundation / App runner、on-demand rollback / recovery readiness |
| EventBridge Scheduler + SQS | drift schedule / missed-run queue | weekly drift coordinator の起動と、起動できなかった run の暗号化済み queue への隔離 |
| IAM | control-plane roles | root、lane、report-only project ごとの最小権限実行 |
Report-only drift は accepted Terraform baseline と exact historical source を使って refresh-enabled plan を実行し、redacted report だけを保存します。Rollback / recovery readiness も同じ隔離された App planning 境界を使います。どちらも Apply、state / pointer write、remediation、rollback / recovery exercise を起動せず、その結果は saved plan や mutation の authority になりません。
図: GitHub から CodePipeline deployment lanes を経て app runtime に到達するデプロイ制御フロー(クリックで拡大)。
個別の承認手順、証跡、昇格判断の詳細は公開仕様の範囲外です。
| Mode | 内容 |
|---|---|
| Normal | accepted toolchain / prover authority から immutable app deployment record を組み立てる |
| Rollback | 過去に受理された deployment record と retained Lambda / frontend artifacts を immutable key / version / digest で選び、現在の control source から reviewed forward deployment として実行する |
Approval の条件は次のとおりです。
- approval を省略できるのは、normal mode で saved plan に変更がないと機械的に証明された場合だけです。
- Rollback は Terraform plan が空でも approval を必須とします。
Post-apply smoke 後の immutable acceptance evidence 保存と accepted-deployment index の compare-and-swap 更新を含め、詳細は Terraform を参照してください。
Runtime wiring
Runtime の連携は Terraform output、SSM reference、Lambda environment、S3 / SQS / Step Functions locator によって行われます。
flowchart TB
subgraph FOUNDATION["infra/terraform/foundation"]
ECR["ECR repositories<br/>prover / toolchain"]
MANIFEST["Manifest storage<br/>release records / promotion evidence"]
CONFIG["Secret/config containers<br/>Secrets Manager / SSM"]
NET["Prover networking inputs"]
RETENTION["Reference-aware ECR retention"]
end
subgraph APP["infra/terraform/app"]
OUT["Terraform outputs<br/>API / bucket / queue / lambda names"]
PARAM["SSM refs<br/>runtime config"]
RUNTIME["Runtime resources<br/>CloudFront / API / DDB / SQS / SFN / ECS"]
GATE["Develop operator gate<br/>KMS signing keys / CloudFront trust group"]
DATA["DynamoDB active pair<br/>primary or validated replacement"]
SEM["Prover semaphore<br/>DDB lock row / janitor"]
ARTIFACTS["Private proof artifact S3<br/>bundle source / protected report"]
OBS["Observability<br/>CloudWatch / EventBridge / SNS"]
end
subgraph BOOT["infra/terraform/bootstrap"]
CONTROL["Deployment control lanes"]
AUTHORITY["Encrypted authority storage<br/>deployables / acceptance / Terraform baseline / reports"]
REPORT["Report-only controls<br/>drift / readiness"]
end
ECR --> RUNTIME
ECR --> RETENTION
MANIFEST --> RUNTIME
MANIFEST --> RETENTION
CONFIG --> PARAM
NET --> RUNTIME
CONTROL --> RUNTIME
CONTROL --> AUTHORITY
AUTHORITY --> CONTROL
AUTHORITY --> RETENTION
AUTHORITY --> REPORT
REPORT --> AUTHORITY
PARAM --> RUNTIME
GATE --> RUNTIME
RUNTIME --> DATA
RUNTIME --> SEM
SEM --> RUNTIME
RUNTIME --> ARTIFACTS
RUNTIME --> OBS
RUNTIME --> RETENTION
REPORT -. "refresh-only inspection" .-> RUNTIME
CONTROL --> OBS
RUNTIME --> OUT
canonical runtime component name と legacy/historical name の対応は次のとおりです。legacy 名は historical context としてのみ扱います。
| Canonical name | Legacy/historical name |
|---|---|
public-api | hono-api |
proof-dispatcher | prover-dispatch-proxy |
finalization-writer | finalize-callback-runner |
verification-worker | verifier-service-runner |
image-signature-verifier | check-image-signature |
semaphore-janitor | —(legacy 名なし) |
現行アーキテクチャの責務分離
現行構成は、一つの hosting/backend lifecycle にすべての責務を集約しません。 短い user-facing Web/API request と数分規模の proof work を分ける理由は 非同期プローバー > 実行リソースと mode 境界、 runtime、前提 resource、deployment control を Terraform root ごとに分ける理由は Terraform > Root ownership が現在の参照先です。
この分離により、proof capacity や image authority の変更を API request lifecycle から切り離し、IaC の plan、権限、review、rollback 判断を change lifecycle ごとに 限定できます。過去の Amplify / AppSync / Next.js 構成や手動 handoff は現行 runtime contract ではありません。
関連する章
- トポロジー:リクエスト経路とコンポーネント間の通信フロー
- 非同期プローバー:SQS → Step Functions → ECS の実行時フロー
- 可観測性設計:CloudWatch / EventBridge / SNS のシグナルと authority 境界
- Terraform:target app/bootstrap state と runtime wiring
トポロジー
AWS 上のサービス配置とコンポーネント間の通信経路を示します。 このページは runtime の責務分担を説明します(個別環境の導入進捗は扱いません)。
本システムは次の 6 つの論理レイヤで構成されます。
- Web レイヤ:静的配信と develop signed-cookie gate
- API レイヤ:API Gateway と
public-apiLambda - Data レイヤ:DynamoDB による永続化
- Prover レイヤ:SQS / Step Functions / ECS による証明生成
- Verification レイヤ:STARK レシート検証
- Storage レイヤ:proof artifact の S3 保存
レイヤ別トポロジー
Web レイヤ
CloudFront が private S3 origin の Vite/React 静的アセットを配信します。
/api/* は同じ CloudFront distribution から API Gateway origin に routing されます。
extensionless browser route は viewer-request fallback で index.html に解決します。
develop で operator gate が有効な場合、static frontend、browser route、/api、/api/* の viewer access は CloudFront signed cookie を要求します。
信頼する public key group は、追跡対象 keyset に従って Terraform が管理する非対称 KMS signing key から public key を導出して構成します。
KMS の private signing material は CloudFront、app runtime role、Terraform state には渡しません。
この gate は develop 固有の条件付き境界であり、すべての環境に一律適用されるものではありません。
API レイヤ
API Gateway(HTTP API)が /api/* パスパターンのリクエストを受け取り、単一の Lambda 関数(public-api)にプロキシします。
public-api は Hono route inventory を実行する runtime adapter で、ルーティング、セッション管理、Turnstile 検証、レート制限を処理します。
下図の「主なリクエスト制御」は、public-api がルートごとに適用有無と順序を切り替える代表的な要素を示します。
固定順のパイプラインではありません。
flowchart LR
Client["クライアント"] --> CF["CloudFront"]
KMS["KMS<br/>(develop signing keys)"] --> KG["CloudFront trusted key group<br/>(public keys only)"]
KG -. develop signed-cookie gate .-> CF
CF --> FRONT["S3<br/>(static frontend)"]
CF --> APIGW["API Gateway<br/>(HTTP API)"]
APIGW --> API["public-api<br/>Lambda"]
API --> DDB["DynamoDB<br/>(sessions / votes / rate limits)"]
API --> SQS["SQS<br/>(非同期証明)"]
API --> VQS["SQS<br/>(検証 work queue)"]
API --> S3["S3<br/>(bundle / report delivery)"]
VQS --> VW["verification-worker<br/>Lambda"]
VW --> S3
VW --> DDB
subgraph "主なリクエスト制御(ルートごとに適用)"
direction TB
RATE["IP / zkVM レート制限"]
SESS["セッション / capability 検証"]
TURN["Turnstile 検証"]
BODY["入力検証"]
HAND["ハンドラー実行"]
RATE -.-> HAND
SESS -.-> HAND
TURN -.-> HAND
BODY -.-> HAND
end
CORS 設定で X-Session-Capability ヘッダーを許可し、session-scoped route は path の sessionId と capability トークンを照合します(契約は API リファレンス > セッション capability)。
POST /api/sessions/{sessionId}/finalizations/{finalizationId}/verification は、target runtime では検証 work を verification-work キューへ登録し、HTTP 応答では verificationStatus=running、finalizationId、推定所要時間、idempotent 判定を返します。
実際の STARK レシート検証と protected report の保存は verification-worker の責務です。
後述のエンドツーエンドのデータフローも参照してください。
Data レイヤ
DynamoDB が、セッション、投票、集計結果、レート制限状態の永続化を担当します。 データプレーンは target Lambda IAM role ごとの最小権限で制限され、ブラウザから DynamoDB へ直接アクセスする経路はありません。
通常時は Terraform-owned primary sessions / votes table pair を参照します。 DynamoDB recovery の 4 phase 定義と切替契約は Terraform を参照してください(quiesced phase の図への影響は エンドツーエンドのデータフロー に注記)。
sessions table は string partition key sessionId を使いますが、単一の aggregate session row は保存しません。
identity、voting、finalization、verification、artifact delivery を、責務ごとの record family と prefixed key に分離します。
votes table は引き続き (sessionId, voteIndex) の複合キーを使います。
| record family | sessions table のキー例 | 主な authority / field |
|---|---|---|
| Session identity | {sessionId} | SESSION_IDENTITY; identity に election config、electionId、logId、作成時刻を固定 |
| Session voting | VOTING#{sessionId} | SESSION_VOTING; bulletin root history、bot count、user participation、last activity、revision |
| Finalization lifecycle | FINALIZATION_CURRENT#{sessionId} / FINALIZATION#... | current pointer と finalization ごとの lifecycle record |
| Verification / evidence | VERIFICATION#... / OBSERVATION#... / BITMAP#... | verification result、browser observation、finalization bitmap を別々に所有 |
| AWS runtime / artifact delivery | FINALIZATION_RUNTIME#... / VERIFICATION_DELIVERY#... | Step Functions / bundle metadata と s3BundleKey / s3ReportKey delivery metadata を semantic state から分離 |
| Vote | votes table の {sessionId} + {voteIndex} | voteId、encrypted vote / rand、commit、timestamp、rootAtCast、isUserVote、expiresAt |
session-owned record には用途に応じた expiresAt を持たせます。
receipt board の global counter は sessions table 上の独立した非 TTL record です。
レート制限用に 2 つの追加テーブル(RateLimitEvents、RateLimitCounters)が PAY_PER_REQUEST モードで運用されています。 これとは別に、同時実行数を制御する prover semaphore table も DynamoDB 上で管理します。
Prover レイヤ
STARK 証明の生成を担当するレイヤです。 SQS キュー、Step Functions ステートマシン、ECS Fargate タスクで構成されます。 詳細は 非同期プローバー を参照してください。
flowchart LR CODEBUILD["CodeBuild<br/>プローバー候補イメージ"] --> META["S3<br/>候補 metadata / promotion evidence"] CODEBUILD -. legacy / optional<br/>非 authority .-> SSM["SSM Parameter<br/>current metadata"] META --> RECORD["Prover release record<br/>accepted pointer"] RECORD --> PREFLIGHT["image-signature-verifier<br/>preflight alias"] PREFLIGHT -. VERIFIED evidence .-> APP["App deployment record"] SQS["SQS<br/>ワークキュー"] --> DP["proof-dispatcher<br/>Lambda"] DP --> SFN["Step Functions<br/>ディスパッチャー"] APP -. digest / verifier authority .-> SFN SFN --> SIG["image-signature-verifier<br/>runtime alias"] SIG -. VERIFIED .-> SFN SFN <--> SEM["DynamoDB<br/>prover semaphore"] JANITOR["semaphore-janitor<br/>Lambda"] --> SEM SFN --> ECS["ECS Fargate<br/>ARM64 Linux / CPU-only"] SFN --> CALLBACK["finalization-writer<br/>Lambda"] ECS --> S3["S3<br/>証明バンドル"]
normal App deployment は、選択した digest 固定 Prover image を immutable verifier version の preflight alias で検証し、その evidence と verifier authority を App deployment record に固定します。
runtime では Step Functions の最初の state が同じ authority の runtime alias を呼び、暗号学的検証に成功した場合だけ semaphore の取得と ECS 起動へ進みます。
ECS タスクは ARM64 アーキテクチャの Fargate で実行され、Foundation-owned VPC(10.42.0.0/20)内のパブリックサブネットに配置されます(セキュリティグループの設定は ネットワーク構成 を参照)。
Step Functions は DynamoDB の semaphore row で prover slot を取得・解放し、semaphore-janitor は terminal execution event と定期 sweep から stale slot を回収します。
Verification レイヤ
STARK レシート検証を担当するレイヤです。
public-api は session capability、finalization state、bundle locator、Image ID を確認したうえで検証 work を SQS に登録します。
verification-worker は work item に含まれる sessionId、finalizationId、bundleKey、expected Image ID を使って private S3 bucket から bundle.zip を取得します。
その後、verifier-service で Receipt::verify(expected_image_id) を実行します。
検証結果は protected report artifact(verification.json)として保存されます(扱いは 公開境界 と後述の Storage レイヤ)。
Storage レイヤ
S3 バケットが、証明バンドルに含まれる配布対象アーティファクトと非公開ワーク入力の保存を担当します。 Target Terraform はこれに加えて、プローバーイメージ候補の metadata、promotion evidence、release record / accepted pointer 用の非公開 S3 storage を管理します。 実行対象は選択された app deployment record から渡される digest 固定 prover image URI で決まります。
| 項目 | 設定 |
|---|---|
| バケット命名 | stark-ballot-simulator-{account-alias}-{environment}-proof-artifacts |
| 暗号化 | AES256(サーバーサイド暗号化) |
| パブリックアクセス | 全ブロック |
| ライフサイクル | proof_artifact_lifecycle_days 日(既定 7 日)で current / noncurrent object を自動削除 |
| バージョニング | Enabled |
プローバー release metadata / evidence は foundation 側の versioned manifest storage に保存します。 CodeBuild からの publish 仕様、legacy/optional SSM current pointer の非 authority 境界、digest 固定 runtime input の関係は イメージ署名 > ビルドと署名の概念 を参照してください。
証明バンドル側のオブジェクトパスは既定で sessions/{sessionId}/{finalizationId}/ 配下です。
target runtime では S3_PROOF_PREFIX=sessions/ が Terraform と runtime validation の契約です。
この prefix を変更するには、public-api、verification-worker、proof-dispatcher、finalization-writer、CLI/report delivery の S3 policy と artifact lookup を同時に変更する必要があります。
影響範囲は 非同期プローバー > Dispatch 処理 を参照してください。
配布対象(bundle.zip とその同梱物)と保護対象(input.json、bitmap sibling object など)の区分は 公開境界 を、bundle member と S3 レイアウトの詳細は バンドル構造 を参照してください。
このレイヤに固有のファイルは次のとおりです。
| ファイル | 区分 | 説明 |
|---|---|---|
*-receipt.json | 中間データ | zkVM host の生出力。bundle.zip 内の receipt.json の元データ |
*-output.json | 中間データ | zkVM host の生出力(集計結果)。bundle.zip 内の journal.json の元データ |
verification.json | 保護 | 検証サービスの出力。target runtime では protected report artifact として保存を要求し、bundle.zip には含めない |
エンドツーエンドのデータフロー
投票から検証までのデータフローを、レイヤ間の通信として示します。
以下は primary_active または replacement_active の非 quiesced phase における処理です。
*_quiesced phase では、この図に含まれる新規 session admission と prover / verification queue consumption は利用できません。
図では API request ごとの CloudFront hop を省略していますが、develop operator gate が有効な場合は frontend と API の両方で viewer request の signed cookie を検証します。
sequenceDiagram
participant C as クライアント
participant W as Web レイヤ<br/>(CloudFront + S3)
participant A as API レイヤ<br/>(API GW + Lambda)
participant Q as 検証キュー<br/>(verification-work)
participant V as 検証レイヤ<br/>(verification-worker)
participant D as Data レイヤ<br/>(DynamoDB)
participant P as Prover レイヤ<br/>(SQS → SFN → ECS)
participant S as Storage レイヤ<br/>(S3)
Note over C,D: 投票フェーズ
C->>W: ページ読み込み
opt develop operator gate enabled
W->>W: CloudFront signed-cookie 検証
end
C->>A: POST /api/sessions
A->>D: セッション作成
C->>A: POST /api/sessions/{sessionId}/votes
A->>D: 投票 + コミットメント保存
A-->>C: 投票レシート返却
Note over A,D: ボット投票を自動追加
Note over C,P: 集計フェーズ
C->>A: POST /api/sessions/{sessionId}/finalizations
A->>P: SQS メッセージ送信
A-->>C: 202 Accepted
P->>S: input.json 保存(SFN 起動前)
P->>P: イメージ署名検証
P->>P: ECS タスクで証明生成
P->>S: 実行成果物 + 公開監査アーティファクト + bundle.zip 保存
P->>D: コールバックで結果書き込み
Note over C,S: 検証フェーズ
C->>A: GET /api/sessions/{sessionId}/finalizations/{finalizationId}/verification
A->>D: identity + voting + finalization + verification record 取得
A-->>C: 検証ペイロード返却
C->>A: POST /api/sessions/{sessionId}/finalizations/{finalizationId}/verification
A->>Q: 検証 work を登録
A->>D: VERIFICATION_RESULT に running を記録
A-->>C: verificationStatus=running + finalizationId 返却
Q->>V: work item 配信
V->>S: bundle.zip 取得
V->>V: verifier-service 実行
V->>S: verification.json を protected report として保存
V->>D: verification result と delivery metadata を別 record に記録
C->>A: GET /api/sessions/{sessionId}/finalizations/{finalizationId}/verification
A->>D: 最新の検証状態取得
A-->>C: 検証結果を含むペイロード返却
verification-worker は bundle.zip を検証入力として読み取り、target AWS runtime では verification.json が存在しない場合や S3 upload に失敗した場合に fail-closed に失敗します。
保存に成功した report locator は VERIFICATION_DELIVERY record に、検証結果は VERIFICATION_RESULT record に保存し、pure finalization state には混在させません。
report 保存に失敗しても delivery metadata の既存 s3BundleKey は維持されますが、verification run 自体は成功扱いになりません(report と bitmap sibling object の扱いは バンドル構造)。
ネットワーク構成
VPC
Foundation Terraform が管理する VPC は、ECS Fargate タスク専用です。
API Gateway、target Lambdas、DynamoDB、S3、CloudFront は VPC 外の managed service boundary で動作します。
VPC は 10.42.0.0/20 で、最初の 2 つの available AZ に /24 public subnet を 1 つずつ配置します。
AZ 名は Terraform が実行時に取得するため、特定の AZ suffix には固定しません。
flowchart TD
subgraph VPC["Foundation-owned VPC (10.42.0.0/20)"]
subgraph AZ1["available AZ 1"]
S1["パブリックサブネット<br/>10.42.0.0/24"]
end
subgraph AZ2["available AZ 2"]
S2["パブリックサブネット<br/>10.42.1.0/24"]
end
SG["セキュリティグループ<br/>Ingress: なし<br/>Egress: all protocols / ports"]
end
IGW["インターネット<br/>ゲートウェイ"]
ECR["ECR<br/>(イメージ取得)"]
S3["S3<br/>(バンドル保存)"]
VPC --> IGW
IGW --> ECR
IGW --> S3
ECS タスクはパブリック IP を持ちますが、セキュリティグループがインバウンドを全拒否するため、外部からのアクセスはできません。
アウトバウンドは 0.0.0.0/0 に対する全 protocol / port を許可します。
この egress 全許可は、public subnet から ECR、S3、CloudWatch Logs などへ到達するための develop prover の割り切りです。
監視とロギング
この節はログの物理配置と保持期間を示します。 ログ、メトリクス、alarm、operations ledger の authority と読み方は 可観測性設計 を参照してください。
主な CloudWatch ログ群
runtime のログ群(ECS Fargate タスク、Step Functions 実行、public-api / proof-dispatcher / finalization-writer / verification-worker / image-signature-verifier / semaphore-janitor の各 Lambda、API Gateway アクセスログ)の保持期間は develop 90 日 / main 180 日で統一しています。
例外は次の 2 つです。
| ログ群 | 対象 | develop | main |
|---|---|---|---|
/aws/ecs/containerinsights/{project}-{account-alias}-{environment}-prover/performance | ECS Container Insights | 7 日 | 30 日 |
/aws/events/{project}-{account-alias}-{environment}-ops-ledger | normalized operations ledger | 365 日 | 731 日 |
ロググループ名は Terraform-managed physical names と component suffix に揃えます(image-signature-verifier の log group だけは stark-ballot-sim- を name prefix に使います)。
deployment pipeline の CodeBuild log group は bootstrap root の管理対象です。
develop / main の保持期間は、bootstrap pipeline が 30 / 365 日、App pipeline が 180 / 365 日、Foundation と image release pipeline が 365 / 365 日です。
CloudTrail
CloudTrail は current target app/bootstrap/foundation Terraform runtime の構成要素ではなく、account-plane または organization-level の監査証跡として別途管理される場合があります。 旧構成の残存ロググループ、旧 standalone prover/toolchain CodeBuild log group、旧 Terraform の multi-region CloudTrail(main 環境のみ、90 日保持)は、この topology の runtime authority に含めません。
関連する章
- 現行構成とサービス一覧:サービスごとの責務一覧
- 非同期プローバー:Prover レイヤの実行時フロー
- 可観測性設計:ログ、検出、相関、通知境界
- バンドル構造:Storage レイヤの artifact 境界
非同期プローバー
このページは、AWS runtime で STARK 証明生成を HTTP リクエストから分離する非同期プローバー経路を説明します。
対象は RUNTIME_PROFILE=target-aws-async の経路です。
STARK 証明生成はブラウザ操作や API Gateway / Lambda の短い応答時間より長くかかります。
そのため、POST /api/sessions/:sessionId/finalizations は作業を受理してすぐ返し、証明生成は SQS、Step Functions、ECS Fargate に委ねます。
ブラウザは capability token 付きの current finalization API をポーリングし、完了後に bundle / report を取得します(Bundle / report delivery。public の分類は 公開境界 の定義に従います)。
実行リソースと mode 境界
target の prover task は ARM64 Linux の CPU-only ECS Fargate task で、現行 IaC
default は 16 vCPU / 32 GiB です。target-aws-async profile は production proof
evidence と async finalization だけを許可し、mock / development receipt や同期
execution を選べません。
local/test profile は同じ target topology を模倣するものではありません。
local-demo と static-mock-e2e は mock evidence、
local-proof-development は development-only receipt、
local-proof-production は production proof をそれぞれ同期実行します。profile
全体の対応表は AWS runtime 境界 > Semantic runtime profiles
を参照してください。
この分離により、短い Web/API request lifecycle は work acceptance と status 取得に集中し、数分規模・高 resource の proof lifecycle は queue、retry、 capacity control、artifact publication を独立して扱えます。CPU/GPU や task resource を変更する場合は、この非同期境界を保ったまま proof-time と cost の evidence を取り直す必要があります。
全体像
flowchart TB
API["POST /api/sessions/:sessionId/finalizations<br/>capability protected"] --> S3IN["Private S3<br/>input.json"]
API --> SQS["SQS<br/>prover work queue"]
SQS --> DISPATCH["proof-dispatcher Lambda"]
DISPATCH --> SFN["Step Functions<br/>finalization state machine"]
SFN --> SIG["image-signature-verifier Lambda<br/>runtime alias"]
SIG --> CHECK{"cryptographic result<br/>VERIFIED?"}
CHECK -->|Yes| SEM["DynamoDB<br/>prover semaphore"]
JANITOR["semaphore-janitor Lambda<br/>compensating cleanup"] -.-> SEM
SEM --> STARTED["finalization-writer Lambda<br/>running admitted"]
STARTED --> ECS["ECS Fargate task<br/>zkVM host"]
CHECK -->|No| CALLBACK_FAIL["finalization-writer Lambda<br/>failed"]
ECS --> S3OUT["Private S3<br/>bundle.zip + sibling artifacts"]
ECS --> CALLBACK_OK["finalization-writer Lambda<br/>succeeded / timeout / failed"]
CALLBACK_OK --> DDB["DynamoDB<br/>session finalization state"]
CALLBACK_FAIL --> DDB
DDB --> STATUS["GET /api/sessions/:sessionId/finalizations/current<br/>capability protected"]
S3OUT --> DOWNLOAD["GET .../:finalizationId/artifacts/bundle<br/>GET .../:finalizationId/artifacts/report<br/>capability protected"]
主要な責務は次のとおりです。
| コンポーネント | 責務 |
|---|---|
| API handler | セッション capability、Turnstile、rate limit、finalize 前提条件を検証し、input.json を private S3 に保存して非同期 work item を作る |
| SQS work queue | finalize 要求を durable な dispatch 単位として保持し、dispatcher の再試行と DLQ 移動を担う |
| proof-dispatcher Lambda | work item、artifact key、session の pending state を検証し、Step Functions execution を開始する |
| Step Functions | cryptographic image verifier、prover semaphore、ECS Fargate runTask.sync、成功、失敗、timeout の callback を順序づける |
| image-signature-verifier | 選択された digest-pinned prover image を固定された trust policy と verifier asset で暗号学的に検証する |
| prover semaphore | DynamoDB の単一 lock item で RunProver への同時進入数を制限し、capacity 待ちを Step Functions 内に閉じ込める |
| semaphore-janitor Lambda | terminal execution event と定期 sweep を使い、通常の release 経路で残った semaphore owner を補償的に解放する |
| ECS Fargate task | zkVM host を実行し、公開可能 artifact と private sibling artifact を private S3 に保存する |
| finalization-writer Lambda | callback payload と S3 bundle key を検証し、DynamoDB の session finalization state を更新する |
| bundle/report API | capability 保護の bundle / report 配信(Bundle / report delivery) |
Finalize 受付
POST /api/sessions/:sessionId/finalizations は session-scoped な操作であり、path の sessionId と X-Session-Capability の検証を通過した場合だけ処理されます(契約は API リファレンス > セッション capability)。
非同期モードでは、API handler は同期的に証明を作らず、次の処理を行います。
- セッションが finalize 可能な状態か検証する。
- zkVM 入力を構築し、finalization state を
pendingとして保存する。 - target AWS runtime では
sessions/{sessionId}/{finalizationId}/input.jsonに zkVM 入力を保存する。 - SQS work queue に、正準 S3 key を含む prover work message を送る。
202 AcceptedとpendingのFinalizationResourceを返す。現行 resource は status URL を含まない。
返却後のクライアントのポーリングと status の判定境界は Client polling と判定境界 を参照してください。
Dispatch 処理
proof-dispatcher Lambda は SQS message を 1 件ずつ処理します。
SQS は長時間の証明生成を直接抱えるのではなく、Step Functions execution を開始するための durable dispatch queue です。
dispatcher は次を検証します。
- strict な message shape と
recordType、session ID、finalization ID input.json、output prefix、bundle.zipの S3 key が session / finalization scope の正準 layout に一致すること- AWS runtime に必要な環境設定が存在すること
検証後、dispatcher は Step Functions を StartExecution で開始します。
dispatcher は input.json を保存せず、StartExecution 成功だけで finalization state を running にもしません(running へ遷移する条件は Step Functions)。
session の current finalization record が存在しない、または message の finalizationId に対する pending state ではない場合、dispatcher は stale work として drop します。
message shape や artifact key の不整合、または一時的な AWS 呼び出し失敗は SQS retry / DLQ の対象です。
DynamoDB recovery phase と quiesce
DynamoDB recovery の phase 定義と切替契約は Terraform を参照してください。
quiesced phase では新規の public session admission と prover / verification work queue の SQS event-source mapping が無効になるため、未消費の prover work は SQS に残り、finalization state は pending のままになることがあります。
event-source consumption を再開するのは active phase だけです。
Step Functions
Step Functions state machine は、証明生成の順序と callback を管理します。
stateDiagram-v2 [*] --> VerifyImageSignature VerifyImageSignature --> CheckImageSignature VerifyImageSignature --> RecordSignatureInvocationFailure: invocation error RecordSignatureInvocationFailure --> CheckImageSignature: normalized rejection CheckImageSignature --> AcquireProverSlot: VERIFIED + cryptographic CheckImageSignature --> CallbackSignatureFailed: other result AcquireProverSlot --> MarkProverStarted: slot acquired AcquireProverSlot --> CallbackCapacityWaitFailed: capacity wait exhausted MarkProverStarted --> RunProver: admitted MarkProverStarted --> ProverStartSuperseded: stale/cancelled RunProver --> ReleaseAfterSuccess: success RunProver --> ReleaseAfterTimeout: timeout RunProver --> ReleaseAfterFailure: task error ReleaseAfterSuccess --> CallbackSucceeded ReleaseAfterTimeout --> CallbackTimedOut ReleaseAfterFailure --> CallbackFailed CallbackSucceeded --> [*] CallbackTimedOut --> [*] CallbackSignatureFailed --> [*] CallbackFailed --> [*] CallbackCapacityWaitFailed --> [*] ProverStartSuperseded --> [*]
VerifyImageSignature は image-signature-verifier Lambda の qualified runtime alias を呼び出し、ECS で実行する digest-pinned prover image を暗号学的に検証します。
固定された trust root と verifier asset を使った検証結果が result = VERIFIED かつ mode = cryptographic の場合だけ semaphore 取得に進みます。
それ以外(署名未完了、profile 不整合、trust policy 不整合、検証失敗、timeout、verifier error)は rejected result として fail-closed に扱います。
Lambda invocation 自体が bounded retry 後も失敗した場合は RecordSignatureInvocationFailure が cryptographic rejection に正規化し、CallbackSignatureFailed に進みます。
signing status の readiness と暗号学的検証の区別を含む考え方は イメージ署名 を参照してください。
AcquireProverSlot は DynamoDB-backed semaphore で RunProver への同時進入数を制限します。
capacity 競合は Step Functions 内で待機 / retry され、この間の user-visible finalization state は pending のままです。
slot 取得後、MarkProverStarted が finalization-writer Lambda を呼び、store guard を通過した場合だけ running と startedAt を記録します。
この guard により、cancel 済みまたは superseded された execution は RunProver に進まず slot を解放して終了します。
RunProver は ECS Fargate task を ecs:runTask.sync で起動します。
Step Functions は task の完了を待ち、slot を解放したうえで成功、timeout、その他の失敗を finalization-writer Lambda に callback します。
通常の slot 解放は state machine 内の ReleaseAfterSuccess / ReleaseAfterTimeout / ReleaseAfterFailure が担います。
semaphore-janitor Lambda はその補償経路です。
Step Functions の terminal status event で owner 解放を試み、さらに 30 分ごとの sweep で prover task timeout に 15 分を加えた期間より古い owner を確認します。
sweep は Step Functions execution がまだ RUNNING なら owner を保持し、terminal または存在しない execution だけを条件付き更新で解放します。
ECS Fargate task
ECS Fargate task は一回限りの証明生成 job です。 ブラウザや外部利用者に直接公開される endpoint ではありません。
task には Step Functions から次のような session 固有の入力が渡されます。
| 環境変数 | 説明 |
|---|---|
INPUT_S3_BUCKET / INPUT_S3_KEY | API handler が保存した zkVM 入力の場所 |
OUTPUT_S3_BUCKET / OUTPUT_S3_PREFIX | bundle と sibling artifact の保存先 |
EXPECTED_IMAGE_ID | 実行する guest image と照合する Image ID |
コンテナの entrypoint は次の順序で処理します。
- private S3 から
input.jsonを取得する。 - 入力 JSON の必須フィールドを検証する。
- zkVM host binary を実行する。
- host output から
journal.jsonを構築する。 public-input.json、election-manifest.json、close-statement.jsonを構築する。journal.jsonと公開監査アーティファクト の method version、input commitment、選挙情報が一致することを検証する。receipt.json、journal.json、public-input.json、election-manifest.json、close-statement.jsonだけを含むbundle.zipを作る。bundle.zipと sibling artifacts を private S3 にアップロードする。
処理が非ゼロ終了した場合、entrypoint は failure-marker.json を best-effort で保存します(契約の詳細は Artifact 境界)。
target-aws-async と prover container は production proof evidence だけを許可します。container entrypoint は retired development-mode variable を host process に渡さず、development receipt を成功証明として扱いません。
Artifact 境界
非同期プローバーの S3 bucket は private proof artifact storage です。 ブラウザは S3 に直接アクセスしません。
公開可能 member と除外 artifact の普遍的なポリシーは 公開境界 を、モード別の bundle allowlist と S3 レイアウトは バンドル構造 を参照してください。 mode 非依存の保護 4 アーティファクトの除外は、非同期モードでも 公開境界 > bundle.zip に入れないファイル と同一です。 そのうえで、非同期経路に固有の点は次のとおりです。
included-bitmap.jsonとseen-bitmap.jsonは、生成された場合にbundle.zip外の sibling object として同じ finalization prefix に保存されます。S3 object の存在だけでは利用可能とせず、callback が内容を検証して finalization に admit した結果を artifact availability に反映します。verification.jsonは finalize callback 時点で常に存在する artifact ではなく、後続の verification run が report を保存した後にのみ、/api/sessions/:sessionId/finalizations/:finalizationId/artifacts/reportの capability 保護 API から取得できます。failure-marker.jsonは非同期経路だけに現れる private diagnostic です。契約は次のとおりです。
failure-marker.json の契約
failure-marker.json は、prover が非ゼロ終了した場合に entrypoint が作る、category だけを持つ小さな private sibling object です。
categoryの値はconfiguration、input、prover_execution、infrastructure_interruption、artifact、upload、timeout、unknownの閉じた集合に制限され、raw error、session ID、artifact path、image authority、credential は含めません。- 同じ finalization scope の private S3 prefix へ create-only で保存し、既存 marker と異なる内容を上書きしません。
finalization-writerは消費前に marker を契約と照合し、認識できる category があるときだけ failure detail に保存します。- marker が存在しない、または契約に一致しない場合は、raw error を公開せず generic な task failure として扱います。
- 公開 bundle や report には含まれず、ブラウザ向け download endpoint からも配信されません。
Callback と session state
finalization-writer Lambda は Step Functions から受け取った callback を session state に反映します。
成功 callback では、writer は S3 上の bundle.zip key が sessions/{sessionId}/{finalizationId}/bundle.zip の正準 layout と一致することを確認します。
その後、bundle から receipt、journal、公開監査アーティファクトを復元し、存在する sibling bitmap artifact を
bitmap Merkle 契約に照らして検証します。terminal result と bitmap availability、
利用可能と宣言する sidecar は一つの transaction で保存し、commit 前の失敗では succeeded を公開せず bounded callback retry に委ねます。
失敗 callback では、署名検証失敗、ECS task 失敗、timeout などの理由を finalization state に保存します。
PROVER_TASK_FAILED の場合、writer は許可された finalization scope の failure-marker.json を契約に従って消費します(詳細は Artifact 境界)。
callback delivery 自体が失敗した場合は Step Functions の bounded retry / failure state に従います。
Client polling と判定境界
クライアントは GET /api/sessions/:sessionId/finalizations/current を capability 付きで呼び出し、current FinalizationResource の進捗を確認します。
stateDiagram-v2 [*] --> pending: POST .../finalizations accepted pending --> running: semaphore slot acquired / MarkProverStarted admitted pending --> failed: signature rejection / capacity wait or start failure running --> succeeded: callback persisted result running --> failed: callback persisted error running --> timeout: timeout callback persisted succeeded --> [*] failed --> [*] timeout --> [*]
| ステータス | 説明 |
|---|---|
pending | finalize request は受理済みで、SQS dispatch、Step Functions setup、または semaphore capacity wait を待っている |
running | semaphore slot 取得後に prover 開始が admitted され、ECS task または task-start retry が進行中 |
succeeded | bundle の復元後、terminal result と bitmap availability、存在する admitted sidecar が原子的に保存された |
failed | 署名検証失敗、prover task 失敗、artifact 復元失敗などで fail-closed に保存された |
timeout | ECS task timeout が callback として保存された |
status response は finalization の進捗を示すものです。 検証 UI の最終的な「Verified」判定は、bundle、receipt、journal、公開監査アーティファクト、必要な検証チェックを別途評価して決まります。
Bundle / report delivery
通常の browser / CLI 経路では、bundle と report は capability 保護 API から取得します(endpoint 一覧とアクセスポリシーは 公開境界 > bundle と report の取得経路、wire 契約は API リファレンス > Artifact 取得 API)。
非同期 / S3 実行に固有の挙動は次のとおりです。
- 大きい
bundle.zipは bundle endpoint の range response で配信できます。 - S3 key が正準 layout と一致しない場合や、許可された finalization scope に属さない場合、API は fail-closed に扱います。
- raw S3 URL や presigned URL は通常の browser / CLI contract ではありません。
関連する章
- AWS runtime 境界:AWS 章全体の runtime 境界
- バンドル構造:
bundle.zipと private artifact の境界 - 可観測性設計:queue、Step Functions、ECS、writer を横断する相関と検出
- イメージ署名:Step Functions 内の署名確認
- Image ID:prover image と guest Image ID の対応
可観測性設計
このページは、AWS target runtime の可観測性を公開仕様として説明します。 対象は、API、非同期 finalization、検証 worker、zkVM prover、deployment を横断して、 障害や予期しない結果を事後に説明するためのログ、メトリクス、検出、相関です。
最も重要な境界は、可観測性は検証判定の authority ではないことです。 ログ、メトリクス、alarm、notification の成否は検証チェックや user-facing verdict を変更せず、可観測性は判定後の事実と runtime の状態を調査するための evidence layer にとどまります。
このページの節構成は次のとおりです。
- 目的と非目的
- 検証プレーンと運用プレーン
- 観測対象とシグナル
- 構造化ログと安全な相関
- Verification observation
- 証拠レベルの読み分け
- メトリクスと検出レイヤ(alarm 分類としきい値、通知の境界 を含む)
- Normalized operations ledger
- 保持期間
- 現在保証しないもの
目的と非目的
可観測性の目的は、「request / work はどこまで到達したか」「どの閉じた reason / category で停止したか」「症状はどの accepted deployment と相関するか」といった問いに、bounded な証拠で答えることです。 surface ごとの具体的な問いは 観測対象とシグナル の表に整理します。
一方で、次を目的にしません。
- alarm の状態だけによる正常性や暗号学的正しさの証明
- production 運用の提供(on-call、validated SLO)や、alarm からの自動介入(retry、redrive、rollback)
- raw AWS event、private artifact、秘密値の長期監査 archive
採用していない仕組みの一覧は 現在保証しないもの を参照してください。
検証プレーンと運用プレーン
検証プレーンは evidence を required check として評価し、verdict を導出します。 運用プレーンは、その完了後に構造化された observation を受け取れますが、検証プレーンへ判定を返しません。
flowchart LR
subgraph VERIFY["検証プレーン(判定 authority)"]
EVIDENCE["投票 / 掲示板 / STARK evidence"]
CHECKS["required checks"]
VERDICT["user-facing verdict"]
EVIDENCE --> CHECKS --> VERDICT
end
subgraph OBS["運用プレーン(診断 evidence)"]
EVENTS["構造化イベント"]
METRICS["AWS native metrics"]
DETECT["metric filters / alarms"]
LEDGER["normalized operations ledger"]
OPERATOR["operator investigation"]
EVENTS --> DETECT
METRICS --> DETECT
DETECT --> OPERATOR
LEDGER --> OPERATOR
end
VERDICT -. "判定後の bounded observation" .-> EVENTS
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations の送信失敗、logging exception、CloudWatch の一時的な利用不能は、
すでに導出された verdict を成功にも失敗にも変更しません(Verified を表示できる条件は、引き続き ゲーティングロジック の required check だけです)。
観測対象とシグナル
| Surface | 主なシグナル | 答えたい問い |
|---|---|---|
| API Gateway | request count、5xx、latency、access log | edge から API integration まで到達したか |
| Lambda | errors、throttles、closed application event | handler が失敗したか、実行前に抑制されたか |
| DynamoDB | read / write throttle | session、vote、rate limit、semaphore の永続化が律速したか |
| SQS | oldest-message age、DLQ depth | prover / verification work が滞留または隔離されたか |
| Step Functions | failed、timed out、execution history | finalization orchestration が terminal failure になったか |
| ECS prover | task stop、Fargate quota、bounded prover_summary | 証明生成が開始・終了し、どの failure category になったか |
| Verification | failure category、required-check contradiction | 通常の検証失敗か、verdict pipeline 自体の矛盾か |
| Deployment | pipeline state change、accepted deployment | runtime の症状が変更時点と相関するか |
Dashboard はこれらの入口をまとめますが、個々の user journey を一枚の graph だけで説明するものではありません。 調査では application event、AWS native metric、deployment evidence をそれぞれ確認します。
構造化ログと安全な相関
Producer ごとの形
Target runtime は producer ごとの実行環境を保ちながら、閉じた event と bounded field を使います。
| Producer | Log shape |
|---|---|
public-api など共通 logger を使う Lambda | Lambda JSON envelope の object-valued message 内に application record を格納 |
image-signature-verifier | 署名対象や credential を含まない closed result / duration bucket |
| API Gateway | request ID、route key、status、latency、integration error の access record |
| Step Functions | execution data を含めない orchestration log |
| ECS prover | start / end、result、closed failure category、duration を持つ flat JSON summary |
| normalized operations ledger | source event を縮約・再検証した flat JSON |
CloudWatch Logs metric filter は Lambda の outer envelope に合わせ、application field を
$.message.* で exact-field matching します。
ECS prover と operations ledger は flat field を使うため、query 側では producer shape の違いを明示的に扱います。
Canonical event は自由記述の message から推測せず、TypeScript の discriminated union と runtime allowlist を authority にします。
代表例は finalization_admitted、dispatch_dropped、finalize_state_transition、queue_publish、
verification_run_summary、bundle_download_failed です。
finalization_admitted は、shared persistence outcome boundary が durable な
none から pending への変更を実際に保存した後だけ出力します。
event 固有 field は bounded finalization_id だけで、failed write、idempotent retry、
競合した writer の結果を admission として数えません。log delivery 自体は exactly once ではないため、
consumer は finalization_id で重複を除きます。
相関キー
| Key | Authority / 用途 |
|---|---|
request_id | API Gateway payload の request ID。同期 request 内の trusted correlation |
client_request_id | 制限された client input。trusted request_id とは分離 |
session_id_hash | raw session ID を保存せず、runtime salt と domain separation を使う相関値 |
finalization_id | SQS、Step Functions、ECS、writer、verification worker を結ぶ async correlation |
deployment_id | accepted deployment と runtime / operations ledger を結ぶ immutable correlation |
app_source_sha | restricted operational log 内で deployment evidence と照合する source identifier |
同期 HTTP request の外では、client-provided request ID を無理に引き回しません。
非同期処理へ移った後は session_id_hash と finalization_id を主な join key にします。
Canonical event と ledger に記録しない情報
Canonical application event、producer summary、normalized operations ledger は、 producer 固有の validator と field allowlist により、次の情報を structured field や相関キーとして定義しません。
- raw session ID、raw source IP、vote choice / random、request body
- capability token、Cookie、secret / config value、署名鍵
- S3 bucket、full key / URI、presigned URL、local private path
- account ID、ARN、digest-pinned image URI
- raw exception object、AWS
Cause、CloudWatch state reason、ECS stopped reason
IP、session ID、storage prefix を相関させる必要がある場合は、runtime reference から解決した salt と 用途別 domain separation を使った hash だけを保存します。 Salt rotation をまたいで同じ hash が維持されるとは仮定しません。
共通 sanitizer は、既知の機密 field / value shape を拒否し payload の深さと byte 数を制限する defense-in-depth であり、任意の文字列からすべての account ID、ARN、resource locator を識別する DLP ではありません。 そのため producer は raw external error を渡さず、closed reason / category へ分類します。 AWS-managed service log や execution history に残り得る resource identifier は restricted operational evidence として扱い、canonical event、ledger、public docs へコピーしません。
Verification observation
Cast-as-Intended の一部はブラウザだけが保持する opening に依存します。
そのため、server-only の
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification だけでは、
最終的な 4 段階 verdict をそのまま observability event にできません。
判定完了後、capability を持つブラウザは
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations へ、
browser-local な cast check 4 個の status だけを castChecks として送ります。
session と finalization の identity は capability-protected path だけから取得し、body の
finalizationId、client-supplied overall verdict、server-side check、秘密値、自由記述は受理しません
(payload と error の契約は API リファレンス)。
Server は path と current finalization の一致、terminal verification result、保存済み scenario、
server-side checks、STH runtime context を再構築し、observation は exact finalization ごとに最初の一件だけを条件付きで記録します
(同じ fingerprint の再送は idempotent、異なる内容や current finalization と一致しない path は conflict)。
記録後、server は required check のうち success でない件数を summary status とは独立に数え、
bounded な verification_run_summary を best-effort で出力します。
この endpoint は telemetry の completeness を補うものであり、verdict の再計算、gate、retry、修正には使いません。
証拠レベルの読み分け
この公開ページは repository-defined design を説明します。 個別 account の live state、日付付き受入証拠、private query、incident response command は扱いません。
そのうえで、可観測性の受入では次の状態を同一視しません。
- Defined:code / Terraform に schema、filter、alarm、subscription が定義されている
- Configured:対象 AWS environment に resource が構成されている
- Matched:実際の bounded event が deployed filter に一致し、想定 metric を生成した
- Delivered:notification が intended recipient まで配送された
Alarm が OK であることは、対象期間に threshold breach が観測されなかったことを示すだけです。
低トラフィック時の平坦な graph や metric absence も同様で、正常な user journey、metric filter の一致、notification delivery をまとめて証明するものではありません。
同様に、dashboard、post-apply smoke、operator-gate probe、full gated journey、
accepted deployment evidence は、それぞれ別の問いへ答える証拠です。
メトリクスと検出レイヤ
AWS native metrics
現在の Terraform target design は、30 個の one-period passive detector、
verification invariant 用の direct-action alarm 1 個、別の 5 個の damped actionable alarm で構成されます。
damped は、単一 period の spike ではなく、連続する複数 period で条件が持続した場合だけ
ALARM へ遷移する actionable alarm の分類名です。
内訳は次の表のとおりです。
| Class | Count | Notification | 主な意味 |
|---|---|---|---|
| DynamoDB read / write throttle | 10 | passive | 5 table の read / write capacity pressure |
| SQS DLQ depth diagnostic | 2 | passive | prover / verification work の隔離 |
| SQS DLQ depth actionable | 2 | ops-actionable via rule | 同じ 2 DLQ の sustained condition |
| Step Functions failed / timeout | 2 | passive | finalization orchestration の terminal failure |
| Lambda errors | 6 | passive | 6 Lambda の handler failure |
| Lambda throttles | 6 | passive | handler 実行前の throttle |
| API Gateway 5xx diagnostic | 1 | passive | HTTP API integration failure |
| API Gateway 5xx actionable | 1 | ops-actionable via rule | volume-gated sustained 5xx ratio |
| Work queue age diagnostic | 2 | passive | prover / verification work の滞留 |
| Work queue age actionable | 2 | ops-actionable via rule | 同じ 2 queue の sustained condition |
| Fargate on-demand vCPU quota | 1 | passive | prover task capacity pressure |
| Verification invariant | 1 | direct invariant SNS | verdict pipeline の矛盾 |
Passive alarm は調査開始点であり、alarm action や自動 remediation を持ちません。
Missing metric data は、低トラフィックの demo runtime で障害と誤認しないよう notBreaching として扱います。
damped actionable alarm の条件と評価は次のとおりです。
| Alarm | 条件 | 評価 |
|---|---|---|
| SQS DLQ depth(2 個) | DLQ depth > 0 | 5 分 period × 3 回連続 |
| Work queue age(2 個) | oldest-message age > 900 秒 | 5 分 period × 3 回連続 |
| API Gateway 5xx(1 個) | request 20 件以上、5xx 5 件以上、かつ 5xx ratio 5% 超 | 5 分 period × 3 回連続 |
これらの alarm の ops-actionable topic への routing は 通知の境界 を参照してください。
Exact-field metric filters
Application decision は、自由記述ログの全文検索ではなく閉じた field から metric に変換します。 論理シグナルは次のとおりで、log group ごとの物理 filter は計 7 個です。
| Event / condition | Metric | Dimension |
|---|---|---|
dispatch_dropped | dispatcher drop count | closed reason |
terminal finalize_state_transition | finalization terminal outcome | closed reason |
finalization_admitted | finalization admission sample | なし |
verification_run_summary | verification runs by failure category | closed failure_category |
fully_verified かつ failed_required_count > 0 | verification invariant contradiction | なし |
Session、finalization、request、IP、storage key は metric dimension に使いません。 これにより、機密性だけでなく custom metric の cardinality も閉じた集合に制限します。
Dashboard
Compact dashboard は直近 24 時間を入口として、次を表示します。
- API request count、5xx、inspect-only の p99 latency
- 全 Lambda の errors / throttles
- prover / verification queue の oldest-message age と DLQ depth
- dispatcher drop、terminal finalization、verification failure category
- expected zero の verification invariant contradiction
- rolling 28 日の provisional API serving success と sample size
- rolling 28 日の finalization admission / terminal sample size
p99 latency は表示用で、現在の target design は latency SLO alarm を定義しません。 28 日 panel は sparse traffic で割合だけを正常性と誤認しないため、必ず sample size と並べます。
Measurement-only reliability view
API serving の rolling 28 日 view は API Gateway Count を全 sample、Count - 5xx を good、
5xx を bad とします。auth、capability、origin、rate limit、invalid input など expected 4xx は
non-5xx として good に含み、failure に数えません。Count = 0 は 100% でも breach でもなく
insufficient_evidence です。
Async finalization は durable な finalization_admitted を denominator にし、bounded
finalization_id ごとに admission と最初の authoritative terminal transition を重複排除します。
completion deadline は admission から 75 分で、late log delivery のために deadline 後へ別の
5 分 grace を置きます。grace は deadline を 80 分へ延長せず、75 分を超えた terminal は good にしません。
- 75 分以内の
succeededは good - 75 分以内に
FINALIZATION_RESULT_INVALID、USER_CANCELLED、INVALID_FINALIZATION_ARTIFACTでfailedになったものは expected-domain として分けたうえで good - platform failure / timeout、75 分を超えた terminal、deadline 内に terminal がない eligible work は bad
- deadline と grace をまだ終えていない work は
inside_window、eligible sample が 0 ならinsufficient_evidence
これらは calibration 中の provisional measurement です。 burn-rate alarm、error-budget deployment gate、approval、rollback、recovery、redrive を認可せず、 verification verdict や AWS/application state を変更しません。 mutation を自動実行しない境界は 通知の境界 と共通です。
通知の境界
Verification invariant の direct alarm action は、次の contradiction だけです。
outcome == fully_verified
AND independently_counted_failed_required_checks > 0
これは選挙改ざんの検出結果ではありません。
Required check を満たさないのに fully_verified と観測された、verdict pipeline の実装矛盾を疑う canary です。
Alarm は evidence の保全と調査開始を促すだけで、verdict は変更しません。
これとは別に、2 個の DLQ、2 個の work queue age、1 個の API 5xx に対する damped alarm と、
owned CodePipeline の exact FAILED event だけが ops-actionable topic へ route されます。
damped alarm は alarm action を直接共有せず、exact alarm name の ALARM state だけを
EventBridge rule が分離された topic へ送ります。
pipeline route は alarm ではありません。どちらの通知経路も diagnosis を開始するだけで、
Apply、rollback、recovery、redrive、delete、pointer change を自動実行しません。
ops-actionable の notification body は closed environment、resource または pipeline、
bounded pipeline execution identity(pipeline event のみ)、timestamp、state、dashboard、runbook ID に限り、
source SHA、ARN、account、session、raw reason、secret、artifact locator を含めません。
Invariant と ops-actionable の email subscription は互いに独立かつ任意です。
それぞれ専用の固定 SSM SecureString endpoint が契約どおり存在する場合だけ構成され、
topic、policy、endpoint を共有しません。
Endpoint は public docs、tracked tfvars、Terraform output、pipeline evidence、log に出しません。
Subscription の configured state、recipient confirmation、実際の test notification delivery は、
経路ごとに別の受入証拠として扱います。
Normalized operations ledger
CloudWatch alarm、ECS task、deployment pipeline は、それぞれ異なる event shape と retention を持ちます。 Operations ledger は次の event type を共通の時間軸へ縮約します。
alarm_state_changedecs_task_stoppedpipeline_execution_changeddeployment_accepted
EventBridge rule は source event から必要な候補 field だけを取り出し、typed normalizer が exact key set と
closed value を再検証してから専用 log group に保存します。
保存する deployment_accepted row の例は次の形です。
{
"ts": "2026-01-01T00:00:00.000Z",
"event_type": "deployment_accepted",
"env": "develop",
"deployment_id": "dep_0123456789abcdef0123456789abcdef",
"deployment_mode": "normal",
"pipeline_execution_id": "01234567-89ab-cdef-0123-456789abcdef",
"plan_disposition": "changed",
"resource_name": "app",
"state": "ACCEPTED"
}
record は one-size-fits-all ではなく、共通の ts、event_type、env、resource_name、state に加え、
event type ごとに次の exact key set を持ちます。
event_type | 共通 field 以外の追加 field |
|---|---|
alarm_state_changed | なし |
ecs_task_stopped | deployment_id |
pipeline_execution_changed | pipeline_execution_id |
deployment_accepted | deployment_id、pipeline_execution_id、deployment_mode、plan_disposition |
pipeline_execution_id は owned CodePipeline event と matching acceptance event の bounded lowercase UUID、
deployment_mode は normal / rollback、plan_disposition は changed / empty だけです。
旧 shape、欠落 field、追加 field、alternate field name を current ingestion の fallback として受理しません。
Ledger 固有の除外として、raw source event / event ID、source SHA、repository / branch、account ID、ARN、
task / container / image detail、free-form reason は保存しません。
例外は、上記の exact event で相関に必要な bounded pipeline_execution_id です
(共通の除外は 記録しない情報 を参照)。
EventBridge は at-least-once delivery であるため、同じ common-schema row が重複する可能性があります。
Ledger は exactly-once audit log ではなく、alarm、task stop、deployment を長い時間軸で相関させる診断 evidence です。
retained change-health query は pipeline_execution_id と closed state で重複を除き、
acceptance 数、start-to-acceptance、terminal pipeline result、次の acceptance までの timeline、
restart / supersession / retry toil を project-level proxy として表示します。
保持期間
保持期間の実際の値と log group の物理配置は トポロジー > 主な CloudWatch ログ群 を参照してください。
これらは Terraform が環境別に定義する target policy であり、main の値は repository-defined target であって live production deployment の主張ではありません。
設計方針は次のとおりです。
- 診断に必要な detailed log と、変更相関に必要な bounded ledger の寿命を分ける
- 可観測性のために session、vote、private proof artifact の TTL / lifecycle を延長しない
- CloudWatch alarm history や account / organization level の CloudTrail は、operations ledger と同一の authority として扱わない
現在保証しないもの
- X-Ray / OpenTelemetry による end-to-end distributed tracing
- browser RUM、frontend error / performance collection
- CloudWatch Synthetics による定期 E2E / real-proof journey
- latency SLO、burn-rate、composite alarm、anomaly detection
- alarm からの自動 redrive、retry、rollback、recovery
- Slack / PagerDuty などの通知経路
- CloudFront access log や app root 所有の CloudTrail
- production environment の運用 readiness
これらを採用していないことは、既存の bounded logging / detection contract を弱める理由にはしません。 将来追加する場合も、機密情報保護、closed cardinality、verdict authority との分離を維持する必要があります。
関連する章
- AWS runtime 境界:公開仕様として扱う runtime と情報の境界
- 現行構成とサービス一覧:CloudWatch / EventBridge / SNS を含むサービス責務
- トポロジー:ログ producer と AWS service の配置
- 非同期プローバー:SQS / Step Functions / ECS の処理フロー
- 検証パイプライン:verdict authority と判定後 observation
- API リファレンス:verification observation の request / response 契約
- Terraform:app root による observability resource ownership
イメージ署名
AWS Signer でプローバーイメージを managed signing し、App deployment の preflight と ECS 実行前の両方で暗号学的に検証する仕組みを扱う章です。
STARK 証明は「特定のゲストプログラムが正しく実行された」ことを保証します。 ただし、そのゲストプログラムを含むコンテナイメージ自体が改ざんされていないことも確認する必要があります。 イメージ署名は、期待する signing profile と trust root で検証できる digest 固定のプローバーイメージだけを証明生成に使うためのセキュリティゲートです。
脅威モデル
署名なしの場合、未承認イメージへの差し替えは検証段階の Image ID 照合や STARK レシート検証で拒否され得ます。 しかし、証明生成インフラ上で未承認イメージが実行されること自体は起動前に止められません。 現在の App は、選択された digest 固定のプローバーイメージに対して、normal App lane の preflight と Step Functions の実行前 gate で同じ暗号学的 verifier contract を適用します。
イメージ署名は、STARK 証明が紐づく Image ID(ゲストバイナリの暗号的識別子)とは別レイヤの防御です(両者の関係は Image ID との関係 を参照)。
| 保証の種類 | メカニズム | 検出対象 |
|---|---|---|
| ゲストプログラムの同一性 | Image ID(RISC Zero) | STARK レシートが示すゲストバイナリと期待値の不一致 |
| プローバーイメージの署名検証 | Notation + AWS Signer plugin + strict trust policy | 期待する signing profile / trust root で検証できない digest |
| 署名処理の readiness | ECR managed-signing status | 署名の欠落、失敗、処理中、予期しない profile |
ステータス確認と暗号学的検証の違い
DescribeImageSigningStatusは expected profile の readiness を確認する前段です。 App による受理には、packaged Notation と AWS Signer plugin が strict trust policy の下で返す暗号学的検証成功も必要です。
責務境界、ビルドと署名、実行時ゲート
責務境界
イメージ署名では、プローバーコンテナのビルド、ECR 上の digest、AWS Signer の readiness、暗号学的 verifier、Image ID、runtime 設定を別々の責務として扱います。 公開仕様では、個別の承認手順や内部証跡ではなく、どの情報がどの境界で使われるかを説明します。
| 領域 | 責務 |
|---|---|
| Foundation | Prover / Toolchain repository の保持と、Prover repository 限定の Signer profile / ECR registry signing configuration |
| Toolchain release | digest 固定 image、scan、provenance(署名・暗号学的署名検証なし) |
| Prover candidate build | digest 固定 URI、Image ID / methodVersion の push 前後一致、scan PASS、managed-signing readiness の確認 |
| Prover release authority | 承認済み candidate の immutable prover release record 保存と、CAS で更新する accepted prover pointer による選択 |
| App verifier preflight | immutable verifier version の preflight alias による、選択した Prover digest の暗号学的検証 |
| App deployment selection | accepted pointer chain / preflight evidence の検証と、digest 固定 URI・expected Image ID・verifier authority の固定 |
| Rejected selection paths | managed release lane 外の直接の CodeBuild output、latest.json、legacy SSM current pointer は選択に使用しない |
| App runtime | digest 固定 Prover image の runtime alias による実行前の再検証 |
| verifier-service / UI | STARK レシートの Image ID と公開 trust material の照合、fail-closed な結果表示 |
表の各領域について、次の条件が成り立ちます。
- Prover candidate build は、metadata を出力する前に対象 profile の managed-signing readiness が
COMPLETEか確認しますが、release lane の semantic image record に署名検証結果を埋め込みません。 - Rejected selection paths は候補 lookup にも target topology の release authority にも使いません。
- 暗号学的検証は Toolchain / Prover release lane ではなく、App verifier preflight と App runtime の 2 境界で行います。App runtime が再検証するのは、app deployment record に結び付く digest 固定 Prover image です。
- App deployment selection では、normal deployment の app deployment record が preflight の immutable evidence と verifier authority を保持します。rollback は新しい verifier を変更せず、選択した accepted deployment に保持された cryptographic authority を再検証して再利用します。
- Candidate metadata、release record、pointer、preflight evidence、app deployment record は public proof bundle の構成要素ではありません(公開検証での最終的な authority は Image ID との関係)。
ビルドと署名の概念
プローバーイメージのビルドでは、ARM64 用のコンテナを作成し、guest Image ID と methodVersion を抽出します。
イメージを ECR に push して digest 固定 URI を解決した後、その digest を pull し直して Image ID と methodVersion を再抽出し、push 前後の一致を確認します。
候補は ECR scan が PASS、対象 digest の ECR managed signing status が COMPLETE になるまで fail-closed です。
candidate metadata は digest 固定 URI、scan 結果、Image ID / methodVersion、ソース識別子、digest 固定 Toolchain authority を記録します。
COMPLETE は metadata 作成の前提ですが、signingStatus フィールドとして semantic record に持ち越しません。
ECR managed signing の authority は、foundation Terraform が Signer profile と registry signing configuration として管理します。
registry signing configuration の対象は Prover repository のみです。
sequenceDiagram participant BUILD as Image build participant ECR as ECR participant SGN as AWS Signer participant EVID as Candidate metadata participant APR as Promotion approval participant AUTH as Immutable release authority participant PREFLIGHT as App preflight verifier participant APP as App deployment record participant RUN as App runtime participant ECS as ECS prover task BUILD->>BUILD: ARM64 prover image build BUILD->>BUILD: push 前 Image ID / methodVersion 抽出 BUILD->>ECR: イメージを push ECR-->>BUILD: digest を解決 ECR->>SGN: ECR managed signing SGN->>ECR: 署名ステータス更新 BUILD->>ECR: digest を pull / scan と signing status を参照 BUILD->>BUILD: Image ID / methodVersion の push 前後一致を確認 ECR-->>BUILD: scan PASS / signing COMPLETE BUILD->>EVID: digest / scan / Image ID / provenance を保存 EVID->>APR: candidate metadata をレビュー APR->>AUTH: immutable prover release record を作成 AUTH->>AUTH: accepted pointer を CAS promotion AUTH->>PREFLIGHT: accepted digest を選択 PREFLIGHT->>ECR: status / expected profile を bounded check PREFLIGHT->>PREFLIGHT: Notation strict verification PREFLIGHT-->>APP: VERIFIED cryptographic evidence APP->>APP: pointer chain / Image ID / verifier authority を固定 APP->>RUN: digest 固定 URI / runtime verifier authority RUN->>ECR: status / expected profile を bounded check RUN->>RUN: Notation strict verification RUN->>ECS: VERIFIED の場合だけ起動
Build/push で運用上のタグを使う場合でも、App preflight と runtime の検証対象は常に digest 固定です(ダイジェスト固定)。
App preflight と実行前確認
App-owned の image-signature-verifier Lambda は、checksum 固定の Notation、AWS Signer plugin、ECR credential helper、trust root を同じ ZIP に含めます。
normal App deployment は verifier-only plan をレビューして immutable Lambda version と preflight alias を用意し、その alias が選択した Prover digest を検証してから app deployment record を生成します。
runtime では、Step Functions ステートマシンの最初のステートが同じ immutable version に結び付く runtime alias を呼びます。
VerifyImageSignature から semaphore 取得、ECS 起動までの state machine 上の流れは 非同期プローバー を参照してください。
image-signature-verifier は以下の処理を fail-closed に行います。
- 呼び出しの account、region、repository、digest、image URI、signing profile が互いに整合することを確認する
- ECR の
DescribeImageSigningStatusを bounded retry / timeout で呼び、期待する profile が一意かつCOMPLETEであることを確認する - packaged asset の allowlist、type、size、mode、checksum と、deployment が期待する asset manifest / trust root を検証する
- 対象 repository と signing profile に限定した strict trust policy を一時 workspace に作る
notation verify <digest-pinned-image-uri>を bounded process として実行する- 成功時は
result = VERIFIED、mode = cryptographic、verifier version、trust-policy hash だけを caller に返す。失敗時は bounded reason code で拒否する
Step Functions は result = VERIFIED と mode = cryptographic の両方がある場合だけ semaphore 取得へ進みます。
readiness、asset authority、trust policy、Notation、timeout、Lambda invocation のいずれかが失敗した場合は署名ゲート失敗を通知します。
ECS タスクは起動されません。
ECR リポジトリと digest 固定
リポジトリ構成
| リポジトリ種別 | 用途 | signing / verification contract |
|---|---|---|
| Prover image repository | ECS Fargate で実行するプローバーイメージ | ECR managed signing + App preflight/runtime cryptographic verification |
| RISC Zero Toolchain repository | プローバーイメージの base Toolchain image | 署名なし。digest-bound scan / provenance のみ |
ECR repository は app runtime の前提リソースです。 app root は repository を作成したり image publication を所有したりせず、digest 固定の prover image URI を入力として消費します。
ダイジェスト固定
Terraform の prover image URI 変数には、digest 固定 URI のみが許可されます。
バリデーションルールにより @sha256:<64-hex> 形式が強制されます。
Build/push では運用上のタグを使う場合がありますが、App preflight と runtime が参照する prover_image_uri、readiness 確認、Notation 検証の対象は常に digest 固定であり、タグの上書きによるイメージのすり替えを防止します。
ベースとなる RISC Zero Toolchain image も digest 固定で扱い、プローバーイメージの provenance と metadata の整合性を保ちますが、Toolchain image 自体は signing 対象ではありません。
Step Functions の定義に含まれるイメージダイジェストは、Terraform の変数から以下のように抽出されます。
- リポジトリ名: URI の
@より前の部分からレジストリホストを除去 - ダイジェスト: URI の
@より後の部分(sha256:...)
この分解により、image-signature-verifier Lambda は正確な registry、repository、digest、signing profile の組み合わせで readiness と暗号学的署名を検証できます。
Image ID との関係
イメージ署名と Image ID は異なるレイヤのセキュリティメカニズムです。 両者は共に「正しいプログラムが実行されたこと」の信頼チェーンを構成します。
flowchart TD
subgraph "ビルド時"
BUILD["コンテナイメージ<br/>ビルド"] --> SIGN["ECR managed signing<br/>(運用設定)"]
BUILD --> IMGID["push 前後の ARM64 Image ID /<br/>methodVersion 一致と scan PASS"]
SIGN -->|COMPLETE| READY["managed-signing<br/>readiness gate"]
IMGID --> META["digest 固定<br/>candidate metadata"]
READY --> APR["promotion approval"]
META --> APR
APR --> RECORD["immutable prover<br/>release record"]
RECORD --> POINTER["CAS accepted pointer"]
POINTER --> PREFLIGHT["App preflight<br/>Notation strict verification"]
PREFLIGHT --> DEPLOY["app deployment record"]
end
subgraph "実行時"
URI["digest 固定<br/>prover image URI"] --> VERIFY_SIG
VERIFY_SIG["暗号学的イメージ署名検証<br/>(runtime alias)"] --> RUN["プローバー実行"]
RUN --> RECEIPT["レシート生成<br/>(Image ID を含む)"]
end
subgraph "検証時"
MAP["imageId-mapping.json<br/>expected Image ID"]
RECEIPT --> VERIFY_RECEIPT["レシート検証<br/>(verifier-service)"]
MAP --> VERIFY_RECEIPT
VERIFY_RECEIPT --> MATCH{"Image ID<br/>一致?"}
end
DEPLOY --> URI
DEPLOY -. "expected Image ID / methodVersion" .-> MAP
Candidate metadata から承認済み release record、accepted pointer、preflight evidence、app deployment record までの chain が、digest 固定 URI と Image ID / methodVersion の対応関係を保持します。
公開検証で「正しいゲストが実行された」と判断する authority は、STARK レシート検証と expected Image ID の照合です。
イメージ署名はその前段で、期待する signing identity で暗号学的に検証できないコンテナイメージを App が選択または実行しないための deployment/runtime gate として機能します。
| 検証ポイント | タイミング | 検証主体 | 失敗時の動作 |
|---|---|---|---|
| managed-signing readiness | Prover candidate build | CodeBuild + ECR | candidate metadata を出力しない |
| cryptographic signature | normal App deployment | App preflight verifier alias | App lane を継続しない |
| cryptographic signature | 証明生成直前 | Step Functions + runtime alias | ECS タスクの起動拒否 |
| STARK receipt / Image ID | 公開検証時 | verifier-service / verification UI | 検証失敗の報告 |
PoC としての境界
この gate は、AWS Signer profile、ECR signing configuration、App pipeline の権限分離、checksum 固定の verifier assets、保持された deployment authority を信頼します。 コンテナの再現可能ビルドを第三者が独立に証明する仕組みでも、Toolchain image を署名する仕組みでもありません。 また、public proof bundle に image-signature preflight evidence を追加するものではありません。
この構成は教育用 PoC の defense in depth です。 本番の投票システムに必要な supply-chain governance、鍵運用、独立監査、incident response を満たすという主張ではありません。
Terraform
Target AWS runtime の Infrastructure as Code は、application runtime、前提リソース、deployment-control リソースを root 単位に分けて管理します。
公開仕様としての対象は IaC ownership model です。 移行作業記録や個別の承認証跡、環境切り替えの進捗は扱いません(AWS アーキテクチャ > この部で扱わないもの)。
このページの節構成は次のとおりです。
- Root ownership
- State identity
- App root
- Foundation root
- Bootstrap root(App saved-plan authority を含む)
- Accepted baselines and report-only controls
- Runtime wiring
- Retention authority
- IAM and guards
- Version constraints
Root ownership
Active root の Terraform source は infra/terraform/ 配下にあります。
Bootstrap root だけは、infra/framework/terraform/modules/codebuild-log-groups/
の bounded shared module を 4 つの明示的な composition call から利用します。
この module は Bootstrap、Foundation、App、report-only project の CodeBuild log group
を共通の形で宣言するためのもので、独立した root や state、deployment lane ではありません。
Target 構成では root ごとに state scope を分け、ひとつの root が別 root の runtime resource を暗黙に作成しないようにします。
root は resource の所有者だけでなく、変更 lifecycle、実行権限、saved-plan review、
rollback 判断の単位にも合わせます。これにより、前提 resource や deployment control
の変更が app runtime の plan に紛れ込まず、影響範囲を root ごとに確認できます。
| パス | State scope | 管理対象 |
|---|---|---|
infra/terraform/app/ | app | static frontend、Hono/API、DynamoDB、Lambda、SQS、Step Functions、ECS prover task、semaphore、proof/report S3、observability / notification、develop operator gate |
infra/terraform/foundation/ | foundation | ECR、Prover の ECR managed signing authority、reference-aware ECR retention、release authority storage、runtime reference containers、prover networking / public-hostname handoff |
infra/terraform/bootstrap/ | bootstrap | remote state と Terraform authority storage、deployment-control roles、CodePipeline/CodeBuild lanes、retained / execution artifact storage、accepted baseline、report-only drift / readiness、backend grant |
infra/terraform/legacy/ | legacy | retained legacy/root context(Target runtime authority ではありません) |
infra/lambdas/ と infra/docker/ は Terraform root ではなく、Lambda handler と prover container の source です。
Terraform root は、それらから作られた artifact や image reference を入力として扱います。
flowchart TB
subgraph "Terraform roots"
BOOT["bootstrap<br/>state + deployment control"]
FOUND["foundation<br/>target prerequisites"]
APP["app<br/>target runtime"]
LEGACY["legacy<br/>retained context"]
end
BOOT --> FOUND
BOOT --> APP
FOUND --> APP
LEGACY -. "not target authority" .-> APP
State identity
packages/deployment-authority/deployment-targets.json は、許可された target と
その lane、state、backend、policy、生成名を保持する単一の reviewed authority です。
3 つの active root はこの JSON を直接 decode し、選択された target と root input が
一致しなければ fail-closed します。@stark-ballot/deployment-authority package は、
同じ target context、immutable reference、conditional publication、saved-plan、
acceptance の admission rule を所有します。各 root や lane が名前を再導出したり、
別 target へ fallback したりする構成は受理しません。
Target state identity は account_alias、deployment_environment、state_scope、state_backend_key の組み合わせで表します。
Tracked backend.tf は partial backend として backend "s3" {} だけを持ち、bucket、region、key、lockfile 設定は root ごとの backend config から与えます。
State key は bare terraform.tfstate ではなく、project / account / environment / scope を含む key にします。
| Root | 例示 state identity | 例示 backend key |
|---|---|---|
| app | stark-dev/develop/app | stark-ballot-simulator/stark-dev/develop/app/terraform.tfstate |
| foundation | stark-dev/develop/foundation | stark-ballot-simulator/stark-dev/develop/foundation/terraform.tfstate |
| bootstrap | stark-dev/develop/bootstrap | stark-ballot-simulator/stark-dev/develop/bootstrap/terraform.tfstate |
State object と lockfile は S3 backend で保護します。 Root ごとの guard と Terraform validation は、意図しない account、environment、state scope への実行を fail-closed にするための境界です。
Local operator workflow は、private handoff record から root ごとの backend.local.hcl と sanitize 済みの local values を render します。
公開ドキュメントではこれを backend / tfvars の handoff として扱い、追跡された具体的な account 設定としては扱いません。
guarded app lane は deployment-control role identity のもとで scripts/terraform/terraform-guarded.sh を通す必要があります。
local での app plan / apply は runtime deployment evidence として受理されません。
App root
infra/terraform/app/ は target application runtime を所有します。
主な責務は、ブラウザと API が実際に使う runtime resource を宣言することです。
App root が管理するもの:
- private static frontend bucket、CloudFront distribution、OAC、cache/origin/response policy
- develop-only operator gate の asymmetric KMS signing key、alias、CloudFront public key / key group
- API Gateway HTTP API と Hono
public-apiLambda - session、vote、rate-limit、prover semaphore 用 DynamoDB table、app data KMS key、validated recovery table selection
- finalization writer、proof dispatcher、verification worker、cryptographic image-signature verifier、semaphore janitor Lambda
- prover work queue、verification work queue、Step Functions、ECS/Fargate prover task
- Step Functions terminal event と 30 分 sweep による semaphore janitor EventBridge trigger
- protected proof/report artifact bucket と lifecycle
- CloudWatch alarms、closed-field metric filter、dashboard、normalized operations ledger / query definition
- verification invariant 専用 SNS topic、分離された
ops-actionableSNS topic とその EventBridge route、それぞれの optional email subscription(routing 対象と構成条件は 可観測性設計 > 通知の境界) - runtime wiring に必要な non-secret SSM String、Lambda env、Terraform outputs
App root が所有しないもの:
- ECR repository creation、prover image build、image publication
- runtime secret value そのもの
- DNS zone ownership、Route 53 zone、ACM certificate issuance
- production deployment authorization や legacy AWS account resource
App root は foundation root が用意した repository name、manifest storage、secret/config reference、networking input などを参照します。 参照先を app root が作り直したり、別 account の値へ暗黙に fallback したりしないことが境界です。
Develop operator gate
Develop の operator/tester gate は tracked keyset manifest で generation と lifecycle を管理します。
App root は generation ごとに RSA_2048 / SIGN_VERIFY KMS key を作成し、導出した public key だけを CloudFront の trusted key group に公開します。
Keyset は issuer を必ず 1 つにし、trusted-retiring と detached を使って overlap と失効を明示します。
KMS private key material は export しません。
Terraform state、repository、pipeline artifact、smoke evidence に private signing material を含めず、plan/apply、post-apply smoke、target runtime の role に kms:Sign を与えないことが境界です。
Cookie issuance は Terraform とは分離した operator authority のみが KMS signing を実行します。
DynamoDB recovery selection
App saved-plan lane は session / vote table pair の recovery を次の 4 phase で明示します。
| Phase | Served table pair | Public admission / async consumption |
|---|---|---|
primary_active | primary | 通常状態 |
primary_quiesced | primary | session create と prover/verifier event source を停止 |
replacement_quiesced | replacement | session create と prover/verifier event source を停止 |
replacement_active | replacement | 通常状態 |
primary_active 以外は、target account / region、recovery point、table pair、schema、KMS key を検証済みの immutable recovery manifest に結びます。
Plan は exact manifest selection と checksum を evidence に残し、apply と post-apply smoke はその選択からの変更を拒否します。
Terraform の phase switch が行うのは runtime が参照する table pair の切り替えのみで、table の restore、record の copy、primary / replacement 間の reconciliation は別作業です。 Activation と switch-back は、対象 data が意図した resume state であることを別途検証した上で行います。
Foundation root
infra/terraform/foundation/ は独立した公開ページにはせず、この Terraform ページ内で target prerequisite ownership として扱います。
Foundation root は、app runtime の前に存在している必要がある authority を所有します。 対象は次の resource です。
- RISC Zero toolchain image と prover image の ECR repository
- Prover repository だけを対象とする ECR managed signing の Signer profile と registry signing configuration
- accepted / rollback / running / in-progress reference を保護する reference-aware ECR retention controller
- encrypted / versioned release authority storage
- Secrets Manager secret container と non-secret SSM config record
- prover networking input
- public hostname disposition record
Foundation root は、App root が管理する runtime resource を作りません。
Release authority storage の規則は、immutable record と versioned mutable pointer の 2 分類で表せます。
- Immutable record(artifact、promotion、deployment、rollback record)
- create-only write を必須とし、object と version の削除を bucket policy で拒否します。
- Prover の signing configuration は immutable かつ execution-bound な image-signing authority object として公開します。
- Toolchain repository は signing filter の対象外であり、その image は署名も cryptographic verification も行わず、digest-bound scan と immutable provenance を release authority として扱います。
- Versioned mutable pointer(Toolchain / Prover の accepted-channel pointer、Foundation の固定 key の networking handoff)
- versioned mutable object として扱います。ただし、初回作成または compare-and-swap 条件を伴わない書き込みは拒否します。
- networking handoff は固定 key の exact
VersionIdと body checksum を sidecar に記録し、app lane はこの exact publication を選択します(unversioned な latest object や再構築した networking facts は使いません)。
Foundation saved-plan lane の smoke は、Terraform output をそのまま downstream authority にはしません。 app lane は mutable tag や最新 object の暗黙の検索ではなく、accepted pointer または retained deployment record の versioned coordinates と checksum を release authority として使います。
ECR retention は count / age ベースの native lifecycle expiry ではなく、Foundation-owned の reference-aware controller が担います(保護対象と動作は Retention authority)。
Bootstrap root
infra/terraform/bootstrap/ は target account の Terraform state と deployment-control resource を所有します。
App runtime や foundation workload ではなく、それらを安全に plan/apply するための control plane を管理します。
Bootstrap root が管理するもの:
- Terraform remote state bucket、state KMS key、bucket policy、lockfile access
- app / foundation root 用の backend access grant
- CodePipeline / CodeBuild project、deployment role、retained authority bucket と lane ごとの execution bucket、log group
- reviewed saved plan、approval、apply、smoke、acceptance を結びつける deployment evidence
- root ごとの accepted Terraform baseline と report を保護する Terraform authority bucket / KMS key
- weekly drift coordinator、Bootstrap / Foundation / App の report-only runner、missed-run queue / schedule
- on-demand の rollback readiness / DynamoDB recovery readiness project
- GitHub trigger role など deployment-control に必要な IAM
Bootstrap root が所有しないもの:
- app runtime resource
- ECR repository、manifest storage、secret container、prover networking など foundation workload
- production bootstrap resource
- legacy AWS account resource や legacy state import
App saved-plan authority
App full lane は normal と rollback を明示的に分けます。
通常の App saved plan より前に、cryptographic image-signature verifier 専用の fail-closed 境界があります。
Source
-> conditional verifier state-refresh plan / approval / apply
-> verifier preflight plan / approval / apply
-> selected Prover digest の cryptographic verification
-> app deployment record / validation
-> App saved plan / conditional approval / exact-plan apply
-> static deploy / smoke / acceptance evidence
State refresh は中断された verifier publication、alias、provider state の allowlist 済み reconciliation だけを扱い、non-empty plan には独立した approval を要求します。 続く target-limited preflight は exact saved plan を適用し、選択された digest 固定 Prover を cryptographically verify できなければ App plan へ進みません。
Normal
- 新しい source SHA の
normal実行では、同じ control SHA の Foundation publication を先に完成させる必要があります。 - App lane は contract-compatible な accepted Prover と、その record が exact-link する Toolchain authority を選択します。
normalは current control source から immutable application artifact と deployment record を作り、検証済みの Foundation / Toolchain / Prover authority に結びます。
Rollback
rollbackは過去に accepted となった deployment record の exact key、version、checksum を必須とします。- verifier state reconciliation と preflight mutation は行わず、その record に結びつく historical approval、release authority、verifier authority、retained Lambda package 6 個 / static artifact を再検証して再利用します。
- Pipeline の current source は control source のままで、selected record が deployable source を与えます。
- Terraform plan が empty でも deployable artifact が変わる可能性があるため、
rollbackは常に approval を必要とします。
Approval
- Apply は plan stage が作成した exact saved plan だけを消費します。
- Develop の
normalでは、plan JSON が resource / output change、move、import のいずれもないことを machine-proven に確認できた場合だけ manual approval を skip できます。 - Parse failure、未解決の判定、non-empty plan は approval へ fail-closed します。
- protected production の apply は無条件に approval-gated です。
Acceptance evidence
- Post-apply smoke が成功した後、app lane は pipeline execution と acceptance attempt に結びつく immutable evidence を保存します。
- Evidence は approval または許可された empty-plan skip、verifier preflight、saved-plan / apply、release authority、recovery selection、retained deployable coordinates を結びます。
- Current accepted pointer を進める前に、その execution / attempt に結びつく immutable accepted Terraform baseline も公開します。App 用の 2 つ目の mutable baseline pointer は作りません。
- Immutable evidence は create-only とし、current accepted deployment pointer の
accepted-deployments/stark-dev/develop/app/current.jsonだけを conditional first-create / compare-and-swap で更新します。 - retired
index.jsonは authority として読み取りも変換も行いません。
Accepted baselines and report-only controls
Bootstrap、Foundation、App の successful smoke は、それぞれ 1 つの durable
accepted-terraform-baseline を公開します。
Baseline が immutable { key, versionId, sha256 } reference に結ぶのは次の項目です。
- exact target context と backend authority
- non-secret input と secret reference
- provider lock / runtime evidence
- reviewed plan checksum、acceptance、root 固有 smoke evidence
Terraform state、saved-plan binary、raw plan JSON、decrypted value、生成された sensitive value は保持しません。
Bootstrap と Foundation は root ごとの current-terraform-baseline pointer を
conditional first-create / compare-and-swap で更新します。App baseline は既存の current
accepted pipeline execution と acceptance attempt から一意に選択し、並行する mutable
pointer を追加しません。
Bootstrap-owned weekly coordinator は exact accepted baseline を選択し、source override なしで 3 つの root-specific runner を起動します。Runner の動作は次の順序です。
- accepted historical source を private workspace に materialize する。
- target / backend / input / provider lock / runtime / source を再検証する。
- refresh-enabled
terraform plan -lock=false -detailed-exitcodeを実行する。 no-drift、drift-detected、check-failedの bounded / redacted report だけを保持し、temporary state view、plan、full plan JSON は cleanup する。
Rollback readiness と DynamoDB recovery readiness は、別々の source-pinned / on-demand CodeBuild project です。
- 許可されるのは、current authority の exact read と、create-once encrypted readiness evidence の publication だけです。
- schedule / alarm trigger、Apply、state / pointer write、restore / table mutation、redrive、task stop、fault injection、exercise authorization は持ちません。
この節の仕組みはすべて報告と inspection のための境界です。 Baseline は再構成と inspection の authority にとどまり、後続の saved plan や mutation を承認しません。 Drift / readiness result も saved-plan input や remediation authority にはなりません。
Runtime wiring
Root 間の接続は、Terraform outputs、SSM reference name、artifact locator、image URI などの明示的な入力で表します。 公開仕様では、これらを「どの root が所有し、どの root が参照するか」という責務境界として扱います。
flowchart LR FOUND["foundation publications<br/>ECR / signing / manifests / references / networking"] BOOT["bootstrap outputs<br/>state + deployment control"] APP["app inputs and outputs<br/>runtime wiring"] RUN["target runtime<br/>frontend / API / queues / workers"] FOUND --> APP BOOT --> APP APP --> RUN
| Producer | Consumer | 情報例 |
|---|---|---|
| foundation | app pipeline | ECR repository identity、Prover signing authority publication、versioned release records / accepted pointers、secret/config references、exact networking handoff |
| bootstrap | managed lanes | backend config、deployment-control roles、saved-plan、retained / execution artifact、verifier-preflight / acceptance / Terraform baseline / report storage |
| app lane | target runtime | API URL、DynamoDB/S3/SQS/SFN/Lambda locator、frontend trust metadata、selected recovery phase |
Foundation release authority と bootstrap/app deployment evidence は同じものではありません。 Foundation は「どの image / application artifact と release record を選択できるか」を保持し、bootstrap-managed lane は「どの saved plan と approval で apply し、smoke と acceptance がどの結果になったか」を保持します。 App deployment は両方を checksum と versioned reference で結びます。
Secrets の値は public docs や tracked tfvars に置きません。 Public docs では account-specific ID、ARN、bucket name、digest-pinned image URI、secret value を placeholder または概念として扱います。
Retention authority
App と Bootstrap の storage retention はそれぞれの storage_retention.tf に固定し、pipeline input や operator の任意入力にはしません。
- App root:queue / DLQ message、proof/report object、static noncurrent version、runtime / Container Insights / operations-ledger log、全 5 DynamoDB table の point-in-time recovery を環境別 policy で管理します。
- Bootstrap root:state の noncurrent version、lane の execution artifact、App build cache、CodeBuild log group、redacted report の retention を管理します。Full App と static-fast は互いにアクセスできない private / versioned / KMS-encrypted transient execution bucket を使い、既存の App bucket は retained authority、deployable、approval / verifier evidence、cache 専用に保ちます。旧 transient object は copy や compatibility reader を追加せず、既存 lifecycle で失効させます。
- Foundation release storage:immutable record と accepted pointer は deletion を拒否し、retired
reissue/*namespace は新しい write も拒否します。 - Accepted Terraform baseline:通常の execution-artifact expiry から分離して durable に保持します。Drift / readiness report は環境別の固定期間で失効します。
- ECR image:native lifecycle policy を設定せず、Foundation-owned controller だけが reference-aware な削除を実行します。image の count や age だけでは accepted、rollback、running task、in-progress reference から外れたことを証明できないためです。
ECR retention controller の動作は次のとおりです。
- 日次で current Toolchain / Prover acceptance、retained App acceptance / rollback reference、live Prover task definition と、各 protected subject の referrer を解決する。
- 関連 pipeline、candidate build、finalization、Prover task が active な間は mutation を行わない。
- 2 回の同一 snapshot を確認してから referrer、subject の順で削除し、結果を再検証する。
Protected reference は環境ごとの retained target count より優先されます。 通常の Foundation Apply は ECR delete authority を持ちません。
IAM and guards
Terraform root は最小権限を前提に分離されています。
| 領域 | 境界 |
|---|---|
| State access | root ごとの backend key と lockfile object に限定 |
| App runtime Lambda | public-api、writer、dispatcher、worker、image-signature verifier ごとに分離 |
| Prover task | proof artifact prefix、ECR pull、CloudWatch Logs に限定 |
| Step Functions | 特定 ECS task definition、Lambda、logs、managed EventBridge rule に限定 |
| Deployment control | root と command class ごとの role に分離 |
| Report-only control | root-specific drift と 2 つの readiness role を Apply / mutation authority から分離 |
| Operator gate | Public key metadata のみ deployment から参照可能。signing authority は分離 |
Guard は、source branch、account alias、deployment environment、state scope、caller role、backend key の取り違えを検出するためのものです。 Public specs では guard の詳細な承認手順ではなく、責務分離と fail-closed 境界を説明します。
Version constraints
| ツール | バージョン |
|---|---|
| Terraform | active app / foundation / bootstrap root は = 1.15.8。shared child module と retained legacy root は >= 1.10.0 |
| AWS provider | active app / foundation / bootstrap root は = 6.58.0。shared child module と retained legacy root は ~> 6.0 |
| Archive provider | target app / foundation / bootstrap roots では未使用。retained legacy root のみ archive_file 用に解決 |
関連する章
- 現行構成とサービス一覧:root ごとの管理対象と runtime 構成
- 可観測性設計:app root が所有するログ、metric filter、alarm、ledger、notification
- イメージ署名:release authority と digest 固定イメージの関係
- AWS runtime 境界:公開仕様として扱う runtime 境界
API リファレンス
この部では、ブラウザクライアント、CLI、第三者検証で使う公開 API を、現行実装に基づいて説明します。
この部の章
- エンドポイント一覧: active browser / CLI API と owner-scoped tooling API のリクエスト/レスポンス仕様
- セッションライフサイクル: クライアントとサーバーのセッション管理実装
想定読者と前提
- 想定読者: ブラウザクライアントや第三者検証ツールから API を呼び出す実装者
- 前提: HTTP とセッションヘッダーの基本、および本書 全体像 のフローを把握していること
この部で扱わないもの
通常の browser / CLI contract は active route と capability 保護 API に限ります。 以下はこの部では扱いません。
- retired な debug / private inspection route(
GET /api/debug/enable、GET /api/botdata/:id)の詳細 - 置換済みの flat route(
POST /api/finalize/callback、GET /api/zkvm-input-hash)の詳細 - internal/operator-only callback(
POST /api/internal/sessions/:sessionId/finalizations/:finalizationId/callback)の protocol details - input commitment の private-data variant。active な
GET /api/sessions/:sessionId/finalizations/current/input-commitmentは hash-only で、includeDataquery を受理しません(詳細はエンドポイント一覧) - raw S3 URL / presigned URL を通常の browser / CLI contract として扱う取得経路。bundle/report の取得契約はエンドポイント一覧に委譲します
- レート制限、Turnstile、capability TTL などの環境変数チューニング
- API Gateway、Hono、Lambda 側の認可とルーティング実装
関連する章
- 検証パイプライン:
GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationが返す検証ペイロードの内訳 - 利用フロー: browser / CLI flow と bundle/report 取得経路
- 第三者検証ガイド:
bundle.zipの取得とローカル監査 - 用語集: capability トークンと session-scoped API の用語定義
エンドポイント一覧
この章は、ブラウザと CLI から利用する外部向け API の現行 contract を記載します。
パス、request/response schema、公開エラー、認証境界の authority は
packages/api-contract/src/routes/inventory.ts と packages/api-contract/src/schemas/ です。
内部 callback と過去の flat route は public API に含めません。
API runtime 構成
同じ route inventory と handler を、ローカルの static Hono server と AWS Lambda runtime から利用します。
| ランタイム | 用途 | 主な入口 |
|---|---|---|
| Static Hono server | ローカルと test API | scripts/dev/static-hono-server.ts |
| Hono on Lambda | AWS Lambda API | infra/lambdas/public-api/handler.ts |
以下のパスはすべて /api を base path とします。
Active API 一覧
共通 contract
セッション capability
POST /api/sessions 以外の active route は、次の組み合わせで session owner を特定します。
sessionId: URL path parameterX-Session-Capability: 必須 request header
X-Session-ID header は現行 contract に含まれません。finalizationId を含む route は、path の
finalizationId も current finalization authority と照合します。
共通の capability エラーは次のとおりです。
SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)SESSION_NOT_FOUND(404; session を読む route)
JSON success envelope
artifact 本文を返す route を除く public JSON success は、strict な envelope を使います。
{
"data": {},
"meta": {
"requestId": "request-id"
}
}
schema にない追加フィールドは受理しません。bulletin の pagination 情報だけは、page request 時に
meta.page として追加されます。
JSON error envelope
public route の標準エラーは HTTP status を本文に重複させず、次の形を使います。
{
"error": {
"code": "INVALID_REQUEST",
"message": "Invalid request",
"details": {
"kind": "validation",
"issues": {
"formErrors": [],
"fieldErrors": {}
}
}
},
"meta": {
"requestId": "request-id"
}
}
details は任意で、公開 schema が許可する tagged detail だけを返します。全 route に共通し得る
エラーは INVALID_REQUEST (400)、target origin 検証の SAME_ORIGIN_REQUIRED (403)、
予期しない handler failure の INTERNAL_ERROR (500) です。
ボディサイズ、Turnstile、レート制限
JSON body を持つ active route は API_REQUEST_BODY_LIMIT_BYTES(既定 16 KiB)の対象で、超過時は
PAYLOAD_TOO_LARGE (413) を返します。
| route | Turnstile | application rate limit |
|---|---|---|
POST /api/sessions | session action | client IP 単位 |
POST .../votes | vote action | session と client IP 単位 |
POST .../finalizations | finalize action | session と client IP 単位 |
POST .../cancel | なし | session と client IP 単位 |
POST .../verification | なし | session と client IP 単位 |
POST .../verification/observations | なし | 専用制限なし。API Gateway の既定 throttle を使用 |
Turnstile token は body の turnstileToken に置きます。production の bypass は認めず、session 作成の
develop 例外も operator-gated runtime に限定されます。
セッションと投票
POST /api/sessions
新規セッションと capability token を発行します。
request body:
turnstileToken(環境により必須)- body 省略時は空 object として扱います
response (200) の data:
sessionIdelectionIdelectionConfigHashlogIdcapabilityToken
主な固有エラー:
CAPTCHA_FAILED(403)GLOBAL_LIMIT_EXCEEDED(503)SESSION_LIMIT_EXCEEDED(503)
POST /api/sessions/:sessionId/votes
ユーザー投票を保存し、ボット投票を開始します。
request body:
commitment: 32-byte hexvote:A/B/C/D/Erand: 32-byte hexturnstileToken(環境により必須)
response (200) の data:
voteIdcommitmentbulletinIndexbulletinRootAtCastcastAtMs
主な固有エラー:
CAPTCHA_FAILED(403)ALREADY_VOTED(400)SESSION_FINALIZED(400)INVALID_COMMITMENT(400)DUPLICATE_VOTE(409)GLOBAL_LIMIT_EXCEEDED(503)
GET /api/sessions/:sessionId/progress
投票進捗を返します。
response (200) の data:
counttotalcompleteduserVotedfinalizeddistribution(任意。A-Eの simulated count)distributionKind(任意。存在する場合はsimulated)updatedAtMs(任意)animationSeed(任意)
Finalization API
Finalization resource
作成、current 取得、キャンセルは同じ finalization resource schema を返します。共通フィールドは次のとおりです。
finalizationIdstatus:pending/running/succeeded/failed/timeoutqueuedAtMsruntimeDiagnosticsasyncMode:enabled/disabledqueue(任意またはnull)progress(任意。running phase の派生進捗)orchestrator(任意またはnull)
status ごとの追加フィールド:
| status | 追加フィールド |
|---|---|
pending | なし |
running | startedAtMs |
succeeded | startedAtMs, completedAtMs, result |
failed | startedAtMs(任意), failedAtMs, error.code, error.message |
timeout | startedAtMs(任意), timeoutAtMs |
succeeded の result は、authority と presentation を混在させず次の group に分けます。
authority:imageId,journalと、任意のreceipt,electionManifest,closeStatementpresentation: tally、bitmap root、input commitment、verification status などの公開表示 projectionvoterEvidence:state=availableまたはstate=verification-gatedartifactAvailability:bundle,report,includedBitmap,seenBitmapごとのavailable/unavailable。bitmap のavailableは current finalization に durable に admit された sidecar があることを示す
POST /api/sessions/:sessionId/finalizations
集計と証明生成を開始します。
request body:
scenarioId:S0-S5turnstileToken(環境により必須)
response:
200: 同期処理後の finalization resource202: 受理された非同期 finalization resource
どちらも { data: FinalizationResource, meta } です。返さない旧フィールドは
現行 response で返さない legacy フィールドにまとめています。
主な固有エラー:
CAPTCHA_FAILED(403)USER_NOT_VOTED(400)VOTING_NOT_COMPLETE(400)SESSION_ALREADY_FINALIZED(400)INVALID_IMAGE_ID(400)VERIFICATION_FAILED(400)ZKVM_RATE_LIMIT_EXCEEDED(429)GLOBAL_LIMIT_EXCEEDED(503)INVALID_FINALIZATION_ARTIFACT(500)
GET /api/sessions/:sessionId/finalizations/current
current finalization を返します。まだ finalization がない場合も成功で、data は null です。
存在する場合は上記の finalization resource を返します。
POST /api/sessions/:sessionId/finalizations/:finalizationId/cancel
path で指定した進行中 finalization をキャンセルします。
request body:
reason(任意、最大 256 文字)- body に
executionIdやfinalizationIdは置きません
response (200) はキャンセル後の finalization resource です。
主な固有エラー:
ASYNC_FINALIZATION_DISABLED(404)FINALIZATION_NOT_CANCELLABLE(409)CANCELLATION_UNSUPPORTED(501)GLOBAL_LIMIT_EXCEEDED(503)
検証 API
GET /api/sessions/:sessionId/finalizations/:finalizationId/verification
指定 finalization の検証 resource を返します。
query:
include=journal(任意)
response (200) の data は availability による strict union です。
availability=available:
finalizationIdproofAuthority: election identity、bulletin root、Image ID、verified tally、bitmap root、input commitment などpresentation: tally、scenario、tamper 情報と fail-closed presentation policyverifierResult:statusと任意の公開reportverification:checksとstepsvoterEvidence:availability=availableまたはavailability=unavailablejournal:representation=includedのvalue、またはrepresentation=omitted
availability=unavailable:
finalizationIdreason:finalization_not_current/user_vote_unavailablemessage
availability=corrupt:
finalizationIdreason:invalid_artifact/incomplete_session_authority/session_identity_mismatch/voter_evidence_mismatchmessage
unavailable/corrupt response に available 用の authority や検証フィールドは混在しません。
主な固有エラー:
SESSION_NOT_FINALIZED(400)USER_NOT_VOTED(400)
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification
サーバー側の STARK receipt 検証を実行します。
- request body: strict な空 object
{} - response (
200) のdata:verificationStatusfinalizationIdestimatedDurationMsidempotent
主な固有エラー:
SESSION_NOT_FINALIZED(400)ZKVM_RATE_LIMIT_EXCEEDED(429)GLOBAL_LIMIT_EXCEEDED(503)
POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations
browser-local の cast check 結果だけを durable observation として記録します。client が overall verdict や server-side check を指定する API ではありません。
request body の castChecks は、次の 4 キーをすべて含む strict object です。各値は
success / failed / not_run のいずれかです。
cast_receipt_presentcast_choice_rangecast_random_formatcast_commitment_match
finalization identity は path で指定するため、body に execution/finalization ID は含めません。
response (200) の data.status:
recorded: 初回記録idempotent: 同一内容の再送
主な固有エラー:
VERIFICATION_OBSERVATION_CONFLICT(409; 異なる内容による再送)SESSION_NOT_FINALIZED(400)USER_NOT_VOTED(400)
Artifact 取得 API
両 route とも X-Session-Capability を要求し、session と finalization identity を検証してから artifact を
返します。通常 contract として raw S3 URL や presigned URL は公開しません。
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/bundle
秘密データを含まない配布対象 bundle.zip を返します。
200: ZIP binary206:Rangeに対する partial content- 大きい S3-backed bundle は range download が必須
主な固有エラー:
INVALID_BUNDLE_REFERENCE(400)BUNDLE_NOT_FOUND(404)BUNDLE_DOWNLOAD_FAILED(500)BUNDLE_REQUIRES_RANGE_DOWNLOAD(413)INVALID_RANGE(416)
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/report
protected report artifact verification.json を JSON 本文として返します。これは配布対象 bundle.zip には
含まれません。
主な固有エラー:
INVALID_BUNDLE_REFERENCE(400)REPORT_NOT_FOUND(404)REPORT_DOWNLOAD_FAILED(500)REPORT_DOWNLOAD_TOO_LARGE(413)
Bulletin と証明 API
GET /api/sessions/:sessionId/bulletin
owner-scoped bulletin inspection を返します。
query:
offset(任意、0 以上の整数)limit(任意、1-1000)
response (200):
data.commitmentsdata.bulletinRootdata.treeSizedata.generatedAtMsdata.rootHistory(任意)meta.page.nextOffset,meta.page.hasMore(pagination request 時)
主な固有エラー:
INVALID_OFFSET(400)
そのほかの不正な query は INVALID_REQUEST (400) です。
GET /api/sessions/:sessionId/bulletin/consistency-proof
RFC 6962 consistency proof を返します。
query:
fromTreeSize(必須、0 以上の整数)toTreeSize(必須、0 以上の整数)
response (200) の data:
fromTreeSizetoTreeSizerootAtFromTreeSizerootAtToTreeSizeproofNodesoldSubtreeHashes/appendSubtreeHashes(任意)generatedAtMs
主な固有エラー:
INVALID_SIZE(400)
GET /api/sessions/:sessionId/votes/:voteId/inclusion-proof
指定した投票の最小包含証明を返します。session は finalized である必要があり、proof ownership と finalization authority を検証します。
response (200) の data:
voteIdproof.leafIndexproof.treeSizeproof.merklePathproof.bulletinRootAtCast
主な固有エラー:
INVALID_VOTE_ID(400)VOTE_NOT_FOUND(404)VERIFICATION_FAILED(400; CT proof unavailable)SESSION_NOT_FINALIZED(400)
GET /api/sessions/:sessionId/finalizations/current/bitmap-proofs/:kind/:voteIndex
succeeded current finalization に admit された bitmap evidence から proof 材料を返します。
path parameter:
kind:included/seenvoteIndex: 0 以上の整数
response (200) の data:
leafChunkauditPath[]:hash,position(left/right)
If-None-Match を受理し、一致時は 304 を返します。
主な固有エラー:
INVALID_BITMAP_PROOF_REQUEST(400)USER_NOT_VOTED(400)
GET /api/sessions/:sessionId/bulletin/sth
finalized session の STH snapshot を返します。
query:
auditorId(任意、1-64 文字の英数字、.、_、-)
browser request は same-origin である必要があります。CLI/server-to-server request でも session capability は 必須です。
response (200) の data.sth:
sthDigestbulletinRoottreeSizetimestamplogId
主な固有エラー:
SESSION_NOT_FINALIZED(404)SAME_ORIGIN_REQUIRED(403; 同一 origin 検証失敗)
GET /api/sessions/:sessionId/finalizations/current/input-commitment
current finalization の zkVM input commitment だけを返します。private input data、raw witness、vote opening は 返しません。
response (200) の data:
inputCommitment
主な固有エラー:
SESSION_NOT_FINALIZED(400)CT_PROOF_UNAVAILABLE(400)INTERNAL_ERROR(500; commitment validation または handler failure)
Retired route と internal surface
過去の flat route(/api/session、/api/vote、/api/finalize、/api/verify、
/api/verification/bundles/...、/api/bulletin、/api/sth、/api/bitmap-proof、
/api/zkvm-input-hash など)は route inventory に登録されず、compatibility alias もありません。
finalization worker callback は internal/operator-only surface であり、public route inventory、browser CORS、 この章の request/response contract の対象外です。debug route と private inspection route も active external API として扱いません。
現行 response で返さない legacy フィールド
現行 finalization、verification、artifact response は次の旧フィールドを返しません。
- artifact URL:
verificationBundleUrl,verificationReportUrl - S3 metadata:
s3BundleUrl,s3BundleKey,s3UploadedAt,s3BundleExpiresAt - count alias:
missingIndices,invalidIndices,countedIndices,excludedCount - 旧 finalization locator/state:
executionId,statusUrl,state,finalizationState
bundle と report は finalization-scoped artifact route から capability 保護付きで取得します。
関連する章
- セッションライフサイクル: セッション、capability、finalization の状態遷移
- チェック一覧: verification checks の意味
- 公開境界: public artifact と owner-scoped access の区別
- 用語集: receipt、bundle、STH、capability など
セッションライフサイクル
このページでは、セッション管理の実装をクライアント側とサーバー側に分けて説明します。
管理責務の分離
| 管理面 | 主な保存先 | 主な責務 |
|---|---|---|
| クライアント共有 | localStorage (starkBallotSession) | 画面遷移フェーズ、クライアント TTL、検証継続状態、UI 復元、保存 shape の strict admission |
| クライアントタブ単位 | sessionStorage (starkBallotSessionLock) | タブごとの session identity lock、stale tab の fail-closed |
| サーバー | VoteStore 実装(Mock/File/DynamoDB) | 投票データ、掲示板、集計結果、検証結果、検証観測メタデータ |
クライアントとサーバーのセッション対応付けには sessionId と X-Session-Capability(署名トークン)が使われます。
ヘッダー、path、query の使い分けはエンドポイント一覧の共通 contractを参照してください。
保存状態の strict admission
admitStoredSessionData() は、サーバーが発行した identity、lifetime、phase、完全な
private opening / cast receipt のフィールド群、canonical な finalizeResult を含む現行 shape だけを
受理します。未知フィールド、欠落したフィールド群、phase と finalization 状態の不一致は
fail-closed です。
読み取り時に保存値が受理できなければ、starkBallotSession、stark-ballot-knowledge、
starkBallotSessionLock をクリアします。書き込み時に受理できない状態は保存せず、
Invalid browser session state として失敗します。version key や旧 shape への移行 fallback はありません。
クライアント側フェーズ
クライアントセッション(apps/web/src/session/client.ts)のフェーズは以下の 3 つです。
votingfinalizingverifying
ここでの「canonical な finalizeResult」とは、現行契約で受理可能な集計スナップショットを指します。
主な遷移トリガー
| トリガー | 動作 |
|---|---|
POST /api/sessions の成功 | initializeSession({ sessionId, capabilityToken, electionId, electionConfigHash, logId }) が voting を開始 |
aggregate 画面で非同期集計の pending または running を検知 | identity-scoped helper が phase: 'finalizing' を保存 |
aggregate 画面または result 画面で canonical な finalizeResult を保存 | identity-scoped helper が phase: 'verifying' へ進める |
/result から /verify へ進む | verificationRequestedAt を保存し、必要に応じて POST /api/sessions/:sessionId/finalizations/:finalizationId/verification を先行起動 |
/verify を開く | 下記の継続判定で進行可否を決定 |
/verify の検証シーケンス完了(pending check なし、サーバー検証済みの STARK 状態が terminal な success / failed / dev_mode) | POST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations を best-effort で送信 |
/verify の継続判定(フロー視点の説明は 設計と実行フロー を参照):
verificationRequestedAtと canonical なfinalizeResultの両方がそろっていれば継続扱い(hasContinuationAuthority)- 上記がなくても、サーバー返却の STARK 状態が
not_run以外なら進行できる hasContinuationAuthority不成立かつ STARK がnot_runの場合はブロックする
クライアント TTL 実装
SESSION_PHASE_TIMEOUTS_MS:
voting: 30 分finalizing: 30 分verifying: 24 時間
TTL 更新箇所
initializeSession(...)は新規セッション作成時にexpiresAtを設定します。saveSessionData(...)とsaveSessionDataForIdentity(...)はフェーズを加味してexpiresAtを再計算します。updateLastActivity(...)とupdateLastActivityForIdentity(...)は現在フェーズでexpiresAtを再延長します。
期限切れ判定
checkTimeout()、getSessionData*()、saveSessionData*()、updateLastActivity*() は有効期限超過を検出すると clearSession() を実行します。
検証画面での延長と fail-closed admission
専用の heartbeat API はありません。
verify 画面では、クライアントが 60 秒間隔で updateLastActivityForIdentity() を呼び出し、ローカル TTL を延長します。
phase: 'verifying' は canonical な finalizeResult を必須とし、verificationRequestedAt は非負の
整数 timestamp だけを許可します。不正な保存状態をフィールド削除や voting への巻き戻しで
修復することはありません。
サーバー側の lifecycle-owned records
サーバーは一つの optional-field aggregate を永続化しません。
| Record family | Authority |
|---|---|
SessionIdentityRecord | required sessionId / election config・hash / electionId / logId / creation time |
SessionVotingRecord | votes、append-only bulletin、root history、user participation、bot count、last activity |
FinalizationRecord | scenario context、state、成功時だけ required な canonical result |
VerificationResultRecord | session と同じ finalizationId に scope された検証結果 |
| verification observation | browser が表示した cast-check 状態の非 authority な冪等観測 |
| AWS runtime/delivery metadata | S3 version/key、Step Functions など adapter-owned metadata |
DynamoDB 上で各 record family に対応する key layout は トポロジーの Data レイヤを参照してください。
Application use case は必要な record を SessionReadModel に合成できますが、その
shape を persistence authority として保存しません。欠落 record や nested
finalizationId の不一致は fail-closed であり、read path は identity や
finalization branch を生成・修復しません。
状態遷移 policy と永続化
finalization と session mutation の semantic decision は application の transition policy/coordinator が所有します。現在状態と command から applied、idempotent、 stale/rejected、fail-closed の結果を決め、adapter はその結果を lock、transaction、 conditional write など各 backend の atomic persistence へ変換します。
Mock、File、Dynamo の各実装は独自の状態遷移表を持たず、I/O、serialization、 TTL、encryption、storage index、delivery metadata に集中します。この責務分離は 特定時点の store method 数や optional field 数には依存しません。
FinalizationRecord.state.status は以下を取り得ます。
pendingrunningsucceededfailedtimeout
検証観測メタデータ
verification observation record は、現在の finalizationId に対応する最初のブラウザ観測を
finalizationId、fingerprint、observedAt で記録します。同じ finalization と
fingerprint の再送は冪等に受理し、finalization または fingerprint が競合する送信は
拒否します。
この記録は、ブラウザで表示された verdict の観測、observability event の重複送信防止、および 送信された限定的な cast check 状態からの observability 用 summary 導出に使う冪等マーカーであり、 その送信の成否や保存によって proof、集計結果、検証結果、ユーザー向け verdict は変わりません。
サーバー側 TTL と失効の実装差分
サーバー側の失効挙動はストア実装で異なります。
| ストア | 失効/TTL の実装 |
|---|---|
MockSessionStore | getActiveSessionCount() 呼び出し時に lastActivity から 5 分超を掃除 |
FileMockSessionStore | getActiveSessionCount() 呼び出し時に同様に 5 分超を掃除 |
DynamoSessionStore | DynamoDB TTL 属性を保存。live session TTL と finalization / verification artifact TTL を runtime config から反映 |
DynamoDB TTL の決定
セッション作成は SESSION_LIVE_TTL_SECONDS で identity と voting の期限を初期化します。
以後の activity / vote mutation は identity、voting、既存・新規 vote record の期限を
max(現在の期限, 現在時刻 + live TTL) に揃え、一つの conditional transaction で更新します。
競合や欠落時は部分更新を残しません。finalization lifecycle の applied transition は全 session record を
VERIFICATION_ARTIFACT_TTL_SECONDS へ昇格し、以後の live activity もその期限を短縮しません。
finalization / verification の artifact、observation、adapter metadata も artifact TTL で保存されます。
getActiveSessionCount() がセッション作成上限用に行う 5 分の最終 activity 判定は、
DynamoDB レコードの物理 TTL とは別の判定です。
セッション作成上限
POST /api/sessions は MAX_SESSIONS を参照します。
上限到達時は SESSION_LIMIT_EXCEEDED を返します。
セッションヘッダーのスコープ
POST /api/sessions 以外の session-scoped API は、path の :sessionId と X-Session-Capability で
session owner を特定します。エンドポイントごとの要否と共通エラーは
エンドポイント一覧の共通 contractを参照してください。
マルチタブ時の分離
localStorage は同一オリジンで共有されます。
現行実装は sessionStorage の tab lock を併用し、別タブがセッションを差し替えたら stale tab を fail-closed にします。
代表的な結果
- 片方のタブで投票済み後、別タブで再投票すると
ALREADY_VOTEDになります。 - 片方のタブで集計完了後、別タブで再集計すると
SESSION_ALREADY_FINALIZEDになります。 - 別タブでセッションが差し替えられた場合、aggregate、result、verify、bot progress を開いている stale tab は進行を停止します。
- セッション作成を並行すると
starkBallotSession自体は共有更新されます。 - 先に開いていたタブは
starkBallotSessionLockと不一致になり、継続利用できません。
第三者検証ガイド
この章は、検証ページでダウンロードした bundle.zip と対応する公開 repository snapshot を使い、第三者がローカルに行える最小監査手順をまとめます。
/verify 画面の最終判定を完全に再現する手順ではなく、下表の不変条件を確認する手順です。
公開 snapshot に関する注意 すべての Step には、検証対象リリースと対応する公開 repository snapshot が必要です。 加えて、ZIP の admission と展開を行う Step 3 以降には、ダウンロード済み ZIP が必要です。
bundle.zip 単体では揃わないもの(検証材料として ZIP に入らないもの):
GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationが返す claimed tally (presentation.tally)、verification.checks、verification.steps- 投票者端末に残る投票意図、乱数、投票レシート
/verifyの Recorded-as-Cast 判定に使う session-scoped な自票 receipt / root-at-cast proof state と、サーバー側の整合性評価結果- 自票 inclusion 用のビットマップ証明
- 有効化されている場合の第三者 STH ソース照合
public-input.json には zkVM 入力として提示された各投票の index、commitment、Merkle path が含まれます。
この範囲は PoC の設計意図に沿っています。
配布対象アーカイブ の構成も参照してください。
この部の章
- ZIP ローカル検証(Ubuntu):
bundle.zipを取得した第三者が Ubuntu 上で実行できる最小監査手順
想定読者と前提
- 想定読者:配布された
bundle.zipを独立にローカル監査したい第三者 - 前提:Ubuntu 系 Linux と
jq、unzipなどの基本 CLI、対応する公開リポジトリ snapshot へのアクセス。 詳細は はじめに を参照してください。
この部で扱わないもの
/verifyUI 最終判定の完全再現と、投票者端末のローカル証跡を使った Cast-as-Intended 検証(必要な材料は上の「bundle.zip単体では揃わないもの」を参照)- AWS インフラのデプロイと運用手順
関連する章
この章は bundle.zip のローカル監査に絞ります。
範囲外の作業は次のページを参照してください。
- チェック一覧:チェック ID と判定ロジック
- API エンドポイント一覧:手動検証に使う API 契約
- 検出メカニズム:改ざんシナリオごとの失敗パターン
- 非同期プローバー:非同期 finalize の処理と障害調査導線
- バンドル構造:配布対象アーカイブの公開可能なアーティファクトと非公開アーティファクト
- 用語集:「検証」と「監査」の使い分けほか
最低限確認する不変条件
| 項目 | 合格条件 |
|---|---|
| STARK レシート | verifier-service verify が status: "success" |
| receipt と journal の結合 | receipt 内の journal bytes を現行 contract で decode した全 proof-bound field が journal.json と一致 |
| 投票の除外有無 | excludedSlots == 0 かつ missingSlots == 0 かつ invalidPresentedSlots == 0 |
| 期待投票数整合 | totalExpected == treeSize |
| 処理投票数 | totalVotes > 0 |
| 集計合計整合 | journal.json の verifiedTally の合計が validVotes と一致 |
| 公開入力の基本整合性 | public-input.json が現行 contract に沿い、入力数、root、重複検査が成立 |
| 公開監査アーティファクト | election-manifest.json と close-statement.json の自己整合と相互整合が成立 |
| 入力整合性 | inputCommitment の再計算値が journal.json と一致 |
ZIP ローカル検証(Ubuntu)
この手順は、検証ページでダウンロードした bundle.zip を対象に、Ubuntu 上で第三者が行える最小監査のガイドです。
確認できるのは 第三者検証ガイド の不変条件表にある範囲であり、/verify UI の総合 Verified 判定の代替ではありません。
各 Step の概要は次のとおりです。
| Step | 確認内容 | 必要ツール |
|---|---|---|
| 1 | Ubuntu セットアップ(Rust toolchain の導入) | apt、rustup |
| 2 | production-feature verifier-service の build | Rust toolchain |
| 3 | bundle.zip の admission と展開 | Node.js / pnpm、unzip |
| 4 | 期待 Image ID の決定 | Node.js / pnpm、jq |
| 5 | STARK レシートの検証 | Step 2 の verifier-service、jq |
| 6 | receipt と journal.json の結合・完全性チェック | Node.js / pnpm、jq |
| 7 | 公開監査アーティファクトの整合性チェック | Node.js / pnpm |
| 8 | inputCommitment 再計算 | Node.js / pnpm、jq |
0. 前提
ツール要件:
- Ubuntu 22.04 / 24.04
- Node.js 24 と Corepack 経由の pnpm 11.x
実行済みであること:
- 検証ページから
bundle.zipをダウンロード済みであること - このリポジトリ(
stark-ballot-simulator)のソースを取得済みであること $REPO_ROOTでcorepack enableとpnpm install --frozen-lockfileを実行済みであること
必要になる Step:
- Node.js / pnpm 前提は Step 3、Step 4、Step 6、Step 7、Step 8 で必要(Step ごとの必要ツールは冒頭の概要表を参照)
手順の前提(ソース取得やビルドが必要なステップ)は、リポジトリが公開されるまで実行できません。 詳細は 第三者検証ガイド を参照してください。
bundle.zip の通常の取得経路は capability 保護 API であり、raw S3 URL や presigned URL は通常のブラウザと CLI 契約ではありません。
public の意味と境界は 公開境界、現行レスポンスに含まれない旧 URL フィールドの扱いは API エンドポイント一覧 を参照してください。
以降の手順では、リポジトリルートを REPO_ROOT として扱います。
実際のクローン先に合わせて先に設定してください。
export REPO_ROOT="$HOME/stark-ballot-simulator"
export AUDIT_ROOT="$HOME/stark-audit"
cd "$REPO_ROOT"
1. Ubuntu セットアップ(Rust)
sudo apt update
sudo apt install -y build-essential pkg-config libssl-dev unzip jq curl ca-certificates
curl https://sh.rustup.rs -sSf | sh -s -- -y
source "$HOME/.cargo/env"
RUST_CHANNEL="$(awk -F'\"' '/^channel *=/ {print $2}' "$REPO_ROOT/rust-toolchain.toml")"
rustup toolchain install "$RUST_CHANNEL"
rustup default "$RUST_CHANNEL"
echo "rust_channel=$RUST_CHANNEL"
rustc --version
cargo --version
2. verifier-service をビルド
cd "$REPO_ROOT/verifier-service"
cargo build --release --features production
この監査では production-feature build を使用します。production feature は
risc0-zkvm/disable-dev-mode を有効にし、production STARK proof の検証境界と一致させます。
fake receipt を診断できる dev-mode-capable build(cargo build --release)は、この手順の合格証拠には使用しません。
生成物:
verifier-service/target/release/verifier-service
3. bundle.zip を admission して展開
mkdir -p "$AUDIT_ROOT"
cp ~/Downloads/stark-ballot-verification-*.zip "$AUDIT_ROOT/bundle.zip"
cd "$REPO_ROOT"
pnpm tsx -e "
import { inspectVerificationBundleArchiveFile } from './packages/node-adapters/src/verification-bundle';
import { resolvePublicBundlePolicy } from './packages/verification/src/verification/public-bundle-manifest';
const archivePath = process.argv[1];
void (async () => {
const failures = [];
for (const mode of ['sync', 'async']) {
try {
const inspection = await inspectVerificationBundleArchiveFile(
archivePath,
resolvePublicBundlePolicy(mode),
);
console.log(JSON.stringify({ bundle_manifest: 'ok', mode, members: inspection.members }, null, 2));
return;
} catch (error) {
failures.push({ mode, error: error instanceof Error ? error.message : String(error) });
}
}
console.error(JSON.stringify({ bundle_manifest: 'ng', failures }, null, 2));
process.exitCode = 1;
})();
" "$AUDIT_ROOT/bundle.zip"
if [ "$?" -ne 0 ]; then
echo 'bundle manifest admission failed'
exit 1
fi
cd "$AUDIT_ROOT"
unzip -o bundle.zip -d bundle
ls -1 bundle
admission は、mode-aware な default-deny manifest に従って次を検査します。
- sync bundle: 下記 5 ファイルと
metadata.jsonが必須。sth.jsonとconsistency-proof.jsonだけが任意 - async bundle: 下記 5 ファイルだけが必須かつ許可対象
- 両 mode: 必須 member の欠落、未登録または非公開 member、duplicate、case conflict、traversal・absolute・非 canonical path は不合格
両 mode で共通して必要なファイルは次のとおりです。
bundle/receipt.jsonbundle/journal.jsonbundle/public-input.jsonbundle/election-manifest.jsonbundle/close-statement.json
metadata.json は sync bundle では必須で、async bundle には含まれません。
bundle_manifest="ok" が出力されるまで ZIP を展開せず、admission が失敗した ZIP を以降の証拠として扱わないでください。
4. 期待 Image ID を決定
Step 4 では public/imageId-mapping.json から、verifier-service に渡す期待 Image ID を決めます。
variant は検証対象の成果物に合わせて選びます(trust material の対応は Image ID > variant とアーキテクチャ を参照)。
| 検証対象の成果物 | variant 指定 |
|---|---|
| target-account prover image(default / ARM64) | 未指定または EXPECTED_IMAGE_ID_VARIANT=default |
| local / CI production-feature build(x86_64) | EXPECTED_IMAGE_ID_VARIANT=x86_64 |
アプリ側と同様に、次のいずれかに該当する場合は暗黙のフォールバックを行わず fail-closed で停止します。
journal.jsonのmethodVersionがCURRENT_METHOD_VERSIONと一致しない- mapping が欠落している、または unsupported variant を指定した
- 選択した variant の Image ID が欠落している
- 選択した authority が
unconfirmed(別 variant へフォールバックしない)
METHOD_VERSION="$(jq -r '.methodVersion' bundle/journal.json)"
CURRENT_METHOD_VERSION="$(awk -F'= ' '/export const CURRENT_METHOD_VERSION/ {print $2; exit}' "$REPO_ROOT/packages/zkvm-contract/src/zkvm/types.ts" | tr -d ';[:space:]')"
if [ "$METHOD_VERSION" != "$CURRENT_METHOD_VERSION" ]; then
echo "methodVersion=$METHOD_VERSION is not the current supported contract ($CURRENT_METHOD_VERSION)"
exit 1
fi
export EXPECTED_IMAGE_ID_VARIANT=default
EXPECTED_IMAGE_ID="$(
node "$REPO_ROOT/verifier-service/scripts/read-image-id.mjs" \
"$REPO_ROOT/public/imageId-mapping.json"
)"
RECEIPT_IMAGE_ID="$(jq -r '.image_id // .imageId // .receipt.image_id // .receipt.imageId // empty' bundle/receipt.json | tr '[:upper:]' '[:lower:]')"
if [ -z "$EXPECTED_IMAGE_ID" ]; then
echo "expected Image ID could not be resolved from imageId-mapping.json"
exit 1
fi
echo "methodVersion=$METHOD_VERSION"
echo "imageIdVariant=$EXPECTED_IMAGE_ID_VARIANT"
echo "receiptImageId=$RECEIPT_IMAGE_ID"
echo "expectedImageId=$EXPECTED_IMAGE_ID"
local / CI production-feature build の成果物を検証する場合だけ、上の export を次のように置き換えます。
export EXPECTED_IMAGE_ID_VARIANT=x86_64
verifier-service/scripts/read-image-id.mjs は EXPECTED_IMAGE_ID_VARIANT=default または未指定の場合に variants.default、EXPECTED_IMAGE_ID_VARIANT=x86_64 の場合に variants.x86_64 を解決します(fail-closed 条件は本 Step 冒頭のリスト参照)。
verifier-service 本体は EXPECTED_IMAGE_ID_VARIANT を直接読みません。
Step 5 では、ここで導出した EXPECTED_IMAGE_ID を --image-id として渡します。
receipt.json の Image ID が期待値と一致しない場合は Step 5 で不合格になります。
5. STARK レシートを検証
"$REPO_ROOT/verifier-service/target/release/verifier-service" verify \
--bundle ./bundle.zip \
--image-id "$EXPECTED_IMAGE_ID" \
--output ./verification.json
echo "exit_code=$?"
jq '{status, expected_image_id, receipt_image_id, dev_mode_receipt, errors}' ./verification.json
判定:
exit_code=0かつstatus="success": 合格exit_code=2またはstatus="dev_mode": dev-mode のフェイクレシート(production STARK proof ではなく、本番検証としては不合格)exit_code=3またはstatus="failed": 不合格
6. receipt と journal.json の結合・完全性チェック
Step 5 の verifier-service は receipt.json 内の STARK receipt を検証しますが、ZIP 内で別ファイルになっている
journal.json との一致までは検証しません。そのため、最初に receipt 内の journal bytes を現行 contract で
decode し、journal.json の全 proof-bound field と一致することを確認します。
cd "$REPO_ROOT"
pnpm tsx -e "
import fs from 'node:fs';
import { parseJournalBytes } from './packages/verification/src/verification/journal-parser';
const [receiptPath, journalPath] = process.argv.slice(1);
const receiptPayload = JSON.parse(fs.readFileSync(receiptPath, 'utf-8'));
const journal = JSON.parse(fs.readFileSync(journalPath, 'utf-8'));
const journalBytes = receiptPayload?.receipt?.journal?.bytes ?? receiptPayload?.journal?.bytes;
if (
!Array.isArray(journalBytes) ||
!journalBytes.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)
) {
console.error('receipt journal bytes are missing or invalid');
process.exit(1);
}
const receiptJournal = parseJournalBytes(journalBytes);
const hexFields = new Set([
'electionConfigHash',
'bulletinRoot',
'sthDigest',
'seenBitmapRoot',
'includedBitmapRoot',
'inputCommitment',
]);
const proofBoundFields = [
'electionId',
'electionConfigHash',
'bulletinRoot',
'treeSize',
'totalExpected',
'sthDigest',
'verifiedTally',
'totalVotes',
'validVotes',
'invalidVotes',
'seenIndicesCount',
'missingSlots',
'invalidPresentedSlots',
'rejectedRecords',
'seenBitmapRoot',
'includedBitmapRoot',
'excludedSlots',
'inputCommitment',
'methodVersion',
];
const normalizeHex = (value) => String(value).replace(/^0x/i, '').toLowerCase();
const mismatches = proofBoundFields.filter((field) => {
const receiptValue = receiptJournal[field];
const journalValue = journal[field];
if (hexFields.has(field)) {
return (
typeof receiptValue !== 'string' ||
typeof journalValue !== 'string' ||
normalizeHex(receiptValue) !== normalizeHex(journalValue)
);
}
return JSON.stringify(receiptValue) !== JSON.stringify(journalValue);
});
console.log(JSON.stringify({ receipt_journal_matches: mismatches.length === 0, mismatches }, null, 2));
process.exit(mismatches.length === 0 ? 0 : 1);
" \
"$AUDIT_ROOT/bundle/receipt.json" \
"$AUDIT_ROOT/bundle/journal.json"
echo "exit_code=$?"
cd "$AUDIT_ROOT"
判定:
exit_code=0かつreceipt_journal_matches=true: receipt で暗号学的に検証された journal とjournal.jsonが一致exit_code!=0またはmismatchesが空でない: 不合格。以降のjournal.jsonを proof-bound evidence として扱わない
結合を確認した後、Counted 段階の count と tally の必須条件を確認します。
jq '{excludedSlots, missingSlots, invalidPresentedSlots, rejectedRecords, totalExpected, treeSize, totalVotes, validVotes, verifiedTally}' bundle/journal.json
jq -e '.excludedSlots == 0 and .missingSlots == 0 and .invalidPresentedSlots == 0' bundle/journal.json >/dev/null \
&& echo 'integrity_counts=ok' \
|| echo 'integrity_counts=ng'
jq -e '.totalExpected == .treeSize' bundle/journal.json >/dev/null \
&& echo 'expected_vs_tree=ok' \
|| echo 'expected_vs_tree=ng'
jq -e '.totalVotes | type == "number" and . > 0 and floor == .' bundle/journal.json >/dev/null \
&& echo 'total_votes_positive=ok' \
|| echo 'total_votes_positive=ng'
jq -e '(.verifiedTally | add) == .validVotes' bundle/journal.json >/dev/null \
&& echo 'tally_sum=ok' \
|| echo 'tally_sum=ng'
ng が 1 つでも出力された場合は、現行の必須チェックを満たしていないため検証失敗として扱います。
7. 公開監査アーティファクトの整合性チェック
public-input.json、election-manifest.json、close-statement.json は bundle.zip に含まれる Counted 段階の必須チェック対象です。
次の 4 点を確認します(フィールド単位の詳細はスクリプト内の checks 参照)。
public-input.jsonが現行 contract に沿い、vote entry、重複 index、commitment、journal.jsonの各フィールドと矛盾しないelection-manifest.jsonのelectionConfigHash再計算値が宣言値、public-input.json、journal.jsonと一致するclose-statement.jsonのsthDigest再計算値が宣言値、public-input.json、journal.jsonと一致するjournal.jsonとpublic-input.jsonのmethodVersionが現行 contract と一致する
cd "$REPO_ROOT"
pnpm tsx -e "
import fs from 'node:fs';
import { buildCloseStatement, recomputeElectionManifestHash } from './packages/verification/src/verification/public-audit-artifacts';
import { parsePublicInputArtifact } from './packages/verification/src/verification/public-input-contract';
import { CURRENT_METHOD_VERSION } from './packages/zkvm-contract/src/zkvm/types';
const [manifestPath, closePath, journalPath, publicInputPath] = process.argv.slice(1);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
const closeStatement = JSON.parse(fs.readFileSync(closePath, 'utf-8'));
const journal = JSON.parse(fs.readFileSync(journalPath, 'utf-8'));
const publicInput = JSON.parse(fs.readFileSync(publicInputPath, 'utf-8'));
const parsedPublicInput = parsePublicInputArtifact(publicInput, { source: 'bundle' });
const publicAuthority = parsedPublicInput.typedAuthority;
const normalizeHex = (value) => String(value).replace(/^0x/i, '').toLowerCase();
const sameHex = (left, right) =>
typeof left === 'string' && typeof right === 'string' && normalizeHex(left) === normalizeHex(right);
const sameNumber = (left, right) => typeof left === 'number' && typeof right === 'number' && left === right;
const recomputedManifestHash = recomputeElectionManifestHash(manifest);
const rebuiltCloseStatement = buildCloseStatement({
logId: closeStatement.logId,
treeSize: closeStatement.treeSize,
timestamp: closeStatement.timestamp,
bulletinRoot: closeStatement.bulletinRoot,
});
const checks = {
public_input_contract_ok: parsedPublicInput.valid && Boolean(publicAuthority),
public_input_current_method_version_ok:
journal.methodVersion === CURRENT_METHOD_VERSION && publicAuthority?.methodVersion === CURRENT_METHOD_VERSION,
public_input_election_id_ok: String(publicAuthority?.electionId) === String(journal.electionId),
public_input_config_hash_ok: sameHex(publicAuthority?.electionConfigHash, journal.electionConfigHash),
public_input_bulletin_root_ok: sameHex(publicAuthority?.bulletinRoot, journal.bulletinRoot),
public_input_tree_size_ok: sameNumber(publicAuthority?.treeSize, journal.treeSize),
public_input_total_expected_ok: sameNumber(publicAuthority?.totalExpected, journal.totalExpected),
public_input_votes_not_over_tree_size_ok:
typeof publicAuthority?.votesCount === 'number' &&
typeof publicAuthority?.treeSize === 'number' &&
publicAuthority.votesCount <= publicAuthority.treeSize,
public_input_tree_size_positive_ok:
typeof publicAuthority?.treeSize === 'number' &&
Number.isInteger(publicAuthority.treeSize) &&
publicAuthority.treeSize > 0,
public_input_bulletin_root_nonzero_ok:
typeof publicAuthority?.bulletinRoot === 'string' &&
!/^(?:0x)?0{64}$/i.test(publicAuthority.bulletinRoot),
public_input_unique_indices_ok: publicAuthority?.uniqueIndices === true,
public_input_unique_commitments_ok: publicAuthority?.uniqueCommitments === true,
manifest_hash_ok: sameHex(recomputedManifestHash, manifest.electionConfigHash),
manifest_election_id_ok:
String(manifest.electionId) === String(publicAuthority?.electionId) &&
String(manifest.electionId) === String(journal.electionId),
manifest_total_expected_ok:
sameNumber(manifest.totalExpected, publicAuthority?.totalExpected) &&
sameNumber(manifest.totalExpected, journal.totalExpected),
manifest_config_hash_ok:
sameHex(manifest.electionConfigHash, publicAuthority?.electionConfigHash) &&
sameHex(manifest.electionConfigHash, journal.electionConfigHash),
close_digest_ok: sameHex(rebuiltCloseStatement.sthDigest, closeStatement.sthDigest),
close_timestamp_ok: sameNumber(closeStatement.timestamp, publicAuthority?.timestamp),
close_log_id_ok: sameHex(closeStatement.logId, publicAuthority?.logId),
close_tree_size_ok:
sameNumber(closeStatement.treeSize, publicAuthority?.treeSize) &&
sameNumber(closeStatement.treeSize, journal.treeSize),
close_bulletin_root_ok:
sameHex(closeStatement.bulletinRoot, publicAuthority?.bulletinRoot) &&
sameHex(closeStatement.bulletinRoot, journal.bulletinRoot),
close_sth_digest_ok: sameHex(closeStatement.sthDigest, journal.sthDigest),
};
console.log(JSON.stringify({ checks, publicInputErrors: parsedPublicInput.errors }, null, 2));
process.exit(Object.values(checks).every(Boolean) ? 0 : 1);
" \
"$AUDIT_ROOT/bundle/election-manifest.json" \
"$AUDIT_ROOT/bundle/close-statement.json" \
"$AUDIT_ROOT/bundle/journal.json" \
"$AUDIT_ROOT/bundle/public-input.json"
echo "exit_code=$?"
判定:
exit_code=0かつ全項目がtrue: 合格- いずれかが
false: 不合格(対応するチェック ID は チェック一覧 を参照)
8. inputCommitment 再計算
public-input.json から再計算した値が journal.json の inputCommitment と一致することを確認します。
Step 0 の Node.js と pnpm 前提を満たしてから実行してください。
RECALC="$(cd "$REPO_ROOT" && pnpm tsx -e "import fs from 'node:fs'; import { computeInputCommitmentFromPublicInput } from './packages/zkvm-contract/src/zkvm/types'; const p = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8')); console.log(computeInputCommitmentFromPublicInput(p));" "$AUDIT_ROOT/bundle/public-input.json")"
JOURNAL_COMMITMENT="$(jq -r '.inputCommitment' "$AUDIT_ROOT/bundle/journal.json")"
echo "recalculated=$RECALC"
echo "journal=$JOURNAL_COMMITMENT"
[ "${RECALC,,}" = "${JOURNAL_COMMITMENT,,}" ] && echo 'input_commitment=ok' || echo 'input_commitment=ng'
合格条件
- Step 3 の archive admission が
bundle_manifest="ok"になる - Step 4 で
EXPECTED_IMAGE_IDを決定できる - Step 5 から Step 8 の判定がすべて合格
いずれかが失敗した場合、Counted 段階または STARK 段階の必須チェックを満たしていないため Verified にはなりません。
すべて合格しても確認できたのは bundle.zip の不変条件までであり、/verify UI の総合 Verified 判定そのものの再現ではありません(範囲外や bundle.zip 単体では揃わない検証材料は 第三者検証ガイド を参照)。
用語集
本書で使用する主要な用語を定義します。 暗号と検証の基礎用語、実装用語、運用用語に分けて掲載します。
カテゴリ: 暗号プリミティブ / STARK 証明 / 検証パイプライン / 掲示板と透明性 / 改ざんシナリオ / インフラストラクチャ / セッションと API / 定数
暗号プリミティブ
コミットメント(総称)
本書で「コミットメント」と書く場合、文脈に応じて次のいずれかを指します。 両者は対象とドメイン分離タグが異なるため、明示が必要な箇所では下位用語(投票コミットメント / 入力コミットメント)を使います。
| 用語 | 対象 | ドメイン分離タグ |
|---|---|---|
| 投票コミットメント | 個々の投票(選挙 ID、選択肢、乱数)の束縛 | stark-ballot:commit|v1.0 |
| 入力コミットメント | zkVM の公開可能入力(掲示板状態と投票一覧)の束縛 | stark-ballot:input|v1.0 |
投票コミットメント(Vote Commitment)
ドメイン分離タグ、選挙 ID、投票選択肢、乱数を結合して SHA-256 でハッシュした値です。 投票内容を秘匿しつつ(隠蔽性)、後から変更できないことを保証します(束縛性)。 投票の Cast-as-Intended 検証の起点です。
詳細: コミットメントスキーム
Merkle ルート(Bulletin Root)
掲示板上の全投票コミットメントから RFC 6962 の規則に従って計算されるハッシュ値です。 掲示板の特定時点における状態を一意に表します。 新しい投票が追加されるたびに更新されます。
Merkle パス(Audit Path)
特定のリーフ(投票コミットメント)からルートまでを再構成するために必要な兄弟ノードのハッシュ列です。 包含証明の構成要素であり、対数オーダーの検証コストを実現します。
包含証明(Inclusion Proof)
特定の投票コミットメントが掲示板に含まれていることを暗号学的に証明するデータです。 リーフインデックス、Merkle パス、ツリーサイズから構成されます。 RFC 6962 のハッシュ規則に従い、リーフとパスからルートを再計算して期待値と照合します。
詳細: CT Merkle ツリー
整合性証明(Consistency Proof)
RFC 6962 で定義された、2 つの時点のツリーが追記関係にあることを暗号学的に証明するデータです。 古いツリーが新しいツリーのプレフィックスであること(投票の削除や並べ替えが行われていないこと)を保証します。
詳細: CT Merkle ツリー
入力コミットメント(Input Commitment)
zkVM が処理した公開可能な入力フィールドの一部を、固定のドメインタグと version を含む正準エンコーディングで SHA-256 ハッシュした値です。
現行実装では electionId、bulletinRoot、treeSize、totalExpected、votesCount、各投票の index、コミットメント、Merkle パスを束縛します。
対象は public-input.json より狭い部分集合です。
詳細: 入力コミットメント
STH ダイジェスト(Signed Tree Head Digest)
掲示板のログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを結合して SHA-256 でハッシュした値です。 特定の時点における掲示板の状態を一意に識別し、複数の独立した監視者間で掲示板の一貫性を検証するために使用します。
詳細: STH ダイジェスト
包含ビットマップルート(Included Bitmap Root)
zkVM ゲストが生成するビットマップ(各投票インデックスが集計に含まれたか否か)の Merkle ルートです。 投票者は自分のインデックスに対応するビットが 1 であることを Merkle 証明で確認できます。
詳細: ビットマップ Merkle
提示ビットマップルート(Seen Bitmap Root)
zkVM ゲストに提示された投票インデックスを表すビットマップの Merkle ルートです。
includedBitmapRoot と組み合わせることで、自票が「counted された」「提示されたが無効だった」「そもそも prover に提示されなかった」のどれに該当するかを説明できます。
詳細: ビットマップ Merkle
正準エンコーディング(Canonical Encoding)
固定のドメインタグ、バージョン番号、フィールド順を含む決定論的なバイト列表現です。 同一の入力から常に同一のバイト列が得られることを保証します。 本システムではコミットメントと入力コミットメントの計算に使用します。
ドメイン分離タグ(Domain Separation Tag)
ハッシュ計算において異なる用途のデータが衝突しないように付与するプレフィックス文字列です。 本システムでは、コミットメント、入力コミットメント、CT Merkle のリーフハッシュ、ノードハッシュにそれぞれ固有のタグを使用します。
投票レシート(Vote Receipt)
投票受理時にサーバーが返す応答データです。
voteId、commitment、bulletinIndex、bulletinRootAtCast を含みます。
検証では voteReceipt として参照されます。
投票者がローカルに保持する投票時データ(選挙 ID、選択肢、乱数)とは別物であり、zkVM が生成する STARK レシート(Receipt)とも別物です。
Cast-as-Intended 検証では、投票時データからコミットメントを再計算し、投票レシートのコミットメント値と照合します。
詳細: コミットメントスキーム、4 段階検証モデル
STARK 証明
STARK(Scalable Transparent ARgument of Knowledge)
Trusted setup(信頼されたセットアップ)を必要としない暗号証明方式です。 ハッシュベースの構成により耐量子計算機性に優位があります。 本システムでは RISC Zero zkVM によって投票集計の正当性を証明するために使用します。
詳細: zkVM の基礎
RISC Zero zkVM
RISC-V アーキテクチャ上で通常の Rust コードを実行し、その実行が正しく行われたことの STARK 証明を生成するゼロ知識仮想マシンです。 ゲストプログラムとホストプログラムから構成されます。
詳細: zkVM の基礎
レシート(Receipt)
zkVM が生成する暗号証明オブジェクトです。
内部に Seal(STARK 証明本体)とジャーナル(公開出力)を含みます。
Receipt::verify(image_id) によって、特定のゲストプログラムが正しく実行されたことを第三者が検証できます。
ジャーナル(Journal)
zkVM ゲストプログラムの公開出力です。
現行契約は methodVersion=14 で、検証済み集計結果、missingSlots / invalidPresentedSlots / excludedSlots、inputCommitment、includedBitmapRoot、seenBitmapRoot などを含みます。
レシートに暗号学的に束縛されており、改ざんできません。
Image ID
コンパイル済みのゲストプログラムバイナリを一意に識別するハッシュ値です。 レシート検証時に期待される Image ID と照合することで、意図したゲストプログラムによって生成された証明であることを確認します。 プローバーイメージの更新時には同期して更新が必要です。
詳細: Image ID
ゲストプログラム(Guest Program)
zkVM 内部で実行される Rust プログラムです。 投票データの検証と集計を行い、結果をジャーナルとして出力します。 ゲストの実行内容は STARK 証明によって保証されます。
詳細: ゲストプログラム
ホストプログラム(Host Program)
zkVM の外部で動作し、ゲストプログラムの実行と証明生成を制御する Rust プログラムです。 入力データの読み込み、zkVM の起動、レシートとジャーナルの出力を担います。
詳細: ホストと証明生成
検証サービス(Verifier Service)
Rust で実装された独立した STARK レシート検証プログラムです。
Receipt::verify(expected_image_id) を実行し、レシートの暗号学的正当性を確認します。
結果は verification.json として保存されます。
詳細: 検証サービス
フェイクレシート(Fake Receipt)
RISC0_DEV_MODE=1 で生成される暗号学的保証のないレシートです。
開発効率のためのモックであり、検証サービスは InnerReceipt::Fake を自動検出して dev_mode ステータスを返します。
本番環境では使用してはなりません。
詳細: ゲーティングロジック
ジャーナル契約(Journal Contract)
methodVersion で識別されるジャーナル出力構造の仕様です。
ゲストプログラムが出力するフィールドの集合と意味を定義します。
現行契約は methodVersion=14(v1.4)です。
ゲストプログラムの変更は新しい Image ID の生成を伴い、検証時には期待 Image ID との一致が確認されます。
レシートラッパー JSON
ホストバイナリが出力する { "receipt": ..., "image_id": "0x..." } 形式のラッパー JSON です。
STARK レシート本体を image_id と一緒に運ぶための受け渡し形式で、検証サービスはこの形式を読み込んで Receipt::verify(expected_image_id) を実行します。
配布対象アーカイブ内のファイル名は receipt.json です。
本書では「レシート」「STARK レシート」「receipt.json」「レシートラッパー JSON」を次のように使い分けます。
| 表記 | 指すもの |
|---|---|
投票レシート(voteReceipt) | サーバーが投票受理時に返す応答データ |
| STARK レシート(Receipt) | zkVM が生成する暗号証明オブジェクト |
receipt.json | レシートラッパー JSON のファイル名 |
| レシートラッパー JSON | { receipt, image_id } 構造のホスト出力 |
詳細: ホストと証明生成
検証パイプライン
「検証」と「監査」の使い分け 本書では、
/verify画面と内部パイプラインによる判定を 検証、第三者がbundle.zipをローカルに取得して独立に行う確認作業を 監査 と呼び分けます。reproducibility/章は主に「監査」の文脈で書かれており、verification/章は「検証」の文脈で書かれています。
fail-closed
検証 API と検証パイプラインが、データ不在や未解決状態を成功側に倒さず、not_run、失敗、Warning 側へ確定させる方針です。
要求された証拠が揃わない限り Verified に到達させない設計姿勢を指します。
cast-time 証跡の not_run 扱い、ゲーティングロジックの不変条件、重要度の required 扱いは、いずれもこの方針の具体化です。
E2E 検証可能投票(End-to-End Verifiable Voting)
投票者が自票について「意図通りに投じた」「正しく記録された」「正しく集計された」の 3 段階を独立に検証できる投票方式です。 システム運営者を信頼せずとも投票の完全性を確認できることを目標とします。
詳細: 4 段階検証モデル
Cast-as-Intended(意図通りの投票)
検証の第 1 段階です。
投票者がローカルに保持する投票時データ(選挙 ID、選択肢、乱数)からコミットメントを再計算し、投票レシート(voteReceipt)のコミットメント値と照合します。
これにより、投票時に意図した選択が正しくコミットメントに反映されたことを確認します。
クライアント側で完結します。
詳細: 4 段階検証モデル
Recorded-as-Cast(記録通りの保存)
検証の第 2 段階です。 コミットメントが掲示板に正しく記録されたことを、RFC 6962 の包含証明と整合性証明によって確認します。 掲示板が追記専用であること(投票が削除や改変を受けていないこと)を暗号学的に保証します。
詳細: 4 段階検証モデル
cast-time 証跡(Cast-Time CT Artifact)
投票受理時に CT ツリーへ書き込んだ時点の証跡です。
具体的には voteReceipt(投票レシート)と userVote.proof(包含証明パラメータ: leafIndex、treeSize、auditPath)の 2 つを指します。
Recorded-as-Cast の検証では両方が必要で、Cast-as-Intended では voteReceipt のみを使用します。
取得経路と fail-closed 挙動の詳細は設計と実行フローを参照してください。
詳細: 4 段階検証モデル
Counted-as-Recorded(記録通りの集計)
検証の第 3 段階です。
掲示板に記録された全投票が zkVM の集計に過不足なく含まれたことを確認します。
除外されたスロットがないこと(excludedSlots == 0)は最重要不変条件です。
詳細: 4 段階検証モデル
STARK 検証(STARK Verification)
検証の第 4 段階です。 STARK レシートが暗号学的に正当であること、および期待される Image ID で生成されたことを確認します。 ジャーナルの内容が正しい実行結果であることの最終的な保証です。
詳細: 4 段階検証モデル
検証チェック(Verification Check)
検証パイプラインを構成する個別の原子的な検証項目です。
それぞれ一意の ID、所属する検証段階、証拠種別、重要度を持ちます(現行のチェック数と内訳は チェック一覧 を参照)。
Counted-as-Recorded には counted_election_manifest_consistent と counted_close_statement_consistent も含まれ、公開監査アーティファクトとの整合も required 条件です。
詳細: チェック一覧
ゲーティングロジック(Gating Logic)
検証チェックの結果を集約し、「Verified」「Verification Failed」「Warning」のいずれを表示するかを決定するロジックです。
required 扱いのチェックが 1 つでも failed なら Verified は表示されず、not_run / pending / running や必須証拠欠落でも Verified にはなりません。
Stage のステータスも、その Stage で required 扱いになるチェック群全体から導出されます。
詳細: ゲーティングロジック
公開監査アーティファクト
election-manifest.json(選挙設定の公開監査用スナップショット)と close-statement.json(集計締切時点のログ境界を表す公開監査レコード)の総称です。
Counted-as-Recorded 段階の必須チェック(counted_election_manifest_consistent、counted_close_statement_consistent)で整合性が検証されます。
締切ステートメント(close-statement.json)
集計締切時点のログ境界を表す公開監査レコードです。
logId、treeSize、bulletinRoot、sthDigest、timestamp を宣言し、counted_close_statement_consistent チェックで検証入力やジャーナルとの整合が確認されます。
本書では close-statement.json の和訳呼称として「締切ステートメント」を使います。
公開可能アーティファクト
秘密データを含まず、第三者検証や監査に利用できるアーティファクトの機密性区分です。 ここでの「公開可能」は無認証で取得できることを意味しません。 通常の配布や取得は capability 保護 API が担当し、対象が S3 にある場合も API が読み出して返します。
配布対象アーカイブ(bundle.zip)
公開許可リストに基づいて作成される ZIP アーカイブです。
証明バンドル のうち公開可能アーティファクトだけを束ねた部分集合で、bundle.zip というファイル名で配布されます。
現行構成は public-input.json、election-manifest.json、close-statement.json、receipt.json、journal.json などを含みます。
input.json、verification.json、included-bitmap.json、seen-bitmap.json は含まれません。
bundle.zip が公開可能アーティファクトだけを含むことは、無認証公開を意味しません。
詳細: バンドル構造
保護された検証レポート(verification.json)
サーバー側検証の詳細結果を記録する protected report artifact です。
配布対象アーカイブ bundle.zip には含まれず、通常のブラウザ / CLI 契約では capability 保護された
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/report から API レスポンスとして取得します。
詳細: エンドポイント一覧
無認証公開
セッション ID や capability トークンなしで誰でも取得できる公開状態です。
本書では「公開可能」や「外部クライアント向け API」と区別して扱います。
現行のセッションスコープ API や bundle.zip 取得経路の多くは capability 保護されており、無認証公開ではありません。
詳細: バンドル構造
zkGate
STARK 検証の結果に基づいて Counted-as-Recorded チェックの評価を制御するゲートです。
STARK 未解決(not_run / running)の間、zkGate 対象チェックは not_run または pending になります。
STARK が failed の場合、zkGate 対象チェックも failed になり得ます。
詳細: ゲーティングロジック
証拠種別
検証チェックに使用するデータの出所を示す分類です。
local(投票時に確定したユーザー固有データ)、public(掲示板や capability 保護 API から取得する、秘密データを含まない検証用データ)、zk(zkVM ジャーナルに含まれるデータ)、demo(dev-mode receipt を許可した場合に、zk 証拠の表示用結果として使うデモ証拠)の 4 種別があります。
demo 証拠は production STARK proof としての Verified にはなりません。
詳細: チェック一覧
重要度(Criticality)
検証チェックの必須性を示す分類です。
required(失敗、未実行、未解決なら Verified をブロック)と optional(補助的で、単独では Verified をブロックしない)の 2 段階です。
recorded_sth_third_party のように、設定状況に応じて optional から blocking な required 相当に昇格するチェックもあります。
詳細: ゲーティングロジック、チェック一覧
掲示板と透明性
掲示板(Public Bulletin Board)
全投票コミットメントを時系列で記録する追記専用のログです。 RFC 6962 の Certificate Transparency モデルに基づき、包含証明と整合性証明によって第三者が監査可能な透明性を実現します。
詳細: CT Merkle ツリー
RFC 6962
Certificate Transparency(証明書の透明性)の標準規格です。
追記専用の Merkle ツリー、リーフハッシュ(0x00 プレフィックス)とノードハッシュ(0x01 プレフィックス)のドメイン分離、包含証明、整合性証明の仕様を定義します。
本システムの掲示板は、この規格のハッシュ規則と証明アルゴリズムを参照した CT スタイル実装を採用しています。
詳細: CT Merkle ツリー
STH(Signed Tree Head)
掲示板の特定時点における状態の要約です。 ログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを含みます。 複数の独立したソースからの STH を比較することで、サーバーが異なるクライアントに異なるツリーを提示するスプリットビュー攻撃を検出します。
詳細: STH ダイジェスト
スプリットビュー攻撃(Split-View Attack)
掲示板サーバーが異なるクライアントに異なるツリー状態を提示する攻撃です。 特定の投票者に対してのみ投票を除外したツリーを見せることで、不正を隠蔽しようとします。 整合性証明と STH の第三者検証によって検出されます。
詳細: STH ダイジェスト
ルート履歴(Root History)
掲示板のルートハッシュ、ツリーサイズ、タイムスタンプの時系列記録です。 投票時のルートが最終ツリーの有効なプレフィックスであることを、整合性証明で検証する際に参照します。
詳細: CT Merkle ツリー
改ざんシナリオ
改ざんシナリオ(Tamper Scenario)
検証システムが不正をどのように検出するかを教育的に示すシミュレーションです。 S0(正常)から S5(ランダムに選んだ 1 票の除外)まで 6 種類が定義されています。
詳細: 改ざんシナリオ
投票除外(Vote Exclusion)
一部の投票を集計から意図的に除外する攻撃です。
zkVM ジャーナルの excludedSlots > 0 として検出されます。
本システムの最重要不変条件により、投票除外がある場合は「Verified」を表示しません。
主張集計改ざん(Claimed-Tally Tampering)
公開表示する集計値(claimed tally)を、zkVM が証明した実際の集計値と異なる値に書き換える攻撃の教育的シミュレーションです。
zkVM の入力、レシート、ジャーナルは正常なまま、公開表示のみを改ざんします。
counted_tally_consistent チェックで検出されます。
excludedSlots
zkVM ジャーナルに含まれる、除外されたスロットの総数です。
0 でなければなりません。
0 より大きい場合は投票の未提示または未計上が発生しており、いかなる場合も「Verified」を表示してはなりません。
excludedSlots が現行の authoritative な公開除外数です。
excludedCount は古い入力を安全側に倒すための互換フィールドとしてだけ扱います。
現行レスポンスでは excludedCount を新規に返しません。
詳細: 4 段階検証モデル、ゲーティングロジック
インフラストラクチャ
ECS Fargate
AWS のサーバーレスコンテナ実行環境です。 本システムでは target の STARK 証明生成 task を実行するために使用します。 アイドル時のコストは 0 です。
詳細: 非同期プローバー
Step Functions
AWS のワークフローオーケストレーションサービスです。 イメージ署名検証、ECS プローバー実行、コールバックの一連のフローを管理します。
詳細: 非同期プローバー
非同期証明モード(Async Proving)
SQS、Step Functions、ECS Fargate の経路で STARK 証明を非同期に生成するモードです。
集計リクエスト(POST /api/sessions/:sessionId/finalizations)は 202 Accepted を返し、クライアントはステータスポーリングで完了を待ちます。
詳細: 非同期プローバー
同期証明モード(Sync Proving)
ローカルプロセスで zkVM ホストバイナリを直接実行し、STARK 証明を同期的に生成するモードです。 開発環境で使用されます。
Prover semaphore
DynamoDB の単一 lock item で、証明生成(RunProver)への同時進入数を制限する仕組みです。
通常の解放は Step Functions が行い、semaphore-janitor Lambda が terminal execution event と定期 sweep で残った owner を補償的に解放します。
詳細: 非同期プローバー
Quiesced phase
DynamoDB recovery の 4 phase(primary_active、primary_quiesced、replacement_quiesced、replacement_active)のうち、新規 session admission と prover / verification queue の consumption を止める切替中 phase です。
切替中の新規書き込みや非同期処理を fail-closed に抑止します。
詳細: Terraform
Develop operator gate
develop target で静的 frontend と API への viewer access を制限する、CloudFront signed-cookie gate です。 署名用の秘密鍵は KMS 内で管理し、導出した公開鍵だけを CloudFront trusted key group に登録します。 環境全体への入口を制限するもので、session-scoped な capability token の代わりにはなりません。
詳細: AWS runtime 境界
Saved-plan lane
plan stage が作成した exact saved plan だけを apply する deployment lane です。
normal と rollback を分け、rollback は Terraform plan が空でも approval を必須とします。
詳細: Terraform
Release authority
どの toolchain / prover image と application artifact をデプロイに使えるかを決める、受理済みの記録群です。 承認済み prover release record、それを選択する accepted pointer、そこから生成された app deployment record、および必要な承認・scan・signing evidence で構成されます。 target は現行 AWS 構成、legacy は退役対象の旧構成を指し、legacy 側のリソースは target runtime の authority に含めません。
イメージ署名検証(Image Signing)
ECS タスクで使用するプローバーコンテナイメージが、信頼できるビルドパイプラインから生成されたことを検証する仕組みです。 AWS Signer を使用し、Step Functions のゲートとして機能します。
詳細: イメージ署名
証明バンドル(Proof Bundle)
zkVM の実行結果を検証可能な形で保存、配布するためのアーティファクト群を指す 上位概念 です。
公開可能アーティファクト(public-input.json など)、protected report artifact(verification.json)、非公開アーティファクト(input.json、included-bitmap.json、seen-bitmap.json など)を含み得ます。
public /「公開可能」は秘密情報を含まず検証に利用可能であるという機密性の分類であり、無認証公開を意味しません。
公開許可リストで取り出した部分集合が 配布対象アーカイブ であり、それを ZIP 化したファイル名が bundle.zip です。
3 者の関係は次のとおりです。
証明バンドル ⊃ 配布対象アーカイブ ⊃ bundle.zip(ファイル)
「証明バンドル」は、AWS / S3 上の隣接オブジェクトや非公開アーティファクトも含めて議論したい箇所で使います。
単に公開可能な ZIP を指す場合は bundle.zip または「配布対象アーカイブ」を使います。
詳細: バンドル構造
隣接オブジェクト(Sibling Object)
S3 上で bundle.zip と同じ prefix(sessions/{sessionId}/{finalizationId}/)に配置される非 bundle ファイルです。
included-bitmap.json、seen-bitmap.json、verification.json などを指します。
bundle.zip には含まれませんが、コールバック復元や capability 保護された検証レポート配信で利用されます。
詳細: バンドル構造
セッションと API
セッション(Session)
一連の投票フロー(セッション作成、投票、集計、検証)を管理する単位です。
一意の sessionId(UUID v4)で識別されます。
クライアント側のローカルセッション TTL は投票 / 集計中が 30 分、検証中が 24 時間です。
サーバー側の保存 TTL は store 実装と runtime config に依存します。
詳細: セッションライフサイクル
選挙(Election)
投票の論理的な単位です。
一意の electionId(UUID v4)で識別され、コミットメントのドメイン分離に使用されます。
選挙設定ハッシュ(electionConfigHash)が期待投票数などの設定を束縛します。
集計確定(Finalization)
全投票の収集後に zkVM 入力を構築し、STARK 証明を生成するプロセスです。 同期モード(ローカル実行)と非同期モード(ECS Fargate)の 2 つの実行パスがあります。
X-Session-ID
過去の contract で使われていたセッションスコーピング用の HTTP ヘッダーです。
現行 contract には含まれません。
現行のセッション特定は、path の sessionId と X-Session-Capability ヘッダーで行います。
詳細: エンドポイント一覧
X-Session-Capability
POST /api/sessions のレスポンスで返る署名付きセッショントークンを運ぶ HTTP ヘッダーです。
このヘッダーが運ぶ値を capability トークンと呼びます。
session-scoped / capability 保護 API で必須です(POST /api/sessions は新規セッション作成のため対象外)。
retired / debug route の扱いはエンドポイント一覧の Retired route と internal surfaceを参照してください。
詳細: エンドポイント一覧
capability 保護 API
セッション capability(X-Session-Capability ヘッダーが運ぶトークン)の提示を要求する API です。
外部クライアント向けに文書化されていても、無認証公開 API ではありません。
詳細: エンドポイント一覧
Turnstile
Cloudflare が提供する CAPTCHA サービスです。
POST /api/sessions、POST /api/sessions/:sessionId/votes、POST /api/sessions/:sessionId/finalizations が body の turnstileToken で Bot による不正アクセスを防止します。
token を要求するかどうかは実行環境の設定に依存します。
bypass の可否は runtime profile が一体として選び、production の bypass は認めません。session 作成の develop 例外も operator-gated runtime に限定されます。
定数
| 定数名 | 値 | 説明 |
|---|---|---|
BOT_COUNT | 63 | サーバーが自動生成するボット投票数 |
MERKLE_TREE_DEPTH | 6 | Merkle ツリーの深度(2^6 = 64 リーフに対応) |
VOTE_CHOICES | A, B, C, D, E | 投票で選択可能な選択肢 |
| コミットドメインタグ | stark-ballot:commit|v1.0 | コミットメントハッシュのドメイン分離タグ |
| 入力ドメインタグ | stark-ballot:input|v1.0 | 入力コミットメントのドメイン分離タグ |
| リーフドメインタグ | stark-ballot:leaf|v1 | CT Merkle リーフハッシュのドメイン分離タグ |
参考文献
本システムの設計に関連する主要文献を掲載します。
[1] J. Benaloh, R. Rivest, P. Y. A. Ryan, P. Stark, V. Teague, P. Vora. “End-to-end verifiability,” arXiv:1504.03778, 2015. https://arxiv.org/abs/1504.03778
E2E 検証可能投票の基本モデル(Cast-as-Intended / Recorded-as-Cast / Counted-as-Recorded)を定義した文献です。 本システムの 4 段階検証モデルは、このフレームワークと同じ構造を採用しています。
[2] M. Harrison, T. Haines. “On the Applicability of STARKs to Counted-as-Collected Verification in Existing Homomorphic E-Voting Systems,” in Financial Cryptography and Data Security: FC 2024 International Workshops, LNCS, Springer, 2025(First Online 2024). https://doi.org/10.1007/978-3-031-69231-4_4
STARK 証明を投票集計の検証に適用する設計根拠を示した論文です。 本システムの Counted-as-Recorded 段階における zkVM 証明設計と関連が深い文献です。
[3] V. Farzaliyev, J. Willemson. “End-To-End Verifiable Internet Voting with Partially Private Bulletin Boards,” in Electronic Voting: E-Vote-ID 2025, LNCS, vol. 16028, Springer, 2026(First Online 2025). https://doi.org/10.1007/978-3-032-05036-6_5
[2] の研究を拡張し、STARK ベースの E2E 検証可能投票において Cast-as-Intended 検証と掲示板のプライバシー設計を統合した論文です。 本システムの掲示板構成と検証パイプライン設計に関連するテーマを扱っています。
[4] B. Laurie, A. Langley, E. Kasper. “Certificate Transparency,” RFC 6962, IETF, 2013. https://datatracker.ietf.org/doc/html/rfc6962
追記専用 Merkle ツリーの包含証明と整合性証明を定義した Experimental RFC です(現在は RFC 9162 により置き換えられています)。
本システムの掲示板は、この仕様のハッシュ規則(0x00 リーフ / 0x01 ノード)と証明アルゴリズムに基づく CT スタイル実装を採用しています。
ライセンス
STARK Ballot Simulator は Apache License 2.0 の条件で提供されています。
- SPDX:
Apache-2.0 - ライセンス本文: 公開リポジトリの
LICENSE
依存ソフトウェアには、それぞれのパッケージ定義に記載されたライセンスが適用されます。
第三者ライセンス情報は公開リポジトリの
THIRD_PARTY_NOTICES.md
と、生成済みメタデータ
docs/licenses/
にまとめています。