Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

はじめに

最終更新: 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.jsonverificationStatus など)はコードフォントの英語表記を保ちます。
  • コンポーネント名:概念としては「検証サービス」、バイナリ名・パスとしては verifier-service を使います。
  • 役割名とリソース名:概念・役割としては「プローバー」「検証 worker」、リソース・コンポーネント固有名(prover semaphore、proof-dispatcherverification-worker など)は原文の英語表記を保ちます。

バンドル関連は、次の 3 語を使い分けます。

指すもの表記
配布されるファイル本体bundle.zip
配布対象としての論理名配布対象アーカイブ
非公開アーティファクトを含む上位概念証明バンドル

verification.json は protected report artifact であり、public bundle.zip のメンバーではありません。 階層関係は バンドル構造公開境界 を参照してください。

本書の読み方

標準ルート

  1. まず 全体像 でシステムの概要を掴む
  2. 利用フロー で Home から Verify までの画面と API の流れを確認する
  3. 暗号プロトコル でコミットメントや Merkle ツリーなどの基盤を理解する
  4. zkVM 設計 でゲストプログラムと証明生成の仕組みを学ぶ
  5. 検証パイプライン で 4 段階検証モデルの全体と公開境界を把握する
  6. 改ざんシナリオ で教育的シミュレーションの動作を確認する
  7. 品質保証と形式手法 でテスト、PBT、Lean による品質境界を確認する
  8. AWS アーキテクチャ で runtime の責務分担を理解する
  9. API リファレンス でエンドポイント仕様を参照する
  10. 実際に検証する場合は 第三者検証ガイドbundle.zip を使ったローカル検証手順を実行する

PoC として受け入れている制約は 全体像、 設計背景の一次資料は 参考文献 から参照してください。

読者別ルート

いずれも標準ルートの 1〜2(全体像利用フロー)を読み終えていることを前提に、そこから先の重点だけを示します。

監査者向け

bundle.zip を検証ページから取得し、独立にローカル監査したい読者向けです。

  1. 検証パイプライン/verify の最終判定ロジックを理解する
  2. 公開境界 で public bundle.zip と protected report artifact(verification.json)の境界を確認する
  3. チェック一覧 で各チェック ID と判定条件を確認する
  4. 第三者検証ガイドbundle.zip のローカル監査手順を実行する

用語集 を手元に置き、テストと形式化が守る境界は 品質保証と形式手法 で補足してください。 飛ばしてよい: 暗号プロトコル の数式詳細、AWS アーキテクチャ のインフラ詳細

実装者向け

クライアント、サーバー、zkVM のいずれかの実装を変更または追従したい読者向けです。

  1. 暗号プロトコル でコミットメント、Merkle、入力コミットメントの正準形を把握する
  2. zkVM 設計 でゲストとホストの責務分担と Image ID 管理を理解する
  3. 検証パイプライン でチェック評価、ゲーティング、公開境界を把握する
  4. API リファレンス でエンドポイント仕様と session-scoped 認可を確認する

テストのレイヤー分担は 品質保証と形式手法 を参照してください。 飛ばしてよい: 第三者検証ガイド(実装変更後の動作確認には 改ざんシナリオ を使う方が早い)

運用者向け

AWS インフラ、非同期プローバー、デプロイを担当する読者向けです。

  1. AWS runtime 境界 で frontend、API、DynamoDB、async prover、artifact delivery の責務分担を把握する
  2. AWS アーキテクチャ で runtime 構成、環境分離、Terraform-managed infrastructure の連携点を把握する
  3. 非同期プローバー で SQS、Step Functions、ECS の責務を理解する
  4. 可観測性設計 で構造化ログ、検出、相関、通知境界を確認する
  5. イメージ署名Image ID で署名検証と Image ID 解決の連動を確認する
  6. 公開境界バンドル構造 で 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-CastRFC 6962 / CT スタイルの掲示板
Counted-as-RecordedzkVM ジャーナル、入力整合、ビットマップ証明
STARK VerificationRISC Zero レシート検証

各段階の目的、必要な証拠、失敗モードは 4 段階検証モデル を参照してください。

バンドル用語の階層

検証で扱うアーティファクト群は、証明バンドル ⊃ 配布対象アーカイブ ⊃ bundle.zip(ファイル) の 3 層で呼び分けます。 定義は 用語集 > 証明バンドル、詳細は バンドル構造 を参照してください。 verification.jsonbundle.zip のメンバーではなく、capability 保護された report artifact として扱います。

PoC として受け入れている制約

次の制約は、検証可能投票の E2E フローを明瞭に示すための意図的な PoC スコープです。実装漏れでも、本番選挙システムとしての安全性・性能の主張でも ありません。正確な定数や mode 条件は各技術章を単一の参照先とします。

制約現行スコープ技術上の参照先
固定されたデモ選挙 shape選択肢、ユーザー/ボット構成、期待票数、Merkle tree depth を固定して E2E を再現するホストと証明生成 > 現行 PoC の選挙 shape
bitmap chunk の開示自票の proof が同じ chunk 内の counted / seen 状態も開示するビットマップ Merkle > プライバシーに関する注意
証明実行の resource/modetarget は 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 の対応

順序画面 / 状態主な操作役割
1Homeセッション作成セッション、capability token、選挙設定の識別子を作る
2Vote投票送信投票コミットメントを送信し、投票レシートと掲示板上の位置を受け取る
3Bot progressボット投票進捗デモ用ボット投票の進捗をポーリングする
4Aggregate集計/証明生成集計、zkVM 実行、監査用アーティファクト生成を要求する
5Async statuscurrent finalization 取得current finalization の pending / running / succeeded / failed / timeout を確認する
6Resultcurrent finalization 取得STARK 検証実行finalization output を表示し、必要なら STARK verification を開始する
7Verify検証 resource 取得STARK 検証実行cast 観測の記録検証ペイロードの取得、STARK verification の開始、非 gating の観測記録を行う
8Bundle / reportバンドル ZIP 取得検証レポート取得capability 保護 API から public bundle.zip と protected report を取得する

正確な path、request/response schema、公開エラーは エンドポイント一覧 を参照してください。

POST /api/sessions を除く session-scoped API は、path の :sessionIdX-Session-Capability ヘッダーだけで現在の session を特定して保護します。 finalization、verification、bundle / report の resource は path の :finalizationId にも結び付けられ、session capability と一致した場合だけ返されます。 ヘッダー要件の詳細は エンドポイント一覧 > 共通 contract を参照してください。

1. Home

Home で開始すると、ブラウザは POST /api/sessions を送ります。 サーバーは sessionIdcapabilityTokenelectionIdelectionConfigHashlogId などを返し、ブラウザはそれらをローカル session state に保存して Vote へ進みます。

この session が以後の API 呼び出しの権限境界です。 別タブや古い session が現在の session と食い違う場合、後続画面は fail-closed に扱います。

2. Vote と bot progress

Vote では、ブラウザが投票選択と乱数からコミットメントを作り、POST /api/sessions/:sessionId/votes に送信します。 成功すると、投票者は voteIdbulletinIndexbulletinRootAtCast を含む投票レシートを受け取ります。

その後、デモ用のボット投票が進むあいだ 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 は pendingrunningsucceededfailedtimeout のいずれかであり(status ごとのフィールドは エンドポイント一覧 > Finalization resource)、ブラウザは succeeded の result だけを Result へ引き継ぎ、failedtimeout は失敗として扱います。

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 は availableunavailablecorrupt を明示します。available の場合は proofAuthoritypresentationverifierResultverification.checks / verification.stepsvoterEvidence、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/bundlepublic bundle.zip
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/reportprotected 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 種を指します。 両者は対象とドメイン分離タグが異なるため、それぞれ独立した章で説明します。

プロトコル章の構成

想定読者と前提

  • 想定読者:暗号プリミティブの仕様を実装または監査する技術者
  • 前提: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/>&quot;stark-ballot:commit|v1.0&quot;"] --> 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"
選挙 ID16 バイト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-RecordedzkVM ゲストが 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 アルゴリズムは、任意のサイズのデータセットからルートハッシュを計算します。

アルゴリズムの定義

  1. 空のツリー: MTH({}) = SHA-256() (空入力のハッシュ)
  2. 単一リーフ: MTH({d₀}) = LeafHash(d₀)
  3. 複数リーフ: サイズ 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 フィールド名
proofNodesmerklePath
rootHashbulletinRootAtCast

finalization-scoped verification GET の voterEvidence.userVote.proof/api/sessions/:sessionId/votes/:voteId/inclusion-proof がこの構造に対応します。

PATH アルゴリズム

RFC 6962 の PATH 関数に従い、Merkle パス(audit path、監査パス)を再帰的に生成します。

  1. ツリーをサイズ k(n 未満の最大の 2 のべき乗)で左右に分割
  2. 対象リーフが左部分木にある場合(index < k):
    • 左部分木の PATH を再帰計算
    • 右部分木のハッシュをMerkle パスに追加
  3. 対象リーフが右部分木にある場合(index >= k):
    • 右部分木の PATH を再帰計算(インデックスを index - k に調整)
    • 左部分木のハッシュをMerkle パスに追加

検証手順

検証者は以下の手順で包含を確認します:

  1. コミットメントのリーフハッシュを計算: LeafHash(commitment)
  2. PATH アルゴリズムと同じ木構造に従い、Merkle パスのノードを順に結合
  3. 計算されたルートが期待するルートハッシュと一致するか確認

Recorded-as-Cast では、包含証明に加えて以下の cast-time 一貫性も確認します:

  • leafIndex がレシートの bulletinIndex と一致すること
  • treeSizebulletinIndex + 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-proofproofNodes に加えて rootAtFromTreeSizerootAtToTreeSize も返します。verification GET の recorded_consistency_proof 判定では、サーバーが取得・検査済みの root と整合性証明だけを 判定に使います。判定では from root を レシートの bulletinRootAtCast、to root を最終 bulletinRoot と照合します。

SUBPROOF アルゴリズム

RFC 6962 の SUBPROOF 関数に基づき、整合性証明を再帰的に生成します。

  1. m = n かつ古いツリーが完全部分木: 空の証明を返す
  2. m = n かつ完全部分木でない: 部分木のルートハッシュを返す
  3. m = 1n = 2、かつ古いツリーが完全部分木: 2 番目のリーフの生データを返す
  4. 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-RecordedzkVM ゲストが同じハッシュ規則で各 vote の包含を内部検証する

Recorded-as-Cast では recorded_inclusion_proofrecorded_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 証明単体のスコープ外です。 入力コミットメントがなければ、悪意あるサーバーは次の攻撃が可能です。

  1. 投票を除外した入力で zkVM を実行し、有効な STARK 証明を取得する
  2. 公開用の入力データには除外されていない投票を含めて提示する
  3. 第三者は 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 LEv1.0 = 10
選挙 ID16 バイトUUID バイナリ選挙スコープの識別子
掲示板ルート32 バイトハッシュ値最終的な Merkle ルート
ツリーサイズ4 バイトu32 LE掲示板のリーフ数
期待投票数4 バイトu32 LE想定される総投票数
投票数4 バイトu32 LE実際に含まれる投票数
各投票インデックス4 バイトu32 LE掲示板上の位置
コミットメント長2 バイトu16 LE固定値 32
コミットメント32 バイトハッシュ値投票コミットメント
パス長2 バイトu16 LEMerkle パスのノード数
パスノード各 32 バイトハッシュ値包含証明の兄弟ハッシュ

public-input.json と公開監査アーティファクトとの関係

要点: public-input.json の全フィールドが入力コミットメントに束縛されるわけではありません。 残りのフィールドは、別のチェックで補完的に検証されます。

public-input.json は、zkVM 検証に使う秘密データを含まない検証用レコードです。 現行実装では schemaversionelectionIdelectionConfigHashbulletinRoottreeSizetotalExpectedlogIdtimestampmethodVersion を含みます。 各投票については、index、コミットメント値、Merkle パスを含みます。

入力コミットメントが直接束縛するのは、フィールド一覧に示した対象のみです。 schemaversionelectionConfigHashlogIdtimestampmethodVersion は対象外です。 対象外のフィールドは、proof bundle 内の election-manifest.jsonclose-statement.json を組み合わせて、次のように照合されます。

  • electionConfigHashcounted_election_manifest_consistent(manifest と journal 等を照合)
  • logIdtimestampcounted_close_statement_consistent(close statement と journal 等を照合)
  • schemaversionpublic-input.json の管理対象 artifact format として artifact 採用時に検証
  • methodVersionpublic-input.json 採用時に journal と照合 Image ID 解決では正規化済み journal 値を使用

正準化規則

エンコーディングの決定性を保つために、次の規則を使います。

ソート規則

投票はエンコーディング前にインデックスの昇順にソートします。 各投票の index は一意であることが前提です。 この規則により、同じ投票集合から常に同一のバイト列が生成されます。 この規則に違反すると、TypeScript と Rust で異なるハッシュ値が計算され、検証が失敗します。

異常系の補助: 重複インデックスはプロトコル違反です。 TS/Rust 双方は決定性のために commitment / merklePath で tie-break しますが、正常系仕様は index 昇順のままです。

エンディアン規則

すべての整数フィールドはリトルエンディアンでエンコードされます。

バイト数エンコーディング
u162リトルエンディアン
u324リトルエンディアン

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 バイトです。

各フィールドの仕様

フィールドサイズエンコーディング説明
ログ ID32 バイトハッシュ値掲示板インスタンスの識別子
ツリーサイズ4 バイトu32 LE掲示板のリーフ数
タイムスタンプ8 バイトu64 LEUnix 時刻(ミリ秒)
掲示板ルート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 ソースの応答は、sthDigestbulletinRoottreeSizetimestamplogId だけを許可する 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-Castrecorded_sth_third_party独立ソースから取得した STH ダイジェストがジャーナルの値と一致するか
Counted-as-Recordedcounted_close_statement_consistentclose-statement.jsonsthDigest が公開入力およびジャーナルの値と整合するか

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_partynot_run(未実行)となり、第三者照合は行いません。

開発用の .env.local.example では、必要な場合に有効化する例として次の値がコメントで示されています。

  • VITE_STH_SOURCES=/api/sessions/:sessionId/bulletin/sth
  • VITE_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) | ...
ビット位置バイトインデックスバイト内ビット位置
000 (LSB)
101
707 (MSB)
810 (LSB)
6377 (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 ツリーの章で解説したものと同一です。

ツリー構築アルゴリズム

  1. 各 32 バイトチャンクにリーフハッシュを適用
  2. ボトムアップでペアを結合し、内部ノードハッシュを計算
  3. 奇数ノードがある場合は、そのまま次のレベルに昇格(ハッシュなし)
  4. ルートに到達するまで繰り返す
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 に対する証明材料を返します。kindvoteIndex はどちらも必須の path parameter です。成功時の標準 JSON envelope は次の形です。

{
  "data": {
    "leafChunk": "...",
    "auditPath": []
  },
  "meta": {
    "requestId": "..."
  }
}

data は以下の要素で構成されます:

フィールド説明
leafChunk対象ビットを含む 32 バイトチャンク(16 進数)
auditPathルートまでの兄弟ハッシュ配列(各要素にハッシュ値と位置)

leafIndexfloor(voteIndex / 256))と bitOffsetvoteIndex 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=includedincluded = true なら、自票がカウントされたことを意味します。 kind=seenincluded = true なら、自票が prover に提示されたことを意味します。

検証手順

  1. ジャーナルの正の整数 treeSize と 32 バイトの対象 root を認証済み context として使い、 bitIndex0 <= bitIndex < treeSize を満たすことを確認
  2. leafIndex = floor(bitIndex / 256)bitOffset = bitIndex mod 256leafCount = ceil(treeSize / 256) をクライアント側で導出
  3. leafChunk が 32 バイト、各 sibling hash が 32 バイトであることに加え、 auditPath の長さと left / right の並びが leafIndexleafCount から 一意に決まる tree shape と一致することを確認
  4. 最終 chunk の treeSize より後ろにある未使用 bit がすべて 0 であることを確認
  5. チャンクのリーフハッシュを計算: SHA-256(0x00 || "stark-ballot:leaf|v1" || chunk)
  6. Merkle パス(auditPath)に沿ってルートまで再計算:
    • 兄弟の位置が leftSHA-256(0x01 || sibling || current)
    • 兄弟の位置が rightSHA-256(0x01 || current || sibling)
  7. 計算されたルートが、kind に対応するジャーナル上のルートと一致するか確認
    • kind=includedincludedBitmapRoot
    • kind=seenseenBitmapRoot
  8. 以上がすべて成功した場合にのみ、チャンクから抽出したビット値を個票の状態として解釈
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 ゲストプログラム内で計算され、ジャーナルにコミットされます。

ゲストプログラムは以下の手順を実行します:

  1. 各投票に対してコミットメントの再計算と包含証明の検証を実施
  2. prover に提示された投票インデックスを seenBitmap に記録
  3. 検証に成功して集計対象になった投票インデックスを includedBitmap に記録
  4. それぞれのビットマップを LSB-first でパッキングし、32 バイトチャンクに分割
  5. CT スタイルのハッシュ規則で Merkle ルートを計算
  6. seenBitmapRootincludedBitmapRoot をジャーナルにコミット

この計算はゲスト内で行われるため、STARK 証明がビットマップの正しさも保証します。 サーバーが事後的にビットマップを改ざんしても、ジャーナルのルート値と一致しなくなるため検出されます。

サーバーのビットマップデータ管理

現行 method 14 のジャーナルは includedBitmapRootseenBitmapRoot の両方を必須で コミットします。full bitmap 自体はジャーナルの公開出力ではありません。production host は admitted prover input から includedBitmapseenBitmap を再構成し、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_includednot_run(証拠不足による fail-closed)。 例:
    • private bitmap sidecar が無い
    • 保存や復元時の root 一致ゲートで採用されなかった
    • cast-time 証跡(voteReceipt / userVote.proof)が store から再構成できず voteReceipt.bulletinIndex が確定しない
  • 採用後にクライアント側の root 照合が失敗counted_my_vote_includedfailed。 サーバーが返した 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 アーキテクチャ を参照してください。

関連する章

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
コンポーネント言語責務
ゲストプログラムRustzkVM 内で投票を検証して集計し、結果をジャーナルにコミットする
ホストプログラム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 公式ドキュメントに基づく実装上の要点です。

  1. 実行モデル: ゲストは RV32IM として決定論的に実行され、外部との境界は syscall (ecall) で扱う
  2. 証明の分割: 長い実行はセグメント証明に分割される(大規模実行を扱うため)
  3. 再帰合成と圧縮: SDK は再帰合成や succinct / Groth16 への圧縮をサポートするが、本 PoC は Composite receipt のまま配布する
  4. 検証 API: Receipt::verify(image_id) が、証明本体と Image ID の束縛を同時に検証する
  5. 公開出力の束縛: journal は証明に束縛されるため、検証成功後に改ざんできない

ジャーナルとレシート

  • ジャーナル: ゲストが公開出力としてコミットするデータ(検証済み集計、除外情報、入力コミットメントなど)
  • レシート: ジャーナルと STARK 証明(seal)のペア。 検証成功時、ジャーナルは正しいゲスト実行結果として受理できる
flowchart TB
  R[レシート]
  R --> S[Seal<br/>STARK 証明]
  R --> J[ジャーナル<br/>公開出力]

ジャーナル各フィールドの定義と検証チェックとの対応は ゲストプログラム > ジャーナル出力 を参照してください。 excludedSlotsrejectedRecords の使い分けは スロット / レコード分離モデル を参照してください。

数学ミニ補足(読み飛ばし可)

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)=0h 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 == 0totalExpected == 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 パスと選挙メタデータ)を受け取り、以下の処理を行います:

  1. 各投票の正当性検証(コミットメント再計算 + 包含証明)
  2. 有効投票の集計
  3. カウント状態と提示状態のビットマップ計算
  4. 入力コミットメントと STH ダイジェストの計算
  5. 結果のジャーナルへのコミット

ゲスト内の処理はすべて STARK 証明に含まれるため、出力(ジャーナル)の正しさはゲストロジックに対して暗号学的に保証されます。

入力構造

ゲストプログラムが受け取る入力(AggregatorInput)の構造を示します。

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

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

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

処理パイプライン

ゲストプログラムの処理は、入力検証、投票検証と集計、出力構築の 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 が 0
  • timestamp が 0
  • tree_size > 1,000,000
  • total_expected > 1,000,000
  • votes.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 上の indexu32rejectedRecords
2インデックス重複既に処理済みの index(2 番目以降)rejectedRecords
3選択肢範囲choice0..=4(A..E)の外rejectedRecords + seenBitmap 反映
4コミットメント照合再計算したコミットメントが入力の commitment と不一致rejectedRecords + seenBitmap 反映
5コミットメント重複既に処理済みのコミットメント値(範囲内かつ初出スロットでも無効化)rejectedRecords + seenBitmap 反映
6RFC 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"]
指標条件意味
validVotes6 段階検証をすべて通過した範囲内かつ初出の投票集計に含まれたスロット数
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 フィールド
missingIndicesmissingSlots
invalidIndicesinvalidPresentedSlots
countedIndicesvalidVotes
excludedCountexcludedSlots

rejectedRecords は record 単位の新設カウントで、旧 invalidIndices の mirror は invalidPresentedSlots 側。

ジャーナル出力

ゲストプログラムがジャーナルにコミットする出力構造(VerificationOutput)を示します。

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

ジャーナルの信頼モデル

ジャーナルの各フィールドは、対応する STARK 証明により「ゲストプログラムが正しく計算した結果」であることが保証されます。

ジャーナル項目STARK 証明で保証される内容
verifiedTally有効投票のみを正しく集計した結果である
excludedSlots未提示または未計上のスロット数がゲストの計算結果と一致する
rejectedRecords却下されたレコード数がゲストの計算結果と一致する
inputCommitmentゲストが処理した入力データを正準エンコードで束縛した値である
seenBitmapRootprover に提示された範囲内かつ初出のインデックス集合から計算したルートである
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 を先頭に付与した上で、electionIdbulletinRoottreeSizetotalExpectedvotesCount と各投票の 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 は、選択肢 AE、ユーザー 1 票、 ボット 63 票、合計 64 slot、Merkle tree depth 6 に固定されています。 default config の authority は packages/zkvm-contract/src/zkvm/contract-constants.tspackages/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

入力構築の主な処理は次のとおりです。

  1. 掲示板の最新 STH スナップショット取得: ルートハッシュ、ツリーサイズ、タイムスタンプを取得します。
  2. 選挙設定の整合性確認: electionConfigelectionConfigHash が一致することを確認します。
  3. 投票データの変換: 各投票の選択肢を整数に変換します(A=0, B=1, C=2, D=3, E=4)。
  4. Merkle パスの解決: 各投票について、掲示板から最新の包含証明を取得します。
  5. 総投票数の設定: 選挙設定の 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 プログラムです。 証明モードでは次の処理を行います。

  1. JSON 形式の入力ファイルを読み込みます。
  2. JSON のバイト配列表現を Rust の固定長配列型へ変換します(Vec<u8>[u8; 16/32])。
  3. ExecutorEnv に入力をシリアライズして設定します。
  4. デフォルトプローバーを使用して zkVM ゲストを実行します。
  5. レシート(STARK 証明 + ジャーナル)を取得します。
  6. ジャーナルをデコードし、出力ファイルに書き出します。

入力 JSON は TypeScript 側のエグゼキューターが事前に正規化して生成します(UUID/ハッシュ文字列をバイト配列へ変換)。

Image ID の確認だけを行う場合は host --print-image-id [--json] を使います。 このモードでは入力ファイルを読まず、証明生成やアーティファクト出力も行いません。 --json 付きでは imageIdmethodVersion を含む 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.jsoncounted bitmap の厳密 artifact(includedBitmapRoot と対応)
*-seen-bitmap.jsonpresented 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.jsonpublic-input.jsonelection-manifest.jsonclose-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.jsonbundle.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 codeJSON レポート
success0stdout または --output
引数不正、bundle 不在など1出力しない
dev_mode2stdout または --output
failed3stdout または --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 なら truestatus とは別に、入力 receipt の生の種別を示す診断信号として使う
errors文字列[]診断文字列の配列。空の場合は省略される

errors は固定のエラーコード一覧ではなく、実装が積む自由形式の診断文字列です。

ステータスの意味づけ

ステータス意味
successSTARK 検証が成功し、Image ID も一致
failedImage 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 での流れは次のとおりです。

  1. この POST は verifier-service の完了を待たず、verificationResult.status=running を保存して SQS work を発行する
  2. verification-worker が SQS event を受け取り、S3 bundle を検査・展開して verifier-service を実行する
  3. 検証が完了した場合は、verification.json の report locator と report を含む terminal verificationResult を finalization-scoped な独立 record としてセッションストアへ保存する
  4. S3 の取得、bundle の検査、verifier-service の起動、artifact の upload などの infrastructure failure は SQS で再試行する
  5. 3 回目の受信でも失敗した場合は terminal failed と internal-only の failureCategory を保存する。この終端失敗には verifier report がないため、report locator も保存しない

既存の verificationResult.status ごとの POST /api/sessions/:sessionId/finalizations/:finalizationId/verification の挙動は次のとおりです。

既存 statusPOST の挙動
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_verifySTARK 証明が暗号学的に有効であるか

stark_image_id_match は、verifier report の expected_image_idreceipt_image_id が一致することを検証します。 検証パイプラインはさらに、claimed 側および comparison 側の Image ID とも整合することを確認します。

これらのチェックが両方成功した場合に限り、「STARK Verified」のステータスが付与されます。 これは STARK 検証段階のステータスであり、全体の Verified 判定とは区別されます。 詳細は 4 段階検証モデル を参照してください。

セキュリティ上の考慮事項

サーバー側検証の信頼境界

検証サービスはサーバー側で実行されるため、クライアントはサーバーの検証結果を信頼する必要があります。 この PoC における信頼モデルは次のとおりです。

  • STARK 証明自体は秘密データを含まない検証データ: レシートと Image ID があれば、第三者が独立に検証可能
  • 検証サービスは利便性のための委任: ブラウザ上で RISC Zero の検証ロジックを実行することは現時点では実用的でないため、サーバー側で検証する。実用的になれば、クライアント側のみで完結させることも理論上は可能
  • 配布対象アーカイブ: レシートと public-input.jsonZIP ローカル検証(Ubuntu) の手順で独立検証できる

verification.json の非公開性

verification.jsonbundle.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 とアーキテクチャ

本システムでは、同一バージョンのゲストに対して defaultx86_64 の 2 つの variant authority をマッピング上で管理します。 各 authority は次の排他的な状態です。

  • state: "confirmed": imageId と由来を示す provenance を持つ
  • state: "unconfirmed": requiredEvidence: "accepted-prover-release" だけを持ち、Image ID として解決できない
variant / アーキテクチャ取得元のビルド
default / ARM64target-account prover image build
x86_64local / 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、説明、機能リストを記録します。

フィールド説明
mappingTypeimage-id-mapping
current現在有効な method version のキー
mappings.*.methodVersionゲストプログラムのバージョン番号
mappings.*.variantsdefaultx86_64 の authority
variants.*.stateconfirmed または unconfirmed
variants.*.imageIdconfirmed の場合だけ存在する Image ID
variants.*.provenanceconfirmed の場合だけ存在するビルドまたは accepted-release の由来
variants.*.requiredEvidenceunconfirmed の場合だけ存在する accepted-prover-release
description / featuresmethod version の説明と機能リスト
deprecated / metadata非推奨 version とファイル全体の管理メタデータ

current と deprecated の扱い

マッピングファイルは、現在信頼するバージョンと、必要な場合に限って非推奨バージョンを保持します。 current フィールドは現在有効なバージョンを指し、deprecated フィールドは過去のバージョンを列挙します。 全 method mapping は両 variant の authority を持ちますが、両方が confirmed とは限りません。

現行実装では current14 で、deprecated は空です。 明示的な trust rotation により v13 以下の mapping は削除されており、現行 mapping からは解決できません。 v14 の mapping は variants.defaultvariants.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_VARIANTdefault または 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_64EXPECTED_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 を更新する必要があります。

  1. ゲスト変更と authority schema を Commit A に入れ、target defaultunconfirmed にする
  2. 管理された Prover lane でイメージを 1 回ビルドし、push 前後の host --print-image-id --json が一致することを確認して release を昇格する(candidate build と promotion の AWS 側の詳細は イメージ署名 を参照)
  3. accepted Prover release の immutable key / VersionId / body SHA-256 を確認する
  4. Commit B で defaultconfirmed にし、accepted release の Image ID、image digest、candidate/release/Toolchain record checksum を provenance に記録する
  5. 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 になりません。verificationStatusstark_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 ではありません。

この部の章

読み順は次のとおりです。

想定読者と前提

  • 想定読者:/verify 画面の最終判定ロジックを把握したい監査者と実装者
  • 前提:暗号プロトコルzkVM 設計 の概要を読み終えていること

この部で扱わないもの

関連する章

設計と実行フロー

この章では、検証パイプラインの設計原則と、リクエストから判定までの実行フローを説明します。

設計原則

本システムの検証パイプラインは、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 証跡voteReceiptuserVote.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 に補正します。
    • サーバーは verificationStatusfail-closed に補正します。unsupported な verifier status でも verificationSteps / verificationChecks を含む 200 応答を返します。
  • 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 へ進みます。

  1. /result は正準な finalization snapshot をクライアント状態に保存します。 「検証へ進む」を押すと verificationRequestedAt を保存し、必要なら POST /api/sessions/:sessionId/finalizations/:finalizationId/verification を非同期に先行起動します(完了を待たずに /verify へ遷移します)。
  2. /verify はその継続状態がある場合に検証シーケンスを続行します。 STARK が未開始ならシーケンス内で起動できます。
  3. 継続状態がなく STARK が not_run のまま直接 /verify へアクセスした場合は、自動続行せずブロックします
  4. /verify の UI シーケンスは、step を順に見せる前に STARK が terminal status に到達するまでポーリングします。 timeout や transport failure は STARK failure として扱われます。
  5. UI の最終判定が確定し、pending check がなく、server response が検証済みで、STARK status が terminal なら、/verifyPOST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations を best-effort で送信します。 current finalizationId は 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 1Cast-as-Intendedクライアント(/verify 画面でローカル再計算)
Stage 2Recorded-as-Castサーバー(finalization-scoped verification GET
Stage 3Counted-as-Recordedサーバー(finalization-scoped verification GET
Stage 4STARK Verificationサーバー(同じ resource への POST

各ステージの導出ルール(required チェック群からの集約、STH source 設定時の昇格、ガード条件)は ゲーティングロジック を参照してください。

検証チェック数

パイプライン全体で 22 個の検証チェックが定義されており、各チェックには一意の ID が割り当てられています。 チェック ID の単一ソースは packages/verification/src/verification/verification-checks.ts です。

段階チェック数
Cast-as-Intended4
Recorded-as-Cast6
Counted-as-Recorded10
STARK Verification2
合計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.voterEvidenceavailability: "available" のときに、その voteReceipt から取得します。

証拠説明
選挙 IDセッション作成時に確定した UUID
選択肢投票者が選択した値(A〜E)
乱数投票時にクライアントが生成した 32 バイト乱数
投票レシートcommitment, voteId, bulletinIndex, bulletinRootAtCast を含む

cast-time 証跡を再構成できない場合、data.voterEvidenceavailability: "unavailable" と reason を返し、投票レシートは得られません(取得と fail-closed 挙動の正本は 設計と実行フロー)。

失敗モード

症状原因深刻度
ローカル証拠の欠落localStorage 消去や別端末アクセスで投票時データを復元できない検証不能
投票レシート証跡の欠落store から cast-time 証跡を再構成できず、voterEvidence が unavailable検証不能
コミットメント不一致投票時データと投票レシートの不整合、またはエンコーディングの不整合重大
選択肢の範囲外不正な入力(AE の範囲外)重大
乱数フォーマット不正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/verificationdata です。

証拠取得元説明
包含証明の検証結果data.verification.checksサーバー側で RFC 6962 包含証明を評価したチェック結果
整合性証明の検証結果data.verification.checksサーバー側で RFC 6962 整合性証明を評価したチェック結果
投票時のルートハッシュdata.voterEvidence.voteReceipt.bulletinRootAtCastvoterEvidence.availability="available" のときの投票受理時ツリールート
投票時のツリーサイズ(oldSize)data.voterEvidence.userVote.proof.treeSizevoterEvidence.availability="available" のときの整合性証明の oldSize
最終ルートハッシュ/最終ツリーサイズdata.proofAuthority.bulletinRoot / data.proofAuthority.treeSize集計時の proof-bound な最終状態
独立検証用の包含証明材料(任意)/api/sessions/:sessionId/votes/:voteId/inclusion-proofcapability で保護された、個別に包含証明を再検証するための材料
補助 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-closedmissing_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=journaldata.journal.valuedata.journal.representation="included" のときの集計結果、除外情報、bitmap root など
集計サマリー(通常応答)同じ verification resource の data.proofAuthoritymissingSlots / invalidPresentedSlots / rejectedRecords / excludedSlots / totalExpected / treeSize など
公開入力サマリーサーバー内部評価用public-input.json 相当から組み立てた、秘密データを含まない入力要約
選挙マニフェスト公開監査アーティファクト(election-manifest.jsonelectionIdelectionConfigHash を束縛する公開可能アーティファクト
締切ステートメント公開監査アーティファクト(close-statement.jsonlogId / treeSize / bulletinRoot / timestamp / sthDigest を束縛する公開可能アーティファクト
ビットマップ証明材料/api/sessions/:sessionId/finalizations/current/bitmap-proofs/:kind/:voteIndexkindincludedseen を使い分け、自分の index が counted されたことや prover に提示されたかを説明する材料
ビットマップルートジャーナル / data.proofAuthorityzkVM ゲストが計算した includedBitmapRootseenBitmapRoot

公開入力サマリーはサーバー内部表現であり、レスポンスにそのまま含まれません。 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
ツリーサイズの不一致totalExpectedtreeSize が異なる(暗黙の除外、または 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サーバー側で解決ゲストプログラムの暗号的識別子(解決順は チェック一覧 参照)
ホスト主張値と比較用メタデータ検証コンテキストと reportimageId, journal.imageId, verificationReport.receipt_image_id を相互照合し、主張の食い違いを検出

開発モードの検出

RISC0_DEV_MODE=1 で生成された dev-mode receipt はフェイクレシートであり、本物の STARK 証明ではありません。 dev_mode ステータスの検出、正規化、Demo Onlydemo_only)表示のルールは ゲーティングロジック を参照してください。 profile ごとの帰結は次節の失敗モード表にまとめています。

失敗モードと非 production evidence

症状原因影響
Image ID 不一致マッピングが古い、ホスト主張値が誤っている、またはプローバーイメージが異なる重大。Verification Failed
レシート検証失敗証明が暗号学的に無効重大。Verification Failed
開発モード検出非 production profile でフェイクレシートを検出Demo OnlyVerified をブロック
未許可の開発モード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 から取得する、秘密データを含まない検証用データ
zkzkVM が束縛した公開証拠(ジャーナル、receipt 検証結果、bitmap root に基づく証明など)
demodev-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カテゴリ証拠重要度派生元
1cast_receipt_presentCastlocalrequired
2cast_choice_rangeCastlocalrequired
3cast_random_formatCastlocalrequired
4cast_commitment_matchCastlocalrequired
5recorded_commitment_in_bulletinRecordedpublicoptionalrecorded_inclusion_proof
6recorded_index_in_rangeRecordedpublicrequired
7recorded_root_at_cast_consistentRecordedpublicoptionalrecorded_consistency_proof
8recorded_inclusion_proofRecordedpublicrequired
9recorded_consistency_proofRecordedpublicrequired
10recorded_sth_third_partyRecordedpublicoptional
11counted_input_sanityCountedpublicrequired
12counted_unique_indicesCountedpublicrequired
13counted_unique_commitmentsCountedpublicrequired
14counted_tally_consistentCountedzkrequired
15counted_missing_indices_zeroCountedzkrequired
16counted_expected_vs_tree_sizeCountedzkrequired
17counted_election_manifest_consistentCountedpublicrequired
18counted_close_statement_consistentCountedpublicrequired
19counted_my_vote_includedCountedzkrequired
20counted_input_commitment_matchCountedpublicrequired
21stark_image_id_matchSTARKzkrequired
22stark_receipt_verifySTARKzkrequired

Cast-as-Intended(4 チェック)

投票者の意図通りにコミットメントが生成されたかを検証するチェック群です。 ブラウザはこのカテゴリを、capability 保護された GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationvoterEvidenceavailable の場合に得られる voteReceipt と、保存済みのローカル投票意図 (electionId / myVote / myRand)から評価します。API 応答上の Cast チェックは いったん not_run とし、ブラウザがローカルで再計算した結果を overlay します。

ID説明証拠種別重要度
cast_receipt_present投票レシートが存在し、voteId とコミットメントを含むlocalrequired
cast_choice_range選択肢が有効範囲内(A〜E)localrequired
cast_random_format乱数が 32 バイトの 16 進数文字列localrequired
cast_commitment_match投票時データから再計算したコミットメントが投票レシートと一致localrequired

判定ロジックの詳細

cast_receipt_present

voteReceipt が存在し、voteIdcommitment フィールドが存在することを確認します。 このチェック単体では UUID/hex 形式までは検証しません。

cast_choice_range

投票時データの選択肢が AE のいずれかであることを確認します。 範囲外の値は不正な入力として failed となります。

cast_random_format

投票時データの乱数が 32 バイト(64 文字)の 16 進数文字列であることを確認します。 0x プレフィックスの有無は正規化により吸収されます。

cast_commitment_match

投票時データから再計算した投票コミットメントが投票レシートの commitment 値と一致することを確認します。 再計算規則(ドメインタグ、正準フォーマット)は コミットメントスキーム を参照してください。


Recorded-as-Cast(6 チェック)

コミットメントが掲示板に正しく記録されたかを検証するチェック群です。

ID説明証拠種別重要度
recorded_commitment_in_bulletinコミットメントが掲示板ツリーに存在するpublicoptional
recorded_index_in_range掲示板インデックスが 0 以上かつツリーサイズ未満publicrequired
recorded_root_at_cast_consistent投票時のルートが最終ツリーの正当なプレフィックスであるpublicoptional
recorded_inclusion_proofRFC 6962 包含証明が暗号学的に検証成功publicrequired
recorded_consistency_proofRFC 6962 整合性証明が暗号学的に検証成功publicrequired
recorded_sth_third_party第三者 STH ソース間で合意が成立(比較可能応答間)publicoptional

判定ロジックの詳細

recorded_inclusion_proofrecorded_consistency_proofcast-time 証跡voteReceiptuserVote.proof)の存在を前提とします。 両チェックは、まず cast snapshot の一貫性(leafIndextreeSizebulletinRootAtCast が receipt と矛盾しないこと)を確認してから個別の検証に進みます。 証跡が揃わない場合はどちらも not_run となり、全体判定は fail-closedmissing_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 の treeSizevoteReceipt.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_runmalformed または 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公開入力サマリーが有効publicrequired
counted_unique_indices入力中の全インデックスが一意publicrequired
counted_unique_commitments入力中の全コミットメントが一意publicrequired
counted_tally_consistentclaimed tally と zkVM の検証済み集計が一致(fallback: 合計整合)zkrequired
counted_missing_indices_zero解決済み fail-closed exclusion count(excludedSlots 優先)が 0zkrequired
counted_expected_vs_tree_sizetotalExpected がツリーサイズと一致zkrequired
counted_election_manifest_consistentelection-manifest.json が自己整合し、選挙 ID / electionConfigHash と一致publicrequired
counted_close_statement_consistentclose-statement.json が自己整合し、log/tree/timestamp/root/sthDigest と一致publicrequired
counted_my_vote_includedビットマップ証明により自分のインデックスが counted 側に含まれたことを確認zkrequired
counted_input_commitment_match公開入力から計算した入力コミットメントがジャーナルの値と一致publicrequired

判定ロジックの詳細

counted_input_sanity

public-input.json 相当から組み立てた公開入力サマリーが存在し、スキーマ検証に成功していることを確認します。 さらに、treeSize が正の整数、votesCount <= treeSizebulletinRoot が 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 とします。

  1. journal がある場合: journal.excludedSlots を proof-bound な除外数として使用し、excludedSlots / missingSlots / invalidPresentedSlots / validVotes が非負整数であることも先に確認する
  2. journal がない場合: excludedSlotsmissingSlots + invalidPresentedSlots の順で探索

rejectedRecords は説明用の補助値で、この判定には使いません。 旧フィールド (excludedCount / missingIndices / invalidIndices) は fail-closed 補正経路でのみ参照され、本判定では使用しません。

counted_expected_vs_tree_size

totalExpected(期待される投票数)が掲示板のツリーサイズと一致することを確認します。 不一致は、暗黙の投票除外を示す可能性があります。

counted_election_manifest_consistent

election-manifest.jsonelectionConfigHash を再計算し、manifest 自身の宣言値と一致することを確認します。 そのうえで electionIdelectionConfigHash を、セッション値、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_matchverifier-confirmed な Image ID と expected / host-side metadata が整合zkrequired
stark_receipt_verifySTARK レシートが暗号学的に検証成功zkrequired

判定ロジックの詳細

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 表示
Verifiedrequired 条件が満たされ、optional チェックの劣化もない(fully_verified緑色
Verification Failed証明失敗、票除外、Recorded/Counted/Cast の必須失敗、または公開集計値と検証済み tally の不一致が確定した場合赤色
Warningrequired チェックが進行中、証拠不足がある、または optional チェックのみ劣化している場合黄色
Demo Onlyrequired 条件は満たしたが dev-mode receipt を含む場合(demo_only黄色

Verified の必要条件は 不変条件のまとめ に集約しています。 要点は、既知の required check がすべて存在してすべて success であること、票除外(excludedSlots > 0)がないこと、dev-mode receipt を production STARK proof として扱っていないことです。

ステータスの判定順序

優先順判定条件最終ステータス
1required チェックに pending / running があるWarning (in_progress)
2STARK 証明系ロールが failedVerification Failed
3completeness ロールが faileduser_vote_excluded / votes_excluded / votes_excluded_unknownVerification Failed
4Recorded-as-Cast の required チェックが failedVerification Failed
5tally consistency だけが失敗し、proof / completeness / user inclusion / input integrity / Recorded required が成功Verification Failed (published_tally_mismatch)
6Counted-as-Recorded または Cast-as-Intended の required チェックが failedVerification Failed
7(a) required に not_run がある (b) 必須ロール不足 (c) 既知チェックと混在する未知チェック (d) required 定義の未解決のいずれかWarning (missing_evidence)
8dev-mode evidence がある、または check evidence に demo があるDemo Only (demo_only)
9optional チェックに 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 チェックへの反映
runningpending
not_runnot_run
failedfailed
successゲートなしで通常評価

core evaluator では、dev_mode は事前に success または not_run に正規化されてから zkGate に入力されます。 一方、現行の GET /api/sessions/:sessionId/finalizations/:finalizationId/verification の表示用ステータス組み立てでは、dev mode が許可されていない場合は fail-closedfailed として反映されます。

ステップとチェックの対応関係

UI に表示される 4 つのステップは、現行実装では 22 個のチェック定義から派生します。

ただし、verificationSteps[].status は「その stage で required 扱いになるチェック群」から導出されます。 verificationSteps[].inputs は stage 内の 全チェック定義 から集約されます。

ステップrequired として集約されるチェック ID
Cast-as-Intendedcast_receipt_present, cast_choice_range, cast_random_format, cast_commitment_match
Recorded-as-Castrecorded_index_in_range, recorded_inclusion_proof, recorded_consistency_proof、および STH source 設定時の recorded_sth_third_party
Counted-as-Recordedcounted_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 Verificationstark_image_id_match, stark_receipt_verify

補足:

  • recorded_commitment_in_bulletinrecorded_inclusion_proof から、recorded_root_at_cast_consistentrecorded_consistency_proof から導出される表示用チェックです。 チェック一覧には現れますが、単独では step status を決定しません。
  • recorded_sth_third_party は通常は optional ですが、STH source が設定されている場合だけ required に昇格し、Recorded-as-Cast の step status と最終判定をブロックし得ます。

ステップのステータスは、required 扱いになったチェックのステータスから次の順序で集約されます。

集約ルール条件
failedrequired チェックのいずれかが failed
runningfailed がなく、required チェックのいずれかが running
pendingfailed/running がなく、いずれかが pending
successrequired チェックがすべて success
not_run上記のいずれにも該当しない

さらに現行実装には、単純集約だけではない 3 つの補正があります。

  • counted_as_recordedjournal が存在しない場合、required チェックに failed がない限り not_run に補正されます。
  • recorded_as_castuserVote.proof.treeSize がない場合、not_run に補正されます。
  • GET /api/sessions/:sessionId/finalizations/:finalizationId/verificationcastSource='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.jsoncounted された index を説明する private bitmap artifact
seen-bitmap.jsonprover に提示された index を説明する private bitmap artifact
failure-marker.jsonasync prover の bounded category だけを持つ private diagnostic

input.jsonverification.jsonincluded-bitmap.jsonseen-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/bundlepublic bundle.zip
GET /api/sessions/:sessionId/finalizations/:finalizationId/artifacts/reportprotected 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.jsonzkVM 検証に使う秘密データを含まない検証用レコード第三者が入力コミットメントを再計算するため
election-manifest.json選挙設定の公開監査用スナップショット選挙設定と electionConfigHash の照合
close-statement.json集計締切時点のログ境界を表す公開監査レコードlogIdtreeSizebulletinRoot の照合
receipt.jsonホストが出力する { receipt, image_id } ラッパー JSON第三者がレシートを独立に検証するため
journal.jsonzkVM ゲストの公開出力(集計結果、除外情報、ビットマップルート等)集計結果と整合性データの確認
metadata.jsonバンドルの作成日時、セッション ID、メソッドバージョン(sync のみ)バンドルの来歴追跡
sth.json第三者 STH 検証のスナップショット(任意)STH 合意の再現可能な証拠
consistency-proof.jsonRFC 6962 整合性証明(任意)追記専用性の独立検証

receipt.json の存在だけでは成功扱いになりません。dev-mode(RISC0_DEV_MODE=1)receipt の扱いは 公開境界 と同じ規則です。 metadata.json は sync bundle で必須です。sth.jsonconsistency-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集計時のタイムスタンプ
methodVersionzkVM メソッドバージョン
votes各投票のインデックス、コミットメント、Merkle パスの配列

votes 配列の内容から、第三者は入力コミットメントを再計算できます。選択肢と乱数はこのファイルに含まれません。

public-input.jsoninputCommitment は同一ではありません。 後者が直接束縛するのはこのレコードの一部です。 計算対象フィールドと非対象フィールドの整理は 入力コミットメント を参照してください。

同期バンドルと非同期バンドルの違い

証明生成には、同期モード(local profile の TypeScript API プロセスが生成)と 非同期モード(ECS Fargate コンテナの entrypoint.sh が生成し、S3 経由でコールバック Lambda がセッションに反映)の 2 つのパスがあります。 両モードとも、public-input.jsonelection-manifest.jsonclose-statement.jsonjournal.json と正準な proof-bound data との整合性検査を通過した場合にのみ bundle 化されます。 不一致があれば fail-closed で処理を中断します。

項目同期モード非同期モード
実行環境local profile の TypeScript API プロセスECS Fargate コンテナ
input.json生成される(非公開保存)ワーク入力として S3 に置かれる(配布対象外)
public-input.jsonTypeScript で生成entrypoint.sh 内で生成
election-manifest.jsonTypeScript で生成entrypoint.sh 内で生成
close-statement.jsonTypeScript で生成entrypoint.sh 内で生成
journal.jsonTypeScript で生成*-output.json から bundle.zip 用に生成
receipt.jsonホストの { receipt, image_id } 出力を保存*-receipt.jsonreceipt.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.zipshared manifest に基づき作成し、completed ZIP を admissionshared manifest の async member set に基づき作成し、completed ZIP を admission
保存先ローカルファイルシステムS3
配信方法capability 保護 API がローカルから読み出して返すcapability 保護 API が S3 から読み出して返す。大きい bundle は range response で配信

非同期バンドルの生成フロー

非同期モードでは、ECS Fargate コンテナの entrypoint.sh が次の手順でバンドルを構築します。

  1. S3 から入力 JSON をダウンロード
  2. ホストバイナリを実行し、レシートと出力を生成
  3. 出力から journal.json を変換生成
  4. 入力と出力から public-input.jsonelection-manifest.jsonclose-statement.json を構築
  5. 整合性検査(本節冒頭参照)を通過したものだけを bundle に含める
  6. shared manifest の async member set から bundle.zip を作る
  7. completed ZIP の member 構成と path を admission する
  8. 次を S3 にアップロード
    • ホストの生出力: *-receipt.json*-output.json
    • 公開可能アーティファクト: public-input.jsonelection-manifest.jsonclose-statement.json
    • 非公開の隣接 artifact: included-bitmap.jsonseen-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.jsonelection-manifest.jsonclose-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.jsonpublic-input.json の methodVersion および inputCommitment が一致し、election-manifest.jsonclose-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.zipfailure-marker.json が同時に生成されることを意味しません。
  • 非同期モードの S3 オブジェクト名は、コンテナ実行時に生成される一時入力ファイル名 inputBase に依存するため、固定の receipt.json / journal.json になりません。
  • target AWS profile の capability 保護 API は、sessionIdfinalizationId から導出した 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 段階モデルを把握していることを前提にします。

この部で扱わないもの

  • 実世界の投票システムに対する攻撃手法の一般論
  • 本番投票システム向けの脅威モデリングや対策ガイド
  • S2S4 を 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/finalizationsscenarioId を 1 つ受け取る
  • totalExpected は 64(ユーザー 1 + ボット 63)
  • 掲示板(CT Merkle)は追記専用で、シナリオ適用で既存エントリは削除しない
  • tamperModenone / input / claim の 3 種

実行モードと検証の前提

  • 本章は実 API 経路(POST /api/sessions/:sessionId/finalizationsfinalizeSessionHandlerfinalizeSessionUsecasefinalizeSync|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

シナリオ一覧表

シナリオ類型tamperModezkVM 入力
S0正常none元の 64 票
S1除外input63 票(ユーザー除外)
S2主張改ざんclaim元の 64 票
S3除外input63 票(ボット除外)
S4主張改ざんclaim元の 64 票
S5ランダム除外input63 票(通常)

シナリオ別に失敗するチェックは 検出メカニズム > シナリオ別の主な失敗チェック を参照してください。

S0: 正常(改ざんなし)

改ざんを適用しない基準シナリオです。

項目
tamperModenone
zkVM 入力票数64
claimed と verified一致
excludedSlots0

S1: ユーザー票の除外

ユーザー票(インデックス 0)を modifiedVotes から削除し、63 票を zkVM に渡します。

項目
tamperModeinput
zkVM 入力票数63
claimed と verified一致(どちらも 63 票入力ベース)
ジャーナル統計missingSlots=1, invalidPresentedSlots=0, excludedSlots=1

失敗するチェックは 検出メカニズム を参照してください。

S2: ユーザー票に関する主張集計の改ざん

ユーザー票に対する「主張集計(表示する tally)」のみ改ざんします。 zkVM には元の 64 票を渡します。

項目
tamperModeclaim
zkVM 入力票数64(元データ)
claimed と verified不一致(ユーザー選択肢が -1、別候補が +1)
excludedSlots0(通常)
inputCommitmentzkVM 入力由来のため通常は一致

失敗するチェックは 検出メカニズム を参照してください。

S3: ボット票の除外

現行実装ではボット票インデックス 1targetBotId 初期値)を削除し、63 票を zkVM に渡します。

項目
tamperModeinput
zkVM 入力票数63
claimed と verified一致(どちらも 63 票入力ベース)
ジャーナル統計missingSlots=1, invalidPresentedSlots=0, excludedSlots=1

S1 との違い

  • S1: ユーザー自身の未集計をビットマップで直接示せる
  • S3: ユーザー票は含まれるが、集計全体の完全性違反で検出される

S4: ボット票に関する主張集計の改ざん

1 票のボット票に関する「主張集計」だけを改ざんします。 zkVM 入力は元の 64 票のままです。

項目
tamperModeclaim
zkVM 入力票数64(元データ)
claimed と verified不一致(対象ボットの元候補が -1、別候補が +1)
excludedSlots0(通常)
inputCommitmentzkVM 入力由来のため通常は一致

S2 と同様に、改ざん対象は tally.counts 側です。失敗するチェックは 検出メカニズム を参照してください。

S5: ランダムな票の除外

現行実装では、64 票からランダムに 1 票を選び、modifiedVotes から削除します。 選んだ票の候補を別候補へ変える処理はありません。

S5 の処理

  • tamperMode は常に input のため、zkVM 入力は常に modifiedVotes が使われる
  • シナリオ変更は IGNORED として記録され、ignoredCount=1, recountedCount=0 になる
  • 元の掲示板インデックスは保存される(失敗するチェックは 検出メカニズム を参照)
項目
tamperModeinput
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 / recountedCounttamperSummarytamperedCount に反映
  • 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.checkscast_* は シナリオに関係なく 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なし正常系
S1counted_missing_indices_zeroユーザー票除外により excludedSlots=1
S2counted_tally_consistentclaimed tally と verified tally が不一致
S3counted_missing_indices_zero現行実装では botId=1 のボット票除外により excludedSlots=1
S4counted_tally_consistentclaimed tally と verified tally が不一致
S5counted_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 サーバー応答)

検証段階S0S1S2S3S4S5
Cast-as-Intendednot_runnot_runnot_runnot_runnot_runnot_run
Recorded-as-Castsuccesssuccesssuccesssuccesssuccesssuccess
Counted-as-Recordedsuccessfailedfailedfailedfailedfailed
STARK Verificationsuccesssuccesssuccesssuccesssuccesssuccess

この表は「シナリオ適用による典型挙動」を示します。 running や追加の not_run は、運用状態や証拠不足により別途発生します。 ここでの値はサーバーが返す verification.steps の状態です。 verifierResult.status は STARK receipt 検証の状態であり、overall verdict そのものではありません。

ブラウザ表示では 前提 の overlay を適用します。 典型フローでは overlay 後の Cast-as-Intended は success になり、ローカル証拠が欠けるか不整合なら not_run または failed のままで、最終表示は Verified になりません。

主要チェック ID マトリクス(STARK 解決後の raw サーバーチェック)

チェック IDS0S1S2S3S4S5
cast_commitment_matchnot_runnot_runnot_runnot_runnot_runnot_run
counted_tally_consistentsuccesssuccessfailedsuccessfailedsuccess
counted_missing_indices_zerosuccessfailedsuccessfailedsuccessfailed
counted_my_vote_includedsuccessfailed または not_runsuccesssuccesssuccess対象依存
counted_input_commitment_matchsuccesssuccesssuccesssuccesssuccesssuccess
  • 証拠不足時の counted_my_vote_includednot_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=inputmodifiedVotes、ジャーナル統計の扱い)はシナリオ一覧 > 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 テストの一般的な区分と、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 / E2Esession 作成から投票、集計、検証までの流れを検査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 による形式化 > 証明していないこと を参照してください。

関連する章

単体、結合、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-service client と 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:runVitest による単体テストと結合テスト
pnpm test:publicpublic snapshot 向けの安全なテスト subset
pnpm test:cli:mockmock zkVM / mock store で CLI voting flow を実行
pnpm test:e2e:mockPlaywright によるブラウザ E2E
pnpm test:e2e:axeaxe accessibility smoke
pnpm test:cli:real-devdevelopment evidence の real zkVM 接続 smoke
pnpm test:cli:real-prod:s0S0 の production STARK proof flow
pnpm formal:verifyLean build / formal vector drift guard
pnpm build:zkvmzkVM guest / host の build
pnpm build:verifier-serviceRust verifier-service の build
pnpm rust:verifier:testverifier-service の Rust tests
pnpm rust:zkvm:testzkVM / 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 devVITE_RUNTIME_PROFILE=local-demo で Vite dev server を起動する。server 側も使う場合は .env.localRUNTIME_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=productionpnpm 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 + rejectedRecords
  • invalidVotes = rejectedRecords
  • seenIndicesCount = validVotes + invalidPresentedSlots
  • validVotes + invalidPresentedSlots + missingSlots = treeSize
  • excludedSlots = 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.leanCheckStatus, SummaryStatus, SummaryTone, CheckId, CheckCategory, CheckRole, Criticality検証チェックと summary model の基礎型
JournalCounts.leanmissingSlotsOf, invalidPresentedSlotsOf, excludedSlotsOfzkVM journal の count 分解を Nat モデルで表す
VerificationSummary.leancheckDefinitions, isRequiredCheck, canFullyVerify, deriveSummaryModel/verify の最終判定に関わる fail-closed model
InputCommitment.leanCommitmentVote, InputCommitmentCase, canonical order, u16LE, u32LE, preimage encodinginput commitment の byte layout と順序安定性をモデル化
Bitmap.leanpackedByteCount, packedAddress, byteValueAt, packBitsLSB-first bitmap packing と bit address をモデル化
GuestModel.leanRejectReason, GuestVote, CandidateTally, GuestState, classifyVote, processVotes, guest boundszkVM guest の抽象 tally / rejection state machine

GuestModel.leanzkvm/methods/guest/src/main.rs を行単位で翻訳したものではありません。 外部的に重要な処理順序を抽象 state machine として表します。 具体的には、インデックス範囲、重複 index、選択肢、コミットメント、重複 commitment、包含証明、集計反映の順序をモデル化します。

Lean で証明していること

領域代表 theorem主張
Journal countexcluded_zero_implies_no_slot_loss, slot_partition_totalexcludedSlots = 0 なら missing / invalid presented が 0 になり、slot loss がない
Verification summaryfully_verified_implies_all_required_success, fully_verified_implies_no_unknown_checks, fully_verified_implies_required_roles_successfully_verified は required check 成功、unknown check 不在、重要 role 成功を要求する
Input commitmentcanonical_vote_order_total, canonical_encoding_permutation_invariantvote の入力順序に依存しない canonical encoding が定義されている
Bitmappack_bits_length, pack_bits_get_bitLSB-first packing の byte 数と bit 取得がモデル通りになる
Guest modelaccepted_votes_count_tally, valid_votes_count_accepted, processVotes_fold_invariant抽象 guest fold が tally / validVotes / seen index の不変条件を保つ
Guest completenesszero_exclusion_guest_model_completeexcludedSlots = 0 が guest model 上で missing / invalid presented の不存在につながる
Bounded countsno_overflow_under_guest_boundstreeSize と 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.jsonTypeScript summary testsLean summary model と deriveVerificationSummary の対応
verification-display-cases.jsonshared presentation-policy testUI が verified を誤表示しないことの drift guard
check-definitions.jsonTypeScript check-definition testcheck ID / category / role / criticality / required 条件の drift guard
input-commitment-cases.jsonTypeScript / Rust testscanonical order と pre-hash bytes の対応
bitmap-cases.jsonTypeScript / Rust testsLSB-first packing と bitmap behavior の対応
guest-model-cases.jsonRust 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:buildLean workspace を build する
pnpm formal:reportformal-report.json を再生成する
pnpm formal:report:checkreport が最新か確認する
pnpm formal:vectorsLean から generated vectors を再生成する
pnpm formal:vectors:checkgenerated vectors が最新か確認する
pnpm formal:audittheorem statement / dependency / proof hygiene audit を生成する
pnpm formal:audit:checkaudit artifact が最新か確認する
pnpm formal:test:tsLean vector を消費する TypeScript tests を実行する
pnpm formal:verifybuild、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 アカウント削除の進捗・完了状態

関連する章

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 の混在を拒否します。

ProfileStoreProof evidenceFinalizationArtifact主な用途
local-demomemorymocksynclocalVite browser mock API と高速 UI 開発
static-mock-e2efilemocksynclocalproduction-build static Hono / Playwright E2E
local-proof-developmentmemorydevelopmentsynclocalRust host 接続を使う fake receipt smoke
local-proof-productionmemoryproductionsynclocallocal production STARK proof
target-aws-asyncDynamoproductionasyncS3Lambda/SQS/Step Functions/ECS target runtime
target-aws-async-quiescedDynamoproductionasyncS3develop 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 の sessionIdX-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 レシートを検証します。検証結果は sessionIdfinalizationId で 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 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 ではない

環境分離

developmain は 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_PROFILElocal-demostatic-mock-e2elocal-proof-developmentlocal-proof-production)であり、置き換え済みの RISC0_DEV_MODE / USE_MOCK_ZKVM selector は拒否します。

項目developmain
private proof artifact S3 lifecycle7 日7 日
CloudWatch log retention(runtime / Container Insights / ops-ledger)90〜365 日(値は トポロジー > 主な CloudWatch ログ群180〜731 日(同左)
CloudTrailapp / 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 の責務として扱います。

全体構成図

AWS 全体構成図 図: STARK Ballot Simulator の AWS 全体構成(クリックで拡大)。

サービス一覧

この runtime が使う主要な AWS サービスと役割です。

App runtime

サービスリソース役割
CloudFront + S3static frontend / operator gateVite/React の静的配信、/api/* origin routing、SPA fallback、develop の signed-cookie gate
API Gateway (HTTP API)/api/{proxy+}capability 保護 API の公開入口
Lambdapublic-apiHono route inventory を実行する API runtime
DynamoDBsessions / votesprimary または検証済み replacement pair を選択し、セッション、投票、集計結果を永続化
DynamoDBrate-limit events / countersAPI rate limit state
DynamoDBprover semaphoreStep Functions が RunProver 前に取得する concurrency slot
Lambdaproof-dispatcherSQS 受信 → artifact key / pending state 検証 → Step Functions 起動
Lambdafinalization-writerStep Functions 結果を session state に反映
Lambdaverification-workerS3 bundle locator を受け取り verifier-service で STARK レシート検証を実行
Lambdaimage-signature-verifier選択された digest-pinned prover image と signing profile の暗号学的署名検証
Lambdasemaphore-janitorsemaphore slot cleanup と、検証済み EventBridge event の normalized operations ledger 記録
SQSprover-work + DLQ非同期証明リクエストのバッファリング
SQSverification-work + DLQdurable verification run のバッファリング
Step Functionsfinalization暗号学的署名検証 → semaphore 取得 → ECS 実行 → slot release / finalization writer
ECS Fargateprover taskzkVM host binary による STARK 証明生成
S3proof artifacts / static frontendprivate proof artifacts、public bundle.zip の読み出し元、frontend artifacts
SSM Parameter Storeruntime refsnon-secret runtime wiring と secret/config reference name の保持
EventBridgejanitor / operations-ledger rulesterminal status / 定期 sweep と、alarm / ECS / pipeline / deployment event の正規化
CloudWatchlogs / metrics / dashboardruntime / access logs、metric filters、passive / damped actionable alarm、ledger / query
SNSinvariant / ops-actionableverification verdict invariant の矛盾と、限定された持続的な運用 signal の通知
KMS + CloudFrontdevelop operator-gate keysetversioned 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

サービスリソース役割
ECRprover / RISC Zero toolchainプローバーコンテナと RISC Zero toolchain image の repository 管理
Lambda + EventBridge + CloudWatchECR retention controlleraccepted / rollback / running / in-progress reference を保護する reference-aware retention を日次実行し、専用 log に記録
S3versioned manifest storageimage / deployment manifest 系メタデータの非公開保存
Secrets Managerruntime secret containersapp runtime が参照する secret container の作成
SSM Parameter Storenon-secret config recordsTurnstile site key など、app runtime が参照する non-secret config
VPC / subnet / security groupprover networking inputsECS 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 + KMSstate / execution / retained authority storageTerraform state / lockfile、lane ごとの暗号化された execution artifact、retained deployable / acceptance evidence、accepted Terraform baseline / report の保護
CodePipelinecontrolled lanesBootstrap / Foundation saved-plan、Toolchain / Prover release、App full(normal / rollback)、App static-fast の分離された実行経路
CodeBuildroot / app build and deploy jobsBootstrap / Foundation / App の validation、saved plan / review / apply / smoke、App artifact build / deploy / acceptance
CodeBuildimage release jobsToolchain / Prover candidate build、Image ID / signing / scan evidence、accepted pointer promotion
CodeBuildreport-only jobsweekly / on-demand drift coordinator、Bootstrap / Foundation / App runner、on-demand rollback / recovery readiness
EventBridge Scheduler + SQSdrift schedule / missed-run queueweekly drift coordinator の起動と、起動できなかった run の暗号化済み queue への隔離
IAMcontrol-plane rolesroot、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 になりません。

AWS deployment control 図: GitHub から CodePipeline deployment lanes を経て app runtime に到達するデプロイ制御フロー(クリックで拡大)。

個別の承認手順、証跡、昇格判断の詳細は公開仕様の範囲外です。

Mode内容
Normalaccepted 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 nameLegacy/historical name
public-apihono-api
proof-dispatcherprover-dispatch-proxy
finalization-writerfinalize-callback-runner
verification-workerverifier-service-runner
image-signature-verifiercheck-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 ではありません。

関連する章

トポロジー

AWS 上のサービス配置とコンポーネント間の通信経路を示します。 このページは runtime の責務分担を説明します(個別環境の導入進捗は扱いません)。

本システムは次の 6 つの論理レイヤで構成されます。

レイヤ別トポロジー

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=runningfinalizationId、推定所要時間、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 familysessions table のキー例主な authority / field
Session identity{sessionId}SESSION_IDENTITY; identity に election config、electionIdlogId、作成時刻を固定
Session votingVOTING#{sessionId}SESSION_VOTING; bulletin root history、bot count、user participation、last activity、revision
Finalization lifecycleFINALIZATION_CURRENT#{sessionId} / FINALIZATION#...current pointer と finalization ごとの lifecycle record
Verification / evidenceVERIFICATION#... / OBSERVATION#... / BITMAP#...verification result、browser observation、finalization bitmap を別々に所有
AWS runtime / artifact deliveryFINALIZATION_RUNTIME#... / VERIFICATION_DELIVERY#...Step Functions / bundle metadata と s3BundleKey / s3ReportKey delivery metadata を semantic state から分離
Votevotes table の {sessionId} + {voteIndex}voteId、encrypted vote / randcommittimestamprootAtCastisUserVoteexpiresAt

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 に含まれる sessionIdfinalizationIdbundleKey、expected Image ID を使って private S3 bucket から bundle.zip を取得します。 その後、verifier-serviceReceipt::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-apiverification-workerproof-dispatcherfinalization-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-workerbundle.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 つです。

ログ群対象developmain
/aws/ecs/containerinsights/{project}-{account-alias}-{environment}-prover/performanceECS Container Insights7 日30 日
/aws/events/{project}-{account-alias}-{environment}-ops-ledgernormalized operations ledger365 日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 に含めません。

関連する章

非同期プローバー

このページは、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 deliverypublic の分類は 公開境界 の定義に従います)。

実行リソースと 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-demostatic-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 queuefinalize 要求を durable な dispatch 単位として保持し、dispatcher の再試行と DLQ 移動を担う
proof-dispatcher Lambdawork item、artifact key、session の pending state を検証し、Step Functions execution を開始する
Step Functionscryptographic image verifier、prover semaphore、ECS Fargate runTask.sync、成功、失敗、timeout の callback を順序づける
image-signature-verifier選択された digest-pinned prover image を固定された trust policy と verifier asset で暗号学的に検証する
prover semaphoreDynamoDB の単一 lock item で RunProver への同時進入数を制限し、capacity 待ちを Step Functions 内に閉じ込める
semaphore-janitor Lambdaterminal execution event と定期 sweep を使い、通常の release 経路で残った semaphore owner を補償的に解放する
ECS Fargate taskzkVM host を実行し、公開可能 artifact と private sibling artifact を private S3 に保存する
finalization-writer Lambdacallback payload と S3 bundle key を検証し、DynamoDB の session finalization state を更新する
bundle/report APIcapability 保護の bundle / report 配信(Bundle / report delivery

Finalize 受付

POST /api/sessions/:sessionId/finalizations は session-scoped な操作であり、path の sessionIdX-Session-Capability の検証を通過した場合だけ処理されます(契約は API リファレンス > セッション capability)。

非同期モードでは、API handler は同期的に証明を作らず、次の処理を行います。

  1. セッションが finalize 可能な状態か検証する。
  2. zkVM 入力を構築し、finalization state を pending として保存する。
  3. target AWS runtime では sessions/{sessionId}/{finalizationId}/input.json に zkVM 入力を保存する。
  4. SQS work queue に、正準 S3 key を含む prover work message を送る。
  5. 202 AcceptedpendingFinalizationResource を返す。現行 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 --> [*]

VerifyImageSignatureimage-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 取得後、MarkProverStartedfinalization-writer Lambda を呼び、store guard を通過した場合だけ runningstartedAt を記録します。 この 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_KEYAPI handler が保存した zkVM 入力の場所
OUTPUT_S3_BUCKET / OUTPUT_S3_PREFIXbundle と sibling artifact の保存先
EXPECTED_IMAGE_ID実行する guest image と照合する Image ID

コンテナの entrypoint は次の順序で処理します。

  1. private S3 から input.json を取得する。
  2. 入力 JSON の必須フィールドを検証する。
  3. zkVM host binary を実行する。
  4. host output から journal.json を構築する。
  5. public-input.jsonelection-manifest.jsonclose-statement.json を構築する。
  6. journal.json と公開監査アーティファクト の method version、input commitment、選挙情報が一致することを検証する。
  7. receipt.jsonjournal.jsonpublic-input.jsonelection-manifest.jsonclose-statement.json だけを含む bundle.zip を作る。
  8. 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.jsonseen-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 の値は configurationinputprover_executioninfrastructure_interruptionartifactuploadtimeoutunknown の閉じた集合に制限され、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 --> [*]
ステータス説明
pendingfinalize request は受理済みで、SQS dispatch、Step Functions setup、または semaphore capacity wait を待っている
runningsemaphore slot 取得後に prover 開始が admitted され、ECS task または task-start retry が進行中
succeededbundle の復元後、terminal result と bitmap availability、存在する admitted sidecar が原子的に保存された
failed署名検証失敗、prover task 失敗、artifact 復元失敗などで fail-closed に保存された
timeoutECS 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 target runtime の可観測性を公開仕様として説明します。 対象は、API、非同期 finalization、検証 worker、zkVM prover、deployment を横断して、 障害や予期しない結果を事後に説明するためのログ、メトリクス、検出、相関です。

最も重要な境界は、可観測性は検証判定の authority ではないことです。 ログ、メトリクス、alarm、notification の成否は検証チェックや user-facing verdict を変更せず、可観測性は判定後の事実と runtime の状態を調査するための evidence layer にとどまります。

このページの節構成は次のとおりです。

目的と非目的

可観測性の目的は、「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 Gatewayrequest count、5xx、latency、access logedge から API integration まで到達したか
Lambdaerrors、throttles、closed application eventhandler が失敗したか、実行前に抑制されたか
DynamoDBread / write throttlesession、vote、rate limit、semaphore の永続化が律速したか
SQSoldest-message age、DLQ depthprover / verification work が滞留または隔離されたか
Step Functionsfailed、timed out、execution historyfinalization orchestration が terminal failure になったか
ECS provertask stop、Fargate quota、bounded prover_summary証明生成が開始・終了し、どの failure category になったか
Verificationfailure category、required-check contradiction通常の検証失敗か、verdict pipeline 自体の矛盾か
Deploymentpipeline state change、accepted deploymentruntime の症状が変更時点と相関するか

Dashboard はこれらの入口をまとめますが、個々の user journey を一枚の graph だけで説明するものではありません。 調査では application event、AWS native metric、deployment evidence をそれぞれ確認します。

構造化ログと安全な相関

Producer ごとの形

Target runtime は producer ごとの実行環境を保ちながら、閉じた event と bounded field を使います。

ProducerLog shape
public-api など共通 logger を使う LambdaLambda JSON envelope の object-valued message 内に application record を格納
image-signature-verifier署名対象や credential を含まない closed result / duration bucket
API Gatewayrequest ID、route key、status、latency、integration error の access record
Step Functionsexecution data を含めない orchestration log
ECS proverstart / end、result、closed failure category、duration を持つ flat JSON summary
normalized operations ledgersource 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_admitteddispatch_droppedfinalize_state_transitionqueue_publishverification_run_summarybundle_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 で重複を除きます。

相関キー

KeyAuthority / 用途
request_idAPI Gateway payload の request ID。同期 request 内の trusted correlation
client_request_id制限された client input。trusted request_id とは分離
session_id_hashraw session ID を保存せず、runtime salt と domain separation を使う相関値
finalization_idSQS、Step Functions、ECS、writer、verification worker を結ぶ async correlation
deployment_idaccepted deployment と runtime / operations ledger を結ぶ immutable correlation
app_source_sharestricted operational log 内で deployment evidence と照合する source identifier

同期 HTTP request の外では、client-provided request ID を無理に引き回しません。 非同期処理へ移った後は session_id_hashfinalization_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 は扱いません。

そのうえで、可観測性の受入では次の状態を同一視しません。

  1. Defined:code / Terraform に schema、filter、alarm、subscription が定義されている
  2. Configured:対象 AWS environment に resource が構成されている
  3. Matched:実際の bounded event が deployed filter に一致し、想定 metric を生成した
  4. 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 の分類名です。 内訳は次の表のとおりです。

ClassCountNotification主な意味
DynamoDB read / write throttle10passive5 table の read / write capacity pressure
SQS DLQ depth diagnostic2passiveprover / verification work の隔離
SQS DLQ depth actionable2ops-actionable via rule同じ 2 DLQ の sustained condition
Step Functions failed / timeout2passivefinalization orchestration の terminal failure
Lambda errors6passive6 Lambda の handler failure
Lambda throttles6passivehandler 実行前の throttle
API Gateway 5xx diagnostic1passiveHTTP API integration failure
API Gateway 5xx actionable1ops-actionable via rulevolume-gated sustained 5xx ratio
Work queue age diagnostic2passiveprover / verification work の滞留
Work queue age actionable2ops-actionable via rule同じ 2 queue の sustained condition
Fargate on-demand vCPU quota1passiveprover task capacity pressure
Verification invariant1direct invariant SNSverdict pipeline の矛盾

Passive alarm は調査開始点であり、alarm action や自動 remediation を持ちません。 Missing metric data は、低トラフィックの demo runtime で障害と誤認しないよう notBreaching として扱います。 damped actionable alarm の条件と評価は次のとおりです。

Alarm条件評価
SQS DLQ depth(2 個)DLQ depth > 05 分 period × 3 回連続
Work queue age(2 個)oldest-message age > 9005 分 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 / conditionMetricDimension
dispatch_droppeddispatcher drop countclosed reason
terminal finalize_state_transitionfinalization terminal outcomeclosed reason
finalization_admittedfinalization admission sampleなし
verification_run_summaryverification runs by failure categoryclosed failure_category
fully_verified かつ failed_required_count > 0verification 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_INVALIDUSER_CANCELLEDINVALID_FINALIZATION_ARTIFACTfailed になったものは 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_changed
  • ecs_task_stopped
  • pipeline_execution_changed
  • deployment_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 ではなく、共通の tsevent_typeenvresource_namestate に加え、 event type ごとに次の exact key set を持ちます。

event_type共通 field 以外の追加 field
alarm_state_changedなし
ecs_task_stoppeddeployment_id
pipeline_execution_changedpipeline_execution_id
deployment_accepteddeployment_idpipeline_execution_iddeployment_modeplan_disposition

pipeline_execution_id は owned CodePipeline event と matching acceptance event の bounded lowercase UUID、 deployment_modenormal / rollbackplan_dispositionchanged / 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 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
署名処理の readinessECR 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 設定を別々の責務として扱います。 公開仕様では、個別の承認手順や内部証跡ではなく、どの情報がどの境界で使われるかを説明します。

領域責務
FoundationProver / Toolchain repository の保持と、Prover repository 限定の Signer profile / ECR registry signing configuration
Toolchain releasedigest 固定 image、scan、provenance(署名・暗号学的署名検証なし)
Prover candidate builddigest 固定 URI、Image ID / methodVersion の push 前後一致、scan PASS、managed-signing readiness の確認
Prover release authority承認済み candidate の immutable prover release record 保存と、CAS で更新する accepted prover pointer による選択
App verifier preflightimmutable verifier version の preflight alias による、選択した Prover digest の暗号学的検証
App deployment selectionaccepted pointer chain / preflight evidence の検証と、digest 固定 URI・expected Image ID・verifier authority の固定
Rejected selection pathsmanaged release lane 外の直接の CodeBuild output、latest.json、legacy SSM current pointer は選択に使用しない
App runtimedigest 固定 Prover image の runtime alias による実行前の再検証
verifier-service / UISTARK レシートの 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 に行います。

  1. 呼び出しの account、region、repository、digest、image URI、signing profile が互いに整合することを確認する
  2. ECR の DescribeImageSigningStatus を bounded retry / timeout で呼び、期待する profile が一意かつ COMPLETE であることを確認する
  3. packaged asset の allowlist、type、size、mode、checksum と、deployment が期待する asset manifest / trust root を検証する
  4. 対象 repository と signing profile に限定した strict trust policy を一時 workspace に作る
  5. notation verify <digest-pinned-image-uri> を bounded process として実行する
  6. 成功時は result = VERIFIEDmode = cryptographic、verifier version、trust-policy hash だけを caller に返す。失敗時は bounded reason code で拒否する

Step Functions は result = VERIFIEDmode = cryptographic の両方がある場合だけ semaphore 取得へ進みます。 readiness、asset authority、trust policy、Notation、timeout、Lambda invocation のいずれかが失敗した場合は署名ゲート失敗を通知します。 ECS タスクは起動されません。

ECR リポジトリと digest 固定

リポジトリ構成

リポジトリ種別用途signing / verification contract
Prover image repositoryECS 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 readinessProver candidate buildCodeBuild + ECRcandidate metadata を出力しない
cryptographic signaturenormal App deploymentApp preflight verifier aliasApp lane を継続しない
cryptographic signature証明生成直前Step Functions + runtime aliasECS タスクの起動拒否
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

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/appstatic frontend、Hono/API、DynamoDB、Lambda、SQS、Step Functions、ECS prover task、semaphore、proof/report S3、observability / notification、develop operator gate
infra/terraform/foundation/foundationECR、Prover の ECR managed signing authority、reference-aware ECR retention、release authority storage、runtime reference containers、prover networking / public-hostname handoff
infra/terraform/bootstrap/bootstrapremote 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/legacyretained 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_aliasdeployment_environmentstate_scopestate_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
appstark-dev/develop/appstark-ballot-simulator/stark-dev/develop/app/terraform.tfstate
foundationstark-dev/develop/foundationstark-ballot-simulator/stark-dev/develop/foundation/terraform.tfstate
bootstrapstark-dev/develop/bootstrapstark-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-api Lambda
  • 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-actionable SNS 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 で明示します。

PhaseServed table pairPublic admission / async consumption
primary_activeprimary通常状態
primary_quiescedprimarysession create と prover/verifier event source を停止
replacement_quiescedreplacementsession create と prover/verifier event source を停止
replacement_activereplacement通常状態

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 は normalrollback を明示的に分けます。 通常の 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 の動作は次の順序です。

  1. accepted historical source を private workspace に materialize する。
  2. target / backend / input / provider lock / runtime / source を再検証する。
  3. refresh-enabled terraform plan -lock=false -detailed-exitcode を実行する。
  4. no-driftdrift-detectedcheck-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
ProducerConsumer情報例
foundationapp pipelineECR repository identity、Prover signing authority publication、versioned release records / accepted pointers、secret/config references、exact networking handoff
bootstrapmanaged lanesbackend config、deployment-control roles、saved-plan、retained / execution artifact、verifier-preflight / acceptance / Terraform baseline / report storage
app lanetarget runtimeAPI 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 の動作は次のとおりです。

  1. 日次で current Toolchain / Prover acceptance、retained App acceptance / rollback reference、live Prover task definition と、各 protected subject の referrer を解決する。
  2. 関連 pipeline、candidate build、finalization、Prover task が active な間は mutation を行わない。
  3. 2 回の同一 snapshot を確認してから referrer、subject の順で削除し、結果を再検証する。

Protected reference は環境ごとの retained target count より優先されます。 通常の Foundation Apply は ECR delete authority を持ちません。

IAM and guards

Terraform root は最小権限を前提に分離されています。

領域境界
State accessroot ごとの backend key と lockfile object に限定
App runtime Lambdapublic-api、writer、dispatcher、worker、image-signature verifier ごとに分離
Prover taskproof artifact prefix、ECR pull、CloudWatch Logs に限定
Step Functions特定 ECS task definition、Lambda、logs、managed EventBridge rule に限定
Deployment controlroot と command class ごとの role に分離
Report-only controlroot-specific drift と 2 つの readiness role を Apply / mutation authority から分離
Operator gatePublic 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

ツールバージョン
Terraformactive app / foundation / bootstrap root は = 1.15.8。shared child module と retained legacy root は >= 1.10.0
AWS provideractive app / foundation / bootstrap root は = 6.58.0。shared child module と retained legacy root は ~> 6.0
Archive providertarget app / foundation / bootstrap roots では未使用。retained legacy root のみ archive_file 用に解決

関連する章

API リファレンス

この部では、ブラウザクライアント、CLI、第三者検証で使う公開 API を、現行実装に基づいて説明します。

この部の章

想定読者と前提

  • 想定読者: ブラウザクライアントや第三者検証ツールから API を呼び出す実装者
  • 前提: HTTP とセッションヘッダーの基本、および本書 全体像 のフローを把握していること

この部で扱わないもの

通常の browser / CLI contract は active route と capability 保護 API に限ります。 以下はこの部では扱いません。

  • retired な debug / private inspection route(GET /api/debug/enableGET /api/botdata/:id)の詳細
  • 置換済みの flat route(POST /api/finalize/callbackGET /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 で、includeData query を受理しません(詳細はエンドポイント一覧
  • raw S3 URL / presigned URL を通常の browser / CLI contract として扱う取得経路。bundle/report の取得契約はエンドポイント一覧に委譲します
  • レート制限、Turnstile、capability TTL などの環境変数チューニング
  • API Gateway、Hono、Lambda 側の認可とルーティング実装

関連する章

エンドポイント一覧

この章は、ブラウザと CLI から利用する外部向け API の現行 contract を記載します。 パス、request/response schema、公開エラー、認証境界の authority は packages/api-contract/src/routes/inventory.tspackages/api-contract/src/schemas/ です。 内部 callback と過去の flat route は public API に含めません。

API runtime 構成

同じ route inventory と handler を、ローカルの static Hono server と AWS Lambda runtime から利用します。

ランタイム用途主な入口
Static Hono serverローカルと test APIscripts/dev/static-hono-server.ts
Hono on LambdaAWS Lambda APIinfra/lambdas/public-api/handler.ts

以下のパスはすべて /api を base path とします。

Active API 一覧

共通 contract

セッション capability

POST /api/sessions 以外の active route は、次の組み合わせで session owner を特定します。

  • sessionId: URL path parameter
  • X-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) を返します。

routeTurnstileapplication rate limit
POST /api/sessionssession actionclient IP 単位
POST .../votesvote actionsession と client IP 単位
POST .../finalizationsfinalize actionsession と 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:

  • sessionId
  • electionId
  • electionConfigHash
  • logId
  • capabilityToken

主な固有エラー:

  • CAPTCHA_FAILED (403)
  • GLOBAL_LIMIT_EXCEEDED (503)
  • SESSION_LIMIT_EXCEEDED (503)

POST /api/sessions/:sessionId/votes

ユーザー投票を保存し、ボット投票を開始します。

request body:

  • commitment: 32-byte hex
  • vote: A / B / C / D / E
  • rand: 32-byte hex
  • turnstileToken(環境により必須)

response (200) の data:

  • voteId
  • commitment
  • bulletinIndex
  • bulletinRootAtCast
  • castAtMs

主な固有エラー:

  • 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:

  • count
  • total
  • completed
  • userVoted
  • finalized
  • distribution(任意。A-E の simulated count)
  • distributionKind(任意。存在する場合は simulated
  • updatedAtMs(任意)
  • animationSeed(任意)

Finalization API

Finalization resource

作成、current 取得、キャンセルは同じ finalization resource schema を返します。共通フィールドは次のとおりです。

  • finalizationId
  • status: pending / running / succeeded / failed / timeout
  • queuedAtMs
  • runtimeDiagnostics
    • asyncMode: enabled / disabled
    • queue(任意または null
    • progress(任意。running phase の派生進捗)
    • orchestrator(任意または null

status ごとの追加フィールド:

status追加フィールド
pendingなし
runningstartedAtMs
succeededstartedAtMs, completedAtMs, result
failedstartedAtMs(任意), failedAtMs, error.code, error.message
timeoutstartedAtMs(任意), timeoutAtMs

succeededresult は、authority と presentation を混在させず次の group に分けます。

  • authority: imageId, journal と、任意の receipt, electionManifest, closeStatement
  • presentation: tally、bitmap root、input commitment、verification status などの公開表示 projection
  • voterEvidence: state=available または state=verification-gated
  • artifactAvailability: bundle, report, includedBitmap, seenBitmap ごとの available / unavailable。bitmap の available は current finalization に durable に admit された sidecar があることを示す

POST /api/sessions/:sessionId/finalizations

集計と証明生成を開始します。

request body:

  • scenarioId: S0-S5
  • turnstileToken(環境により必須)

response:

  • 200: 同期処理後の finalization resource
  • 202: 受理された非同期 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 がない場合も成功で、datanull です。 存在する場合は上記の finalization resource を返します。

POST /api/sessions/:sessionId/finalizations/:finalizationId/cancel

path で指定した進行中 finalization をキャンセルします。

request body:

  • reason(任意、最大 256 文字)
  • body に executionIdfinalizationId は置きません

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) の dataavailability による strict union です。

availability=available:

  • finalizationId
  • proofAuthority: election identity、bulletin root、Image ID、verified tally、bitmap root、input commitment など
  • presentation: tally、scenario、tamper 情報と fail-closed presentation policy
  • verifierResult: status と任意の公開 report
  • verification: checkssteps
  • voterEvidence: availability=available または availability=unavailable
  • journal: representation=includedvalue、または representation=omitted

availability=unavailable:

  • finalizationId
  • reason: finalization_not_current / user_vote_unavailable
  • message

availability=corrupt:

  • finalizationId
  • reason: invalid_artifact / incomplete_session_authority / session_identity_mismatch / voter_evidence_mismatch
  • message

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:
    • verificationStatus
    • finalizationId
    • estimatedDurationMs
    • idempotent

主な固有エラー:

  • 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_present
  • cast_choice_range
  • cast_random_format
  • cast_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 binary
  • 206: 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.commitments
  • data.bulletinRoot
  • data.treeSize
  • data.generatedAtMs
  • data.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:

  • fromTreeSize
  • toTreeSize
  • rootAtFromTreeSize
  • rootAtToTreeSize
  • proofNodes
  • oldSubtreeHashes / appendSubtreeHashes(任意)
  • generatedAtMs

主な固有エラー:

  • INVALID_SIZE (400)

GET /api/sessions/:sessionId/votes/:voteId/inclusion-proof

指定した投票の最小包含証明を返します。session は finalized である必要があり、proof ownership と finalization authority を検証します。

response (200) の data:

  • voteId
  • proof.leafIndex
  • proof.treeSize
  • proof.merklePath
  • proof.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 / seen
  • voteIndex: 0 以上の整数

response (200) の data:

  • leafChunk
  • auditPath[]: 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:

  • sthDigest
  • bulletinRoot
  • treeSize
  • timestamp
  • logId

主な固有エラー:

  • 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 保護付きで取得します。

関連する章

セッションライフサイクル

このページでは、セッション管理の実装をクライアント側とサーバー側に分けて説明します。

管理責務の分離

管理面主な保存先主な責務
クライアント共有localStorage (starkBallotSession)画面遷移フェーズ、クライアント TTL、検証継続状態、UI 復元、保存 shape の strict admission
クライアントタブ単位sessionStorage (starkBallotSessionLock)タブごとの session identity lock、stale tab の fail-closed
サーバーVoteStore 実装(Mock/File/DynamoDB)投票データ、掲示板、集計結果、検証結果、検証観測メタデータ

クライアントとサーバーのセッション対応付けには sessionIdX-Session-Capability(署名トークン)が使われます。 ヘッダー、path、query の使い分けはエンドポイント一覧の共通 contractを参照してください。

保存状態の strict admission

admitStoredSessionData() は、サーバーが発行した identity、lifetime、phase、完全な private opening / cast receipt のフィールド群、canonical な finalizeResult を含む現行 shape だけを 受理します。未知フィールド、欠落したフィールド群、phase と finalization 状態の不一致は fail-closed です。

読み取り時に保存値が受理できなければ、starkBallotSessionstark-ballot-knowledgestarkBallotSessionLock をクリアします。書き込み時に受理できない状態は保存せず、 Invalid browser session state として失敗します。version key や旧 shape への移行 fallback はありません。

クライアント側フェーズ

クライアントセッション(apps/web/src/session/client.ts)のフェーズは以下の 3 つです。

  • voting
  • finalizing
  • verifying

ここでの「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_modePOST /api/sessions/:sessionId/finalizations/:finalizationId/verification/observations を best-effort で送信

/verify の継続判定(フロー視点の説明は 設計と実行フロー を参照):

  1. verificationRequestedAt と canonical な finalizeResult の両方がそろっていれば継続扱い(hasContinuationAuthority
  2. 上記がなくても、サーバー返却の STARK 状態が not_run 以外なら進行できる
  3. 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 familyAuthority
SessionIdentityRecordrequired sessionId / election config・hash / electionId / logId / creation time
SessionVotingRecordvotes、append-only bulletin、root history、user participation、bot count、last activity
FinalizationRecordscenario context、state、成功時だけ required な canonical result
VerificationResultRecordsession と同じ finalizationId に scope された検証結果
verification observationbrowser が表示した cast-check 状態の非 authority な冪等観測
AWS runtime/delivery metadataS3 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 は以下を取り得ます。

  • pending
  • running
  • succeeded
  • failed
  • timeout

検証観測メタデータ

verification observation record は、現在の finalizationId に対応する最初のブラウザ観測を finalizationIdfingerprintobservedAt で記録します。同じ finalization と fingerprint の再送は冪等に受理し、finalization または fingerprint が競合する送信は 拒否します。

この記録は、ブラウザで表示された verdict の観測、observability event の重複送信防止、および 送信された限定的な cast check 状態からの observability 用 summary 導出に使う冪等マーカーであり、 その送信の成否や保存によって proof、集計結果、検証結果、ユーザー向け verdict は変わりません。

サーバー側 TTL と失効の実装差分

サーバー側の失効挙動はストア実装で異なります。

ストア失効/TTL の実装
MockSessionStoregetActiveSessionCount() 呼び出し時に lastActivity から 5 分超を掃除
FileMockSessionStoregetActiveSessionCount() 呼び出し時に同様に 5 分超を掃除
DynamoSessionStoreDynamoDB 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/sessionsMAX_SESSIONS を参照します。 上限到達時は SESSION_LIMIT_EXCEEDED を返します。

セッションヘッダーのスコープ

POST /api/sessions 以外の session-scoped API は、path の :sessionIdX-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.checksverification.steps
  • 投票者端末に残る投票意図、乱数、投票レシート
  • /verify の Recorded-as-Cast 判定に使う session-scoped な自票 receipt / root-at-cast proof state と、サーバー側の整合性評価結果
  • 自票 inclusion 用のビットマップ証明
  • 有効化されている場合の第三者 STH ソース照合

public-input.json には zkVM 入力として提示された各投票の index、commitment、Merkle path が含まれます。 この範囲は PoC の設計意図に沿っています。 配布対象アーカイブ の構成も参照してください。

この部の章

想定読者と前提

  • 想定読者:配布された bundle.zip を独立にローカル監査したい第三者
  • 前提:Ubuntu 系 Linux と jqunzip などの基本 CLI、対応する公開リポジトリ snapshot へのアクセス。 詳細は はじめに を参照してください。

この部で扱わないもの

  • /verify UI 最終判定の完全再現と、投票者端末のローカル証跡を使った Cast-as-Intended 検証(必要な材料は上の「bundle.zip 単体では揃わないもの」を参照)
  • AWS インフラのデプロイと運用手順

関連する章

この章は bundle.zip のローカル監査に絞ります。 範囲外の作業は次のページを参照してください。

最低限確認する不変条件

項目合格条件
STARK レシートverifier-service verifystatus: "success"
receipt と journal の結合receipt 内の journal bytes を現行 contract で decode した全 proof-bound field が journal.json と一致
投票の除外有無excludedSlots == 0 かつ missingSlots == 0 かつ invalidPresentedSlots == 0
期待投票数整合totalExpected == treeSize
処理投票数totalVotes > 0
集計合計整合journal.jsonverifiedTally の合計が validVotes と一致
公開入力の基本整合性public-input.json が現行 contract に沿い、入力数、root、重複検査が成立
公開監査アーティファクトelection-manifest.jsonclose-statement.json の自己整合と相互整合が成立
入力整合性inputCommitment の再計算値が journal.json と一致

ZIP ローカル検証(Ubuntu)

この手順は、検証ページでダウンロードした bundle.zip を対象に、Ubuntu 上で第三者が行える最小監査のガイドです。 確認できるのは 第三者検証ガイド の不変条件表にある範囲であり、/verify UI の総合 Verified 判定の代替ではありません。

各 Step の概要は次のとおりです。

Step確認内容必要ツール
1Ubuntu セットアップ(Rust toolchain の導入)aptrustup
2production-feature verifier-service の buildRust toolchain
3bundle.zip の admission と展開Node.js / pnpm、unzip
4期待 Image ID の決定Node.js / pnpm、jq
5STARK レシートの検証Step 2 の verifier-servicejq
6receipt と journal.json の結合・完全性チェックNode.js / pnpm、jq
7公開監査アーティファクトの整合性チェックNode.js / pnpm
8inputCommitment 再計算Node.js / pnpm、jq

0. 前提

ツール要件:

  • Ubuntu 22.04 / 24.04
  • Node.js 24 と Corepack 経由の pnpm 11.x

実行済みであること:

  • 検証ページから bundle.zip をダウンロード済みであること
  • このリポジトリ(stark-ballot-simulator)のソースを取得済みであること
  • $REPO_ROOTcorepack enablepnpm 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.jsonconsistency-proof.json だけが任意
  • async bundle: 下記 5 ファイルだけが必須かつ許可対象
  • 両 mode: 必須 member の欠落、未登録または非公開 member、duplicate、case conflict、traversal・absolute・非 canonical path は不合格

両 mode で共通して必要なファイルは次のとおりです。

  • bundle/receipt.json
  • bundle/journal.json
  • bundle/public-input.json
  • bundle/election-manifest.json
  • bundle/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.jsonmethodVersionCURRENT_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.mjsEXPECTED_IMAGE_ID_VARIANT=default または未指定の場合に variants.defaultEXPECTED_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-servicereceipt.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.jsonelection-manifest.jsonclose-statement.jsonbundle.zip に含まれる Counted 段階の必須チェック対象です。 次の 4 点を確認します(フィールド単位の詳細はスクリプト内の checks 参照)。

  • public-input.json が現行 contract に沿い、vote entry、重複 index、commitment、journal.json の各フィールドと矛盾しない
  • election-manifest.jsonelectionConfigHash 再計算値が宣言値、public-input.jsonjournal.json と一致する
  • close-statement.jsonsthDigest 再計算値が宣言値、public-input.jsonjournal.json と一致する
  • journal.jsonpublic-input.jsonmethodVersion が現行 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.jsoninputCommitment と一致することを確認します。 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 ハッシュした値です。 現行実装では electionIdbulletinRoottreeSizetotalExpectedvotesCount、各投票の 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)

投票受理時にサーバーが返す応答データです。 voteIdcommitmentbulletinIndexbulletinRootAtCast を含みます。 検証では 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 / excludedSlotsinputCommitmentincludedBitmapRootseenBitmapRoot などを含みます。 レシートに暗号学的に束縛されており、改ざんできません。

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(包含証明パラメータ: leafIndextreeSizeauditPath)の 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_consistentcounted_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_consistentcounted_close_statement_consistent)で整合性が検証されます。

詳細: チェック一覧バンドル構造

締切ステートメント(close-statement.json

集計締切時点のログ境界を表す公開監査レコードです。 logIdtreeSizebulletinRootsthDigesttimestamp を宣言し、counted_close_statement_consistent チェックで検証入力やジャーナルとの整合が確認されます。 本書では close-statement.json の和訳呼称として「締切ステートメント」を使います。

詳細: チェック一覧バンドル構造

公開可能アーティファクト

秘密データを含まず、第三者検証や監査に利用できるアーティファクトの機密性区分です。 ここでの「公開可能」は無認証で取得できることを意味しません。 通常の配布や取得は capability 保護 API が担当し、対象が S3 にある場合も API が読み出して返します。

詳細: 公開境界バンドル構造

配布対象アーカイブ(bundle.zip

公開許可リストに基づいて作成される ZIP アーカイブです。 証明バンドル のうち公開可能アーティファクトだけを束ねた部分集合で、bundle.zip というファイル名で配布されます。 現行構成は public-input.jsonelection-manifest.jsonclose-statement.jsonreceipt.jsonjournal.json などを含みます。 input.jsonverification.jsonincluded-bitmap.jsonseen-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_activeprimary_quiescedreplacement_quiescedreplacement_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 です。 normalrollback を分け、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 に含めません。

詳細: イメージ署名Terraform

イメージ署名検証(Image Signing)

ECS タスクで使用するプローバーコンテナイメージが、信頼できるビルドパイプラインから生成されたことを検証する仕組みです。 AWS Signer を使用し、Step Functions のゲートとして機能します。

詳細: イメージ署名

証明バンドル(Proof Bundle)

zkVM の実行結果を検証可能な形で保存、配布するためのアーティファクト群を指す 上位概念 です。 公開可能アーティファクト(public-input.json など)、protected report artifact(verification.json)、非公開アーティファクト(input.jsonincluded-bitmap.jsonseen-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.jsonseen-bitmap.jsonverification.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 の sessionIdX-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/sessionsPOST /api/sessions/:sessionId/votesPOST /api/sessions/:sessionId/finalizations が body の turnstileToken で Bot による不正アクセスを防止します。 token を要求するかどうかは実行環境の設定に依存します。 bypass の可否は runtime profile が一体として選び、production の bypass は認めません。session 作成の develop 例外も operator-gated runtime に限定されます。

詳細: エンドポイント一覧AWS runtime 境界

定数

定数名説明
BOT_COUNT63サーバーが自動生成するボット投票数
MERKLE_TREE_DEPTH6Merkle ツリーの深度(2^6 = 64 リーフに対応)
VOTE_CHOICESA, B, C, D, E投票で選択可能な選択肢
コミットドメインタグstark-ballot:commit|v1.0コミットメントハッシュのドメイン分離タグ
入力ドメインタグstark-ballot:input|v1.0入力コミットメントのドメイン分離タグ
リーフドメインタグstark-ballot:leaf|v1CT 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/ にまとめています。