はじめに
最終更新: 2026-05-24
このドキュメントは、STARK Ballot Simulator の公開向けガイドです。
目的
- システムの全体像を短時間で把握できるようにする
- 暗号プロトコルと検証パイプラインの設計根拠を説明する
- 検証手順を再現できる情報を提供する
公開状態
本書はライブデモと公開用ソース snapshot の読者に向けたドキュメントです。公開 repository snapshot は hwatanabe-jp/stark-ballot-simulator-public で確認できます。ソースコードへのアクセスが必要な再現手順は、対象リリースの公開 repository snapshot と照合して実行してください。bundle.zip だけで実行できる確認範囲は 第三者検証ガイド にまとめています。
想定読者
- 暗号検証・監査に関心のある技術者
- 本アプリケーションに興味のある技術者
本書の用語表記
語彙の揺れを避けるため、本書では次のように表記を統一しています。詳細な定義は 用語集 を参照してください。
- 日本語に統一する語: コミットメント(文脈に応じて「投票コミットメント」「入力コミットメント」を区別)、包含証明、整合性証明、投票レシート、掲示板、集計確定
- 英語のまま使う語: STARK、zkVM、Image ID、RFC 6962、capability、
bundle.zip、fail-closed、journal - バンドル関連の正規形: 配布されるファイル本体は
`bundle.zip`(コードフォント)、配布対象としての論理名は「配布対象アーカイブ」、上位概念(非公開アーティファクトを含む全体)は「証明バンドル」を使い分けます。階層関係は バンドル構造 を参照。
本書の読み方
標準ルート
- まず 全体像 でシステムの概要を掴む
- アーキテクチャ語彙マップ(試験的) で UI、API、掲示板、zkVM、検証、バンドル境界の関係を 1 枚で確認する(試験的な語彙地図)
- 暗号プロトコル でコミットメント・Merkle ツリー等の基盤を理解する
- zkVM 設計 でゲストプログラムと証明生成の仕組みを学ぶ
- 検証パイプライン で 4 段階検証モデルの全体を把握する
- 改ざんシナリオ で教育的シミュレーションの動作を確認する
- 品質保証と形式手法 でテスト・PBT・Lean による品質境界を確認する
- AWS アーキテクチャ で非同期証明インフラを理解する
- API リファレンス でエンドポイント仕様を参照する
- 実際に検証する場合は 第三者検証ガイド で
bundle.zipを使ったローカル検証手順を実行する - 設計上の判断については 設計判断 を参照する
- 設計根拠の一次資料は 参考文献 を参照する
読者別ルート
監査者向け
bundle.zip を検証ページから取得し、独立にローカル監査したい読者向け。
- 全体像 で 4 段階モデルとバンドル階層を把握する
- 検証パイプライン で
/verifyの最終判定ロジックを理解する - チェック一覧 で各チェック ID と判定条件を確認する
- 第三者検証ガイド で
bundle.zipのローカル監査手順を実行する - 用語集 で「検証」「監査」「fail-closed」などの用語を確認する
- 品質保証と形式手法 で、テストと形式化がどの境界を守っているかを確認する
飛ばしてよい: 暗号プロトコル の数式詳細、AWS アーキテクチャ のインフラ詳細
実装者向け
クライアント/サーバー/zkVM のいずれかの実装を変更・追従したい読者向け。
- 全体像 でシステム境界を確認する
- アーキテクチャ語彙マップ(試験的) で境界づけられた語彙と主要データフローを共有する(試験的な語彙地図)
- 暗号プロトコル でコミットメント・Merkle・入力コミットメントの正準形を把握する
- zkVM 設計 でゲスト/ホストの責務分担と Image ID 管理を理解する
- 検証パイプライン でチェック評価とゲーティングを把握する
- 品質保証と形式手法 で、テスト・PBT・Lean のレイヤー分担を確認する
- API リファレンス でエンドポイント仕様と session-scoped 認可を確認する
飛ばしてよい: 第三者検証ガイド(実装変更後の動作確認には 改ざんシナリオ を使う方が早い)
運用者向け
AWS インフラ・非同期プローバー・デプロイを担当する読者向け。
- 全体像 で sync / async finalize の違いを確認する
- AWS アーキテクチャ で現行構成、環境分離、Amplify / Terraform の連携点を把握する
- 非同期プローバー で SQS / Step Functions / ECS の責務を理解する
- イメージ署名 と Image ID で署名検証と Image ID 解決の連動を確認する
- バンドル構造 で公開/非公開アーティファクトの境界を把握する
- API リファレンス で本番運用で監視すべきエンドポイントを確認する
飛ばしてよい: 暗号プロトコル の数式、改ざんシナリオ の教育的デモ詳細
全体像
STARK Ballot Simulator は、投票の完全性を段階的に検証するための PoC です。
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: ボット投票を自動追加 S->>Z: 全投票 + Merkle パス Z-->>S: STARK レシート + ジャーナル S->>VS: STARK レシート検証 VS-->>S: 検証レポート S-->>V: 検証結果
コアコンセプト
- 投票コミットメントと投票レシートにより Cast-as-Intended を検証
- RFC 6962 / CT スタイルの掲示板で Recorded-as-Cast を検証
- zkVM ジャーナル、入力整合、ビットマップ証明により Counted-as-Recorded を検証
- RISC Zero レシート検証で STARK 実行の正当性を検証
- AWS クラウド費用の目標月額を 1 USD(デプロイなし、アプリのアクセスなし時)
バンドル用語の階層
検証で扱うアーティファクト群は階層的な 3 つの用語で呼び分けます。
証明バンドル ⊃ 配布対象アーカイブ ⊃ bundle.zip(ファイル)
詳細: バンドル構造、定義: 用語集 > 証明バンドル。
プロジェクト規模
概算(2026-05-23 時点、tracked files ベース、生成物を除く、千行単位に丸め)。
| 区分 | 行数 |
|---|---|
| TypeScript / React(アプリ本体) | 約 57,000 行 |
| TypeScript(テストコード) | 約 67,000 行 |
| Rust(zkVM ゲスト + ホスト + 検証サービス) | 約 5,000 行 |
| Terraform / Shell / 補助スクリプト | 約 7,000 行 |
| 合計 | 約 136,000 行 |
各章への案内
| 部 | 内容 |
|---|---|
| 暗号プロトコル | コミットメント、掲示板 (CT Merkle)、入力コミットメント、STH ダイジェスト、ビットマップ Merkle |
| zkVM 設計 | ゲストプログラム、ホスト・証明生成、検証サービス、Image ID |
| 検証パイプライン | 4 段階モデル、チェック一覧、バンドル構造、ゲーティングロジック |
| 改ざんシナリオ | S0〜S5 シナリオ、検出メカニズム |
| 品質保証と形式手法 | 単体・結合・E2E、Property-based Testing、Lean による形式化 |
| AWS アーキテクチャ | トポロジー、非同期プローバー、イメージ署名、Terraform |
| API リファレンス | エンドポイント一覧、セッションライフサイクル |
| 第三者検証ガイド | 検証ページで取得した bundle.zip を使う Ubuntu 向けローカル検証手順 |
| 設計判断 | PoC の意図的な制約、設計ふりかえり |
アーキテクチャ語彙マップ(試験的)
注意: このページは試験的な語彙整理であり、
overview.mdのような確定仕様ではありません。 Bounded context の切り方や Aggregate / Value Object の対応付けは PoC 進行に合わせて変更される可能性があります。 実装上の真実は各章(protocol/,zkvm/,verification/)および現行コードを優先してください。
この図は、STARK Ballot Simulator の bounded context を 1 枚の context map にまとめた語彙地図です。中心には「必要な証拠が揃い、required checks が成功するまで Verified と表示しない」という中核の約束があります。
%%{init: {"flowchart": {"nodeSpacing": 36, "rankSpacing": 58, "curve": "basis"}}}%%
flowchart TB
Promise["中核の約束<br/>必要な証拠が揃い、required checks が成功するまで<br/>Verified と表示しない"]
subgraph VOTE["Bounded Context: 投票セッション"]
direction TB
VoteLang["Ubiquitous Language<br/>Election Session / Voter / Vote Choice<br/>Opening / Commitment / Vote Receipt"]
VoteAggregate["Aggregate Root<br/>Session<br/>(electionId, electionConfigHash, logId,<br/>phase, contractGeneration)"]
VoteVO["Value Objects<br/>Capability / Opening<br/>Commitment / Vote Receipt"]
VoteEvent["Conceptual Domain Events<br/>SessionOpened / VoteCast / SessionFinalized"]
VoteInvariant["Invariants<br/>opening から commitment が再計算可能<br/>private opening は公開 bundle に含めない"]
VoteLang --> VoteAggregate
VoteAggregate --> VoteVO
VoteVO --> VoteEvent
VoteEvent --> VoteInvariant
end
subgraph BOARD["Bounded Context: 公開掲示板"]
direction TB
BoardLang["Ubiquitous Language<br/>Append-only Log / Leaf / Tree Size<br/>Root / Inclusion Proof / Consistency Proof / STH"]
BoardAggregate["Aggregate Root<br/>BulletinLog<br/>(logId, treeSize, root)"]
BoardVO["Value Objects<br/>VoteEntry / InclusionProof<br/>ConsistencyProof / STH Digest"]
BoardEvent["Conceptual Domain Events<br/>EntryAppended / LogClosed"]
BoardInvariant["Invariants<br/>Log は append-only<br/>検証成功条件として<br/>rootAtCast → finalRoot の consistency proof が必須<br/>(欠落時は fail-closed)"]
BoardLang --> BoardAggregate
BoardAggregate --> BoardVO
BoardVO --> BoardEvent
BoardEvent --> BoardInvariant
end
subgraph PROOF["Bounded Context: 集計証明"]
direction TB
ProofLang["Ubiquitous Language<br/>ZkVM Input / Witness / Journal<br/>Receipt / Image ID / Input Commitment / Method Version"]
ProofAggregate["Aggregate Root<br/>ProofRun<br/>(methodVersion, imageId)"]
ProofVO["Value Objects<br/>ZkVMInput / Journal<br/>RISC Zero Receipt / Input Commitment"]
ProofEvent["Conceptual Domain Events<br/>ProofRequested / ProofGenerated"]
ProofInvariant["Invariants<br/>Receipt は expected Image ID で verify 成功<br/>RISC0_DEV_MODE=1 は production proof ではない"]
ProofLang --> ProofAggregate
ProofAggregate --> ProofVO
ProofVO --> ProofEvent
ProofEvent --> ProofInvariant
end
subgraph AUDIT["Bounded Context: 検証監査"]
direction TB
AuditLang["Ubiquitous Language<br/>Evidence / Check / Stage / Report<br/>Bundle / Verdict / Fail-closed"]
AuditAggregate["Aggregate Root<br/>VerificationRun<br/>(sessionId, executionId)"]
AuditVO["Value Objects<br/>Public Bundle / Verification Report<br/>Check / Stage / Verdict"]
AuditEvent["Conceptual Domain Events<br/>VerificationCompleted"]
AuditInvariant["Invariants<br/>Verified only when effective required checks all succeed,<br/>no unresolved required checks remain (not_run / pending / running),<br/>configured STH consensus is not violated,<br/>and no fail-closed exclusion signal remains<br/>private artifacts は公開 bundle に入らない"]
AuditLang --> AuditAggregate
AuditAggregate --> AuditVO
AuditVO --> AuditEvent
AuditEvent --> AuditInvariant
end
subgraph POLICY["Domain Policy: 教育的シナリオ"]
direction TB
Scenario["Scenario<br/>S0 normal<br/>S1/S3 exclusion<br/>S2/S4 claimed tally tamper<br/>S5 combined educational case"]
ClaimedTally["Claimed Tally<br/>UI に見せる主張値"]
VerifiedTally["Verified Tally<br/>journal が束縛する集計値"]
Scenario --> ClaimedTally
Scenario --> VerifiedTally
end
subgraph ADAPTERS["Adapters / delivery mechanisms"]
direction TB
Browser["Browser UI<br/>/ /vote /aggregate /result /verify"]
SharedApi["Shared API<br/>Next route wrappers と Hono route registry"]
VoteStore["VoteStore implementations<br/>Mock / FileMock / Amplify"]
SyncProver["Sync finalize<br/>local zkVM executor + ProofBundleService"]
AsyncAws["Async AWS<br/>SQS / Step Functions / ECS / S3 / callback runners"]
ReportDelivery["Bundle and report delivery<br/>public bundle.zip<br/>protected verification.json"]
Browser --> SharedApi
SharedApi --> VoteStore
SharedApi --> SyncProver
SharedApi --> AsyncAws
SharedApi --> ReportDelivery
end
Promise --> VOTE
Promise --> BOARD
Promise --> PROOF
Promise --> AUDIT
VOTE -- "Published Language<br/>commitment + vote receipt" --> BOARD
VOTE -- "Private witness<br/>opening data for proving" --> PROOF
BOARD -- "Published Language<br/>logId + timestamp + closed root + inclusion paths" --> PROOF
PROOF -- "Published Language<br/>journal + RISC Zero receipt + inputCommitment" --> AUDIT
VOTE -- "Private knowledge<br/>opening recomputes commitment" --> AUDIT
BOARD -- "Evidence<br/>inclusion + consistency + optional STH" --> AUDIT
POLICY -- "Demo policy<br/>exclusion scenarios affect proof input" --> PROOF
POLICY -- "Mismatch becomes audit evidence" --> AUDIT
ADAPTERS -. "drives use cases" .-> VOTE
ADAPTERS -. "persists log and session state" .-> BOARD
ADAPTERS -. "runs or queues prover work" .-> PROOF
ADAPTERS -. "serves bundles and reports" .-> AUDIT
AUDIT --> Promise
DDD としての読み方
- Bounded Context は 4 つ(投票セッション / 公開掲示板 / 集計証明 / 検証監査)。
- 各 context の内側は Ubiquitous Language を頭に、Aggregate Root / Value Objects / Conceptual Domain Events / Invariants の 4 段で表す。
- Published Language ラベル付きの太矢印は公開契約を示し、同じ単語でも context が違えば意味を分ける(例:「投票レシート」は VOTE の語彙、「RISC Zero receipt」は PROOF の語彙)。
- 教育的シナリオ S0–S5 は bounded context ではなく Domain Policy として外側に置き、claimed tally と verified tally の不一致を AUDIT へ伝える。
- UI / API / Store / AWS は別ドメインの言語ではなくアダプタなので、
Adapters / delivery mechanismsとして図の下部に分離する。
Verified を表示してよい厳密な条件は ゲーティングロジック を参照してください。
暗号プロトコル
投票の検証可能性を支える暗号プリミティブをまとめる部です。
公開データに対する投票の秘匿性(hiding)と束縛性(binding)を支えるコミットメントスキームから、RFC 6962 に基づく CT スタイルの Merkle ツリー、zkVM 入力の正準エンコーディングまで、検証可能性の基盤となる暗号構成要素を網羅します。
本書で「コミットメント」は 投票コミットメント(個々の投票の束縛)と 入力コミットメント(zkVM 公開入力全体の束縛)の 2 種を指します。両者は対象とドメイン分離タグが異なるため、それぞれ独立した章を設けています。
この部に含まれる章
- コミットメントスキーム — 投票コミットメントの構成と安全性
- CT Merkle ツリー — CT スタイルの追記専用掲示板
- 入力コミットメント — zkVM 公開入力に対するコミットメントの正準エンコーディング
- STH ダイジェスト — 分割ビュー緩和のためのツリーヘッドダイジェスト
- ビットマップ Merkle — 投票カウント証明のためのビットマップツリー
想定読者と前提
- 想定読者: 暗号プリミティブの仕様を実装・監査する技術者
- 前提: SHA-256 ハッシュ計算と Merkle ツリーの基本概念を把握していること
本章で扱わないもの
- SHA-256 や Pedersen コミットメントなど暗号プリミティブの数学的安全性証明
- RFC 6962 / CT エコシステムの運用詳細(証明書ログ、Monitor 役割など)
- 暗号ライブラリ実装の詳細(定数時間実装、サイドチャネル対策)
関連する章
コミットメントスキーム
投票者の選択を秘匿しつつ後から変更できないようにするコミットメントスキームを扱う章です。
SHA-256 ベースのコミットメントにより、投票内容の hiding(秘匿性)と binding(束縛性)を実現します。ドメイン分離タグにより、他プロトコルとのコミットメント衝突を防止します。
概要
投票コミットメントは、投票者が選んだ選択肢を公開データからは隠したまま、その選択に束縛されることを可能にする暗号プリミティブです。掲示板に記録されるのはコミットメント値のみで、選択肢と乱数(opening)は公開配布物(掲示板や bundle.zip 内の public-input.json など)には現れません。投票者は Cast-as-Intended 検証のために opening をローカルに保持します。
flowchart LR
subgraph 入力
E[選挙 ID<br/>16 バイト UUID]
C[選択肢<br/>1 バイト]
R[乱数<br/>32 バイト]
end
E --> H[SHA-256]
C --> H
R --> H
T["ドメインタグ<br/>"stark-ballot:commit|v1.0""] --> H
H --> CM[コミットメント<br/>32 バイト]
コミットメントの正準フォーマット
コミットメント値は、以下の入力を連結し SHA-256 で圧縮して生成されます。
commitment = SHA-256(
domain_tag || ← "stark-ballot:commit|v1.0" (24 バイト, UTF-8)
election_id || ← UUID v4 のバイナリ表現 (16 バイト)
choice || ← 選択肢の値 (1 バイト, 0〜4)
random ← 一様乱数 (32 バイト)
)
各フィールドの仕様
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ドメインタグ | 24 バイト | UTF-8 固定文字列 | "stark-ballot:commit|v1.0" |
| 選挙 ID | 16 バイト | UUID v4 からハイフンを除去し、16 進数をバイト列に変換 | 選挙スコープの識別子 |
| 選択肢 | 1 バイト | 符号なし整数 (0 = A, 1 = B, 2 = C, 3 = D, 4 = E) | 投票者の選択 |
| 乱数 | 32 バイト | 暗号学的に安全な一様乱数 | hiding 性を保証 |
SHA-256 への入力は合計 73 バイト、出力は 32 バイト(16 進数表記で 64 文字、0x プレフィックス付きでは 66 文字)です。
ドメイン分離
ドメインタグ "stark-ballot:commit|v1.0" は、このコミットメントが他のプロトコルで使用されるハッシュ値と偶発的に衝突することを防ぐための仕組みです。
本システムの主要なハッシュ用途では、用途ごとに以下のようなドメインタグまたは構造的プレフィックスを使い、ハッシュ入力の意味を分離します。
| プリミティブ | タグ / プレフィックス |
|---|---|
| コミットメント | "stark-ballot:commit|v1.0" |
| 入力コミットメント | "stark-ballot:input|v1.0" |
| Merkle リーフ | 0x00 || "stark-ballot:leaf|v1" |
| Merkle ノード | 0x01 |
| ログ ID | "stark-ballot:bulletin-log|v1.0" |
STH ダイジェストは専用のドメインタグを持たず、ログ ID を含む正準フォーマットで束縛されます(STH ダイジェスト を参照)。
安全性
Hiding(秘匿性)
乱数フィールドが 32 バイト(256 ビット)のエントロピーを持つため、公開されたコミットメント値から選択肢を推測することは計算量的に不可能です。
前提条件:
- 乱数は暗号学的に安全な乱数生成器(CSPRNG)から生成される
- 同じ乱数は決して再利用しない(再利用すると、同じ選択肢で同じコミットメント値が現れて情報が漏洩する)
PoC の制約: 本 PoC は operator に対する完全な秘匿性を目的としていません。投票 API に送信された opening(選択肢と乱数)はサーバー側ストアに保持されうるため、ここでいう hiding は公開観測者と公開配布物に対する性質を指します。
Binding(束縛性)
SHA-256 の原像耐性(preimage resistance)と第二原像耐性(second-preimage resistance)により、一度コミットした値と異なる選択肢に対して同じコミットメント値を生成することは計算量的に不可能です。
つまり、投票者はコミットメント公開後に「別の選択肢に投票した」と主張を変えることができません。
TypeScript と Rust の実装同期
コミットメントは TypeScript(クライアント・サーバー)と Rust 実装(zkVM guest/host から共有される zkvm/contract-core)の双方で計算されます。この 2 系統の実装は、バイトレベルで完全に同一の出力を生成する必要があります。
同期が必要な要素:
- ドメインタグの文字列とエンコーディング(UTF-8)
- UUID からバイト列への変換規則(ハイフン除去 → 16 進数デコード)
- 選択肢の整数エンコーディング(1 バイト、符号なし)
- 乱数の 16 進数デコード規則
ドメインタグやエンコーディング規則を変更する場合は、TypeScript と Rust の両実装を同時に更新する必要があります。どちらか一方のみの変更は、コミットメント照合の失敗を引き起こします。
検証パイプラインにおける役割
コミットメントは、4 段階検証モデルの最初の 3 段階で中心的な役割を果たします。
| 検証段階 | コミットメントの役割 |
|---|---|
| Cast-as-Intended | 投票者がローカルに保持する(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、投票レシートと照合する |
| Recorded-as-Cast | 掲示板上でコミットメントの包含証明を検証し、投票時点のツリー状態に対して正しく記録されたことを確認する |
| Counted-as-Recorded | zkVM ゲストが prover から渡された各 vote opening でコミットメントを再計算し、掲示板上の値と整合する票だけを tally に含める |
注意: Cast-as-Intended と Counted-as-Recorded は同じコミットメント計算式を使いますが、opening の出所が異なります(投票者ローカル / prover に渡された値)。
Recorded-as-Cast は投票時点(cast-time)のツリー状態に対する包含証明を使い、レシートの bulletinRootAtCast と整合させます。rootAtCast の保存と再導出の詳細は CT Merkle ツリー を参照してください。
各チェックの判定ロジックは チェック一覧 > Cast-as-Intended を参照してください。
sequenceDiagram
participant V as 投票者
participant S as サーバー
participant B as 掲示板
participant Z as zkVM
V->>V: (選択肢, 乱数) を選び<br/>コミットメントを計算
V->>S: コミットメント, 選択肢, 乱数を送信
S->>S: opening から<br/>コミットメントを再計算して照合
S->>B: コミットメントを掲示板に追記
S-->>V: 投票レシート(インデックス,<br/>bulletinRootAtCast)
Note over V: ローカルに (選択肢, 乱数) を保存
Note over Z: ゲストプログラムが<br/>コミットメントを再計算し<br/>掲示板の値と照合
CT Merkle ツリー
RFC 6962 を参照した追記専用 Merkle ツリーで、掲示板の透明性をどう作るかを扱う章です。
リーフハッシュ(0x00 プレフィックス)とノードハッシュ(0x01 プレフィックス)の区別により、second-preimage 攻撃を防止します。包含証明と整合性証明により、投票の記録と掲示板の追記専用性を検証可能にします。
なぜ RFC 6962 CT スタイルか
- 整合性証明で追記専用性を示せるため、Recorded-as-Cast に必要な「後から削除・改変されていない」保証を与えられる。
- 包含証明と整合性証明の両方を同一モデルで扱える。
- STH ダイジェストと組み合わせてスプリットビュー攻撃の検出力を上げられる。
概要
本システムの掲示板(Bulletin Board)は、Certificate Transparency (CT) で実績のある追記専用ログの設計を投票に応用しています。各投票コミットメントは Merkle ツリーのリーフとして追記され、一度記録されたエントリは削除も改変もできません。
graph TD
subgraph "8 リーフのツリー(N=8)"
R["ルート<br/>SHA-256(0x01 || L || R)"]
N1["ノード"]
N2["ノード"]
N3["ノード"]
N4["ノード"]
N5["ノード"]
N6["ノード"]
L0["リーフ 0"]
L1["リーフ 1"]
L2["リーフ 2"]
L3["リーフ 3"]
L4["リーフ 4"]
L5["リーフ 5"]
L6["リーフ 6"]
L7["リーフ 7"]
R --> N1
R --> N2
N1 --> N3
N1 --> N4
N2 --> N5
N2 --> N6
N3 --> L0
N3 --> L1
N4 --> L2
N4 --> L3
N5 --> L4
N5 --> L5
N6 --> L6
N6 --> L7
end
RFC 6962 参照ハッシュ規則
RFC 6962 Section 2 に従い、リーフノードと内部ノードに異なるドメイン分離プレフィックスを適用します。
リーフハッシュ
LeafHash = SHA-256(0x00 || "stark-ballot:leaf|v1" || leaf_data)
| 要素 | サイズ | 説明 |
|---|---|---|
| プレフィックス | 1 バイト | 0x00(リーフ識別子) |
| 使用タグ | 20 バイト | "stark-ballot:leaf|v1"(UTF-8) |
| リーフデータ | 可変 | コミットメント hex をデコードした生バイト列(32 バイト) |
内部ノードハッシュ
NodeHash = SHA-256(0x01 || left_hash || right_hash)
| 要素 | サイズ | 説明 |
|---|---|---|
| プレフィックス | 1 バイト | 0x01(内部ノード識別子) |
| 左子ハッシュ | 32 バイト | 左部分木のハッシュ |
| 右子ハッシュ | 32 バイト | 右部分木のハッシュ |
ドメイン分離の安全性
0x00(リーフ)と 0x01(内部ノード)のプレフィックス区別は、second-preimage 攻撃を防止するために不可欠です。この区別がなければ、攻撃者はリーフノードを内部ノードとして解釈させる(またはその逆の)偽造データを構築できる可能性があります。
使用タグ "stark-ballot:leaf|v1" は、他システムのリーフハッシュとの偶発的な衝突を防止する追加の防御層です。
Merkle Tree Hash(MTH)アルゴリズム
RFC 6962 で定義される MTH アルゴリズムは、任意のサイズのデータセットからルートハッシュを計算します。
アルゴリズムの定義
- 空のツリー:
MTH({}) = SHA-256()(空入力のハッシュ) - 単一リーフ:
MTH({d₀}) = LeafHash(d₀) - 複数リーフ: サイズ n のツリーに対し、k を n 未満の最大の 2 のべき乗とする
MTH({d₀, ..., dₙ₋₁}) = SHA-256(0x01 || MTH({d₀, ..., dₖ₋₁}) || MTH({dₖ, ..., dₙ₋₁}))
非 2 のべき乗サイズへの対応
ツリーサイズが 2 のべき乗でない場合(例: 5, 6, 7 リーフ)、MTH アルゴリズムは「n 未満の最大の 2 のべき乗」で分割を行います。これにより、左部分木は常に完全二分木(2 のべき乗サイズ)となり、右部分木にのみ不完全さが集中します。
graph TD
subgraph "5 リーフのツリー(k=4 で分割)"
Root["ルート"]
Left["左部分木<br/>MTH(d₀..d₃)<br/>完全二分木"]
Right["右部分木<br/>MTH(d₄)<br/>単一リーフ"]
Root --> Left
Root --> Right
end
デフォルトのデモ構成では 64 票(2⁶)を扱うため、最終的なツリーは完全二分木になります。ただし投票の追加途中や小規模な検証ケースでは非 2 のべき乗サイズが現れるため、実装は任意の treeSize に対する一般対応を備えます。
掲示板のリーフデータ形式
掲示板に追記される各投票のリーフデータは、コミットメントの正規化された 16 進数表現(0x なし、小文字、64 文字)を 32 バイトにデコードした生バイト列です。
leaf_data = hex_decode(normalized_commitment_hex) (32 バイト)
掲示板は以下の不変条件を維持します:
- 単調増加インデックス: 各投票に 0 から始まる連番が割り当てられる
- 重複排除: 同一の投票 ID やコミットメントの二重追記を拒否する
- ルート履歴: 各追記時点のルートハッシュをタイムスタンプとともに保存する
包含証明(Inclusion Proof)
包含証明は、特定のコミットメントがツリーの特定位置に含まれていることを、ルートハッシュに対して暗号学的に証明するものです。
構造
包含証明は以下の要素で構成されます:
| フィールド | 説明 |
|---|---|
| leafIndex | リーフの 0 始まりインデックス |
| proofNodes | 兄弟ハッシュの配列(監査パス) |
| treeSize | 証明時点のツリーサイズ |
| rootHash | 検証対象のルートハッシュ |
実装での名称
本章では RFC 6962 の抽象名を使いますが、API では異なるフィールド名で返します。
| 抽象名 | API フィールド名 |
|---|---|
proofNodes | merklePath |
rootHash | bulletinRootAtCast |
/api/verify の userVote.proof や /api/bulletin/:voteId/proof がこの構造に対応します。
PATH アルゴリズム
RFC 6962 の PATH 関数に従い、監査パスを再帰的に生成します。
- ツリーをサイズ k(n 未満の最大の 2 のべき乗)で左右に分割
- 対象リーフが左部分木にある場合(
index < k):- 左部分木の PATH を再帰計算
- 右部分木のハッシュを監査パスに追加
- 対象リーフが右部分木にある場合(
index >= k):- 右部分木の PATH を再帰計算(インデックスを
index - kに調整) - 左部分木のハッシュを監査パスに追加
- 右部分木の PATH を再帰計算(インデックスを
検証手順
検証者は以下の手順で包含を確認します:
- コミットメントのリーフハッシュを計算:
LeafHash(commitment) - PATH アルゴリズムと同じ木構造に従い、監査パスのノードを順に結合
- 計算されたルートが期待するルートハッシュと一致するか確認
Recorded-as-Cast では、包含証明に加えて以下の cast-time 一貫性も確認します:
leafIndexがレシートのbulletinIndexと一致することtreeSizeがbulletinIndex + 1と一致すること(投票時点のツリーサイズ)
監査パスのサイズは O(log n) であり、64 票のツリーでは最大 6 ノードです。
graph TD
subgraph "インデックス 5 の包含証明"
Root["ルート ✓"]
N1["H(N3, N4)"]
N2["H(N5, N6) ← 計算対象"]
N3["H(L0,L1)<br/>監査パス"]
N4["H(L2,L3)<br/>監査パス"]
N5["H(L4,L5) ← 計算対象"]
N6["H(L6,L7)<br/>監査パス"]
L4["L4<br/>監査パス"]
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 N3 fill:#2196F3,color:#fff
style N4 fill:#2196F3,color:#fff
style N6 fill:#2196F3,color:#fff
style L4 fill:#2196F3,color:#fff
整合性証明(Consistency Proof)
整合性証明は、古いツリー状態(サイズ m)が新しいツリー状態(サイズ n)の前方互換的なプレフィックスであること、つまり追記専用性を暗号学的に証明するものです。
構造
| フィールド | 説明 |
|---|---|
| oldSize | 古いツリーのサイズ(m) |
| newSize | 新しいツリーのサイズ(n) |
| proofNodes | 整合性を証明するハッシュの配列 |
補足:
/api/bulletin/consistency-proofはproofNodesに加えてrootAtOldSizeとrootAtNewSizeも返します。/verifyのrecorded_consistency_proof判定は、主に bulletin provider から取得した old/new root と整合性証明を検証する経路で、HTTP エンドポイントはconsistency-verifier.tsの補助系で利用されます。判定では old root をレシートのbulletinRootAtCast、new root を最終bulletinRootと照合します。
SUBPROOF アルゴリズム
RFC 6962 の SUBPROOF 関数に基づき、整合性証明を再帰的に生成します。
m = nかつ古いツリーが完全部分木: 空の証明を返すm = nかつ完全部分木でない: 部分木のルートハッシュを返す- k を n 未満の最大の 2 のべき乗とし:
m <= kの場合: 左部分木の SUBPROOF + 右部分木のハッシュm > kの場合: 右部分木の SUBPROOF + 左部分木のハッシュ
検証の意味
整合性証明の検証が成功することは、以下を意味します:
- 古いツリーのすべてのリーフが、新しいツリーにも同じ位置・同じ値で存在する
- 新しいツリーは古いツリーの末尾にリーフを追加しただけで構成されている
- 古いツリーのルートハッシュと新しいツリーのルートハッシュの両方が、提供された証明ノードから独立に再構成できる
これにより、サーバーが過去に記録した投票を密かに削除したり順序を変更したりする攻撃を検出できます。
検証パイプラインにおける役割
CT Merkle ツリーは、4 段階検証モデルの主に 2 段階で使用されます。
| 検証段階 | CT Merkle の役割 |
|---|---|
| Recorded-as-Cast | 包含証明でコミットメントの記録を確認し、整合性証明で追記専用性を確認する |
| Counted-as-Recorded | zkVM ゲストが同じハッシュ規則で各 vote の包含を内部検証する |
Recorded-as-Cast では recorded_inclusion_proof と recorded_consistency_proof が表の役割を担当し、他の派生チェックもこれらの結果に基づきます。ハッシュ規則の不一致は zkVM ゲスト内での検証失敗として即座に検出されます。
各チェックの判定ロジックは チェック一覧 > Recorded-as-Cast を参照してください。
RFC 6962 参照範囲
| 要件 | 対応状況 | 備考 |
|---|---|---|
| リーフ・ノードのドメイン分離 | 実装済 | 0x00 / 0x01 プレフィックス |
| MTH アルゴリズム | 実装済 | 再帰的分割 + キャッシュ最適化 |
| PATH 関数(包含証明) | 実装済 | O(log n) サイズの監査パス |
| 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 証明は「ゲストプログラムが正しく実行された」ことを証明しますが、「どの入力に対して実行されたか」は証明のスコープ外です。入力コミットメントがなければ、悪意あるサーバーは以下の攻撃が可能になります:
- 投票を除外した入力で zkVM を実行し、有効な STARK 証明を取得する
- 公開用の入力データには除外されていない投票を含めて提示する
- 第三者は STARK 証明が有効であることを確認できるが、実際に処理された入力は異なる
入力コミットメントをジャーナルに含めることで、第三者は「公開データから再計算した入力コミットメント」と「ジャーナルに記録された入力コミットメント」を照合し、不一致を検出できます。
正準エンコーディング
入力コミットメントの計算には、すべてのフィールドを決定的な順序・エンコーディングで連結する正準化が不可欠です。
バイトレイアウト
input_commitment = SHA-256(
domain_tag ← "stark-ballot:input|v1.0" (23 バイト, UTF-8)
|| version ← u32 リトルエンディアン (4 バイト) = 10
|| election_id ← UUID v4 バイナリ (16 バイト)
|| bulletin_root ← 32 バイト
|| tree_size ← u32 リトルエンディアン (4 バイト)
|| total_expected← u32 リトルエンディアン (4 バイト)
|| votes_count ← u32 リトルエンディアン (4 バイト)
|| [投票データ] ← インデックス昇順でソートされた各投票
)
各投票のエンコーディング
投票配列の各要素は以下の形式でエンコードされます:
vote_entry =
index ← u32 リトルエンディアン (4 バイト)
|| commitment_len← u16 リトルエンディアン (2 バイト) = 32 (固定)
|| commitment ← 32 バイト
|| path_len ← u16 リトルエンディアン (2 バイト)
|| path_nodes ← path_len × 32 バイト
フィールド一覧
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ドメインタグ | 23 バイト | UTF-8 固定文字列 | "stark-ballot:input|v1.0" |
| バージョン | 4 バイト | u32 LE | v1.0 = 10 |
| 選挙 ID | 16 バイト | UUID バイナリ | 選挙スコープの識別子 |
| 掲示板ルート | 32 バイト | ハッシュ値 | 最終的な Merkle ルート |
| ツリーサイズ | 4 バイト | u32 LE | 掲示板のリーフ数 |
| 期待投票数 | 4 バイト | u32 LE | 想定される総投票数 |
| 投票数 | 4 バイト | u32 LE | 実際に含まれる投票数 |
| 各投票インデックス | 4 バイト | u32 LE | 掲示板上の位置 |
| コミットメント長 | 2 バイト | u16 LE | 固定値 32 |
| コミットメント | 32 バイト | ハッシュ値 | 投票コミットメント |
| パス長 | 2 バイト | u16 LE | Merkle パスのノード数 |
| パスノード | 各 32 バイト | ハッシュ値 | 包含証明の兄弟ハッシュ |
public-input.json と公開監査アーティファクトとの関係
要点: public-input.json の全フィールドが入力コミットメントに束縛されるわけではありません。残りのフィールドは別チェックで補完的に検証されます。
public-input.json は、zkVM 検証に使う秘密データを含まない検証用レコードです。現行実装では schema、version、contractGeneration、electionId、electionConfigHash、bulletinRoot、treeSize、totalExpected、logId、timestamp、methodVersion と、各投票の index・コミットメント値・Merkle パスを含みます。
入力コミットメントが直接束縛するのはフィールド一覧に示した対象のみで、schema、version、contractGeneration、electionConfigHash、logId、timestamp、methodVersion は対象外です。これら対象外のフィールドは proof bundle 内の election-manifest.json や close-statement.json を組み合わせて、次のように照合されます。
electionConfigHash→counted_election_manifest_consistent(manifest と journal 等を照合)logId・timestamp→counted_close_statement_consistent(close statement と journal 等を照合)schema・version・contractGeneration→public-input.jsonの互換性マーカーとして artifact 採用時に検証methodVersion→public-input.json採用時に journal と照合。Image ID 解決では正規化済み journal 値を使用
正準化規則
エンコーディングの決定性を保証するために、以下の規則が厳守されます。
ソート規則
投票はエンコーディング前にインデックスの昇順にソートします。各投票の index は一意であることが前提であり、これにより同じ投票集合から常に同一のバイト列が生成されます。この規則に違反すると、TypeScript と Rust で異なるハッシュ値が計算され、検証が失敗します。
異常系の補助: 重複インデックスはプロトコル違反です。TS/Rust 双方は決定性のために
commitment/merklePathで tie-break しますが、正常系仕様はindex昇順のままです。
エンディアン規則
すべての整数フィールドはリトルエンディアンでエンコードされます。
| 型 | バイト数 | エンコーディング |
|---|---|---|
| u16 | 2 | リトルエンディアン |
| u32 | 4 | リトルエンディアン |
16 進数正規化
コミットメント値やパスノードなどの 16 進数表現は、0x プレフィックスを除去した上でバイト列にデコードされます。16 進数文字列のまま連結するのではなく、常にバイナリ表現を使用します。
TypeScript と Rust の同期
入力コミットメントは TypeScript(サーバー側)と Rust(zkVM ゲスト内)の双方で独立に計算され、結果が一致する必要があります。
flowchart LR
subgraph TypeScript
TS_IN[public-input.json から抽出した<br/>入力コミットメント対象フィールド] --> TS_CALC[正準エンコーディング<br/>+ SHA-256]
TS_CALC --> TS_IC[入力コミットメント A]
end
subgraph "Rust (zkVM ゲスト)"
RS_IN[ゲスト入力] --> RS_CALC[正準エンコーディング<br/>+ SHA-256]
RS_CALC --> RS_IC[入力コミットメント B]
end
TS_IC --> CMP{A = B ?}
RS_IC --> CMP
CMP -->|一致| OK[検証成功]
CMP -->|不一致| NG[検証失敗:<br/>入力データが異なる]
同期が破壊される典型的な原因:
- ソート順序の不一致
- エンディアンの不一致
- ドメインタグの文字列差異
- バージョン番号の不一致
- 16 進数正規化規則の差異(大文字/小文字、
0xプレフィックスの有無)
検証パイプラインにおける役割
入力コミットメントは、Counted-as-Recorded 段階での検証チェック counted_input_commitment_match として使用されます。
| チェック ID | 検証内容 |
|---|---|
counted_input_commitment_match | 公開可能な検証データから再計算した入力コミットメントがジャーナルの値と一致するか |
このチェックが失敗すると、zkVM が処理した入力データと公開可能な検証データから再構成される対象フィールドが食い違うことを意味し、結果の信頼性が根本的に損なわれます。なお対象外フィールドは counted_election_manifest_consistent と counted_close_statement_consistent で補完的に検証されます(対応関係は上記を参照)。
各チェックの判定ロジックは チェック一覧 > Counted-as-Recorded を参照してください。
注意事項
入力コミットメントには投票者の秘密データ(選択肢や乱数)は含まれません。したがって、入力コミットメントの公開は投票の秘密性を損ないません。
入力順序に依存しない正準エンコーディングは、Property-based Testing の permutation invariance と、Lean による形式化 の input-commitment vectors で検査します。
STH ダイジェスト
Signed Tree Head ダイジェストを第三者と照合することで、分割ビュー攻撃をどう緩和するかを扱う章です。
ログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートを束縛するダイジェストにより、サーバーが異なるクライアントに異なるツリー状態を提示する攻撃を検出可能にします。
概要
分割ビュー攻撃(split-view attack)とは、悪意あるサーバーが異なる検証者に対して異なる掲示板の状態を提示する攻撃です。例えば、投票者 A には「全 64 票が含まれたツリー」を見せながら、投票者 B には「特定の票が除外されたツリー」を見せることが考えられます。
STH ダイジェストは、掲示板の状態を(ログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュ)の組として束縛し、独立した第三者ソースとの合意確認を通じてこの攻撃を検出します。
flowchart TD
subgraph "STH ダイジェストの構成"
LID[ログ ID<br/>32 バイト]
TSZ[ツリーサイズ<br/>4 バイト]
TS[タイムスタンプ<br/>8 バイト]
BR[掲示板ルート<br/>32 バイト]
end
LID --> H[SHA-256]
TSZ --> H
TS --> H
BR --> H
H --> STH[STH ダイジェスト<br/>32 バイト]
STH --> J[zkVM ジャーナルに記録]
STH --> CS[close-statement.json に記録]
STH --> TP[第三者ソースの STH と照合]
本実装での検証対象: 本章で言う「STH ダイジェスト」は
sthDigest自体です。本実装は STH の署名検証は行いません(スコープの詳細は 合意ロジック を参照)。
ダイジェストフォーマット
sth_digest = SHA-256(
log_id ← 32 バイト
|| tree_size ← u32 リトルエンディアン (4 バイト)
|| timestamp ← u64 リトルエンディアン (8 バイト, Unix 時刻ミリ秒)
|| bulletin_root ← 32 バイト
)
SHA-256 への入力は合計 76 バイトです。
各フィールドの仕様
| フィールド | サイズ | エンコーディング | 説明 |
|---|---|---|---|
| ログ ID | 32 バイト | ハッシュ値 | 掲示板インスタンスの識別子 |
| ツリーサイズ | 4 バイト | u32 LE | 掲示板のリーフ数 |
| タイムスタンプ | 8 バイト | u64 LE | Unix 時刻(ミリ秒) |
| 掲示板ルート | 32 バイト | ハッシュ値 | Merkle ツリーのルート |
ログ ID
ログ ID は掲示板インスタンスを一意に識別するための値であり、以下のように生成されます:
log_id = SHA-256("stark-ballot:bulletin-log|v1.0" || seed)
ドメインタグ "stark-ballot:bulletin-log|v1.0" と任意のシード値を連結し、SHA-256 でハッシュします。ログ ID は掲示板のライフタイム中に変化しない固定値です。
ログ ID を STH ダイジェストに含めることで、異なる掲示板インスタンスの STH が偶然に衝突することを防止します。
分割ビュー攻撃と検出メカニズム
攻撃シナリオ
sequenceDiagram
participant A as 投票者 A
participant S as 悪意あるサーバー
participant B as 投票者 B
S->>A: ツリー状態 X<br/>(64 票、ルート R₁)
S->>B: ツリー状態 Y<br/>(63 票、ルート R₂)
Note over A,B: A と B は互いに異なる<br/>ツリー状態を見ている
この攻撃では、サーバーは投票者 B に対して特定の票を除外したツリーを見せています。投票者 B は自身に提示されたツリーに対する包含証明や整合性証明を検証できますが、投票者 A とは異なるツリーを見ていることに気づけません。
第三者合意による検出
検証者は、NEXT_PUBLIC_STH_SOURCES に設定された独立ソースへ問い合わせ、ジャーナル内の STH ダイジェストと照合することで分割ビューを検出できます。
sequenceDiagram
participant V as 検証者
participant S as サーバー
participant T1 as 第三者ソース 1
participant T2 as 第三者ソース 2
V->>S: ジャーナルから STH ダイジェスト D' を取得
V->>T1: STH を問い合わせ → D₁
V->>T2: STH を問い合わせ → D₂
V->>V: D' = D₁ = D₂ ?
alt 全て一致
V->>V: 合意成立 → 検証成功
else 不一致あり
V->>V: 分割ビューの疑い → 検証失敗
end
合意ロジック
第三者 STH 検証は以下の条件をすべて満たした場合に成功します:
- 十分な一致数: 一致するソースの数が最小要求数(コードフォールバック値: 2)以上
- 全会一致: 応答可能なすべてのソースが一致すること(
matchingSources = comparableSources)
各ソースに対して以下のフィールドが照合されます:
| 照合フィールド | 条件 |
|---|---|
| STH ダイジェスト | 必須一致 |
| 掲示板ルート | 提供されている場合は一致 |
| ツリーサイズ | 提供されている場合は一致 |
第三者ソースの応答は sthDigest を中心としたスナップショット情報として扱います。応答署名の検証、外部アンカリング、STH の自動公開は本実装のスコープ外です。
zkVM との連携
zkVM ゲストプログラムは、入力として受け取ったログ ID、ツリーサイズ、タイムスタンプ、掲示板ルートから STH ダイジェストを再計算し、ジャーナルにコミットします。
finalize 時には、同じツリー状態から close-statement.json も構築され、sthDigest が配布対象アーカイブ bundle.zip に含まれる公開監査アーティファクトへ反映されます。
この仕組みにより、STARK 証明と bundle.zip 内の close-statement.json がともに特定のツリー状態へ束縛されます。第三者はジャーナルの STH ダイジェストを独立ソースの値と照合することで、サーバーが証明と異なるツリー状態を提示していないかを確認できます。
検証パイプラインにおける役割
STH ダイジェストは 2 つの段階で利用されます。
| 段階 | チェック ID | 検証内容 |
|---|---|---|
| Recorded-as-Cast | recorded_sth_third_party | 独立ソースから取得した STH ダイジェストがジャーナルの値と一致するか |
| Counted-as-Recorded | counted_close_statement_consistent | close-statement.json の sthDigest が公開入力およびジャーナルの値と整合するか |
recorded_sth_third_party は既定では任意チェック(optional)ですが、STH ソースが設定されている場合は必須扱いへ昇格します。昇格後は成功以外の状態にある限り「Verified」になりません(状態区分の扱いは チェック一覧 を参照)。
counted_close_statement_consistent は常に必須チェック(required)です。close-statement.json がジャーナルと整合しない場合、検証は失敗します。
各チェックの判定ロジックは チェック一覧 > Recorded-as-Cast と チェック一覧 > Counted-as-Recorded を参照してください。
設定
第三者 STH 検証は環境変数で制御されます。
| 環境変数 | 説明 | コードフォールバック値 |
|---|---|---|
NEXT_PUBLIC_STH_SOURCES | カンマ区切りの STH ソース URL | 未設定(第三者照合を実行しない) |
NEXT_PUBLIC_STH_MIN_MATCHES | 必要な最小一致ソース数 | 2 |
STH ソースが未設定の場合、recorded_sth_third_party は not_run(未実行)となり、第三者照合は行いません。
開発用の .env.local.example では次の値が設定されています:
NEXT_PUBLIC_STH_SOURCES=/api/sthNEXT_PUBLIC_STH_MIN_MATCHES=1
same-origin と /api/sth の取り扱い
same-origin 解決と認証ヘッダ転送: 相対パス(例: /api/sth)はリクエスト元のオリジンに対して解決されます。/api/sth は same-origin の session-scoped 開発用 API で、アクセスにはセッション capability が必要です。検証ロジックはセッション認証ヘッダーを same-origin ソースにのみ転送し、cross-origin ソースへは送りません。独立第三者ソースを absolute URL で構成する場合、それらはセッション認証に依存しない公開 STH エンドポイントである必要があります。
/api/sth の timestamp: /api/sth が返す timestamp はジャーナル内の canonical な時刻ではなく session.lastActivity です。そのため、第三者合意の一致判定で実際に照合するのは必須の sthDigest と、ソースが返した場合の bulletinRoot / treeSize です。
PoC における制約
本 PoC の開発用テンプレート(.env.local.example)では、STH ソースとして同一サーバー上の API エンドポイント(/api/sth)を使用します。同一サーバー上のソースのみでは防御力が限定的であるため、独立した組織が運営する複数ソースを NEXT_PUBLIC_STH_MIN_MATCHES >= 2 で構成することを推奨します。
ビットマップ Merkle
自票が集計に含まれたかを Merkle 証明で開示するためのビットマップツリーを扱う章です。
zkVM ゲスト内で計算されるビットマップにより、各投票インデックスが集計に含まれたかどうかを個別に検証可能にします。Merkle 証明により、自分の投票が含まれていることをサーバーを信頼せずに確認できます。
概要
Counted-as-Recorded 段階の検証では、zkVM に提示された入力に対する集計の正しさは STARK 証明で保証されますが、個々の投票者にとって「自分の票が集計に含まれたか」を直接確認する手段が別途必要です。
ビットマップ Merkle ツリーは、この「個別のカウント証明」を提供します。zkVM ゲストは投票ごとの状態をビットマップとしてエンコードし、その Merkle ルートをジャーナルにコミットします。投票者は自分のインデックスに対応するビットの Merkle 証明を取得し、「自分の票がカウントされたか」「そもそも prover に提示されたか」を独立に検証できます。
flowchart TD
subgraph "zkVM ゲスト内"
BM["ビットマップ<br/>[true, true, false, true, ...]"] --> PK[ビットパッキング<br/>LSB-first]
PK --> CH[32 バイトチャンク分割]
CH --> LH["リーフハッシュ<br/>SHA-256(0x00 || tag || chunk)"]
LH --> MT[Merkle ツリー構築]
MT --> ROOT["includedBitmapRoot / seenBitmapRoot"]
end
ROOT --> JNL[ジャーナルにコミット]
ビットマップの構造
ビットマップの定義
ビットマップは、ツリーサイズ(投票数)と同じ長さのブール配列です。現行実装では同じエンコーディング規則を持つ 2 種類のビットマップを扱います。
includedBitmap[i] = true: インデックス i の投票が正常に検証され、集計に含まれたincludedBitmap[i] = false: インデックス i の投票が除外されたseenBitmap[i] = true: インデックス i の投票が prover に提示されたseenBitmap[i] = false: インデックス i の投票が prover に提示されなかった
本 PoC では 64 票を扱うため、ビットマップは 64 ビット(= 8 バイト)です。
LSB-first ビットパッキング
ブール配列は LSB-first(Least Significant Bit first)方式でバイト列にパッキングされます。
ビット配列: [b₀, b₁, b₂, b₃, b₄, b₅, b₆, b₇, b₈, ...]
バイト 0 = b₀ | (b₁ << 1) | (b₂ << 2) | ... | (b₇ << 7)
バイト 1 = b₈ | (b₉ << 1) | ...
| ビット位置 | バイトインデックス | バイト内ビット位置 |
|---|---|---|
| 0 | 0 | 0 (LSB) |
| 1 | 0 | 1 |
| 7 | 0 | 7 (MSB) |
| 8 | 1 | 0 (LSB) |
| 63 | 7 | 7 (MSB) |
64 ビットのビットマップは 8 バイトにパッキングされます。
32 バイトチャンク分割
パッキングされたバイト列は 32 バイト(256 ビット)単位のチャンクに分割されます。各チャンクが Merkle ツリーの 1 つのリーフとなります。
- 1 チャンク = 32 バイト = 256 ビット分の投票カウント状態
- 最後のチャンクが 32 バイトに満たない場合はゼロパディング
本 PoC の 64 票は 8 バイトであるため、1 つのチャンク(24 バイトのゼロパディング付き)に収まります。
Merkle ツリーの構築
ハッシュ規則
ビットマップ Merkle ツリーは、CT Merkle ツリーと同一のハッシュ規則を使用します。
リーフハッシュ:
LeafHash = SHA-256(0x00 || "stark-ballot:leaf|v1" || chunk)
内部ノードハッシュ:
NodeHash = SHA-256(0x01 || left_hash || right_hash)
ドメイン分離プレフィックス(0x00 / 0x01)と使用タグ("stark-ballot:leaf|v1")は、CT Merkle ツリーの章で解説したものと同一です。
ツリー構築アルゴリズム
- 各 32 バイトチャンクにリーフハッシュを適用
- ボトムアップでペアを結合し、内部ノードハッシュを計算
- 奇数ノードがある場合は、そのまま次のレベルに昇格(ハッシュなし)
- ルートに到達するまで繰り返す
graph TD
subgraph "3 チャンクの場合"
R["ルート<br/>SHA-256(0x01 || N1 || C2)"]
N1["ノード<br/>SHA-256(0x01 || C0 || C1)"]
C2["リーフ 2<br/>SHA-256(0x00 || tag || chunk₂)"]
C0["リーフ 0<br/>SHA-256(0x00 || tag || chunk₀)"]
C1["リーフ 1<br/>SHA-256(0x00 || tag || chunk₁)"]
R --> N1
R --> C2
N1 --> C0
N1 --> C1
end
Merkle 証明の生成と検証
証明の構造
GET /api/bitmap-proof?i=<bitIndex>&kind=included|seen のレスポンスは、以下の要素で構成されます:
| フィールド | 説明 |
|---|---|
| leafChunk | 対象ビットを含む 32 バイトチャンク(16 進数) |
| auditPath | ルートまでの兄弟ハッシュ配列(各要素にハッシュ値と位置) |
leafIndex(floor(bitIndex / 256))と bitOffset(bitIndex mod 256)は、クライアント側で bitIndex クエリから導出します。サーバーは返しません。
kind は省略可能で、既定値は included です。
kind=included:includedBitmapRootに対する証明を返すkind=seen:seenBitmapRootに対する証明を返す
このエンドポイントは無認証の公開 API ではなく、セッション ID と capability token を要する証明材料 API です(仕様は外部クライアント向けに文書化されています)。
ビット抽出
投票者は受け取ったチャンクから、自分が指定した bitIndex のビットを以下の手順で抽出します:
bit_offset = bit_index mod 256
byte_index = bit_offset / 8 (整数除算)
bit_in_byte = bit_offset mod 8
included = (chunk[byte_index] AND (1 << bit_in_byte)) != 0
kind=included で included = true なら、自分の投票がカウントされたことを意味します。kind=seen で included = true なら、自分の投票が prover に提示されたことを意味します。
検証手順
- チャンクからビット値を抽出し、自分の投票がカウントされたか確認
- チャンクのリーフハッシュを計算:
SHA-256(0x00 || "stark-ballot:leaf|v1" || chunk) - 監査パスに沿ってルートまで再計算:
- 兄弟の位置が
left→SHA-256(0x01 || sibling || current) - 兄弟の位置が
right→SHA-256(0x01 || current || sibling)
- 兄弟の位置が
- 計算されたルートが、
kindに対応するジャーナル上のルートと一致するか確認kind=included→includedBitmapRootkind=seen→seenBitmapRoot
flowchart TD
CK[チャンク受信] --> EX[ビット抽出<br/>included = true/false]
CK --> LH["リーフハッシュ計算"]
LH --> AP[監査パスに沿って<br/>ルートを再計算]
AP --> CMP{計算ルート =<br/>bitmap root ?}
CMP -->|一致| V[証明有効]
CMP -->|不一致| IV[証明無効]
zkVM ゲストとの連携
ビットマップルートは zkVM ゲストプログラム内で計算され、ジャーナルにコミットされます。
ゲストプログラムは以下の手順を実行します:
- 各投票に対してコミットメントの再計算と包含証明の検証を実施
- prover に提示された投票インデックスを
seenBitmapに記録 - 検証に成功して集計対象になった投票インデックスを
includedBitmapに記録 - それぞれのビットマップを LSB-first でパッキングし、32 バイトチャンクに分割
- CT スタイルのハッシュ規則で Merkle ルートを計算
seenBitmapRootとincludedBitmapRootをジャーナルにコミット
この計算はゲスト内で行われるため、STARK 証明がビットマップの正しさも保証します。サーバーが事後的にビットマップを改ざんしても、ジャーナルのルート値と一致しなくなるため検出されます。
サーバーのビットマップデータ管理
サーバーは /api/bitmap-proof 用に、最終化時の zkVM 出力(includedBitmap、ある場合は seenBitmap / seenBitmapRoot)を非公開 sidecar として保持します。これらは配布対象アーカイブ bundle.zip には含まれず、async finalize 経路では必要に応じて S3 の sibling object から復元されます。
安全性ゲートと検証結果の分岐
採用前と採用後で、counted_my_vote_included の判定が次の 2 つに分岐します。
- 採用前に弾かれる、または証明材料が取得できない →
counted_my_vote_includedはnot_run(証拠不足による fail-closed)。例:- zkVM 出力に bitmap データが無い
- 保存・復元時の root 一致ゲートで採用されなかった
- cast-time 証跡(
voteReceipt/userVote.proof)が store から再構成できずvoteReceipt.bulletinIndexが確定しない
- 採用後にクライアント側の root 照合が失敗 →
counted_my_vote_includedはfailed。サーバーが返した chunk と audit path から再計算したルートが、ジャーナルのincludedBitmapRoot(またはseenBitmapRoot)と一致しない。
採用前ゲートおよび counted_my_vote_included の評価詳細は チェック一覧 > counted_my_vote_included を参照してください。
検証パイプラインにおける役割
ビットマップ Merkle 証明は、Counted-as-Recorded 段階のチェックとして使用されます。
| チェック ID | 検証内容 |
|---|---|
counted_my_vote_included | ビットマップ Merkle 証明により、自分の投票インデックスがカウントされたことを確認する |
seenBitmapRoot が利用可能な場合は、このチェックが「prover に提示されたが無効化された」と「そもそも提示されなかった」も区別して説明します。
各チェックの判定ロジックは チェック一覧 > Counted-as-Recorded を参照してください。
プライバシーに関する注意
ビットマップ Merkle 証明では、対象ビットを含む 32 バイトチャンク全体がクライアントに提供されます。1 チャンクは 256 ビット分のカウント状態を含むため、近傍のインデックスのカウント状態が同時に開示されます。
本 PoC では 64 票が 1 チャンクに収まるため、チャンクを受け取った投票者は全 64 票のカウント状態を知ることができます。これは 63 票がボットのためチャンク漏洩の情報価値が限定的という割り切りで成立しています。影響評価と将来の緩和策は PoC の意図的な制約 > ビットマップチャンク漏洩 を参照してください。
LSB-first packing とビット境界の扱いは、Property-based Testing で生成入力を使って境界条件を探索し、Lean による形式化 の bitmap vectors で抽象モデルとの対応を検査します。
zkVM 設計
投票集計の正当性を STARK 証明として外部に持ち出す zkVM パイプラインを扱う部です。
この部に含まれる章
- zkVM の基礎 — zkVM の概念、RISC Zero の選択理由、データフロー、保証境界
- ゲストプログラム — zkVM 内で実行される検証・集計ロジック
- ホストと証明生成 — ホストプログラムと同期/非同期の証明パス
- 検証サービス — Rust ベースのレシート検証
- Image ID — ゲストバイナリの暗号的識別子と管理
想定読者と前提
- 想定読者: 集計の正当性を STARK で証明したい実装者・運用者
- 前提: 暗号プロトコル の入力コミットメントと Merkle ツリーを把握していること
本章で扱わないもの
- RISC Zero SDK の API リファレンスやアップグレード手順
- STARK / FRI の数学的構成証明(概念のみ zkVM の基礎 で扱う)
- ECS Fargate などインフラ側の構成(AWS アーキテクチャ を参照)
関連する章
- 暗号プロトコル — ゲストプログラムが入力として受け取るプリミティブ
- 検証パイプライン — 生成されたレシートとジャーナルがどのように検証されるか
- AWS アーキテクチャ — 非同期プローバーの実行環境
- 第三者検証ガイド —
bundle.zipでレシートをローカル監査する手順
zkVM の基礎
RISC Zero zkVM を採用した理由、データフロー、暗号学的保証の境界を整理する章です。
なぜ RISC Zero か
- Trusted setup 不要で扱いやすい。
- RISC-V 上で通常の Rust コードを使えるため、実装と監査の往復がしやすい。
- Image ID による実行バイナリ照合と組み合わせやすい。
アーキテクチャ概要
zkVM パイプラインは 3 つのコンポーネントで構成されます。
flowchart TB
subgraph C1["第1フェーズ: 入力構築"]
SES[セッションデータ] --> IB[入力ビルダー]
IB --> INP[ZkVMInput]
end
subgraph C2["第2フェーズ: 証明生成"]
INP --> HOST[ホストプログラム]
HOST --> GUEST["ゲストプログラム<br/>(zkVM 内実行)"]
GUEST --> JNL[ジャーナル]
HOST --> RCP[レシート<br/>STARK 証明]
end
subgraph C3["第3フェーズ: 検証"]
RCP --> VS[検証サービス]
VS --> VR[検証レポート]
end
| コンポーネント | 言語 | 責務 |
|---|---|---|
| ゲストプログラム | Rust | zkVM 内で投票を検証・集計し、結果をジャーナルにコミットする |
| ホストプログラム | Rust | 入力を受け取り、zkVM を起動して STARK 証明(レシート)を生成する |
| 検証サービス | Rust | レシートの STARK 検証を実行し、期待される Image ID との一致を確認する |
zkVM とは
RISC Zero zkVM は、RISC-V(RV32IM)命令列として実行されるゲストプログラムに対して、その実行が正しいことを STARK 証明で示すゼロ知識 VM です。
本システムでは「投票検証と集計」をゲストとして実行し、ホストがレシート(seal + journal)を生成します。第三者は Receipt::verify(image_id) により、指定したゲストバイナリ(Image ID)で実行された結果であることを検証できます。
30 秒で要点
| 観点 | 要点 |
|---|---|
| 何を証明するか | 「このゲストコードをこの入力で実行した結果がジャーナルである」こと |
| 何を公開するか | ジャーナル(公開出力)とレシート(証明本体) |
| どこが信頼アンカーか | Image ID(どのゲストを検証対象にするか) |
| この PoC での意味 | 集計値・除外件数・入力整合性を暗号学的に検証可能にする |
ジャーナルは証明に束縛される公開出力で、検証成功時に改ざんできません。ジャーナル項目の一覧と各フィールドが何を保証するかは ゲストプログラム > ジャーナル出力 を参照してください。配布対象ファイル(bundle.zip 等)との違いは バンドル構造 を参照してください。
RISC Zero zkVM 実装の要点
RISC Zero 公式ドキュメントに基づく実装上の要点:
- 実行モデル: ゲストは RV32IM として決定論的に実行され、外部との境界は syscall (
ecall) で扱う - 証明の分割: 長い実行はセグメント証明に分割される(大規模実行を扱うため)
- 再帰合成と圧縮: SDK は再帰合成や
succinct/Groth16への圧縮をサポートするが、本 PoC はCompositereceipt のまま配布する - 検証 API:
Receipt::verify(image_id)が、証明本体と Image ID の束縛を同時に検証する - 公開出力の束縛:
journalは証明に束縛されるため、検証成功後に改ざんできない
ジャーナルとレシート
- ジャーナル: ゲストが公開出力としてコミットするデータ(検証済み集計、除外情報、入力コミットメントなど)
- レシート: ジャーナルと STARK 証明(seal)のペア。検証成功時、ジャーナルは正しいゲスト実行結果として受理できる
flowchart TB R[レシート] R --> S[Seal<br/>STARK 証明] R --> J[ジャーナル<br/>公開出力]
ジャーナル各フィールドの定義と検証チェックとの対応は ゲストプログラム > ジャーナル出力 を、excludedSlots と rejectedRecords の使い分けは スロット / レコード分離モデル を参照してください。
数学ミニ補足(読み飛ばし可)
STARK の直感的理解に必要な最小限の説明だけを記します。
1. 巡回ドメイン(cyclic domain)
有限体 F の乗法群の部分群 H = {1, w, w^2, ..., w^(n-1)} を評価点集合(ドメイン)として使います。RISC Zero はこれを cyclic domain と呼びます。実行トレース(レジスタ値や遷移)は、この H 上で評価される多項式として扱われます。
2. 有限体上の多項式除算
制約違反の有無は、概念的には次の形で整理できます。
- 制約多項式を
C(x)とする - 評価領域
Hの零化多項式をZ_H(x)とする(典型例:Z_H(x) = x^n - 1) - すべての点で制約を満たすなら
C(h)=0(h in H)となり、C(x)はZ_H(x)で割り切れる - したがって
Q(x) = C(x) / Z_H(x)が多項式として成立するかを検査すれば、制約満足性をチェックできる
実際の STARK では、これを FRI(Fast Reed-Solomon IOP)と Merkle コミットメントを組み合わせて効率的に検証します。
この PoC での保証境界
Receipt::verify(image_id) の成功は強い保証ですが、それ単体で「全票が提示された」ことまでは保証しません。Verified 表示は次がすべて揃った場合にのみ成立します。
- STARK 検証成功(正しいゲスト実行)
excludedSlots == 0(除外スロットなし)totalExpected == treeSize(期待数と掲示板ツリーサイズの一致)- 追記専用性と締切 STH の必須チェック成功
加えて、必須チェックが not_run / pending / running のままでは Verified にはなりません。第三者 STH チェックは source 設定時に限り required に昇格します。詳細なゲーティング規範と判定ロジックは ゲーティングロジック を参照してください。
参考資料(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 プログラムです。ホストから投票データ(選択肢・乱数・コミットメント・Merkle パスと選挙メタデータ)を受け取り、以下の処理を行います:
- 各投票の正当性検証(コミットメント再計算 + 包含証明)
- 有効投票の集計
- カウント状態と提示状態のビットマップ計算
- 入力コミットメントと STH ダイジェストの計算
- 結果のジャーナルへのコミット
ゲスト内の処理はすべて STARK 証明に含まれるため、出力(ジャーナル)の正しさが暗号学的に保証されます。
入力構造
ゲストプログラムが受け取る入力(AggregatorInput)の構造を示します。
| フィールド | 型 | 説明 |
|---|---|---|
| election_id | 16 バイト | 選挙の UUID v4 バイナリ表現 |
| bulletin_root | 32 バイト | 掲示板 Merkle ツリーの最終ルート |
| tree_size | u32 | 掲示板のリーフ数(= 投票スロット数) |
| log_id | 32 バイト | 掲示板のログ識別子 |
| timestamp | u64 | 入力構築時に採用された最新 STH スナップショットの Unix タイムスタンプ |
| total_expected | u32 | 想定される総投票数 |
| election_config_hash | 32 バイト | 選挙設定のハッシュ値 |
| votes | VoteWithProof[ ] | 投票データと Merkle パスの配列 |
各 VoteWithProof は以下のフィールドを持ちます:
| フィールド | 型 | 説明 |
|---|---|---|
| index | u32 | 掲示板上のインデックス |
| choice | u8 | 選択肢(0 = A, 1 = B, 2 = C, 3 = D, 4 = E) |
| random | 32 バイト | コミットメント計算に使用した乱数 |
| commitment | 32 バイト | 投票コミットメント値 |
| merkle_path | 32 バイト[ ] | RFC 6962 Merkle 包含証明のパスノード |
処理パイプライン
ゲストプログラムの処理は、入力検証、投票検証・集計、出力構築の 3 フェーズで構成されます。
flowchart TD
subgraph "フェーズ 1: 入力検証"
I1[入力デシリアライズ] --> I2{掲示板ルート<br/>が非ゼロ?}
I2 -->|Yes| I3{ツリーサイズ<br/>が正の値?}
I2 -->|No| FAIL[不正入力]
I3 -->|Yes| I4{Phase 4 境界と<br/>Merkle パス長が有効?}
I3 -->|No| FAIL
I4 -->|Yes| NEXT[フェーズ 2 へ]
I4 -->|No| FAIL
end
subgraph "フェーズ 2: 投票検証と集計"
NEXT --> LOOP[各投票に対して]
LOOP --> V1[6 段階検証]
V1 -->|有効| TALLY[集計に加算]
V1 -->|無効| EXCL[却下カウントと<br/>スロット統計に反映]
TALLY --> BIT[ビットマップ更新]
end
subgraph "フェーズ 3: 出力構築"
LOOP -->|全投票完了| O1[ビットマップルート計算]
O1 --> O2[入力コミットメント計算]
O2 --> O3[STH ダイジェスト計算]
O3 --> O4[ジャーナルにコミット]
end
Note: 次のいずれかに該当する入力は、ジャーナル生成前に fail-closed で拒否されます。
bulletin_rootがゼロtree_sizeが 0tree_size > 1,000,000total_expected > 1,000,000votes.length > 1,000,000- 候補別 tally bucket が 1,000,000 を超える
- Merkle パス長が
u16::MAXを超える
votes.length > tree_sizeのような入力は、上記境界内であれば事前 reject されません。重複や範囲外は record 単位でrejectedRecordsに反映されます。
投票の 6 段階検証
各投票に対して、以下の 6 つの検証が順に実行されます。いずれかが失敗した投票は即座に「無効」として除外され、以降の検証はスキップされます。
flowchart TD
V[投票] --> C1{"第1検証<br/>インデックス範囲チェック"}
C1 -->|失敗| INV[無効として除外]
C1 -->|成功| C2{"第2検証<br/>インデックス重複チェック"}
C2 -->|失敗| INV
C2 -->|成功| C3{"第3検証<br/>選択肢範囲チェック"}
C3 -->|失敗| INV
C3 -->|成功| C4{"第4検証<br/>コミットメント再計算と照合"}
C4 -->|失敗| INV
C4 -->|成功| C5{"第5検証<br/>コミットメント重複チェック"}
C5 -->|失敗| INV
C5 -->|成功| C6{"第6検証<br/>包含証明検証"}
C6 -->|失敗| INV
C6 -->|成功| VALID[有効: 集計に加算]
各検証の失敗条件と、失敗したレコードがどのカウンタに反映されるかを示します。
| # | 検証 | 失敗条件 | 反映先 |
|---|---|---|---|
| 1 | インデックス範囲 | index >= tree_size(guest contract 上の index は u32) | rejectedRecords |
| 2 | インデックス重複 | 既に処理済みの index(2 番目以降) | rejectedRecords |
| 3 | 選択肢範囲 | choice が 0..=4(A..E)の外 | rejectedRecords + seenBitmap 反映 |
| 4 | コミットメント照合 | 再計算したコミットメントが入力の commitment と不一致 | rejectedRecords + seenBitmap 反映 |
| 5 | コミットメント重複 | 既に処理済みのコミットメント値(範囲内かつ初出スロットでも無効化) | rejectedRecords + seenBitmap 反映 |
| 6 | RFC 6962 包含証明 | Merkle パスから再計算したルートが bulletin_root と不一致 | rejectedRecords + seenBitmap 反映 |
範囲内かつ初出のスロットが #3〜#6 で無効化された場合、seenBitmap ではビットが立ち(提示はされた)、includedBitmap ではビットが立たない(計上されない)ため、invalidPresentedSlots として観測されます。範囲外(#1)や既処理スロットへの重複レコード(#2)は rejectedRecords のみに反映され、スロット単位の指標には影響しません。
コミットメント再計算と照合(#4)
ゲスト内で投票者の(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、入力として渡された値と照合します。これにより、投票者が主張する選択肢が掲示板上のコミットメントと一致することが保証されます。計算規則は コミットメントスキーム を参照してください。
RFC 6962 包含証明検証(#6)
投票のコミットメントが掲示板 Merkle ツリーに含まれることを、RFC 6962 PATH 関数ベースの CT スタイル包含証明で検証します。投票のインデックスと Merkle パスから掲示板ルートを再計算し、入力の bulletin_root と一致するかを確認します。リーフ・ノードハッシュの規則とドメインタグは CT Merkle ツリー を参照してください。
集計ロジック
6 段階検証をすべて通過した投票は「有効」として集計に加算されます。
- 集計は選択肢ごとの配列(5 要素)で管理
- 有効投票のインデックスに対応する
includedBitmapのビットをtrueに設定 - 無効投票はカウントされず、
includedBitmapのビットはfalseのまま - 範囲内かつ初出として提示された無効票は
seenBitmapではtrueになり、提示済みだが未計上のスロットとして扱われる
スロット / レコード分離モデル
現行のゲストプログラムは、掲示板スロットに対する完全性と、入力レコードの異常を別々に記録します。
flowchart LR TS["ツリーサイズ<br/>(全スロット)"] TS --> CNT["カウント済み<br/>validVotes"] TS --> INV["提示されたが未計上<br/>invalidPresentedSlots"] TS --> MIS["未提示<br/>missingSlots"] REC["入力レコード"] --> REJ["却下レコード<br/>rejectedRecords"]
| 指標 | 条件 | 意味 |
|---|---|---|
validVotes | 6 段階検証をすべて通過した範囲内かつ初出の投票 | 集計に含まれたスロット数 |
invalidPresentedSlots | 範囲内かつ初出のスロットが提示されたが、最終的に計上されなかった | 提示はされたがカウントに失敗したスロット数 |
missingSlots | 範囲内スロットが一度も提示されなかった | サーバーが prover に提示しなかったスロット数 |
rejectedRecords | 検証に失敗したレコード全体 | 重複 index、範囲外 index、重複 commitment なども含むレコード単位の却下数 |
スロット単位の 3 分類は、現行実装では常に次の関係を満たします:
validVotes + invalidPresentedSlots + missingSlots = treeSize
fail-closed 判定に使われる除外数は、スロット単位の excludedSlots です:
excludedSlots = missingSlots + invalidPresentedSlots
excludedSlots > 0 は検証失敗の決定的な指標です。1 スロットでも未提示または未計上であれば、集計結果の完全性が損なわれていることを意味します。
一方で rejectedRecords はレコード単位の補助指標です。たとえば次のようなケースでは rejectedRecords は増えても excludedSlots は増えません。
- 既に正しくカウント済みのスロットに対する重複インデックスレコード
tree_sizeの外側を指す範囲外レコード
旧 public contract の互換名は現行 journal にも公開レスポンスにも現れませんが、参考までに 1 対 1 対応を示します。
| 旧名 (compatibility mirror) | 現行 journal フィールド |
|---|---|
missingIndices | missingSlots |
invalidIndices | invalidPresentedSlots |
countedIndices | validVotes |
excludedCount | excludedSlots |
※ rejectedRecords は record 単位の新設カウントで、旧 invalidIndices の mirror は invalidPresentedSlots 側。
ジャーナル出力
ゲストプログラムがジャーナルにコミットする出力構造(VerificationOutput)を示します。
| フィールド | 型 | 説明 |
|---|---|---|
| electionId | UUID | 対象選挙 ID(入力の election_id をエコー) |
| electionConfigHash | 32 バイト | 選挙設定ハッシュ(入力の election_config_hash をエコー) |
| bulletinRoot | 32 バイト | 掲示板ルート(入力の bulletin_root をエコー) |
| treeSize | u32 | 掲示板のツリーサイズ(入力をエコー) |
| totalExpected | u32 | 想定総投票数(入力をエコー) |
| sthDigest | 32 バイト | STH ダイジェスト |
| verifiedTally | u32[5] | 選択肢 A〜E ごとの得票数 |
| totalVotes | u32 | zkVM が受け取った投票レコード数 |
| validVotes | u32 | 検証に成功した投票数 |
| invalidVotes | u32 | 検証に失敗した投票数 |
| seenIndicesCount | u32 | 範囲内かつ初出のインデックスとして処理した件数 |
| missingSlots | u32 | 一度も提示されなかった掲示板スロット数 |
| invalidPresentedSlots | u32 | 提示はされたが計上されなかった範囲内スロット数 |
| rejectedRecords | u32 | 却下されたレコード数(重複・範囲外・各種検証失敗を含む) |
| seenBitmapRoot | 32 バイト | prover に提示されたインデックス集合の ビットマップ Merkle ルート |
| includedBitmapRoot | 32 バイト | 実際にカウントされたインデックス集合の ビットマップ Merkle ルート |
| excludedSlots | u32 | 除外されたスロットの総数(= missingSlots + invalidPresentedSlots) |
| inputCommitment | 32 バイト | 入力コミットメント |
| methodVersion | u32 | ゲストプログラムのバージョン(現行 = 14) |
ジャーナルの信頼モデル
ジャーナルの各フィールドは、対応する STARK 証明により「ゲストプログラムが正しく計算した結果」であることが保証されます。
| ジャーナル項目 | STARK 証明で保証される内容 |
|---|---|
verifiedTally | 有効投票のみを正しく集計した結果である |
excludedSlots | 未提示または未計上のスロット数がゲストの計算結果と一致する |
rejectedRecords | 却下されたレコード数がゲストの計算結果と一致する |
inputCommitment | ゲストが処理した入力データを正準エンコードで束縛した値である |
seenBitmapRoot | prover に提示された範囲内かつ初出のインデックス集合から計算したルートである |
includedBitmapRoot | 実際にカウントされたインデックス集合から計算したルートである |
sthDigest | その実行で参照した掲示板状態から計算した値である |
一方、STARK 証明だけでは保証されないものがあります。「ゲストに提示されなかった票」「第三者 STH との合意」「ホストやサーバーの正直性」はジャーナル外の独立チェックで確認します。第三者はレシートの STARK 検証を行うだけで上記の保証を取得でき、ゲストロジックの信頼以外にホスト・サーバーを信頼する必要はありません。
ビットマップルートの計算
ゲストプログラムは投票検証と並行して seenBitmap(範囲内かつ初出として提示されたインデックス集合)と includedBitmap(6 段階検証を通過したインデックス集合)の 2 種類を構築し、それぞれの Merkle ルートをジャーナルにコミットします。この 2 つのルートを併用することで、公開検証側は「prover に提示されたが無効化された票」と「そもそも提示されなかった票」を区別できます。
LSB-first のバイト列パッキング、32 バイト境界による単一リーフ / 分割リーフの扱い、leaf / node hash 規則は ビットマップ Merkle を参照してください。
入力コミットメントと STH ダイジェスト
ゲストプログラムは投票処理の後、2 つの追加ハッシュ値を計算してジャーナルにコミットします。
入力コミットメント
ゲストに渡された入力のうち、公開フィールドを正準エンコーディングで連結し SHA-256 で圧縮します。現行実装では固定のドメインタグと format version を先頭に付与した上で、electionId・bulletinRoot・treeSize・totalExpected・votesCount と各投票の index・コミットメント値・Merkle パスを束縛します。投票列はハッシュ前に index 昇順で正規化されます(異常入力時の tie-break 補助ルールは 入力コミットメント > ソート規則 を参照)。
第三者は public-input.json などの公開検証用レコードから同じ値を再計算し、ジャーナルの値と照合することで、zkVM が処理した入力の同一性を検証できます。
詳細は 入力コミットメント を参照してください。
STH ダイジェスト
掲示板のログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを結合して SHA-256 で圧縮します。このダイジェストは第三者の STH ソースとの照合に使用され、サーバーが異なる投票者に異なる掲示板ビューを提示する分割ビュー攻撃を緩和します。
詳細は STH ダイジェスト を参照してください。
ゲストプログラムのバージョニング
ゲストプログラムにはバージョン番号が割り当てられ、ジャーナルの methodVersion フィールドに記録されます。現行のジャーナル契約は 14 です。
バージョン番号は Image ID の管理と連動しており、ゲストプログラムの変更は新しい Image ID の生成を伴います。検証時には、期待 Image ID との一致が確認されます。
ゲストの抽象 tally / rejection model と guest bounds は、Lean による形式化 で説明しています。Rust 側の guest-vector tests は、抽象モデルと実装の対応付けを検査します。
ホストと証明生成
ホストプログラムが zkVM 入力を組み立て、同期 / 非同期で STARK 証明を生成する流れを扱う章です。
同期モード(ローカルプロセス起動)と非同期モード(ECS Fargate タスク)の 2 つの証明パスがあり、どちらも同一のホストバイナリを使用します。入力構築からレシート出力までのフローを、両モードの差異とともに説明します。
パイプライン全体像
証明生成パイプラインは、入力構築、ホスト実行、出力処理の 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
入力構築
セッションデータからの抽出
入力ビルダーは、投票セッションに蓄積されたデータから zkVM 入力を構築します。
flowchart LR
subgraph セッションデータ
EID[選挙 ID]
ECFG[選挙設定<br/>+ 設定ハッシュ]
VOTES["投票データ<br/>(選択肢, 乱数, コミットメント)"]
BULL["掲示板<br/>(ルート履歴, 包含証明)"]
LID[ログ ID]
end
subgraph ZkVMInput
EID2[election_id]
ECFG2[election_config_hash]
BR[bulletin_root]
TS[tree_size]
TE[total_expected]
VWP["votes[]<br/>(VoteWithProof)"]
LID2[log_id]
TSTAMP[timestamp]
end
EID --> EID2
ECFG --> ECFG2
ECFG --> TE
BULL --> BR
BULL --> TS
VOTES --> VWP
LID --> LID2
入力構築で行われる主要な処理:
- 掲示板の最新 STH スナップショット取得: ルートハッシュ、ツリーサイズ、タイムスタンプを取得
- 選挙設定の整合性確認:
electionConfigとelectionConfigHashが一致することを確認 - 投票データの変換: 各投票の選択肢を整数に変換(A=0, B=1, C=2, D=3, E=4)
- Merkle パスの解決: 各投票について、掲示板から最新の包含証明を取得
- 総投票数の設定: 選挙設定の
totalExpected(本 PoC ではボット 63 票 + ユーザー 1 票 = 64)
通常のセッション入力では、投票インデックスは 0 から連続する canonical CT index であることを要求します。教育的な除外シナリオでは、元の掲示板インデックスを保つために sparse index を許可します。これによりゲスト側で missingSlots として観測できるようになります。
Merkle パスの解決戦略
各投票の Merkle パスは、以下の優先順位で解決されます:
flowchart TD
START[Merkle パス解決] --> P1{掲示板から<br/>包含証明を取得可能?}
P1 -->|Yes| USE1[掲示板の証明を使用]
P1 -->|No| P2{投票データに<br/>事前計算パスが存在?}
P2 -->|Yes| CHK{treeSize が一致?}
CHK -->|Yes| USE2[事前計算パスを使用]
CHK -->|No| ERR[エラー]
P2 -->|No| ERR
ホストプログラムの実行
ホストバイナリの役割
ホストバイナリは Rust で記述された CLI プログラムです。証明モードでは以下の処理を行います:
- JSON 形式の入力ファイルを読み込み
- JSON のバイト配列表現を Rust の固定長配列型へ変換(
Vec<u8>→[u8; 16/32]) ExecutorEnvに入力をシリアライズして設定- デフォルトプローバーを使用して zkVM ゲストを実行
- レシート(STARK 証明 + ジャーナル)を取得
- ジャーナルをデコードし、出力ファイルに書き出し
入力 JSON は TypeScript 側のエグゼキューターが事前に正規化して生成します(UUID/ハッシュ文字列をバイト配列へ変換)。
ImageID の確認だけを行う場合は host --print-image-id [--json] を使います。このモードでは入力ファイルを読まず、証明生成やアーティファクト出力も行いません。--json 付きでは imageId と methodVersion を含む JSON を stdout に出力します。
境界に違反する入力が渡された場合、host run は fail-closed で停止し、receipt / journal を出力しません。境界条件の詳細は 処理パイプライン を参照してください。
非同期モードで S3 に置かれる work input には、host CLI の証明入力に加えて contractGeneration と election_config も含まれます(コンテナ entrypoint 側で public-input.json / election-manifest.json を生成・検査するために使う)。
出力ファイル
証明モードのホストバイナリは 2 つの JSON ファイルを出力し、ビットマップ整合性検査を通過した場合は private bitmap artifact も追加で出力します。
| ファイル | 内容 |
|---|---|
| レシートラッパー JSON | { "receipt": ..., "image_id": "0x..." } 形式のラッパー JSON |
| 出力 JSON | デコード済みのジャーナル(集計結果、除外情報、各種ハッシュ値) |
レシートラッパー JSON には top-level image_id フィールドも含まれます。検証サービスでの使われ方は 検証サービス を参照してください。
また、ホストはビットマップの整合性を確認し、一致した場合のみ以下の非公開アーティファクトを出力します。
| ファイル | 内容 |
|---|---|
*-bitmap.json | counted bitmap の厳密 artifact(includedBitmapRoot と対応) |
*-seen-bitmap.json | presented bitmap の厳密 artifact(seenBitmapRoot と対応) |
非同期モードでは、これらの host 生出力は included-bitmap.json / seen-bitmap.json として bundle.zip の隣に配置されます。どちらも非公開アーティファクトであり、配布対象アーカイブ bundle.zip には含めません。
同期モード
同期モードでは、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 を引き継ぐ(特に RISC0_DEV_MODE と RUST_LOG が証明モード・ログに影響) |
| エラー処理 | 終了コード非ゼロ、タイムアウト、ファイル不在で失敗 |
結果の変換
エグゼキューターは出力 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->>SQS: ファイナライズリクエスト SQS->>D: work message D->>S3: 入力 JSON アップロード 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 は methodVersion 14 の host output を受け付け、journal.json / public-input.json / election-manifest.json / close-statement.json を生成する際に methodVersion と inputCommitment の整合性を検査します。methodVersion 14 契約と一致しない host artifact は fail-closed で停止します。
非同期モードの特性
| 項目 | 値 |
|---|---|
| タイムアウト | 15 分(デフォルト、環境変数で変更可能) |
| リトライ | S3 アップロードは指数バックオフで 3 回リトライ |
| エラー処理 | Step Functions がタスク失敗を検出し、失敗コールバックを実行 |
| ステータス確認 | クライアントは /api/sessions/:id/status でポーリング |
開発モードの動作
RISC0_DEV_MODE=1 を設定すると、RISC Zero は STARK 証明を生成せず、フェイクレシートを返します。
| 項目 | 開発モード (RISC0_DEV_MODE=1) | 本番モード |
|---|---|---|
| 証明の種類 | フェイクレシート | 本物の STARK 証明 |
| 実行時間 | 約 100 ミリ秒 | 約 370 秒(64 票の場合) |
| 安全性 | なし(検証を省略) | 暗号学的に完全 |
| 検証サービス | Fake として検出 | 完全な STARK 検証を実行 |
開発モードのレシートは内部的には InnerReceipt::Fake 型で、検証サービス では通常 dev_mode 扱い(image_id 不一致などの事前条件違反時のみ Failed)になります。dev_mode は診断ステータスであり、本番モードの成功検証としては採用されません。
開発モードは以下の用途に限定されます:
- host CLI や verifier 連携を含むローカルの高速フィードバック
- TypeScript と Rust の契約を短時間で確認する smoke test
- dev-mode receipt 分岐を明示的に通す CLI / E2E 検証
UI 開発などで使う USE_MOCK_ZKVM=true は、TypeScript の mock executor を選ぶ別経路です。この経路はホストバイナリも RISC Zero SDK も呼ばず、TypeScript 内で完結します。
検証サービス
STARK レシートを検証する Rust サービスの構造と、ローカル / Lambda 双方での使い方を扱う章です。
レシートの STARK 検証をサーバー側で行い、結果をレポートとしてクライアントに提供する Rust コンポーネントです。
概要
STARK 証明の検証は計算コストが証明生成に比べて低いものの、ブラウザ上で RISC Zero の検証ロジックを実行することは現時点では実用的ではありません。そのため、本システムではサーバー側で検証を行い、その結果をレポートとしてクライアントに提供する設計を採用しています。
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 フィールドで、レシート本体の内部フィールドではありません。同期ファイナライズ経路では proof bundle ディレクトリ全体を渡し、その中の receipt.json を解決させる実装になっています。
| 形式 | 説明 |
|---|---|
| フラット JSON | レシートオブジェクトが直接 JSON のトップレベルにある |
| ネスト JSON | { "receipt": {...}, "image_id": "0x..." } 構造 |
| ディレクトリ | receipt.json または *-receipt.json を探索して読み込む |
| ZIP アーカイブ | エントリ名の末尾が receipt.json のファイルを探索して読み込む |
Image ID 照合と Fake receipt の扱い
ラッパーの image_id を期待値と照合し、不一致なら failed として即時拒否します。Image ID の管理は Image ID を参照してください。
レシートの内部構造(InnerReceipt)が Fake 型の場合は開発モードで生成されたレシートですが、即 dev_mode にはなりません。Image ID 不一致なら failed、Image ID 一致時のみ Receipt::verify の結果に応じて dev_mode に振り分けられます。
STARK 検証の実行
Image ID 照合に成功した後、RISC Zero SDK の Receipt::verify(expected_image_id) で STARK 証明を検証します。検証成功は次を保証します。
- レシートに含まれる seal(証明データ)が有効
- ジャーナルが指定 Image ID のゲスト実行結果である
- 証明生成後にレシートが改ざんされていない
検証レポート
検証サービスは、検証が最後まで到達した試行について JSON レポートを出力します。exit code とレポート出力の関係は次のとおりです。
| 状況 | exit code | JSON レポート |
|---|---|---|
success | 0 | stdout または --output |
| 引数不正・bundle 不在など | 1 | 出力しない |
dev_mode | 2 | stdout または --output |
failed | 3 | stdout または --output |
呼び出し側は exit code とレポートの両方を見ます。--quiet を指定すると stdout 出力は抑制されるので、その場合は --output も併せて指定してレポートを保存します。
| フィールド | 型 | 説明 |
|---|---|---|
| status | 列挙型 | success / failed / dev_mode |
| verifier_version | 文字列 | verifier-service のバージョン |
| verified_at | 文字列 | RFC 3339 形式の検証完了時刻 |
| duration_ms | 数値 | 検証処理時間(ミリ秒) |
| expected_image_id | 文字列 | 検証に使用した期待 Image ID |
| receipt_image_id | 文字列? | 入力 JSON の top-level image_id から抽出した値 |
| bundle_path | 文字列 | 入力 bundle パスの basename のみ |
| receipt_path | 文字列 | 解決されたレシートファイル名の basename のみ |
| dev_mode_receipt | 真偽値 | Fake receipt なら true。status 単独では success と dev_mode を区別できないため判定に併用 |
| errors | 文字列[] | 診断文字列の配列。空の場合は省略される |
errors は固定のエラーコード一覧ではなく、実装が積む自由形式の診断文字列です。
ステータスの意味づけ
| ステータス | 意味 | UI への影響 |
|---|---|---|
success | STARK 検証が成功し、Image ID も一致 | STARK Verified を表示可能 |
failed | Image ID 不一致または STARK 検証失敗 | 検証失敗として表示 |
dev_mode | 開発モードのフェイクレシート | 開発モード警告を表示 |
デプロイメントモデル
検証サービス(Rust バイナリ verifier-service)は、呼び出し経路ごとに実行場所が異なります。詳細は下の「呼び出しパターン」を参照してください。
sequenceDiagram
participant C as クライアント
participant API as API サーバー
participant RUNNER as verifier-service-runner Lambda
participant VS as verifier-service バイナリ
participant S3 as S3
C->>API: POST /api/verification/run
API->>API: session capability と finalization result を確認
alt S3 bundle locator がある
API->>RUNNER: 実行要求(sessionId, executionId, bundleKey, expectedImageId)
RUNNER->>S3: bundle.zip を取得・展開
RUNNER->>VS: bundle ディレクトリ + 期待 Image ID + reportPath
VS->>VS: STARK 検証実行 + verification.json 書き出し
VS-->>RUNNER: 検証レポート
RUNNER-->>API: 検証レポート + S3 report locator
else 信頼済み local bundle がある
API->>VS: bundle ディレクトリ + 期待 Image ID + reportPath
VS->>VS: STARK 検証実行 + verification.json 書き出し
VS-->>API: 検証レポート
end
API->>API: verificationResult / execution 状態を更新
API-->>C: 検証結果
呼び出しパターン
検証サービスの呼び出しには主に 3 つのパターンがあります。明示的検証はいずれも、サーバー側で保持している finalization result に紐付いた 権威ある bundle locator だけを使い、クライアントが任意の S3 キーや local パスを指定して検証させることはできません。
| パターン | トリガー | 説明 |
|---|---|---|
| 同期実行 | 同期ファイナライズ (POST /api/finalize) | real executor 時のみ実行。mock executor 時は verifier-service を呼ばず dev_mode 扱いとして帰る |
| 明示的 S3 実行 | クライアントが検証を要求 | POST /api/verification/run が verifier-service-runner Lambda に S3 bundle locator を渡す |
| 明示的 local 実行 | クライアントが検証を要求 | POST /api/verification/run が信頼済み local bundle を API サーバープロセス内で直接検証する |
非同期ファイナライズのコールバック Lambda は、結果の復元と保存を担当します。STARK 検証は自動実行されず、POST /api/verification/run で実行します。
verificationResult.status が success / failed / dev_mode のような終端状態にある場合、POST /api/verification/run は再検証せず idempotent な応答を返します。running の場合も実行中として idempotent に扱い、status 未設定または not_run のときだけ verifier-service の実行に進みます。
検証パイプラインにおける役割
検証サービスは、4 段階検証モデルの最終段階である STARK 検証を担当します。
| チェック ID | 検証内容 |
|---|---|
stark_image_id_match | レシートに記録された Image ID が期待値と一致するか |
stark_receipt_verify | STARK 証明が暗号学的に有効であるか |
stark_image_id_match は verifier report の expected_image_id と receipt_image_id が一致することを検証します。検証パイプラインはさらに claimed / comparison 側の Image ID とも整合することを確認します。
これらのチェックが両方成功した場合に限り、「STARK Verified」のステータスが付与されます。詳細は 4 段階検証モデル を参照してください。
セキュリティ上の考慮事項
サーバー側検証の信頼境界
検証サービスはサーバー側で実行されるため、クライアントはサーバーの検証結果を信頼する必要があります。この PoC における信頼モデルは以下の通りです:
- STARK 証明自体は秘密データを含まない検証データ: レシートと Image ID があれば、第三者が独立に検証可能
- 検証サービスは利便性のための委任: ブラウザでの STARK 検証が実用的になれば、クライアント側のみで完結させることも理論上は可能
- 配布対象アーカイブ: レシートと
public-input.jsonは ZIP ローカル検証(Ubuntu) の手順で独立検証できる
verification.json の非公開性
verification.json は配布対象アーカイブ bundle.zip には含まれません。必要に応じてレポート用の capability 保護エンドポイント経由で配布されますが、第三者検証ではレシートファイルを直接使った独立検証が推奨されます。
Image ID
ゲストバイナリを一意に識別する Image ID をどう発行し、どう検証で照合するかを扱う章です。
Image ID はゲストプログラムの ELF バイナリから決定的に導出される 256 ビットのハッシュ値です。レシート検証時に期待値との一致を確認することで、正しいプログラムの実行結果であることを保証します。
概要
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 が生成され得ます:
| 要因 | 影響 |
|---|---|
| ゲストコードの変更 | ロジックの変更は異なるバイナリを生成 |
| コンパイラバージョン | Rust ツールチェインのバージョン差異 |
| ターゲットアーキテクチャ | 同一コードでも x86_64 と ARM64 で異なる Image ID |
| RISC Zero SDK バージョン | SDK の変更がゲストバイナリの構造に影響 |
アーキテクチャによる差異
本システムでは、同一バージョンのゲストに対して既定 variant 用 (expectedImageID) と x86_64 用 (expectedImageID_x86_64) の 2 つの Image ID をマッピング上で管理できます。expectedImageID は通常、昇格済みの ARM64/ECS 用 Image ID として扱います。
| アーキテクチャ | 用途 |
|---|---|
| ARM64 | ECS Fargate (Graviton) での本番証明生成 |
| x86_64 | ローカル開発、CI/CD 環境での証明生成 |
現行実装では、実行環境の自動判定で x86_64 を選びません。未指定時は default variant として expectedImageID を使い、x86_64 用の値を使うには EXPECTED_IMAGE_ID_VARIANT=x86_64 または呼び出し側の明示 variant で選択します。variant 選択と EXPECTED_IMAGE_ID オーバーライドの優先順位は下の Image ID の解決 を参照してください。
Image ID マッピング
期待される Image ID は、バージョンごとにマッピングファイルで管理されます。
マッピングの構造
マッピングファイルには、各バージョンの Image ID、説明、ビルド情報、機能リストが記録されます。
| フィールド | 説明 |
|---|---|
| methodVersion | ゲストプログラムのバージョン番号 |
| expectedImageID | ARM64 環境での Image ID |
| expectedImageID_x86_64 | x86_64 環境での Image ID |
| description | バージョンの説明 |
| compiledAt | Image ID を取得したビルド時刻 |
| rustVersion | ビルドに使用した Rust バージョン |
| risc0Version | ビルドに使用した RISC Zero SDK バージョン |
| guestToolchain | ゲストビルド用ツールチェイン |
| features | このバージョンで実装された機能リスト |
| current | 現在有効なバージョン番号 |
| deprecated | 非推奨バージョンの一覧 |
| metadata | マッピングファイル全体の管理メタデータ |
バージョン履歴の管理
マッピングファイルはバージョンの履歴を保持します。current フィールドが現在有効なバージョンを指し、deprecated フィールドが過去のバージョンを列挙します。current バージョンは既定 variant と x86_64 の 2 系統の Image ID を持ちます。
現行実装では current は 14 で、v8〜v13 が deprecated 側にあります。v14 の mapping は、ECS Fargate で使う正式な既定 variant (expectedImageID) と、ローカル / CI の x86_64 検証で使う expectedImageID_x86_64 を保持します。
flowchart LR CUR["current<br/>(ARM64 + x86_64)"] DEP["deprecated<br/>(履歴のみ)"] CUR -. version up .-> DEP
Image ID の解決
検証時に使用する期待 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[expectedImageID]
P3 -->|x86_64| F2[expectedImageID_x86_64]
F1 --> CHK[fail-closed 条件を適用]
F2 --> CHK
解決ルールの要点:
EXPECTED_IMAGE_ID環境変数は最優先のオーバーライドです。resolveExpectedImageId()//api/verification/runの version 選択:- 省略時: マッピングの
currentを使用 - 明示時:
CURRENT_METHOD_VERSIONと一致する場合のみ受理。deprecated側は拒否
- 省略時: マッピングの
- 低レベルのマッピング読み取り API は
deprecatedも含めて明示 version を解決できる(この検証実行経路では使わない)。 - variant は
EXPECTED_IMAGE_ID_VARIANT(defaultまたはx86_64)または呼び出し側の明示 option で選択し、未指定時はdefault。それ以外の値は受け付けません。 - 未対応
methodVersion、マッピング読み込み失敗、選択した variant の値が空、いずれも暗黙のフォールバックを行わず fail-closed でエラーになります。 - 現行の検証実行フローでは、正規化済みのジャーナルから
methodVersionを取得してresolveExpectedImageId(methodVersion)を呼びます(public-input.jsonフォールバックは現行未使用)。
検証パイプラインにおける役割
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(expectedImageID)を実行する
詳細は 検証サービス を参照してください。
Image ID が不一致の場合、以下のいずれかの状況を意味します:
| 原因 | 対処 |
|---|---|
| マッピングが古い | ゲストの再ビルド後にマッピングを更新する |
| 異なるゲストで証明が生成された | レシートの出所を調査する |
| アーキテクチャの不一致 | 正しいアーキテクチャの Image ID で照合する |
Image ID の更新手順
ゲストプログラムを変更した場合、Image ID を更新する必要があります。
- ゲストコードを変更する
- zkVM ゲストをビルドし、
host --print-image-id [--json]で新しい Image ID を取得する public/imageId-mapping.jsonを更新する(必要に応じてexpectedImageID_x86_64も更新)- 関連コードに残る定数参照も必要に応じて更新する(現行実装では
src/lib/verification/expected-image-id.tsのDEFAULT_POC_IMAGE_IDがテストなどで参照される) - プローバーイメージとマッピングを同時にデプロイする
--json を付けると {"imageId":"0x...","methodVersion":14} の形で出力されます。CodeBuild は ARM64 prover image のビルド後にこの JSON を取得し、imageId と methodVersion を image metadata として記録します。
更新の同期要件
プローバーイメージと imageId-mapping.json は同一リリースで切り替えます。片方だけが新しい場合は検証時に Image ID 不一致で失敗します。
- 通常の
/api/verification/runフローでは、現行の 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 段階検証モデル — 検証の全体設計と各段階の保証
- チェック一覧 — 全検証チェック ID とその判定ロジック
- バンドル構造 — 証明バンドルの公開可能・非公開アーティファクト
- ゲーティングロジック — 「Verified」表示の条件と不変条件
想定読者と前提
本章で扱わないもの
関連する章
- 暗号プロトコル — チェック対象となるプリミティブ
- zkVM 設計 — ジャーナルとレシートの構造
- 改ざんシナリオ — どのチェックがどの改ざんを検出するか
- 第三者検証ガイド —
bundle.zipを使ったローカル監査 - 用語集 — チェック種別・ゲーティング用語の定義
設計と実行フロー
検証パイプラインの設計原則と、リクエストから判定までの実行フローを扱う章です。
設計原則
本システムの検証パイプラインは、以下の 3 つの原則に基づいて設計されています。
原則 1: 必要な検証が未実行なら Verified を表示しない
required チェックが not_run(未実行)、pending(依存待ち)、running(実行中)のいずれかにある場合、システムは「Verified」を表示しません。証拠の不在や未解決状態を成功として扱わないという姿勢です。
原則 2: 失敗した検証は即座にブロックする
いずれかの必須チェックが失敗すれば、「Verified」表示は即座にブロックされます。代表的な失敗条件:
excludedSlots > 0(除外されたスロットが存在する)- 整合性証明の失敗
- 公開監査アーティファクトとの不一致
- 第三者 STH 合意の不成立(設定時)
原則 3: チェック評価はサーバー中心、集約はサーバーとクライアントの双方で実施
GET /api/verifyは 22 チェックのレスポンスを組み立てます。サーバーは Stage 2-4 の 18 チェックを評価し、Cast-as-Intended の 4 チェックはnot_runで返したうえで、クライアントがローカル再評価で上書きします。- Recorded-as-Cast は cast-time 証跡(
voteReceiptとuserVote.proof)を前提とします。store から再構成できない場合でも/api/verifyは200を返し、関連チェックをnot_runにして全体判定をmissing_evidence側へ fail-closed に倒します。 - STARK 検証は専用サービス(
POST /api/verification/run)で実行され、GET /api/verifyがその結果を読み取ります。 deriveVerificationSummaryはサーバー側の/api/verifyとクライアント側の/verifyの両方で使われます。- サーバーは
verificationStatusを fail-closed に補正します。unsupported な verifier status でもverificationSteps/verificationChecksを含む200応答を返します。 - クライアントの最終判定は、明示的な STARK/server failure、hard-failure チェックの override、summary tone、pending state を優先順に解決します。UI 側の補助分岐については ゲーティングロジック を参照してください。
必要なデータ(userVote.proof.treeSize、journal など)が不在のときの not_run 補正など、ステップ status のガード条件の詳細は ゲーティングロジック を参照してください。
検証パイプラインの全体構造
1. 段階の依存関係
- Stage 1 — Cast-as-Intended
- Stage 2 — Recorded-as-Cast
- Stage 3 — Counted-as-Recorded
- Stage 4 — STARK Verification
- 結果表示
2. 実行責務(どこで評価・検証されるか)
flowchart TD
subgraph SERVER["サーバー側"]
VFY["GET /api/verify<br/>Stage 2-4 を評価<br/>Cast は not_run(クライアント再評価)"]
RUN["POST /api/verification/run<br/>bundle 参照と expected Image ID で<br/>Stage 4 を検証"]
end
subgraph CLIENT["クライアント(UI)"]
UI["Cast のローカル再評価<br/>STARK 解決後に step 表示を開始<br/>明示的 failure / hard-failure override / summary / pending を反映"]
end
VFY --> UI
RUN --> UI
検証の実行フロー
検証導線では通常 /result から /verify へ進みます。
/resultは正準な finalization snapshot をクライアント状態に保存します。「検証へ進む」を押すとverificationRequestedAtを保存し、必要ならPOST /api/verification/runを非同期に先行起動します(完了を待たずに/verifyへ遷移します)/verifyはその継続状態がある場合に検証シーケンスを続行します。STARK が未開始ならシーケンス内で起動できます- 継続状態がなく STARK が
not_runのまま直接/verifyへアクセスした場合は、自動続行せずブロックします /verifyの UI シーケンスは、step を順に見せる前に STARK が terminal status に到達するまでポーリングします。timeout や transport failure は STARK failure として扱われます
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 /api/verification/run(fire-and-forget)
R-->>V: /verify へ遷移(run 完了は待たない)
Note over V: /verify 到達時に未開始なら<br/>同じ POST /api/verification/run を起動
A->>VS: bundle 参照 + expected Image ID
VS->>VS: Receipt::verify(imageId)
VS-->>A: 検証レポート保存
loop STARK 完了までポーリング
V->>A: GET /api/verify
A-->>V: 検証ペイロード<br/>(ステップ, チェック, 証明材料)
end
Note over V: 継続には verificationRequestedAt と finalization snapshot が必要<br/>not_run の direct access はブロック<br/>step 表示は STARK 解決後に開始
4 段階の概要
| 段階 | 名称 | 証明する内容 | 検証の実行場所 |
|---|---|---|---|
| Stage 1 | Cast-as-Intended | 投票者の意図通りにコミットメントが生成された | クライアント(/verify 画面でローカル再計算) |
| Stage 2 | Recorded-as-Cast | コミットメントが追記専用掲示板に正しく記録された | サーバー(GET /api/verify) |
| Stage 3 | Counted-as-Recorded | 記録された全投票が正しく集計に含まれた | サーバー(GET /api/verify) |
| Stage 4 | STARK Verification | zkVM の実行が正しく行われたことの暗号学的証明 | サーバー(POST /api/verification/run) |
各ステージの導出ルール(required チェック群からの集約、STH source 設定時の昇格、ガード条件)は ゲーティングロジック を参照してください。
各段階の詳細は 4 段階検証モデル を参照してください。
検証チェック数
パイプライン全体で 22 個の検証チェックが定義されており、各チェックには一意の ID が割り当てられています。チェック ID の単一ソースは src/lib/verification/verification-checks.ts です。
| 段階 | チェック数 |
|---|---|
| Cast-as-Intended | 4 |
| Recorded-as-Cast | 6 |
| Counted-as-Recorded | 10 |
| STARK Verification | 2 |
| 合計 | 22 |
Counted-as-Recorded の required には counted_election_manifest_consistent と counted_close_statement_consistent も含まれ、公開された election-manifest.json / close-statement.json との整合が stage success の条件に入ります。チェック詳細は チェック一覧 を参照してください。
4 段階検証モデル
E2E 検証可能投票の各段階(Cast-as-Intended / Recorded-as-Cast / Counted-as-Recorded / STARK Verification)でどんな保証が成り立つかを扱う章です。
段階間の依存関係
概念モデルとしては 4 段階を順に評価しますが、実行場所は段階ごとに異なります(Stage 1 はクライアント、Stage 2-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) | セッション作成時に確定した UUID |
| 選択肢 | クライアントセッション(localStorage.starkBallotSession) | 投票者が選択した値(A〜E) |
| 乱数 | クライアントセッション(localStorage.starkBallotSession) | 投票時にクライアントが生成した 32 バイト乱数 |
| 投票レシート | GET /api/verify 応答(voteReceipt) | 投票レシート(commitment, voteId, bulletinIndex, bulletinRootAtCast) |
voteReceipt は cast-time 証跡が再構成できた場合のみ返されます(設計原則 3 参照)。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
| ローカル証拠の欠落 | localStorage 消去や別端末アクセスで投票時データを復元できない | 検証不能 |
| 投票レシート証跡の欠落 | store から cast-time 証跡を再構成できず、voteReceipt が省略 | 検証不能 |
| コミットメント不一致 | 投票時データと投票レシートの不整合、またはエンコーディングの不整合 | 重大 |
| 選択肢の範囲外 | 不正な入力(A〜E の範囲外) | 重大 |
| 乱数フォーマット不正 | 32 バイト hex でない | 重大 |
限界
この段階は投票者の手元データに依存するため、localStorage 消去後や別端末からは検証できません。
Stage 2: Recorded-as-Cast
目的
投票者のコミットメントが、追記専用の掲示板(CT Merkle ツリー)に正しく記録されていることを確認します。さらに、掲示板が追記専用性を維持していること(過去のエントリが削除・改変されていないこと)を検証します。
検証する内容
この段階には 3 系統の検証があります。UI ステップとの対応関係は ゲーティングロジック を参照してください。
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 包含証明を検証し、投票者のコミットメントが掲示板のツリーに含まれていることを確認します。検証者はリーフハッシュと監査パスからルートハッシュを再計算し、期待されるルートと照合します。
2b: 整合性証明(Consistency Proof)
投票時点のツリー状態(ルートとサイズ)から、最終的なツリー状態への遷移が追記のみで行われたことを検証します。RFC 6962 の SUBPROOF アルゴリズムに基づく整合性証明により、サーバーが過去のエントリを密かに削除したり順序を変更したりするスプリットビュー攻撃を検出します。
2c: 第三者 STH 検証(オプション)
複数の独立した STH(Signed Tree Head)ソースに問い合わせ、合意が成立しているかを確認します。これにより、サーバーが検証者ごとに異なるツリー状態を提示するスプリットビュー攻撃を検出します。照合条件の詳細は チェック一覧 を参照してください。
必要な証拠
| 証拠 | 取得元 | 説明 |
|---|---|---|
| 包含証明の検証結果 | /api/verify | サーバー側で RFC 6962 包含証明を評価したチェック結果 |
| 整合性証明の検証結果 | /api/verify | サーバー側で RFC 6962 整合性証明を評価したチェック結果 |
| 投票時のルートハッシュ | 投票レシート | 投票受理時のツリールート |
| 投票時のツリーサイズ(oldSize) | /api/verify の userVote.proof.treeSize | 整合性証明の oldSize |
| 最終ルートハッシュ/最終ツリーサイズ | /api/verify | 集計時の最終状態 |
| 独立検証用の包含証明材料(任意) | /api/bulletin/:voteId/proof | クライアント外で個別に包含証明を再検証するための材料 |
| 補助 tooling の整合性証明材料(任意) | /api/bulletin/consistency-proof | 補助 tooling 用(現行 /verify の最終判定には使用しない) |
| STH スナップショット | 設定済み STH ソース(例: /api/sth + 外部) | 第三者ソースの照合対象(必須は digest、root/treeSize は返却時のみ) |
userVote.proof.treeSize は整合性証明の oldSize として参照されます。
関連: fail-closed(cast-time 証跡が欠ける場合の動作) / 設計原則 3 / チェック一覧(判定の詳細)。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
| 包含証明の検証失敗 | ツリーサイズ/インデックスの不一致、掲示板のリセット | 重大 |
| 整合性証明の検証失敗 | 追記専用性の違反(スプリットビュー攻撃の可能性) | 重大 |
| cast-time 証跡の欠落 | store から voteReceipt / userVote.proof を再構成できず、Recorded が未実行 | 検証不能 |
| 第三者 STH 合意の不成立 | サーバーが検証者ごとに異なるツリーを提示 | 重大(有効時) |
| ルートが履歴に存在しない | ルート履歴の不整合 | 重大 |
Stage 3: Counted-as-Recorded
目的
掲示板に記録された全投票が、zkVM の集計処理に正しく含まれたことを確認します。投票の除外、欠落、重複がないことに加え、公開された claimed tally(表示用集計値)が zkVM の verifiedTally と一致することを検証し、集計結果の完全性と整合性を保証します。
検証する内容
この段階では 10 個の required チェックが全て success であることを要求します。導出ルールと journal 省略時のガード補正は ゲーティングロジック を参照してください。
flowchart TD
subgraph PUBLIC["public 系 required (6)"]
P["counted_input_sanity<br/>counted_unique_indices<br/>counted_unique_<br/>commitments<br/>counted_election_<br/>manifest_consistent<br/>counted_close_<br/>statement_consistent<br/>counted_input_<br/>commitment_match"]
end
subgraph ZK["zk 系 required (4)"]
Z["counted_tally_consistent<br/>counted_missing_<br/>indices_zero<br/>counted_expected_<br/>vs_tree_size<br/>counted_my_vote_<br/>included"]
end
RESULT{"10 required 全て success?"}
PASS["Counted: success"]
FAIL["Verified をブロック"]
P --> RESULT
Z --> RESULT
RESULT -->|Yes| PASS
RESULT -->|No| FAIL
必要な証拠
| 証拠 | 取得元 | 説明 |
|---|---|---|
| zkVM ジャーナル(詳細) | /api/verify?includeJournal=1 | 集計結果、除外情報、inputCommitment、includedBitmapRoot、seenBitmapRoot などの詳細 |
| 集計サマリー(通常応答) | /api/verify | missingSlots / invalidPresentedSlots / rejectedRecords / excludedSlots / totalExpected / treeSize などの上位値 |
| 公開入力サマリー | サーバー内部評価用 | public-input.json 相当から組み立てた、秘密データを含まない入力要約 |
| 選挙マニフェスト | 公開監査アーティファクト(election-manifest.json) | electionId と electionConfigHash を束縛する公開可能アーティファクト |
| 締め処理ステートメント | 公開監査アーティファクト(close-statement.json) | logId / treeSize / bulletinRoot / timestamp / sthDigest を束縛する公開可能アーティファクト |
| ビットマップ証明材料 | /api/bitmap-proof | kind=included と kind=seen を使い分け、自分のインデックスが counted されたことや prover に提示されたかを説明する材料 |
| ビットマップルート | ジャーナル | zkVM ゲストが計算した includedBitmapRoot と seenBitmapRoot |
公開入力サマリーはサーバー内部表現であり、レスポンスにそのまま含まれません。inputCommitment が束縛するのは public-input.json の部分集合です。各チェックの判定ロジックは チェック一覧 を参照してください。
重要な判定: 除外数(excludedSlots)
excludedSlots は authoritative な公開除外数です。
excludedSlots == 0: 除外なし(正常)excludedSlots > 0: 掲示板スロットの未提示または計上失敗(即座に検証失敗)
counted_missing_indices_zero が除外数を解決し、0 でなければ failed になります。解決順序と legacy aliases の扱いは チェック一覧 を参照してください。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
excludedSlots > 0 | 欠落スロットまたは計上失敗スロットが存在する | 重大(即座にブロック) |
| 欠落スロット / invalid presented slot | 一部の bulletin slot が prover に提示されなかった、または提示後に計上されなかった | 重大 |
| 公開集計値の不一致 | 公開表示された tally.counts が zkVM の verifiedTally と一致しない | 重大(claimed tally 改ざんシナリオ S2/S4 で発火) |
| 集計合計の不一致 | verifiedTally の合計が validVotes または tally.totalVotes と一致しない | 重大 |
| 選挙マニフェスト不整合 | electionId または electionConfigHash が verification inputs と一致しない | 重大(必須チェック失敗) |
| 締め処理ステートメント不整合 | logId / timestamp / sthDigest / bulletinRoot / treeSize が一致しない | 重大(必須チェック失敗) |
| 入力コミットメント不一致 | 公開入力のうち inputCommitment 対象フィールドと zkVM 実行で使用された入力が異なる | 重大 |
| 自票のビットマップ証明が失敗または欠落 | bit が 0、proof source 不可、またはルート不一致 | 重大(required check が failed/not_run) |
| ツリーサイズの不一致 | totalExpected と treeSize が異なる(暗黙の除外、または close/input side の不整合を示す) | 重大(必須チェック失敗) |
Stage 4: STARK Verification
目的
zkVM の実行が正しく行われたことを STARK 証明(レシート)の暗号学的検証により確認します。レシートの検証に成功すれば、ジャーナルの内容(集計結果、除外情報、入力コミットメント等)がゲストプログラムの正しい実行の結果であることが保証されます。
検証する内容
flowchart LR
subgraph "入力"
RCP[レシート<br/>Seal + Journal]
EID[期待 Image ID]
end
subgraph "Rust 検証サービス"
VF["Receipt::verify(imageId)"]
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() を呼び出し、Seal(STARK 証明)がジャーナルと Image ID に対して暗号学的に正当であることを検証します。この検証は計算量が多いため、サーバー側の Rust 検証サービスで実行されます。
必要な証拠
| 証拠 | 取得元 | 説明 |
|---|---|---|
| レシート(Seal + Journal) | 証明バンドル | zkVM ホストが生成した STARK 証明 |
| 期待 Image ID | サーバー側で解決 | ゲストプログラムの暗号的識別子(解決順は チェック一覧 参照) |
| ホスト主張値と比較用メタデータ | 検証コンテキストと report | imageId, journal.imageId, verificationReport.receipt_image_id を相互照合し、主張の食い違いを検出 |
開発モードの検出
RISC0_DEV_MODE=1 で生成されたレシートは InnerReceipt::Fake 型であり、暗号学的な保証を持たず、本番 STARK 検証としては受理されません。検証サービスはこれを dev_mode ステータスとして報告し、core evaluator では明示的な dev-mode allowance が有効な場合だけ success に正規化し、それ以外では not_run として扱います。判定の詳細は ゲーティングロジック を参照してください。
失敗モード
| 症状 | 原因 | 深刻度 |
|---|---|---|
| Image ID 不一致 | マッピングが古い、ホスト主張値が誤っている、またはプローバーイメージが異なる | 重大 |
| レシート検証失敗 | 証明が暗号学的に無効 | 重大 |
| 開発モード検出 | フェイクレシートが混入 | 重大 |
UI ステップの対応チェック、集約ルール、最終判定の決定方法は ゲーティングロジック を参照してください。
チェック一覧
全検証チェック ID の定義、判定ロジック、失敗時の影響を一覧で解説します。
各チェックは一意の ID を持ち、success / failed / not_run / running / pending のステータスで管理されます。required チェックが not_run / running / pending のまま残っている場合、「Verified」は表示されません。optional チェックの取り扱いは ゲーティングロジック を参照してください。
チェックの属性
各チェックには以下の属性が定義されています。
| 属性 | 説明 |
|---|---|
| ID | チェックの一意な識別子(スネークケース) |
| カテゴリ | 所属する検証段階 |
| 証拠種別 | チェックに使用するデータの出所 |
| 重要度 | required(必須)または optional(任意) |
| 派生元 | 他のチェックから結果を導出する場合のソースチェック ID |
証拠種別
現行 22 チェックで使う証拠種別です。
| 種別 | 説明 |
|---|---|
local | 投票者の端末に保持されたユーザー固有データ(localStorage の投票意図など) |
public | 掲示板や capability 保護 API から取得する、秘密データを含まない検証用データ |
zk | zkVM が束縛した公開証拠(ジャーナル、receipt 検証結果、bitmap root に基づく証明など) |
重要度
| 重要度 | 説明 |
|---|---|
required | 「Verified」表示に必須。失敗すれば即座にブロック |
optional | 補助的な検証。通常は単独で失敗扱いにしないが、実行時条件により blocking に昇格する場合がある |
代表例は recorded_sth_third_party です。
注意: verificationChecks と UI 表示用の verificationSteps は 1 対 1 ではありません。対応関係は ゲーティングロジック を参照してください。
Cast-as-Intended(4 チェック)
投票者の意図通りにコミットメントが生成されたかを検証するチェック群です。
このカテゴリはクライアントが voteReceipt(/api/verify 応答)とローカル投票意図(electionId/myVote/myRand)を使って評価します。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
cast_receipt_present | 投票レシートが存在し、voteId とコミットメントを含む | local | required |
cast_choice_range | 選択肢が有効範囲内(A〜E) | local | required |
cast_random_format | 乱数が 32 バイトの 16 進数文字列 | local | required |
cast_commitment_match | 投票時データから再計算したコミットメントが投票レシートと一致 | local | required |
判定ロジックの詳細
cast_receipt_present
voteReceipt が存在し、voteId と commitment フィールドが存在することを確認します(このチェック単体では UUID/hex 形式までは検証しません)。
cast_choice_range
投票時データの選択肢が A〜E のいずれかであることを確認します。範囲外の値は不正な入力として failed となります。
cast_random_format
投票時データの乱数が 32 バイト(64 文字)の 16 進数文字列であることを確認します。0x プレフィックスの有無は正規化により吸収されます。
cast_commitment_match
投票時データから再計算した投票コミットメントが投票レシートの commitment 値と一致することを確認します。再計算規則(ドメインタグ・正準フォーマット)は コミットメントスキーム を参照してください。
Recorded-as-Cast(6 チェック)
コミットメントが掲示板に正しく記録されたかを検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
recorded_commitment_in_bulletin | コミットメントが掲示板ツリーに存在する | public | optional |
recorded_index_in_range | 掲示板インデックスが 0 以上かつツリーサイズ未満 | public | required |
recorded_root_at_cast_consistent | 投票時のルートが最終ツリーの正当なプレフィックスである | public | optional |
recorded_inclusion_proof | RFC 6962 包含証明が暗号学的に検証成功 | public | required |
recorded_consistency_proof | RFC 6962 整合性証明が暗号学的に検証成功 | public | required |
recorded_sth_third_party | 第三者 STH ソース間で合意が成立(比較可能応答間) | public | optional |
判定ロジックの詳細
recorded_inclusion_proof と recorded_consistency_proof は cast-time 証跡(voteReceipt と userVote.proof)の存在を前提とし、まず cast snapshot の一貫性(leafIndex、treeSize、bulletinRootAtCast が receipt と矛盾しないこと)を確認してから個別の検証に進みます。証跡が揃わない場合はどちらも not_run となり、全体判定は fail-closed で missing_evidence 側へ倒れます。
recorded_commitment_in_bulletin
包含証明(recorded_inclusion_proof)の結果から派生します。包含証明が成功すれば、コミットメントがツリーに存在することが暗号学的に証明されています。
recorded_index_in_range
掲示板インデックスが 0 <= index < treeSize の範囲内であることを確認します。範囲外のインデックスは、データの不整合を示します。
recorded_root_at_cast_consistent
整合性証明(recorded_consistency_proof)の結果から派生します。整合性証明が成功すれば、投票時のルートが最終ツリーの有効な追記専用プレフィックスであることが証明されています。
recorded_inclusion_proof
投票者のコミットメントに対する RFC 6962 包含証明(監査パス)を検証します。リーフハッシュと監査パスから cast 時点のルートを再計算し、receipt の bulletinRootAtCast と一致することを確認します。proof の treeSize が voteReceipt.bulletinIndex + 1 と一致しない場合は failed です。
recorded_consistency_proof
投票時のツリー(oldSize, oldRoot)から最終ツリー(newSize, newRoot)への RFC 6962 整合性証明を検証します。bulletin provider から取得した old/new 両時点の root が期待値と一致することを確認し、投票時ルートが最終ツリーの追記専用プレフィックスであることを保証します。treeSize チェックは包含証明と同条件です。
recorded_sth_third_party
設定された STH ソースからスナップショットを取得し、比較可能な応答同士で合意を確認します。判定は matchingSources >= minMatches(デフォルト: 2)に加えて、比較対象になった応答間の全会一致(consensus)が必要です。照合対象は STH ダイジェストが必須で、bulletinRoot / treeSize は各ソースが返した場合に追加で照合されます。STH ソースが未設定の場合は not_run になります。
早見表では optional ですが、STH ソース設定時は required 相当に昇格します。詳細は ゲーティングロジック を参照してください。
Counted-as-Recorded(10 チェック)
記録された全投票が正しく集計に含まれたかを検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
counted_input_sanity | 公開入力サマリーが有効 | public | required |
counted_unique_indices | 入力中の全インデックスが一意 | public | required |
counted_unique_commitments | 入力中の全コミットメントが一意 | public | required |
counted_tally_consistent | claimed tally と zkVM の検証済み集計が一致(fallback: 合計整合) | zk | required |
counted_missing_indices_zero | 解決済み fail-closed exclusion count(excludedSlots 優先)が 0 | zk | required |
counted_expected_vs_tree_size | totalExpected がツリーサイズと一致 | zk | required |
counted_election_manifest_consistent | election-manifest.json が自己整合し、選挙 ID / electionConfigHash と一致 | public | required |
counted_close_statement_consistent | close-statement.json が自己整合し、log/tree/timestamp/root/sthDigest と一致 | public | required |
counted_my_vote_included | bitmap 証明により自分のインデックスが counted 側に含まれたことを確認 | zk | required |
counted_input_commitment_match | 公開入力から計算した入力コミットメントがジャーナルの値と一致 | public | required |
判定ロジックの詳細
counted_input_sanity
public-input.json 相当から組み立てた公開入力サマリーが存在し、スキーマ検証に成功していることを確認します。加えて、treeSize が正の整数、votesCount <= treeSize、bulletinRoot が 32 バイト hex かつゼロ値でないことを要求します。
counted_unique_indices
zkVM に渡された全投票のインデックスが重複なく一意であることを確認します。重複インデックスは、同一投票の二重カウントを示す可能性があります。
counted_unique_commitments
zkVM に渡された全投票のコミットメントが重複なく一意であることを確認します。重複コミットメントは、同一コミットメントに対する複数投票を示す可能性があります。
counted_tally_consistent
主経路では、公開される claimed tally(tally.counts)が zkVM の verifiedTally と選択肢ごとに一致し、かつ verifiedTally の合計が tally.totalVotes と一致することを確認します。これにより「公開表示された集計値」と「zkVM が証明した集計値」の不一致を検出します。claimed tally が利用できない場合は、フォールバックとして journal.verifiedTally の合計と journal.validVotes の一致を検証します。
counted_missing_indices_zero
fail-closed(安全側に倒す)な除外数が 0 であることを確認します。除外数は次の優先順で解決し、0 でなければ即座に failed とします。
journalがある場合:journal.excludedSlotsを proof-bound な除外数として使用。excludedSlots/missingSlots/invalidPresentedSlots/validVotesが非負整数であることも先に確認するjournalがない場合:excludedSlots→missingSlots + invalidPresentedSlotsの順で探索
rejectedRecords は説明用の補助値で、この判定には使いません。旧フィールド (excludedCount / missingIndices / invalidIndices) は fail-closed 補正経路でのみ参照され、本判定では使用しません。
counted_expected_vs_tree_size
totalExpected(期待される投票数)が掲示板のツリーサイズと一致することを確認します。不一致は、暗黙の投票除外を示す可能性があります。
counted_election_manifest_consistent
election-manifest.json の electionConfigHash を再計算し、manifest 自身の宣言値と一致することを確認します。そのうえで electionId と electionConfigHash を、セッション値・publicInputArtifact から導出した内部 publicInputSummary・ジャーナルに含まれる対応値と相互照合します。
counted_close_statement_consistent
close-statement.json から sthDigest を再計算し、宣言された snapshot と一致することを確認します。そのうえで timestamp が publicInputArtifact から導出した内部 publicInputSummary.timestamp と一致し、logId / treeSize / bulletinRoot / sthDigest が検証入力やジャーナルと矛盾しないことを確認します。
counted_my_vote_included
includedBitmapRoot に対する bitmap Merkle 証明を検証し、投票者のインデックスに対応するビットが 1(counted に含まれた)であることを確認します。
seenBitmapRoot もある場合は /api/bitmap-proof?kind=included|seen の両方を使い、presented but invalid / not presented to the prover / unknown excluded を説明可能にします。
証明材料が取得できない場合は not_run になり、補助 note が付くことがあります。
counted_input_commitment_match
公開入力から再構成した入力コミットメントが、zkVM ジャーナルの値と一致することを確認します。対象フィールドの集合と正準エンコーディングは 入力コミットメント を参照してください(public-input.json 全体の単純なハッシュではない点に注意)。
STARK Verification(2 チェック)
STARK 証明の暗号学的正当性を検証するチェック群です。
| ID | 説明 | 証拠種別 | 重要度 |
|---|---|---|---|
stark_image_id_match | verifier-confirmed な Image ID と expected / host-side metadata が整合 | zk | required |
stark_receipt_verify | STARK レシートが暗号学的に検証成功 | zk | required |
判定ロジックの詳細
stark_image_id_match
STARK status が success に解決された後で、期待 Image ID・verifier-confirmed Image ID・ホスト主張値が相互に矛盾しないことを確認します。整合条件(report に Image ID フィールドが揃わない場合の挙動を含む)と fail-closed の取り扱いは Image ID を参照してください。
期待 Image ID の解決順:
| 優先順 | ソース |
|---|---|
| 1 | EXPECTED_IMAGE_ID 環境変数 |
| 2 | public/imageId-mapping.json の current mapping |
variant は EXPECTED_IMAGE_ID_VARIANT(default または x86_64)で明示指定します(未指定時は default)。
stark_receipt_verify
サーバー側の Rust 検証サービスが Receipt::verify(image_id) を実行し、Seal(STARK 証明)が暗号学的に正当であることを確認します。チェック結果は success / failed / not_run / running で表現され、dev_mode は ゲーティングロジック に従って正規化されます。
チェックステータスの遷移
stateDiagram-v2 [*] --> not_run: 初期状態 not_run --> pending: 依存条件の待機 not_run --> running: 検証開始 pending --> running: 依存条件の解消 running --> success: 検証成功 running --> failed: 検証失敗
| ステータス | 説明 | required の場合の影響 |
|---|---|---|
not_run | 関連データが未取得、または検証未開始 | ブロック |
pending | 依存する検証の完了を待機中 | ブロック |
running | 検証を実行中 | ブロック |
success | 検証成功 | 通過 |
failed | 検証失敗 | ブロック |
全体判定(集約結果)の決まり方は ゲーティングロジック を参照してください。
全チェック早見表
| # | チェック ID | カテゴリ | 証拠 | 重要度 | 派生元 |
|---|---|---|---|---|---|
| 1 | cast_receipt_present | Cast | local | required | — |
| 2 | cast_choice_range | Cast | local | required | — |
| 3 | cast_random_format | Cast | local | required | — |
| 4 | cast_commitment_match | Cast | local | required | — |
| 5 | recorded_commitment_in_bulletin | Recorded | public | optional | recorded_inclusion_proof |
| 6 | recorded_index_in_range | Recorded | public | required | — |
| 7 | recorded_root_at_cast_consistent | Recorded | public | optional | recorded_consistency_proof |
| 8 | recorded_inclusion_proof | Recorded | public | required | — |
| 9 | recorded_consistency_proof | Recorded | public | required | — |
| 10 | recorded_sth_third_party | Recorded | public | optional | — |
| 11 | counted_input_sanity | Counted | public | required | — |
| 12 | counted_unique_indices | Counted | public | required | — |
| 13 | counted_unique_commitments | Counted | public | required | — |
| 14 | counted_tally_consistent | Counted | zk | required | — |
| 15 | counted_missing_indices_zero | Counted | zk | required | — |
| 16 | counted_expected_vs_tree_size | Counted | zk | required | — |
| 17 | counted_election_manifest_consistent | Counted | public | required | — |
| 18 | counted_close_statement_consistent | Counted | public | required | — |
| 19 | counted_my_vote_included | Counted | zk | required | — |
| 20 | counted_input_commitment_match | Counted | public | required | — |
| 21 | stark_image_id_match | STARK | zk | required | — |
| 22 | stark_receipt_verify | STARK | zk | required | — |
バンドル構造
証明バンドルの内訳と公開許可リスト、同期・非同期それぞれのモードでの差分を扱う章です。
概要
証明バンドル は、zkVM の実行結果を検証可能な形で保存・配布するためのアーティファクト群です。実際に配布される bundle.zip は、厳格な許可リストによって公開可能アーティファクトだけを含める配布対象アーカイブです。
本書で 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"]
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
公開許可リスト
バンドルアーカイブ(bundle.zip)に含められるファイルは、許可リストによって厳格に制限されています。
公開可能なアーティファクト
| ファイル | 内容 | 用途 |
|---|---|---|
public-input.json | zkVM 検証に使う秘密データを含まない検証用レコード | 第三者が入力コミットメントを再計算するため |
election-manifest.json | 選挙設定の公開監査用スナップショット | 選挙設定と electionConfigHash の照合 |
close-statement.json | 集計締切時点のログ境界を表す公開監査レコード | logId / treeSize / bulletinRoot の照合 |
receipt.json | ホストが出力する { receipt, image_id } ラッパー JSON | 第三者がレシートを独立に検証するため |
journal.json | zkVM ゲストの公開出力(集計結果、除外情報、ビットマップルート等) | 集計結果と整合性データの確認 |
metadata.json | バンドルの作成日時、セッション ID、メソッドバージョン(sync のみ) | バンドルの来歴追跡 |
sth.json | 第三者 STH 検証のスナップショット(任意) | STH 合意の再現可能な証拠 |
consistency-proof.json | RFC 6962 整合性証明(任意) | 追記専用性の独立検証 |
sth.json と consistency-proof.json は許可リストに含まれますが、現行フローでは通常生成されません。
非公開アーティファクト
| ファイル | 内容 | 非公開の理由 |
|---|---|---|
input.json | zkVM への完全な入力(選択肢・乱数・コミットメント・Merkle パス等) | 投票の秘匿入力(ウィットネス)を含む |
verification.json | 検証サービスの詳細レポート | bundle.zip の許可リスト外(必要時は専用エンドポイントで参照) |
included-bitmap.json | 厳密な counted bitmap artifact | 個票 explainability 用の非公開アーティファクトであり bundle.zip 対象外 |
seen-bitmap.json | 厳密な presented bitmap artifact | 個票 explainability 用の非公開アーティファクトであり bundle.zip 対象外 |
input.json が公開されると投票の秘匿性が失われます。verification.json は必要時のみ専用の capability 保護エンドポイント経由で扱います。included-bitmap.json と seen-bitmap.json は /api/bitmap-proof の trusted source ですが、配布対象アーカイブ bundle.zip には含めません。
公開入力の構造
public-input.json は、第三者検証に必要で、かつ選択肢と乱数を含まない入力側レコードです。input.json の単純なサブセットではありません。
| フィールド | 説明 |
|---|---|
schema | スキーマ識別子("stark-ballot.public_input") |
version | スキーマバージョン("1.1") |
contractGeneration | 現行契約世代を示す互換性マーカー |
electionId | 選挙 ID(UUID) |
electionConfigHash | 選挙設定のハッシュ |
bulletinRoot | 掲示板の最終ルートハッシュ |
treeSize | 掲示板のツリーサイズ |
totalExpected | 期待される投票数 |
logId | 掲示板ログ ID |
timestamp | 集計時のタイムスタンプ |
methodVersion | zkVM メソッドバージョン |
votes | 各投票のインデックス、コミットメント、Merkle パスの配列 |
votes 配列には各投票のインデックスとコミットメント、Merkle パスが含まれますが、選択肢と乱数は含まれません。これにより、入力コミットメントの再計算が可能でありながら、投票内容の秘匿性は維持されます。
public-input.json と inputCommitment は同一ではなく、後者が直接束縛するのはこのレコードの一部です。計算対象フィールドと非対象フィールドの整理は 入力コミットメント を参照してください。
同期バンドルと非同期バンドルの違い
証明生成には 2 つのパスがあります。同期モード はローカルプロセス / Lambda 上で TypeScript がアーティファクトを生成し、検証サービスから戻った verification.json を含めて bundle.zip を作成します。非同期モード は ECS Fargate コンテナの entrypoint.sh が公開可能アーティファクトを生成し、bundle.zip を S3 に置いた後コールバック Lambda がセッションに結果を反映します。両モードとも、public-input.json / election-manifest.json / close-statement.json は journal.json と正準な proof-bound data との整合性検査を通過した場合にのみ bundle 化されます。不一致があれば fail-closed で処理を中断します。
| 項目 | 同期モード | 非同期モード |
|---|---|---|
| 実行環境 | ローカルプロセス / Lambda | ECS Fargate コンテナ |
input.json | 生成される(非公開保存) | ワーク入力として S3 に置かれる(配布対象外) |
public-input.json | TypeScript で生成 | entrypoint.sh 内で生成 |
election-manifest.json | TypeScript で生成 | entrypoint.sh 内で生成 |
close-statement.json | TypeScript で生成 | entrypoint.sh 内で生成 |
journal.json | TypeScript で生成 | *-output.json から bundle.zip 用に生成 |
receipt.json | ホストの { receipt, image_id } 出力を保存 | *-receipt.json を receipt.json としてコピーして同梱 |
included-bitmap.json | 生成される場合は private に保持 | 生成される場合は隣接オブジェクト(sibling object)として保持 |
seen-bitmap.json | 生成される場合は private に保持 | 生成される場合は隣接オブジェクトとして保持 |
metadata.json | 生成される | 生成されない |
verification.json | 検証サービス呼び出し後に保存 | finalize コールバック時点では生成されない。後続の /api/verification/run で report が保存された場合に限り参照可能 |
bundle.zip | 許可リストに基づき作成 | receipt.json / journal.json / public-input.json / election-manifest.json / close-statement.json を同梱して作成 |
| 保存先 | ローカルファイルシステム(+ 任意で S3) | S3 |
| presigned URL | S3 アップロード時に生成 | コールバック Lambda が生成 |
非同期バンドルの生成フロー
非同期モードでは、ECS Fargate コンテナの entrypoint.sh が以下の手順でバンドルを構築します。
- S3 から入力 JSON をダウンロード
- ホストバイナリを実行し、レシートと出力を生成
- 出力から
journal.jsonを変換生成 - 入力と出力から
public-input.json、election-manifest.json、close-statement.jsonを構築 - 整合性検査(本節冒頭参照)を通過したものだけを bundle に含める
receipt.json、journal.json、public-input.json、election-manifest.json、close-statement.jsonをbundle.zipにアーカイブ*-receipt.json/*-output.json/public-input.json/election-manifest.json/close-statement.json/included-bitmap.json/seen-bitmap.json/bundle.zipなどを S3 にアップロード
コールバック Lambda は S3 の bundle.zip からジャーナル、レシート、public-input.json、election-manifest.json、close-statement.json を復元します。利用可能な場合は隣接オブジェクトの bitmap artifact も取り込み、presigned URL の生成と監査用データの保存を行います。
docker/entrypoint.sh は methodVersion 14 のホスト出力を検証し、journal.json / public-input.json / election-manifest.json / close-statement.json の methodVersion と inputCommitment が一致することを確認してから bundle.zip を生成します。methodVersion が現行契約と一致しない出力は fail-closed で停止します。
バンドルディレクトリ構造
同期モード(ローカルファイルシステム)
{VERIFIER_WORK_DIR}/
{sessionId}/
{executionId}/
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 ← 配布対象: 許可リストファイルのアーカイブ
非同期モード(S3)
s3://{BUCKET}/sessions/{sessionId}/{executionId}/
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 ← `/api/verification/run` 後に参照可能になる場合あり(非公開)
補足
- 非同期モードの S3 オブジェクト名は固定の
receipt.json/journal.jsonになりません。inputBaseがコンテナ実行時に生成される一時入力ファイル名に依存するためです。 - 非同期モードの
verification.jsonはbundle.zipの構成要素ではなく、report エンドポイントから参照する別アーティファクトです。S3 上の report がこのセッションの配信対象として記録されていれば短命な presigned URL にリダイレクトされ、未記録ならローカル report を探し、いずれもなければ404を返します。
バンドルのアクセス方法
ダウンロードエンドポイント
| エンドポイント | 内容 |
|---|---|
GET /api/verification/bundles/:sessionId/:executionId | bundle.zip のダウンロード |
GET /api/verification/bundles/:sessionId/:executionId/report | verification.json の取得 |
両エンドポイントとも X-Session-Capability ヘッダーが必須です。つまり、ダウンロードは capability 保護 API から開始されます。
加えて、executionId はそのセッションで現在許可されている verificationExecutionId と一致しなければなりません。不一致や未設定の場合、API は 404 を返します。
S3 バンドルは capability 保護済みのダウンロードエンドポイント経由でのみ配布されます。エンドポイントは現在の verificationExecutionId と S3 key を確認したうえで、そのつど短命な presigned URL にリダイレクトします。
アーカイブの再現性
同期モード(verification-bundle.ts)の bundle.zip は再現性を確保するため、以下の措置を講じています。
- エントリのタイムスタンプをゼロに固定
- 許可リストに一致するファイルのみを含める
- ファイル名のアルファベット順でエントリを追加
非同期モード(docker/entrypoint.sh)は zip -r で作成され、上記の再現性制御とは実装が異なります。
セキュリティ上の制約
パストラバーサル防止
バンドルのパスセグメント(セッション ID、実行 ID)は英数字とハイフンのみに制限されています。.. を含むパスや許可されていない文字を含むパスは拒否されます。
非公開ファイルの保護
input.json、verification.json、included-bitmap.json、seen-bitmap.json は bundle.zip には含まれません。
ゲーティングロジック
「Verified」を表示してよい条件と、表示を必ず阻止する不変条件を厳密に並べる章です。
「必要な検証が未実行または失敗なら Verified を表示しない」という原則のもと、各チェックの結果がどう最終判定に集約されるかを定式化します。
最終判定の種類
検証パイプラインの結果は、主経路では deriveVerificationSummary によって集約され、UI 上は以下の 3 トーンに整理されます。なお /verify ページには STARK タイムアウト時の失敗表示など、集約結果に対する限定的な上書きもあります。
| 表示ステータス | 主な条件 | UI 表示 |
|---|---|---|
| Verified | required 条件が満たされ、optional チェックの劣化もない(fully_verified) | 緑色 |
| Verification Failed | 証明失敗、票除外、Recorded/Counted/Cast の必須失敗、または公開集計値と検証済み tally の不一致が確定した場合 | 赤色 |
| Warning | required チェックが進行中、証拠不足がある、または optional チェックのみ劣化している場合 | 黄色 |
ステータスの判定順序
| 優先順 | 判定条件 | 最終ステータス |
|---|---|---|
| 1 | required チェックに pending / running がある | Warning (in_progress) |
| 2 | STARK 証明系ロールが failed | Verification Failed |
| 3 | completeness ロールが failed(user_vote_excluded / votes_excluded / votes_excluded_unknown) | Verification Failed |
| 4 | Recorded-as-Cast の required チェックが failed | Verification Failed |
| 5 | tally consistency だけが失敗し、proof / completeness / user inclusion / input integrity / Recorded required が成功 | Verification Failed (published_tally_mismatch) |
| 6 | Counted-as-Recorded または Cast-as-Intended の required チェックが failed | Verification Failed |
| 7 | (a) required に not_run がある (b) 必須ロール不足 (c) 既知チェックと混在する未知チェック (d) required 定義の未解決 のいずれか | Warning (missing_evidence) |
| 8 | optional チェックに failed / not_run がある | Warning (verified_with_limitations) |
| 9 | 上記いずれにも該当しない | Verified |
この表は deriveVerificationSummary の集約結果です。チェックが空、または未知チェックだけで既知チェックが 1 件も解決できない場合、summary は null になり、Verified ではなく最終サマリー未表示として扱われます。
現行 /verify ページでは、pending / running のチェックが残っている間は最終サマリー自体を表示せず、ステップ表示が完了したあとに summary を表示します。UI レベルの最終表示は (1) 明示的なサーバー失敗、(2) hard-failure fallback、(3) summary、(4) pending warning の順で解決されます。
補助判定のゲーティング(validateVotingIntegrity)
現行 /verify の最終判定では使われない内部 helper ですが、整合性証明・完全性・第三者 STH 合意・ユーザーインデックス範囲を順に評価し、いずれかが失敗すれば canShowVerified = false を返します。意味論は チェック一覧 の recorded_consistency_proof / counted_missing_indices_zero / counted_expected_vs_tree_size / recorded_sth_third_party に対応します。
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 が not_run のまま最終判定が Verified になる経路はありません(整合性チェックだけでは Verified に到達できません)。
zkGate: STARK 結果に基づく Counted チェックの制御
STARK 検証の結果は、Counted-as-Recorded 段階のチェック評価にも影響します。これを zkGate と呼びます。
| STARK 解決後ステータス | Counted チェックへの反映 |
|---|---|
running | pending |
not_run | not_run |
failed | failed |
success | ゲートなしで通常評価 |
core evaluator では、dev_mode は事前に success または not_run に正規化されてから zkGate に入力されます。一方、現行 /api/verify の表示用ステータス組み立てでは、dev mode が許可されていない場合は fail-closed の failed として反映されます。
ステップとチェックの対応関係
UI に表示される 4 つのステップは、現行実装では 22 個のチェック定義から派生します。
ただし、verificationSteps[].status は「その stage で required 扱いになるチェック群」から導出され、verificationSteps[].inputs は stage 内の 全チェック定義 から集約されます。
| ステップ | required として集約されるチェック ID |
|---|---|
| Cast-as-Intended | cast_receipt_present, cast_choice_range, cast_random_format, cast_commitment_match |
| Recorded-as-Cast | recorded_index_in_range, recorded_inclusion_proof, recorded_consistency_proof、および STH source 設定時の recorded_sth_third_party |
| Counted-as-Recorded | counted_input_sanity, counted_unique_indices, counted_unique_commitments, counted_tally_consistent, counted_missing_indices_zero, counted_expected_vs_tree_size, counted_election_manifest_consistent, counted_close_statement_consistent, counted_my_vote_included, counted_input_commitment_match |
| STARK Verification | stark_image_id_match, stark_receipt_verify |
補足:
recorded_commitment_in_bulletinはrecorded_inclusion_proofから、recorded_root_at_cast_consistentはrecorded_consistency_proofから導出される表示用チェックです。チェック一覧には現れますが、単独では step status を決定しません。recorded_sth_third_partyは通常は optional ですが、STH source が設定されている場合だけ required に昇格し、Recorded-as-Cast の step status と最終判定をブロックし得ます。
ステップのステータスは、required 扱いになったチェックのステータスから次の順序で集約されます。
| 集約ルール | 条件 |
|---|---|
failed | required チェックのいずれかが failed |
running | failed がなく、required チェックのいずれかが running |
pending | failed/running がなく、いずれかが pending |
success | required チェックがすべて success |
not_run | 上記のいずれにも該当しない |
さらに現行実装には、単純集約だけではない 3 つの補正があります。
counted_as_recordedはjournalが存在しない場合、required チェックにfailedがない限りnot_runに補正されます。recorded_as_castはuserVote.proof.treeSizeがない場合、not_runに補正されます。/api/verifyはcastSource='client'でverificationSteps/verificationChecksを組み立てるため、API 応答上の Cast-as-Intended はいったんnot_runです。その後ブラウザ側で保存済みセッション情報から Cast チェックを再評価して上書きし、最終的な UI 表示と summary にはそのローカル結果が反映されます。
UI シーケンスとポーリング
検証ページでは、4 つのステップが順次アニメーション表示されます。
sequenceDiagram
participant U as ブラウザ
participant S as サーバー
Note over U: ページロード時
U->>S: GET /api/verify
S-->>U: 検証ペイロード
Note over U: STARK 完了まで待機
alt verificationStatus = not_run
U->>S: POST /api/verification/run
S-->>U: 実行受付
end
loop STARK ポーリング
U->>S: GET /api/verify
S-->>U: verificationStatus 確認
end
Note over U: STARK 解決後に<br/>ステップを順次表示
Note over U: Step 1: Cast-as-Intended
Note over U: Step 2: Recorded-as-Cast
Note over U: Step 3: Counted-as-Recorded
Note over U: Step 4: STARK Verification
Note over U: 最終判定を表示
不変条件のまとめ
以下の不変条件は、コードの変更によっても決して緩和してはなりません。
| 不変条件 | 根拠 |
|---|---|
| 解決済み fail-closed 除外数 > 0 → Verified を表示しない | 投票除外は最も深刻な不正 |
| 整合性証明の失敗 → Verified を表示しない | 追記専用性が保証されない |
| STH 合意の不成立(有効時) → Verified を表示しない | スプリットビュー攻撃の可能性 |
not_run チェックの存在 → Verified を表示しない | 証拠の不在を成功として扱わない |
| STARK 検証の失敗 → Verified を表示しない | ジャーナルの正当性が保証されない |
| 非公開アーティファクトをバンドルに含めない | 投票の秘匿性を維持 |
これらの不変条件は、改ざんシナリオ(S0〜S5)の検出を保証する基盤です。各シナリオがどの不変条件によって検出されるかは、改ざんシナリオ を参照してください。
この fail-closed モデルは、単体・結合・E2E テスト で「Verified を誤表示しない」ケースを継続的に検査しています。形式化側では Lean による形式化 の verification summary / display vectors を通じて、モデルと実装の対応を確認します。
改ざんシナリオ
STARK Ballot Simulator は、E2E 検証可能投票の教育的デモとして、正常系 S0 と改ざんシナリオ S1〜S5 を提供します。S1〜S5 は投票システムに対する特定の攻撃を模擬し、検証パイプラインがどのチェックで異常を検出するかを実演します。
この部に含まれる章
想定読者と前提
- 想定読者: 検証パイプラインの教育的デモを試したい技術者
- 前提: 検証パイプライン の 4 段階モデルを把握していること
本章で扱わないもの
- 実世界の投票システムに対する攻撃手法の一般論
- 本番投票システム向けの脅威モデリングや対策ガイド
- S2/S4 を proof-tampering に変更するなど PoC スコープを超える攻撃シナリオ
関連する章
シナリオ一覧
改ざんシナリオ S0〜S5 の定義と、実装上どこを改変するかを整理します。ここでは「zkVM 入力」「主張集計(claimed tally)」「ジャーナル統計(missing/invalid/excluded)」の関係を中心に説明します。
教育モードの目的
改ざんシナリオは、暗号的検証が実際に機能することを確認するために設計されています。
- 正常ケース(S0)を基準として、検証パイプラインが通過する状態を確認する
- 攻撃シナリオ(S1〜S5)を適用して、どの不変条件が破れると検証が失敗するかを確認する
攻撃の 2 類型
- 入力改ざん (
tamperMode=input): S1 / S3 / S5 - 主張改ざん (
tamperMode=claim): S2 / S4
この図は「改ざんがどこに入るか」の分類のみを示します。どのチェックで失敗するかの詳細は 検出メカニズム を参照してください。
実装上の共通前提
- 1 回の finalize で選択されるシナリオは 1 つ(S0〜S5)
- UI:
/aggregateは single-select(S0〜S5 のラジオボタン) - API:
POST /api/finalizeはscenarioIdを 1 つ受け取る
- UI:
totalExpectedは 64(ユーザー 1 + ボット 63)- 掲示板(CT Merkle)は追記専用で、シナリオ適用で既存エントリは削除しない
tamperModeはnone/input/claimの 3 種
注意事項:
- 本章は実 API 経路(
/api/finalize→finalize-session→finalize-sync|async)を基準に説明する - finalize 実行モードは
FINALIZE_ASYNC_MODEで切り替わる(false: 同期,true: 非同期)。AWS 運用では通常true - mock mode の差分:
NEXT_PUBLIC_USE_MOCK_API=trueの mock API fixture は本章と異なるチェック結果を返すことがあるUSE_MOCK_ZKVM=trueの mock zkVM executor は CT inclusion proof を簡略化するため、特に S5 再集計分岐の journal 統計は real zkVM と異なることがある
- 本章の「主な失敗点」は 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
S0: 正常(改ざんなし)
改ざんを適用しない基準シナリオです。
| 項目 | 値 |
|---|---|
| tamperMode | none |
| zkVM 入力票数 | 64 |
| claimed と verified | 一致 |
excludedSlots | 0 |
S1: ユーザー票の除外
ユーザー票(インデックス 0)を modifiedVotes から削除し、63 票を zkVM に渡します。
| 項目 | 値 |
|---|---|
| tamperMode | input |
| zkVM 入力票数 | 63 |
| claimed と verified | 一致(どちらも 63 票入力ベース) |
| ジャーナル統計 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
ポイント:
- 掲示板上のユーザー票エントリは残る
- 検出は主に完全性チェック(
excludedSlots > 0) - ビットマップ証明が利用可能なら
counted_my_vote_includedでも検出可能
S2: ユーザー票に関する主張集計の改ざん
ユーザー票に対する「主張集計(表示する tally)」のみ改ざんします。zkVM には元の 64 票を渡します。
| 項目 | 値 |
|---|---|
| tamperMode | claim |
| zkVM 入力票数 | 64(元データ) |
| claimed と verified | 不一致(ユーザー選択肢が -1、別候補が +1) |
excludedSlots | 0(通常) |
inputCommitment | zkVM 入力由来のため通常は一致 |
ポイント:
- 「票の中身を zkVM 入力で差し替える」実装ではない
- レシートや STARK 証明は通常どおり有効
- 検出の主因は
counted_tally_consistentの失敗
S3: ボット票の除外
現行実装ではボット票インデックス 1(targetBotId 初期値)を削除し、63 票を zkVM に渡します。
| 項目 | 値 |
|---|---|
| tamperMode | input |
| zkVM 入力票数 | 63 |
| claimed と verified | 一致(どちらも 63 票入力ベース) |
| ジャーナル統計 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
S1 との違い:
- S1: ユーザー自身の未集計をビットマップで直接示せる
- S3: ユーザー票は含まれるが、集計全体の完全性違反で検出される
S4: ボット票に関する主張集計の改ざん
1 票のボット票に関する「主張集計」だけを改ざんします。zkVM 入力は元の 64 票のままです。
| 項目 | 値 |
|---|---|
| tamperMode | claim |
| zkVM 入力票数 | 64(元データ) |
| claimed と verified | 不一致(対象ボットの元候補が -1、別候補が +1) |
excludedSlots | 0(通常) |
inputCommitment | zkVM 入力由来のため通常は一致 |
ポイント:
- S2 と同様に、改ざん対象は
tally.counts側 - 検出の主因は
counted_tally_consistentの失敗
S5: ランダムエラー注入
64 票からランダムに 1 票を選び、50% で「除外」または「再集計(別候補化)」を行います。
実装上の重要点:
tamperModeは常にinput- そのため zkVM 入力は常に
modifiedVotesが使われる - 除外パスでは
missingSlotsが増え、real zkVM の再集計パスでは CT inclusion proof の不整合によりinvalidPresentedSlotsが増えるため、いずれもexcludedSlots > 0になる - 再集計パスでは
counted_tally_consistentも失敗する(claimedCountsは 64 票ベース、verifiedTallyは inclusion proof 不整合の票を除外した 63 票ベース)
| 分岐 | zkVM 入力 | 代表的な統計 |
|---|---|---|
| 除外パス | 63 票 | missingSlots=1, invalidPresentedSlots=0, excludedSlots=1 |
| 再集計パス | 64 票 | missingSlots=0, invalidPresentedSlots=1, excludedSlots=1 |
シナリオ一覧表
| シナリオ | 類型 | tamperMode | zkVM 入力 | 主な失敗点(STARK 解決後) |
|---|---|---|---|---|
| S0 | 正常 | none | 元の 64 票 | なし |
| S1 | 除外 | input | 63 票(ユーザー除外) | excludedSlots > 0 |
| S2 | 主張改ざん | claim | 元の 64 票 | claimed ≠ verified |
| S3 | 除外 | input | 63 票(ボット除外) | excludedSlots > 0 |
| S4 | 主張改ざん | claim | 元の 64 票 | claimed ≠ verified |
| S5 | ランダム(実装上 input) | input | 63 または 64 票 | excludedSlots > 0(再集計では claimed ≠ verified も発生) |
ジャーナル統計の扱い(sync / async 共通)
現行実装では、missingSlots / invalidPresentedSlots / excludedSlots / validVotes などのジャーナル統計は、sync / async いずれも zkVM が返した proof-derived な値をそのまま使います。
- sync / async いずれも、finalize 後にジャーナル統計を上書きしない
- async finalize はコールバックで
bundle.zipから復元したjournalをそのまま使う
シナリオ由来の ignoredCount / recountedCount / claimedCounts は presentation 用であり、journal の統計値を書き換えません。
ignoredCount/recountedCount→tamperSummaryやtamperedCountに反映claimedCounts→ S2/S4 や S5 再集計分岐での表示用 tally
tamperMode=claim(S2/S4)でも同様に、journal 統計は 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 になった後の挙動を前提とします。
前提
NEXT_PUBLIC_USE_MOCK_API=trueの mock API fixture は本章と異なるチェック結果を返すことがある。USE_MOCK_ZKVM=trueの mock zkVM executor は CT inclusion proof を簡略化するため、S5 再集計分岐の journal 統計は real zkVM と異なることがある。以降、再集計分岐の挙動は real zkVM を前提に記述する。- 現行 API は
castSource=clientのため、cast_*チェックはシナリオに関係なくnot_run。
Counted 系チェックの zkGate について
/api/verify の Counted 系チェックには、STARK 前に評価できる項目と、STARK 状態でゲートされる項目が混在します。
counted_input_sanity/counted_unique_indices/counted_unique_commitmentsはpublicInputArtifactから導出した内部publicInputSummaryがあれば 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になります verificationStatus=failedでは、zkGate 対象チェックもfailedになり得ます
検出の 2 つの原理
- 原理1: 完全性違反 (
excludedSlots > 0) →counted_missing_indices_zeroが失敗(主に S1/S3/S5) - 原理2: 主張集計の不整合 (claimed ≠ verified) →
counted_tally_consistentが失敗(主に S2/S4)
シナリオ別の主な失敗チェック(STARK 解決後)
| シナリオ | 主に失敗するチェック | 説明 |
|---|---|---|
| S0 | なし | 正常系 |
| S1 | counted_missing_indices_zero | ユーザー票除外により excludedSlots=1 |
| S2 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S3 | counted_missing_indices_zero | 現行実装では botId=1 のボット票除外により excludedSlots=1 |
| S4 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S5 | counted_missing_indices_zero | excludedSlots>0 が発生し、除外・再集計どちらでも完全性違反として検出される |
補足:
- S1 では、ビットマップ証明が利用可能な場合
counted_my_vote_includedも失敗し得ます - S2/S4 では、
counted_input_commitment_matchは通常成功します(zkVM 入力を改変していないため) - S5 の再集計分岐では
counted_tally_consistentも失敗します(詳細は下記「S5 の実装依存ポイント」参照)
4 段階検証モデルとの対応(/api/verify 応答)
| 検証段階 | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
| Cast-as-Intended | not_run | not_run | not_run | not_run | not_run | not_run |
| Recorded-as-Cast | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Counted-as-Recorded | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| STARK Verification | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
この表は「シナリオ適用による典型挙動」を示します。running や追加の not_run は運用状態や証拠不足により別途発生します。
なお、ここでの STARK Verification は段階別の表示ステータスです。全体の verificationStatus は fail-closed ルールにより failed になることがあります。
チェックID(主要項目)マトリクス(STARK 解決後)
| チェック ID | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
cast_commitment_match | not_run | not_run | not_run | not_run | not_run | not_run |
counted_tally_consistent | ✅ | ✅ | ❌ | ✅ | ❌ | 分岐依存 |
counted_missing_indices_zero | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
counted_my_vote_included | ✅ | ❌ または not_run | ✅ | ✅ | ✅ | 分岐依存 |
counted_input_commitment_match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
- S5 の
counted_my_vote_includedは、ランダム対象がユーザー票(除外または再集計)なら失敗し得ます。証拠不足時はnot_runになります - S5 の
counted_tally_consistentは、除外パスでは成功、再集計パスでは失敗します(詳細は下記「S5 の実装依存ポイント」参照) - いずれの分岐でも主な失敗は
counted_missing_indices_zeroです
S2/S4 で何が起きるか
S2/S4 は「入力改ざん」ではなく「主張集計改ざん」です。
flowchart TD
A["tamperMode=claim (S2/S4)"]
A --> V1["元の votes を zkVM 入力へ"]
V1 --> V2["verifiedTally"]
A --> C1["claimedCounts を改変"]
C1 --> C2["API の tally.counts"]
V2 --> X{"claimed と verified は一致?"}
C2 --> X
X -->|不一致| F["counted_tally_consistent = failed"]
このため counted_input_commitment_match の失敗は通常発生しません(zkVM 入力は元票)。
S5 の実装依存ポイント
S5 はランダムに除外または再集計を選びますが、実装上は常に tamperMode=input です。つまり claim tamper ではなく、改変後の votes が zkVM 入力に入ります。ジャーナル統計は sync / async どちらでも zkVM が返した値をそのまま使います(詳細はシナリオ一覧 > ジャーナル統計の扱いを参照)。
- 除外分岐:
missingSlots=1となり、counted_missing_indices_zeroが失敗 - 再集計分岐:
invalidPresentedSlots=1となり、counted_missing_indices_zeroが失敗 - 再集計分岐では
counted_tally_consistentも失敗します。claimedCountsは改変後の 64 票から算出されますが、zkVM は元の CT 掲示板 proof/root と新しい commitment が整合しない票を除外するためverifiedTallyは 63 票ベースになります
ビットマップ証明の役割
counted_my_vote_included は、チェック定義上 required のユーザー包含チェックです。
- S1(ユーザー票除外)では、証明が利用可能なら失敗して「自分の票が未集計」であることを直接示せる
- 証拠不足で
not_runになる場合でも、最終判定は Verified になりません:- 完全性違反が同時にある場合 →
votes_excluded_unknown - 完全性違反が無く required evidence が欠ける場合 →
missing_evidence
- 完全性違反が同時にある場合 →
最終判定(Verified 表示)
最終表示は verification-summary のルールで決定されます。
flowchart TB
A[検証チェック集合] --> B{必須チェック failed?}
B -- yes --> F[failed 系ステータス]
B -- no --> C{必須チェック not_run / running?}
C -- yes --> W[in_progress / missing_evidence]
C -- no --> D{任意チェックに劣化あり?}
D -- yes --> L[verified_with_limitations]
D -- no --> V[fully_verified]
代表的な失敗ステータス:
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 フローと、Property-based Testing の Merkle / journal 不変条件で補強しています。
品質保証と形式手法
この部では、STARK Ballot Simulator の検証ロジックをどの品質境界で支えているかを説明します。
本プロジェクトは AI コーディングエージェントと協業して実装を進めました。実装速度を上げるだけでなく、AI 協業で混入しやすい次のような乖離を検出できる境界が必要です。
- 仕様と実装のドリフト
- 暗黙の fallback
- 公開してはいけないアーティファクトの混入
Verified判定ロジックの分散
この部に含まれる章
- 単体・結合・E2E テスト — example-based テストでの局所退行検出と CLI / E2E 経路
- Property-based Testing —
fast-checkと Rustproptestによる入力空間探索 - Lean による形式化 — 抽象モデルでの不変条件証明と CI 連携
想定読者と前提
- 想定読者: 実装に追従するテストや形式化の設計判断を確認したい開発者・監査者
- 前提: 単体テスト・結合テスト・E2E テストの一般的な区分、および Property-based Testing の基本概念を把握していること
品質保証のレイヤー
本書では、example-based tests、property-based testing、Lean による形式化、それらを CI に接続する仕組みを次のレイヤーで使い分けます。
| レイヤー | 目的 | 主な対象 |
|---|---|---|
| 単体テスト | 純粋関数、UI component、API helper の局所退行を検出 | src/lib, src/components, src/app/api |
| 結合テスト | API route、store、finalization、bundle 境界を検査 | src/server/api, src/lib/finalize, src/lib/store, src/lib/verification |
| CLI / E2E | session 作成から投票、集計、検証までの流れを検査 | scripts/tests/cli-e2e-voting-flow.ts, tests/e2e |
| PBT | 手書き fixture では漏れやすい入力空間を property で探索 | fast-check, Rust proptest |
| Lean | 抽象モデル上の重要な不変条件を証明 | formal/StarkBallotFormal |
| CI / audit | 成果物 freshness、proof hygiene、公開境界を検査 | formal:verify, public safety scan, docs build checks |
中心に置く不変条件
最も重要な品質目標は、ユーザーに Verified と表示してよい条件を緩めないことです。
- required check が失敗・未実行・実行中なら Verified にしない
excludedSlots > 0を成功状態にしない- STARK receipt verification だけを根拠に全体成功としない
input.json、verification.json、included-bitmap.json、seen-bitmap.jsonを公開配布対象に含めない- mock / dev receipt / production STARK proof の違いをテスト階層で明示する
これらは ゲーティングロジック と バンドル構造 で説明した安全境界を、テストと形式化の側から支えるものです。
本章で扱わないもの
この部は、システム全体の完全な形式検証を主張するものではありません。SHA-256 や RISC Zero の暗号学的健全性、各種ランタイムや AWS の正しさ、本番選挙システムとしての安全性は対象外です。Lean が扱う射程と扱わない射程の詳細は Lean による形式化 > 証明していないこと を参照してください。
関連する章
- 検証パイプライン — テストと形式化が守る
Verified判定の本体 - ゲーティングロジック — 不変条件として品質保証が支える側のロジック
- バンドル構造 — 公開境界の判定に関わる安全境界
単体・結合・E2E テスト
このページでは、example-based tests がどのリスクを守っているかを整理します。ここでの中心はテスト数ではなく、検証アプリとして失敗してはいけない境界に、どの層のテストを置いているかです。
テストピラミッド
単体テスト
単体テストは、純粋関数や小さな UI component、schema、環境変数 guard、エラー整形の退行検出に使います。
主な対象:
- 検証チェック定義と summary logic
- Lean 生成 vector と照合する verification summary / display / check definition
- zkVM journal / input commitment / bitmap helper
- session capability token と Turnstile bypass guard
- UI component と hooks
- i18n translation consistency
結合テスト
結合テストは、境界をまたいだ契約が崩れていないかを検査します。
主な対象:
- Next / Hono で共有する API route registry
- 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: production-mode の Next test server 上で主要画面を通す
- axe smoke: 主要ページの重大な accessibility violation を検出する
- real zkVM dev / prod flow: proof contract 変更時に mock だけで完了扱いにしないための重い確認経路
主なコマンド
| コマンド | 目的 |
|---|---|
pnpm test:run | Vitest による単体・結合テスト |
pnpm test:public | public snapshot 向けの安全なテスト subset |
pnpm test:cli:mock | mock zkVM / mock store で CLI voting flow を実行 |
pnpm test:e2e:mock | Playwright によるブラウザ E2E |
pnpm test:e2e:axe | axe accessibility smoke |
pnpm test:cli:real-dev | RISC0_DEV_MODE=1 の real zkVM 接続 smoke |
pnpm test:cli:real-prod:s0 | S0 の production STARK proof flow |
pnpm formal:verify | Lean build / formal vector drift guard |
pnpm build:zkvm | zkVM guest / host の build |
pnpm build:verifier-service | Rust verifier-service の build |
cd verifier-service && cargo test | verifier-service の Rust tests |
cd zkvm && cargo test | zkVM / contract-core 側の Rust tests |
RISC0_DEV_MODE=1 の receipt は production STARK proof ではありません。UI/API の回帰検出には有用ですが、proof soundness を確認したことにはなりません。
重要な検査観点
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 で管理し、次の artifact は配布対象に含めません。
input.jsonverification.jsonincluded-bitmap.jsonseen-bitmap.json
この境界は sync 生成、async container、bundle/report delivery の複数箇所にまたがるため、テストで契約として固定します。
mock / 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 server 上で確認します。テスト ID は翻訳文ではなく安定した data-testid を使い、i18n やレイアウト変更に引きずられにくくしています。
CI での位置づけ
CI では、TypeScript の core checks、UI mock E2E、Rust tests、formal checks、public snapshot checks が役割を分けて動きます。
すべてを常に最重構成で走らせるのではなく、変更領域に応じて必要な gate を選ぶ構成にしています。特に production STARK proof は高コストなので、proof 入力や journal contract に関わる変更で使う確認経路として扱います。
Property-based Testing
Property-based Testing (PBT) は、少数の fixture では見落としやすい境界条件を、生成入力と「常に成り立つべき性質」で検査するために使います。
本プロジェクトでは、Merkle tree、bitmap packing、input commitment、journal count のように、順序・境界・改ざん耐性が重要なロジックに PBT を置いています。
PBT を導入した理由
example-based tests は既知のシナリオを守ります。PBT はそれに加えて、次のような仕様レベルの性質を入力空間全体に近い形で探索します。
- 同じ vote multiset なら入力順序を変えても input commitment が変わらない
- root、leaf、index、proof node を改ざんすると Merkle proof が失敗する
- LSB-first bitmap packing の bit address が境界で崩れない
- journal count の分解式が常に成立する
PBT は証明ではありませんが、実装に対して広い入力空間を継続的に探索できるため、暗号周辺の encoding drift や境界条件の退行を早期に検出できます。
TypeScript 側の PBT
RFC 6962 Merkle tree
対象:
src/lib/merkle/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
対象:
src/lib/merkle/bitmap-merkle-tree.property.test.ts
検査する性質:
- 生成した bitmap の任意 index について proof が round-trip する
- proof から抽出した
includedが元の bit と一致する - leaf chunk や audit path の改ざんを拒否する
- 0, 1, 7, 8, 255, 256, 257, 511, 512 bit などの境界を固定ケースで検査する
Input commitment
対象:
src/lib/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 path を変えると commitment が変わる
Journal invariants
対象:
src/lib/zkvm/__tests__/journal-invariants.property.test.ts
検査する性質:
totalVotes = validVotes + rejectedRecordsinvalidVotes = rejectedRecordsseenIndicesCount = validVotes + invalidPresentedSlotsvalidVotes + invalidPresentedSlots + missingSlots = treeSizeexcludedSlots = missingSlots + invalidPresentedSlots- included bitmap の
trueは seen bitmap のtrueを含意する
Rust 側の PBT
対象:
zkvm/methods/guest/src/property_tests.rs
検査する性質:
- input commitment が vote order に対して permutation invariant
- duplicate index の tie-break を含めても permutation invariant
- RFC 6962 inclusion proof が reference tree と一致する
- root / leaf / path 改ざんを拒否する
- bitmap root が reference oracle と一致する
- bit flip で bitmap root が変わる
Rust 側の PBT は、zkVM guest / contract-core で使う低レベル実装に近い場所で、TypeScript 側と同じ性質を別実装として検査します。
Lean との関係
PBT は実装に対して広い入力空間を探索し、Lean は同種の不変条件を抽象モデル上で証明します。両者の役割分担と、Lean から出力した generated vectors を介して実装テストに接続する仕組みは Lean による形式化 > 実装との接続 に整理しています。
限界
- PBT は数学的証明ではない
- 生成範囲は CI 実行時間とのバランスで制限する
- SHA-256 の衝突困難性は PBT では証明しない
- RISC Zero receipt soundness も PBT の対象ではない
- 生成器に含めていない入力領域は探索されない
そのため、PBT は example-based tests や Lean formalization を置き換えるものではなく、境界条件と実装 drift を検出する追加レイヤーとして扱います。
Lean による形式化
本プロジェクトでは Lean 4 を使い、Verified 表示の fail-closed 条件、journal count の整合性、input commitment の canonical encoding、LSB-first bitmap packing、抽象 guest tally model の不変条件を形式化しています。
Lean は実装を直接証明するのではなく、抽象モデル上で不変条件を証明し、そこから生成した generated vectors / formal report / formal audit を TypeScript・Rust のテストと CI に接続することで、モデルと実装の対応付けを継続的に検査します。Lean が扱わない範囲は末尾の 証明していないこと を参照してください。
Lean で定義しているもの
| Lean module | 主な定義 | 役割 |
|---|---|---|
Basic.lean | CheckStatus, SummaryStatus, SummaryTone, CheckId, CheckCategory, CheckRole, Criticality | 検証チェックと summary model の基礎型 |
JournalCounts.lean | missingSlotsOf, invalidPresentedSlotsOf, excludedSlotsOf | zkVM journal の count 分解を Nat モデルで表す |
VerificationSummary.lean | checkDefinitions, isRequiredCheck, canFullyVerify, deriveSummaryModel | /verify の最終判定に関わる fail-closed model |
InputCommitment.lean | CommitmentVote, InputCommitmentCase, canonical order, u16LE, u32LE, preimage encoding | input commitment の byte layout と順序安定性をモデル化 |
Bitmap.lean | packedByteCount, packedAddress, byteValueAt, packBits | LSB-first bitmap packing と bit address をモデル化 |
GuestModel.lean | RejectReason, GuestVote, CandidateTally, GuestState, classifyVote, processVotes, guest bounds | zkVM guest の抽象 tally / rejection state machine |
GuestModel.lean は zkvm/methods/guest/src/main.rs を行単位で翻訳したものではありません。外部的に重要な処理順序、つまりインデックス範囲、重複 index、選択肢、コミットメント、重複 commitment、包含証明、集計反映の順序を抽象 state machine として表します。
Lean で証明していること
| 領域 | 代表 theorem | 主張 |
|---|---|---|
| Journal count | excluded_zero_implies_no_slot_loss, slot_partition_total | excludedSlots = 0 なら missing / invalid presented が 0 になり、slot loss がない |
| Verification summary | fully_verified_implies_all_required_success, fully_verified_implies_no_unknown_checks, fully_verified_implies_required_roles_success | fully_verified は required check 成功、unknown check 不在、重要 role 成功を要求する |
| Input commitment | canonical_vote_order_total, canonical_encoding_permutation_invariant | vote の入力順序に依存しない canonical encoding が定義されている |
| Bitmap | pack_bits_length, pack_bits_get_bit | LSB-first packing の byte 数と bit 取得がモデル通りになる |
| Guest model | accepted_votes_count_tally, valid_votes_count_accepted, processVotes_fold_invariant | 抽象 guest fold が tally / validVotes / seen index の不変条件を保つ |
| Guest completeness | zero_exclusion_guest_model_complete | excludedSlots = 0 が guest model 上で missing / invalid presented の不存在につながる |
| Bounded counts | no_overflow_under_guest_bounds | 明示した guest bounds 内で seen / valid / rejected / tally bucket が Rust u32 に収まる |
これらは抽象モデル上の主張です。実装との対応は、次の generated vectors とテストで検査します。
実装との接続
flowchart LR L["Lean models<br/>formal/StarkBallotFormal/*.lean"] T["Theorems<br/>形式的な不変条件"] R["formal-report.json<br/>主張と前提"] V["generated-vectors/*.json<br/>実装対応付けケース"] A["formal-audit.json<br/>theorem hash / dependency / hygiene"] TS["TypeScript tests<br/>Vitest"] RS["Rust vector tests<br/>cargo test"] FCI["formal CI<br/>pnpm formal:verify"] RCI["Rust tests workflow<br/>cargo test"] L --> T T --> R L --> V T --> A V --> TS V --> RS R --> FCI V --> FCI A --> FCI TS --> FCI RS --> RCI
| generated vector | 消費先 | 目的 |
|---|---|---|
verification-summary-cases.json | TypeScript summary tests | Lean summary model と deriveVerificationSummary の対応 |
verification-display-cases.json | verify page overall-status tests | UI が verified を誤表示しないことの drift guard |
check-definitions.json | TypeScript check-definition test | check ID / category / role / criticality / required 条件の drift guard |
input-commitment-cases.json | TypeScript / Rust tests | canonical order と pre-hash bytes の対応 |
bitmap-cases.json | TypeScript / Rust tests | LSB-first packing と bitmap behavior の対応 |
guest-model-cases.json | Rust guest tests | 抽象 guest model と Rust guest inspection surface の対応 |
pnpm formal:verify は Lean build、formal report / generated vectors / audit の freshness、TypeScript の vector-consuming tests、生成 JSON の format check をまとめて実行します。Rust 側の vector-consuming tests は docs/current/formal/generated-vectors/** の変更で起動する Rust tests workflow の cargo test によって検査します。
この接続により、Lean model が変わったのに実装テストが追従していない場合、または実装側の encoding / summary / guest behavior が model から外れた場合に、CI 上で drift を検出できます。
対応付けを支える成果物
Lean の成果物を public documentation と実装テストに接続するため、次を組み込んでいます。
- guest-model generated vectors
- check-definition drift vector
- theorem statement hash
- generated-vector hash
#print axiomsに基づく theorem dependency audit- proof hygiene scan
- explicit guest bounds
- TypeScript / Rust 側の vector-consuming tests
pnpm formal:verifyによる freshness gate
実行コマンド
| コマンド | 目的 |
|---|---|
pnpm formal:build | Lean workspace を build する |
pnpm formal:report | formal-report.json を再生成する |
pnpm formal:report:check | report が最新か確認する |
pnpm formal:vectors | Lean から generated vectors を再生成する |
pnpm formal:vectors:check | generated vectors が最新か確認する |
pnpm formal:audit | theorem statement / dependency / proof hygiene audit を生成する |
pnpm formal:audit:check | audit artifact が最新か確認する |
pnpm formal:test:ts | Lean vector を消費する TypeScript tests を実行する |
pnpm formal:verify | build、report、vectors、audit、TS tests、format checks をまとめて検証する |
証明していないこと
Lean は次を証明しません。
- SHA-256 の衝突困難性
- RISC Zero receipt soundness
- Rust compiler / TypeScript runtime / browser runtime の完全な正しさ
- AWS runtime behavior
- React rendering 全体の正しさ
- 本番選挙システムとしての安全性
- zkVM guest Rust 実装全体の line-by-line verification
したがって、正確な主張は「投票システム全体を形式検証した」ではありません。正確には、選択した安全モデルを Lean で証明し、generated vectors と CI drift guard によって TypeScript / Rust 実装との対応を検査している、というものです。
AWS アーキテクチャ
現行の AWS 構成は、Amplify Gen 2 が担うアプリ層と、Terraform で管理する非同期プローバー層に分かれています。この部では、その構成、成立経緯、連携点、運用上の制約を扱います。
この部に含まれる章
- 現行構成とサービス一覧 — 現行構成の成立経緯、環境分離、サービス構成
- トポロジー — レイヤ別のサービス構成と通信フロー
- 非同期プローバー — SQS → Step Functions → ECS による証明パイプライン
- イメージ署名 — AWS Signer によるコンテナイメージ検証
- Terraform — IaC による構成管理とワークスペース運用
想定読者と前提
- 想定読者: 非同期プローバーや IaC の構成を把握したい運用者
- 前提: AWS の基本サービス(S3 / SQS / Step Functions / ECS)と Terraform の概念を把握していること
本章で扱わないもの
- Amplify Gen 2 の Auth / Data 機能の詳細
- Terraform 文法・モジュール設計の一般論
- CloudWatch アラート設計やコスト最適化ガイド
関連する章
現行構成とサービス一覧
現行の AWS 構成(管理境界・環境分離・利用サービス)を整理する章です。ハイブリッド構成に至った経緯と改善候補は 設計ふりかえり § 6 を参照してください。
現行の管理境界
Amplify Gen 2 がアプリ層、Terraform が非同期プローバー基盤を担う 2 系統管理になっています。
| 管理系 | 現行の役割 | 残る制約 |
|---|---|---|
| Amplify Gen 2 | Web ホスティング、API(Lambda)、データ(AppSync + DynamoDB)、認証基盤(Cognito + IAM) | branch override、環境変数同期、Amplify 管理リソース名への依存が残る |
| Terraform | ECS Fargate、Step Functions、SQS、S3、ECR、CodeBuild、SSM Parameter Store、VPC、IAM | Amplify 側リソース ARN を入力として受け取るため、完全な単独 IaC にはなっていない |
環境分離
develop と main の 2 環境を運用し、主要なアプリケーション実行系リソースは Terraform ワークスペースと Amplify ブランチデプロイで分離しています。両環境とも証明モードは実 STARK 証明(代表実測はECS Fargate タスク仕様参照)で、RISC0_DEV_MODE=1 / USE_MOCK_ZKVM=true はローカル同期実行用の設定です。
| 項目 | develop | main |
|---|---|---|
| S3 ライフサイクル | 7 日 | 30 日 |
| ログ保持期間 | 7 日 | 14 日 |
| CloudTrail | 無効 | 有効(90 日保持) |
ただし全リソースが環境ごとに二重化されているわけではなく、RISC Zero ツールチェーン用の ECR リポジトリと CodeBuild プロジェクトは aws.shared provider で共有されます。環境別に分かれるのは prover 用 ECR、証明バンドル S3、prover image metadata S3、SSM current metadata parameter、SQS、Step Functions、ECS、CloudTrail などの実行系リソースです。
全体構成図
図: STARK Ballot Simulator の AWS 全体構成。現行では Amplify 管理領域(上)と Terraform 管理領域(下)に分かれている。
サービス一覧
本システムで使用する主要な AWS サービスと、その役割の概要です。
Amplify 管理
| サービス | リソース | 役割 |
|---|---|---|
| Amplify Hosting | Web アプリ | Next.js のビルド・ホスティング |
| API Gateway (HTTP API) | stark-ballot-simulator-hono-api | /api/* ルートのプロキシ |
| Lambda | hono-api | Hono フレームワークによる API 処理 |
| Lambda | prover-dispatch-proxy | SQS 受信 → input.json を S3 保存 → Step Functions 起動 |
| Lambda | finalize-callback-runner | Step Functions コールバック → セッション更新 |
| Lambda | verifier-service-runner | STARK レシート検証の実行 |
| AppSync + DynamoDB | データモデル | セッション・投票・集計結果の永続化 |
| DynamoDB | RateLimitEvents / RateLimitCounters | hono-api の API レート制限状態 |
| Cognito | Identity Pool / User Pool | 認証基盤(未認証 ID 無効) |
Terraform 管理
| サービス | リソース | 役割 |
|---|---|---|
| ECS Fargate | プローバータスク | zkVM ホストバイナリによる STARK 証明生成 |
| Step Functions | プローバーディスパッチャー | イメージ署名検証 → ECS 実行 → コールバック |
| SQS | ワークキュー + DLQ | 非同期証明リクエストのバッファリング |
| S3 | 証明バンドルバケット / prover metadata | 入力・実行成果物・検証用バンドル、prover image metadata の保存 |
| ECR | イメージリポジトリ | プローバーコンテナイメージの管理 |
| CodeBuild | 環境別プローバー + 共有 toolchain builder | Docker イメージのビルド、ARM64 ImageID / methodVersion metadata の抽出と公開 |
| SSM Parameter Store | current metadata pointer | 現行 prover image metadata candidate JSON の保持 |
| Lambda | check-image-signature | ECR イメージ署名の実行前検証 |
| VPC | パブリックサブネット | ECS タスクのネットワーク |
| CloudWatch | ログ群 | ECS / Step Functions / CodeBuild などのログ |
| CloudTrail | 監査証跡(main のみ) | API 呼び出しの監査ログ |
これらのサービスに紐づく IAM ロール / ポリシーも同じ Terraform 管理下にあります。詳細は Terraform > IAM 設計 を参照してください。
Amplify と Terraform の境界
2 つのインフラ管理ツール間の連携は、ARN と環境変数によって行われます。
flowchart TB
subgraph TF["Terraform 管理"]
OUT["Amplify 同期出力<br/>SFN ARN / SQS ARN / SQS URL / 証明バンドル S3 名"]
OPS["運用出力<br/>metadata S3 名 / SSM parameter 名"]
IN["入力変数<br/>finalize_callback_lambda_arn"]
end
subgraph AMP["Amplify Gen 2 管理"]
ENV["環境変数"]
CB["finalize-callback-runner<br/>Lambda ARN"]
end
OUT -.対応する値.-> ENV
CB -.ARN を入力変数へ設定.-> IN
Terraform の出力値(Step Functions ARN、SQS ARN、SQS URL、S3 バケット名など)は Amplify の環境変数に手動で反映する運用で、PROVER_STATE_MACHINE_ARN と PROVER_WORK_QUEUE_ARN は Amplify backend のデプロイ時にも必須(未設定で fail-closed)です。同期手順と一覧は Terraform > Amplify との連携ポイント を参照してください。
なお、prover image metadata bucket 名と current metadata SSM parameter 名は CodeBuild / 運用確認向けの Terraform 出力であり、Amplify 環境変数へ同期する実行時契約ではありません。
関連する章
- トポロジー — リクエスト経路とコンポーネント間の通信フロー
- 非同期プローバー — SQS → Step Functions → ECS の実行時フロー
- Terraform — Amplify との連携ポイント(ARN / 環境変数名)
- 設計ふりかえり § Amplify 単独構成からハイブリッド構成への移行 — 現行構成に至った設計判断
トポロジー
AWS 上のサービス配置と、コンポーネント間の通信経路を俯瞰する章です。
本システムは 5 つの論理レイヤ(Web、API、Data、Prover、Storage)で構成され、各レイヤは明確な責務を持ちます。
レイヤ別トポロジー
Web レイヤ
Amplify Hosting が Next.js アプリケーションのビルドとホスティングを担当します。GitHub リポジトリのブランチ(develop / main)と Amplify の環境が 1 対 1 で対応し、プッシュをトリガーとする自動デプロイが行われます。
API レイヤ
API Gateway(HTTP API)が /api/* パスパターンのリクエストを受け取り、単一の Lambda 関数(hono-api)にプロキシします。hono-api は Hono フレームワーク上に構築されており、ルーティング、セッション管理、Turnstile 検証、レート制限を処理します。下図の「主なリクエスト制御」は、hono-api がルートごとに適用有無と順序を切り替える代表的な要素を示します(固定順のパイプラインではありません)。
flowchart LR
Client["クライアント"] --> APIGW["API Gateway<br/>(HTTP API)"]
APIGW --> HONO["hono-api<br/>Lambda"]
HONO --> APPSYNC["AppSync<br/>(Data)"]
HONO --> SQS["SQS<br/>(非同期証明)"]
HONO --> S3["S3<br/>(バンドル取得)"]
subgraph "主なリクエスト制御(ルートごとに適用)"
direction TB
RATE["IP / zkVM レート制限"]
SESS["セッション / capability 検証"]
TURN["Turnstile 検証"]
BODY["入力検証"]
HAND["ハンドラー実行"]
RATE -.-> HAND
SESS -.-> HAND
TURN -.-> HAND
BODY -.-> HAND
end
CORS 設定では X-Session-ID と X-Session-Capability ヘッダーを許可し、セッションスコープと capability トークンによるリクエスト制御を実現しています。なお POST /api/verification/run だけは、hono-api から専用の verifier-service-runner Lambda を同期起動する構成です(後述のエンドツーエンドのデータフローを参照)。
Data レイヤ
AppSync(GraphQL API)と DynamoDB が、セッション・投票・集計結果のデータ永続化を担当します。データプレーンは allow.resource(...) により hono-api / prover-dispatch-proxy / finalize-callback-runner からの SigV4 呼び出しに限定され、未認証アクセスは無効です(Amplify Data の検証要件として fallback group は定義していますが、エンドユーザーには割り当てていません)。
主要なデータモデルは以下の 2 つです(集計結果は VotingSession.finalizationResultJson に格納)。
| モデル | キー/識別子 | 主要フィールド | TTL |
|---|---|---|---|
| VotingSession | id | electionId, botCount, finalized, userVoteIndex, finalizationResultJson | ttl |
| Vote | 識別子: sessionId + voteIndex | choice, random, commitment, rootAtCast, isUserVote | — |
レート制限用に 2 つの追加テーブル(RateLimitEvents、RateLimitCounters)が PAY_PER_REQUEST モードで運用されています。
Prover レイヤ
STARK 証明の生成を担当するレイヤです。SQS キュー、Step Functions ステートマシン、ECS Fargate タスクで構成されます。詳細は 非同期プローバー を参照してください。
flowchart LR CODEBUILD["CodeBuild<br/>プローバーイメージ"] --> META["S3<br/>イメージメタデータ"] CODEBUILD --> SSM["SSM Parameter<br/>current metadata"] SQS["SQS<br/>ワークキュー"] --> DP["prover-dispatch-proxy<br/>Lambda"] DP --> SFN["Step Functions<br/>ディスパッチャー"] SFN --> SIG["check-image-signature<br/>Lambda"] SFN --> ECS["ECS Fargate<br/>16 vCPU / 32 GB"] SFN --> CALLBACK["finalize-callback-runner<br/>Lambda"] ECS --> S3["S3<br/>証明バンドル"]
ECS タスクは ARM64 アーキテクチャの Fargate で実行され、専用の VPC(10.0.0.0/16)内のパブリックサブネットに配置されます。セキュリティグループは HTTPS エグレスのみを許可し、インバウンドトラフィックは一切受け付けません。
Storage レイヤ
S3 バケットが、証明バンドルに含まれる配布対象アーティファクトと非公開ワーク入力の保存を担当します。 Terraform はこれに加えて、プローバーイメージのビルドメタデータを保存する S3 バケットと、現在候補を指す SSM Parameter も管理します。
| 項目 | 設定 |
|---|---|
| バケット命名 | stark-ballot-simulator-proof-bundles-{環境名} |
| 暗号化 | AES256(サーバーサイド暗号化) |
| パブリックアクセス | 全ブロック |
| ライフサイクル | develop: 7 日、main: 30 日で自動削除 |
| バージョニング | Suspended(新規バージョン作成なし) |
プローバーイメージメタデータ用バケット({project}-prover-metadata-{環境名})も Terraform 管理下にあり、AES256 暗号化・パブリックアクセス全ブロック・バージョニング Enabled で運用されます。CodeBuild からの publish 仕様と SSM current pointer の関係は イメージ署名 > ビルドと署名 を参照してください。
証明バンドル側のオブジェクトパスは既定で sessions/{sessionId}/{executionId}/ 配下です。先頭プレフィックスは s3_proof_prefix で変更可能ですが Amplify 側に sessions/ を前提とする箇所があり、変更時の影響範囲は 非同期プローバー > フェーズ 2: ディスパッチ を参照してください。
| ファイル | 区分 | 説明 |
|---|---|---|
input.json | 保護 | zkVM への完全な入力(プライベートウィットネス) |
*-receipt.json | 中間データ | zkVM host の生出力。bundle.zip 内の receipt.json の元データ |
*-output.json | 中間データ | zkVM host の生出力(集計結果)。bundle.zip 内の journal.json の元データ |
public-input.json | 配布対象 | zkVM 検証に使う、秘密データを含まない検証用レコード |
election-manifest.json | 配布対象 | 選挙設定の公開監査用スナップショット |
close-statement.json | 配布対象 | 集計締切時点のログ境界を表す公開監査レコード |
included-bitmap.json | 保護 | 厳密な counted bitmap。bundle.zip 外の sibling object として保持 |
seen-bitmap.json | 保護 | 厳密な presented bitmap。bundle.zip 外の sibling object として保持 |
bundle.zip | 配布対象 | receipt.json + journal.json + public-input.json + election-manifest.json + close-statement.json を同梱した配布アーカイブ |
verification.json | 保護 | 検証サービスの出力。POST /api/verification/run 後に sibling object として追加されうる |
S3 バケット自体はパブリックアクセス全ブロックです。「区分」は機密性の扱いを示すもので、実際の配布経路(presigned URL 等)は バンドル構造 を参照してください。
エンドツーエンドのデータフロー
投票から検証までの全データフローを、レイヤ間の通信として示します。
sequenceDiagram
participant C as クライアント
participant W as Web レイヤ<br/>(Amplify Hosting)
participant A as API レイヤ<br/>(API GW + Lambda)
participant V as 検証レイヤ<br/>(verifier-service-runner)
participant D as Data レイヤ<br/>(AppSync + DDB)
participant P as Prover レイヤ<br/>(SQS → SFN → ECS)
participant S as Storage レイヤ<br/>(S3)
Note over C,D: 投票フェーズ
C->>W: ページ読み込み
C->>A: POST /api/session
A->>D: セッション作成
C->>A: POST /api/vote
A->>D: 投票 + コミットメント保存
A-->>C: 投票レシート返却
Note over A,D: ボット投票を自動追加
Note over C,P: 集計フェーズ
C->>A: POST /api/finalize
alt FINALIZE_ASYNC_MODE=true
A->>P: SQS メッセージ送信
A-->>C: 202 Accepted
P->>S: input.json 保存(SFN 起動前)
P->>P: イメージ署名検証
P->>P: ECS タスクで証明生成
P->>S: 実行成果物 + 公開監査アーティファクト + bundle.zip 保存
P->>D: コールバックで結果書き込み
else FINALIZE_ASYNC_MODE=false
A->>A: 同期で zkVM 実行
A-->>C: 200 OK(集計結果)
end
Note over C,S: 検証フェーズ
C->>A: GET /api/verify
A->>D: セッション + 集計結果取得
A-->>C: 検証ペイロード返却
C->>A: POST /api/verification/run
A->>V: verifier-service-runner を同期起動
V->>S: bundle.zip 取得
V->>V: verifier-service 実行
opt USE_S3=true
V->>S: verification.json / bundle.zip の書き戻しを試行
end
V-->>A: 検証結果返却
A-->>C: 検証結果返却
verifier-service-runner は S3 書き戻しが有効な構成で verification.json と更新済み bundle.zip の保存を試み、成功した場合のみ新しい key を finalization state に反映します。失敗時はもとの s3BundleKey を維持するため配信元は失われません(書き戻し挙動の詳細と bitmap sibling object の扱いは バンドル構造 を参照)。
ネットワーク構成
VPC
Terraform が管理する VPC は、ECS Fargate タスク専用です。Amplify 管理のリソース(Lambda、AppSync)は VPC 外で動作します。
flowchart TD
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ1["ap-northeast-1a"]
S1["パブリックサブネット<br/>10.0.1.0/24"]
end
subgraph AZ2["ap-northeast-1c"]
S2["パブリックサブネット<br/>10.0.2.0/24"]
end
SG["セキュリティグループ<br/>Egress: 443 のみ"]
end
IGW["インターネット<br/>ゲートウェイ"]
ECR["ECR<br/>(イメージ取得)"]
S3["S3<br/>(バンドル保存)"]
VPC --> IGW
IGW --> ECR
IGW --> S3
ECS タスクはパブリック IP を持ちますが、セキュリティグループがインバウンドを全拒否するため、外部からのアクセスはできません。アウトバウンドは HTTPS(ポート 443)のみ許可され、ECR からのイメージ取得、S3 へのアップロード、CloudWatch Logs などの AWS API 通信に使用されます。
監視とロギング
主な CloudWatch ログ群
| ログ群 | 対象 | 保持期間 |
|---|---|---|
/aws/ecs/{project}-prover-{env} | ECS Fargate タスク | develop: 7 日 / main: 14 日 |
/aws/stepfunctions/{project}-prover-{env} | Step Functions 実行 | develop: 7 日 / main: 14 日 |
/aws/codebuild/{project}-fargate-prover-{env} | 環境別 prover CodeBuild | develop: 7 日 / main: 14 日 |
/aws/codebuild/stark-ballot-simulator-risc0-toolchain-builder | 共有 toolchain CodeBuild | 14 日 |
/aws/lambda/{project}-check-image-signature-{env} | イメージ署名検証 Lambda | develop: 7 日 / main: 14 日 |
/aws/lambda/*hono-api* などの Amplify 生成ログ群 | Amplify 管理 Lambda 4 種 | develop: 7 日 / main: 14 日 |
/aws/apigateway/{project}-hono-api-{env} | API Gateway アクセスログ | develop: 7 日 / main: 14 日 |
/aws/appsync/apis/* などの Amplify 生成ログ群 | AppSync field/error ログ | develop: 7 日 / main: 14 日 |
Amplify 管理 Lambda と AppSync の実際のロググループ名には app / branch / API 由来の識別子が入るため、運用時は runbook の discovery クエリで特定します。
CloudTrail(main 環境のみ)
main 環境では CloudTrail による全リージョン監査ログが有効です。
| 項目 | 設定 |
|---|---|
| 対象リージョン | 全リージョン |
| ログ検証 | 有効 |
| 保存先 | 専用 S3 バケット + CloudWatch Logs |
| 保持期間 | 90 日 |
非同期プローバー
SQS → Step Functions → ECS Fargate で証明を非同期生成する経路と、各段階の責務を扱う章です。
STARK 証明の生成は 64 票で約 6 分を要するため、同期的な HTTP リクエスト内では完了できません。非同期パイプラインにより、Web リクエストのタイムアウトを回避しつつ、高負荷な証明生成を安全に実行します。
この章は FINALIZE_ASYNC_MODE=true で動作する非同期経路を対象としています。
パイプライン全体像
flowchart TB
API["POST /api/finalize"] --> SQS["SQS<br/>ワークキュー"]
SQS --> DP["prover-dispatch-proxy<br/>Lambda"]
DP --> S3U["S3<br/>input.json 保存"]
DP --> SFN["Step Functions<br/>StartExecution"]
SFN --> SIG["check-image-signature<br/>Lambda"]
SIG --> CHK{"署名 COMPLETE?"}
CHK -->|Yes| ECS["ECS Fargate<br/>RunTask"]
CHK -->|No| SFNFAIL["Step Functions<br/>署名失敗分岐"]
ECS --> S3W["S3<br/>bundle / sibling artifacts 保存"]
ECS --> SFNRET["Step Functions<br/>成功/失敗を判定"]
SFNFAIL --> CB["finalize-callback-runner<br/>Lambda"]
SFNRET --> CB
CB --> DDB["AppSync/DynamoDB<br/>セッション更新"]
詳細な失敗分岐(署名検証 NG、プローバー実行失敗)は、後続のステートマシン図で示します。
各フェーズの詳細
フェーズ 1: リクエスト受付
クライアントが POST /api/finalize を呼び出すと、API ハンドラーは以下の処理を行います。
以下は FINALIZE_ASYNC_MODE=true の場合です。
- セッションの状態を検証(全投票が完了していること)
- zkVM 入力を構築(入力ビルダーが投票データ + Merkle パスを組み立て)
- セッションの
finalizationStateを「pending」に更新 - SQS キューにメッセージを送信
- クライアントに 202 Accepted を返却
クライアントはその後、GET /api/sessions/:id/status を X-Session-Capability 付きでポーリングして進捗を確認します。
フェーズ 2: ディスパッチ
prover-dispatch-proxy Lambda が SQS メッセージを受信し、以下を実行します。
- SQS メッセージと現在のセッション状態を検証
- zkVM 入力 JSON を S3 にアップロード(
sessions/{sessionId}/{executionId}/input.json) - Step Functions のステートマシンを
StartExecutionで起動 - セッションの
finalizationStateを「running」に更新し、executionIdと Step Functions execution ARN を記録
フェーズ 2 の補足事項
前提条件チェック: dispatch 前に、セッションが存在し、finalizationState.status が pending、finalizationState.executionId が SQS メッセージの executionId と一致することを確認します。揃わない場合は高コストな証明実行に進まず中断し、伝播遅延など再試行で回復し得る状態は SQS の受信回数に応じて retry / DLQ に委ねます。
contract generation の互換性: SQS メッセージに付いている contract generation が保存済みの finalization contract と互換でない場合、unsupported_current_artifact として既存の session state を fail-closed に確定させます(再試行対象外)。
同時実行制御: Amplify 側の SQS trigger は batchSize=1 で、PROVER_LAMBDA_CONCURRENCY により予約同時実行数を制御します(既定 2)。Step Functions 起動時の throttle 系エラーは Lambda が例外として返し、SQS retry の対象にします。
s3_proof_prefix と Amplify 側の連動: S3 パスプレフィックスは Terraform 変数 s3_proof_prefix で変更可能ですが、Amplify 側に sessions/* を前提とする以下の箇所があるため、変更時は同時に更新が必要です。
verifier-service-runnerLambda の S3 アクセスポリシー(sessions/*のみにGet/Put/Listを許可)- CLI / verifier-service の管理ポリシー(同様に
sessions/*限定) - 関連 Amplify 環境変数
フェーズ 3: 証明生成
Step Functions ステートマシンが 3 つのステップを順次実行します。
stateDiagram-v2 [*] --> VerifyImageSignature VerifyImageSignature --> CheckImageSignature VerifyImageSignature --> FinalizeFailed: Lambda 呼び出し失敗 CheckImageSignature --> RunProver: 署名 COMPLETE CheckImageSignature --> FinalizeSignatureFailed: 署名 NG RunProver --> FinalizeSucceeded: 成功 RunProver --> FinalizeFailed: 失敗 FinalizeSignatureFailed --> [*] FinalizeSucceeded --> [*] FinalizeFailed --> [*]
VerifyImageSignature
check-image-signature Lambda を呼び出し、ECR イメージのダイジェストに対する AWS Signer 署名のステータスを確認します。詳細は イメージ署名 を参照してください。
CheckImageSignature
Choice ステートで署名ステータスを判定します。COMPLETE であれば RunProver に進み、それ以外は FinalizeSignatureFailed に遷移してコールバック Lambda に失敗を通知します。
RunProver
ECS Fargate タスクを ecs:runTask.sync(同期モード)で起動します。Step Functions はタスクの完了を待機し、成功/失敗に応じて対応するコールバックステートに遷移します。終端ステートの一覧と意味はクライアント側のポーリングの表にまとめています。
ECS タスクのコンテナには、Step Functions の入力からセッション固有の環境変数が注入されます。
| 環境変数 | 値の由来 | 説明 |
|---|---|---|
ENV_NAME | Terraform 変数 | 環境名(develop / main) |
S3_PROOF_BUCKET | Terraform 変数 | 証明バンドルバケット名 |
S3_PROOF_PREFIX | Terraform 変数 | S3 artifact prefix(既定 sessions/) |
INPUT_S3_BUCKET | Terraform 変数 | 入力ファイルのバケット(同上) |
INPUT_S3_KEY | Step Functions 入力 | セッション固有の入力パス |
OUTPUT_S3_BUCKET | Terraform 変数 | 出力先バケット(同上) |
OUTPUT_S3_PREFIX | Step Functions 入力 | ${S3_PROOF_PREFIX}{sessionId}/{executionId} |
フェーズ 4: 結果通知
Step Functions が finalize-callback-runner Lambda を呼び出し、以下の情報をセッションに書き戻します。
- 成功時: bundle メタデータ(
s3BundleKey、s3UploadedAt)と、bundle.zip/ bitmap artifact から復元したfinalizationResult - 失敗時: 失敗状態とエラー情報(イメージ署名失敗、プローバーエラーなど)
bundle / report の配信は、capability 検証付きの短命 presigned URL 経由で行います。詳細は バンドル構造 を参照してください。
ECS タスクの実行フロー
ECS Fargate タスク内のコンテナは、エントリポイントスクリプトにより以下の処理を順次実行します。
- S3 から入力 JSON をダウンロード
- 入力の構造を検証(必須フィールド確認)
- zkVM ホストバイナリを実行(タイムアウト: 900 秒)
- ジャーナルを JSON に変換
public-input.json、election-manifest.json、close-statement.jsonを構築journal.jsonと公開監査アーティファクトの整合性を検証bundle.zipを作成(receipt.json+journal.json+public-input.json+election-manifest.json+close-statement.json)- 生成物と private sibling artifacts を S3 にアップロード(リトライ付き)
入力の検証
エントリポイントは、zkVM 入力 JSON に必要なフィールドが存在することを確認します。欠落があればタスクは即座に失敗し、Step Functions が失敗コールバックを実行します。
zkVM ホストバイナリの実行
コンテナ内の /opt/zkvm/bin/host がプローバーとして起動されます。タイムアウトと代表実測値は ECS Fargate タスク仕様 を参照してください。本番モード(RISC0_DEV_MODE 未設定)では実際の STARK 証明が生成されます。
S3 アップロード
生成されたアーティファクトは、指数バックオフ付きのリトライ(最大 3 回、基底 2 秒)で S3 にアップロードされます。主にアップロードされるファイルは以下です。
| ファイル | 説明 |
|---|---|
*-receipt.json | zkVM host の生出力(レシート) |
*-output.json | zkVM host の生出力(集計結果) |
*-journal.json | zkVM host が出力した場合のみ残る生の journal artifact |
public-input.json | エントリポイントが構築する、秘密データを含まない検証用レコード |
election-manifest.json | 選挙設定の公開監査用スナップショット |
close-statement.json | 集計締切時点のログ境界を表す公開監査レコード |
included-bitmap.json | 厳密な counted bitmap。bundle.zip には含めず、隣接オブジェクト(sibling object)として保持 |
seen-bitmap.json | 厳密な presented bitmap。bundle.zip には含めず、隣接オブジェクト(sibling object)として保持 |
bundle.zip | 配布対象アーカイブ。同梱物は バンドル構造 を参照 |
配布と callback 復元の主経路は bundle.zip 内の receipt.json / journal.json / 公開監査アーティファクトです。bitmap artifact は利用可能な場合に sibling object として callback から追加復元されます。配布経路の詳細は バンドル構造 を参照してください。
SQS キュー設計
ワークキュー
| 項目 | 設定 | 理由 |
|---|---|---|
| 可視性タイムアウト | 1000 秒 | zkVM 実行タイムアウト(900 秒)+ バッファ |
| メッセージ保持期間 | 4 日 | 一時的な障害からの回復猶予 |
| ロングポーリング | 20 秒 | Lambda のポーリングコスト最適化 |
| 暗号化 | SQS マネージド SSE | デフォルト暗号化 |
デッドレターキュー(DLQ)
3 回の受信失敗後、メッセージは DLQ に移動されます。DLQ のメッセージ保持期間は 14 日で、手動での障害調査と再処理に使用されます。
ECS Fargate タスク仕様
| 項目 | 設定 |
|---|---|
| CPU | 16 vCPU(16384 ユニット) |
| メモリ | 32 GB(32768 MiB) |
| アーキテクチャ | ARM64(Graviton) |
| ネットワークモード | awsvpc |
| 起動モデル | RunTask(サービスなし、1 回限りのタスク) |
| イメージ指定 | ダイジェスト固定(@sha256:...) |
| ログドライバー | CloudWatch Logs(awslogs) |
| タイムアウト | 900 秒(15 分) |
| 代表実測 | 本番モードで 64 票あたり約 370 秒 |
ARM64 アーキテクチャの選択は、RISC Zero の STARK 証明生成における Graviton プロセッサのコスト効率に基づいています。非 GPU 前提のこの構成は PoC の意図的な制約です。詳細は PoC の意図的な制約 > 非 GPU 前提の証明実行 を参照してください。
クライアント側のポーリング
非同期証明の進捗は、クライアントが GET /api/sessions/:id/status を X-Session-Capability 付きでポーリングして確認します。
stateDiagram-v2 [*] --> pending: POST /api/finalize → 202 pending --> running: dispatch-proxy が SFN を起動 running --> succeeded: callback-runner が結果を書き込み running --> failed: エラー発生 succeeded --> [*] failed --> [*]
| ステータス | 説明 |
|---|---|
pending | ファイナライズ要求を受理済み(実装上は pending 更新後に SQS 送信) |
running | Step Functions が実行中 |
succeeded | 証明生成と結果の書き戻しが完了 |
failed | コールバック経由で失敗が書き戻された状態(署名検証失敗、プローバーエラー等) |
timeout | finalize-callback-runner が TIMED_OUT を受理した場合の状態(現行 State Machine では通常未使用) |
注: prover-dispatch-proxy が Step Functions 起動前に失敗した場合、コールバックが走らないため pending のまま再試行/DLQ 待ちになることがあります。dispatch 前提条件や contract generation の互換チェックで非再試行扱いになる条件はフェーズ 2を参照してください。
障害時の調査導線
非同期証明がスタックした場合の最小調査パスです。
flowchart TD
START["finalize がスタック"] --> Q1{"SQS に<br/>メッセージあり?"}
Q1 -->|No| A1["API → SQS の送信を確認<br/>環境変数 PROVER_WORK_QUEUE_URL"]
Q1 -->|Yes| Q2{"dispatch-proxy<br/>ログに成功あり?"}
Q2 -->|No| A2["Lambda ログを確認<br/>SQS → Lambda のトリガー設定"]
Q2 -->|Yes| Q3{"SFN の<br/>実行状態は?"}
Q3 -->|署名失敗| A3["ECR イメージ署名を確認<br/>Signer プロファイル設定"]
Q3 -->|ECS 失敗| Q4{"ECS タスク<br/>ログに出力あり?"}
Q4 -->|No| A4["タスク起動失敗<br/>IAM / サブネット / イメージ URI"]
Q4 -->|Yes| A5["エントリポイントエラーを確認<br/>入力検証 / プローバー実行"]
Q3 -->|コールバック失敗| A6["callback-runner ログを確認<br/>AppSync 書き込み権限"]
関連する章
- バンドル構造 —
bundle.zipと隣接オブジェクトの公開境界 - イメージ署名 — Step Functions 内の署名検証ステップの詳細
- Image ID — プローバーイメージと Image ID の対応関係
イメージ署名
AWS Signer でプローバーイメージを署名し、ECS 実行前にゲートとして検証する仕組みを扱う章です。
STARK 証明は「特定のゲストプログラムが正しく実行された」ことを保証しますが、そもそもそのゲストプログラムを含むコンテナイメージ自体が改ざんされていないことも保証する必要があります。イメージ署名は、信頼されたビルドパイプラインが生成したイメージのみが証明生成に使用されることを担保するセキュリティゲートです。
脅威モデル
署名なしの場合、未承認イメージへの差し替えは検証段階の Image ID 照合や STARK レシート検証で拒否され得るものの、証明生成インフラ上で未承認イメージが実行されること自体は起動前に止められません。署名ありの場合は、Step Functions が起動前に署名ステータスを確認し、署名なし/未完了であればタスク起動を拒否します。
STARK 証明は Image ID(ゲストバイナリの暗号的識別子)に紐づきますが、イメージ署名はそれとは別レイヤで「未承認コンテナイメージの実行」を起動前に抑止します。両者は相補的な防御です。
| 保証の種類 | メカニズム | 検出対象 |
|---|---|---|
| ゲストプログラムの同一性 | Image ID(RISC Zero) | ゲストバイナリの改変 |
| イメージ実行許可 | AWS Signer | 未承認または署名未完了のコンテナイメージ |
署名フロー
ビルドと署名
CodeBuild がプローバーコンテナイメージをビルドし、ビルド済み ARM64 コンテナから host --print-image-id --json を実行して guest ImageID と methodVersion を抽出します。その後 ECR に push し、ECR 上で解決されたイメージ digest、digest 固定 URI、ImageID、methodVersion、Git SHA、RISC Zero toolchain image を image-metadata.json として出力します。その digest を運用手順で Terraform の ecs_image_uri に反映し、Step Functions は digest 固定のイメージ参照に対して署名ステータスを確認します。
ECR マネージド署名が有効な環境では、push 後に AWS Signer プロファイルに基づく署名ステータスが対象 digest に付与されます。
sequenceDiagram participant GH as GitHub participant CB as CodeBuild participant ECR as ECR participant SGN as AWS Signer GH->>CB: ソースコード取得 CB->>CB: Docker イメージビルド<br/>(ARM64) CB->>CB: ImageID / methodVersion 抽出 CB->>ECR: イメージをプッシュ<br/>(タグ付き) ECR-->>CB: digest を解決<br/>metadata 出力 CB->>CB: image-metadata.json 生成 ECR->>SGN: (ECR managed signing 有効時)署名処理 SGN->>ECR: 署名ステータス更新 Note over ECR: deploy/runtime では<br/>Terraform に反映した digest 固定で参照
注: このリポジトリでコード化されているのは署名ステータス確認(
DescribeImageSigningStatus)です。
署名付与そのもの(ECR managed signing の有効化)は、ECR 側の設定・運用が前提です。
CodeBuild の build/push では運用上のタグを使用できますが、Terraform に渡す ecs_image_uri と Step Functions が署名確認する対象は常にダイジェスト固定(@sha256:<64-hex>)です。これにより、タグの上書きによるイメージのすり替えを防止します。ベースとなる RISC Zero ツールチェーンイメージも同様に、ECR 上のタグから digest を解決した RISC0_TOOLCHAIN_IMAGE として Docker build に渡され、非 digest 形式は buildspec 側で拒否されます。
生成された image-metadata.json は、S3 metadata bucket の prover-images/<env>/latest.json、prover-images/<env>/by-digest/sha256-<digest>.json、prover-images/<env>/by-git-sha/<sha>.json に保存されます。さらに SSM Parameter Store の current pointer に同じ候補 metadata JSON を書き込み、ImageID と digest 固定 URI が別々にずれないようにします。これらは昇格前の候補であり、運用では ECR 署名ステータスと必要な proof smoke を確認してから imageId-mapping.json と Terraform の ecs_image_uri に反映します。
実行前確認
Step Functions ステートマシンの最初のステートで、check-image-signature Lambda がイメージの署名ステータスを確認します。
stateDiagram-v2
[*] --> VerifyImageSignature: Step Functions 開始
VerifyImageSignature --> CheckImageSignature
state CheckImageSignature <<choice>>
CheckImageSignature --> RunProver: status = COMPLETE
CheckImageSignature --> FinalizeSignatureFailed: status ≠ COMPLETE
state FinalizeSignatureFailed {
[*] --> CallbackFailed: エラー情報を通知
}
state RunProver {
[*] --> ECSTask: 署名ステータス確認済みイメージで実行
}
check-image-signature Lambda は以下の処理を行います。
- ECR の
DescribeImageSigningStatusAPI を呼び出す - 指定されたリポジトリ名とイメージダイジェストに対する署名ステータスを取得
- 取得した
status(COMPLETE/ それ以外)を Step Functions に返す
署名ステータスが COMPLETE でない場合、Step Functions の Choice ステートが FinalizeSignatureFailed に遷移し、コールバック Lambda に ImageSignatureVerificationFailed エラーを通知します。ECS タスクは一切起動されません。
ステータス確認と暗号学的検証の違い 本システムの実行前チェックは ECR の
DescribeImageSigningStatusが返すステータス参照であり、署名値そのものの暗号学的検証(証明書チェーン検証など)は行いません。独立検証が必要な場合は Notation などの外部ツールを併用してください。
ECR リポジトリとイメージ管理
リポジトリ構成
| リポジトリ | 用途 | ライフサイクル |
|---|---|---|
stark-ballot-simulator/zkvm-prover-{env} | プローバーコンテナイメージ | 最新 10 イメージを保持 |
stark-ballot-simulator/risc0-toolchain | RISC Zero ツールチェーンベースイメージ | 最新 5 イメージを保持 |
両リポジトリとも、プッシュ時の脆弱性スキャン(Scan on Push)が有効です。
ダイジェスト固定
Terraform の ecs_image_uri 変数には、ダイジェスト固定の URI のみが許可されます。バリデーションルールにより @sha256:<64-hex> 形式が強制されます。
Step Functions の定義に含まれるイメージダイジェストは、Terraform の変数から以下のように抽出されます。
- リポジトリ名: URI の
@より前の部分からレジストリホストを除去 - ダイジェスト: URI の
@より後の部分(sha256:...)
この分解により、check-image-signature Lambda は正確なリポジトリとダイジェストの組み合わせで署名ステータスを確認できます。
ビルドパイプライン
CodeBuild プロジェクト
2 つの CodeBuild プロジェクトがイメージのビルドを担当します。
| プロジェクト | ビルド対象 | タイムアウト | インスタンス |
|---|---|---|---|
stark-ballot-simulator-fargate-prover-{env} | プローバーイメージ | 30 分 | ARM64 Small |
stark-ballot-simulator-risc0-toolchain-builder | ベースイメージ | 120 分 | ARM64 Large |
RISC Zero ツールチェーンのビルドは低頻度(ツールチェーンバージョン更新時のみ)ですが、ビルドに時間を要するため Large インスタンスと長いタイムアウトが設定されています。
CodeBuild の IAM 権限
CodeBuild ロールには以下の権限が付与されています。
| 権限カテゴリ | 対象 API | 目的 |
|---|---|---|
| ECR | GetAuthorizationToken, PutImage 等 | イメージのプッシュ |
| AWS Signer | SignPayload, GetSigningProfile | 署名連携用の権限(運用/拡張時) |
| CloudWatch Logs | CreateLogGroup, PutLogEvents 等 | ビルドログの出力 |
| S3 | GetObject, PutObject | CodePipeline 連携時のアーティファクト入出力、metadata bucket への候補 metadata 書き込み |
| SSM Parameter Store | PutParameter | current prover image metadata pointer の更新 |
| CodeStar Connections | codestar-connections:UseConnection, GetConnectionToken | 接続方式切り替えに備えた権限(現行 CodeBuild source は GITHUB) |
Image ID との関係
イメージ署名と Image ID は異なるレイヤのセキュリティメカニズムですが、共に「正しいプログラムが実行されたこと」の信頼チェーンを構成します。
flowchart TD
subgraph "ビルド時"
BUILD["コンテナイメージ<br/>ビルド"] --> SIGN["ECR managed signing<br/>(運用設定)"]
BUILD --> IMGID["ARM64 ImageID / methodVersion 抽出"]
IMGID --> META["candidate metadata<br/>(S3 + SSM current)"]
META --> PROMOTE["mapping / Terraform 値へ昇格"]
PROMOTE --> MAP["imageId-mapping.json<br/>と ecs_image_uri に反映"]
end
subgraph "実行時"
VERIFY_SIG["イメージ署名ステータス確認<br/>(Step Functions)"] --> RUN["プローバー実行"]
RUN --> RECEIPT["レシート生成<br/>(Image ID を含む)"]
end
subgraph "検証時"
RECEIPT --> VERIFY_RECEIPT["レシート検証<br/>(verifier-service)"]
MAP --> VERIFY_RECEIPT
VERIFY_RECEIPT --> MATCH{"Image ID<br/>一致?"}
end
SIGN --> VERIFY_SIG
候補 metadata から imageId-mapping.json / ecs_image_uri への昇格手順はビルドと署名に記載しています。
| 検証ポイント | タイミング | 検証主体 | 失敗時の動作 |
|---|---|---|---|
| イメージ署名 | 証明生成前 | Step Functions + Lambda | ECS タスクの起動拒否 |
| Image ID 照合 | 検証時 | verifier-service | 検証失敗の報告 |
Terraform
Terraform で非同期プローバーインフラを宣言的に管理する構成、ワークスペース運用、Amplify 管理領域との連携点を扱う章です。
Terraform は、証明生成パイプラインに関わる AWS リソース(ECS、Step Functions、SQS、S3、ECR、CodeBuild、VPC、Lambda、IAM、CloudWatch、CloudTrail、SSM Parameter)を宣言的に管理します。Amplify Gen 2 管理領域との連携点は本章末の Amplify との連携ポイント を参照してください。
ディレクトリ構成
Terraform の構成ファイルは terraform/ ディレクトリに配置され、機能別に分割されています。
| ファイル | 管理対象 |
|---|---|
backend.tf | S3 ステートバックエンド宣言(実値は別ファイルで注入) |
versions.tf | Terraform / プロバイダーのバージョン制約 |
main.tf | ローカル変数、環境設定、データソース |
variables.tf | 入力変数の定義とバリデーション |
outputs.tf | 他ツール連携用の出力値 |
terraform.tfvars.example | 公開向け sanitized tfvars 例 |
develop.tfvars | develop 用の公開可能な placeholder |
main.tfvars | main 用の公開可能な placeholder |
backend.local.hcl | 実 backend 値(git 管理外、生成ファイル) |
*.local.tfvars | 実 deploy 値(git 管理外、生成ファイル) |
iam.tf | IAM ロール / ポリシー(ECS、Step Functions、CodeBuild) |
ecs.tf | ECS クラスター + Fargate タスク定義 |
step_functions.tf | ステートマシン定義(ASL) |
sqs.tf | ワークキュー + デッドレターキュー |
s3.tf | 証明バンドルバケット、prover image metadata バケット |
ssm.tf | 現行 prover image metadata 候補の SSM Parameter |
ecr.tf | ECR リポジトリ + ライフサイクルポリシー |
codebuild.tf | ビルドプロジェクト(プローバー + ツールチェーン) |
lambda_check_image_signature.tf | .tmp に bundle したイメージ署名検証 Lambda |
lambda/check-image-signature/ | イメージ署名検証 Lambda のソース |
.tmp/check-image-signature/ | pnpm terraform:build-lambdas が生成する Lambda bundle |
principal_guard.tf | Terraform 実行 principal の fail-fast guard |
vpc.tf | VPC + サブネット + インターネットゲートウェイ |
security_groups.tf | ECS タスク用セキュリティグループ |
cloudwatch.tf | ログ群 + 保持期間設定 |
cloudtrail.tf | 監査証跡(main 環境のみ) |
環境分離
ワークスペース戦略
develop と main の 2 環境を、Terraform ワークスペースと git 管理外の *.local.tfvars ファイルの組み合わせで管理します。
flowchart LR
subgraph "Terraform State"
S3["S3 バケット<br/>terraform-state"]
S3 --> DEV["develop<br/>workspace"]
S3 --> MAIN["main<br/>workspace"]
end
subgraph "local tfvars"
DEVF["develop.local.tfvars"]
MAINF["main.local.tfvars"]
end
DEVF --> DEV
MAINF --> MAIN
environment 変数はバリデーションにより develop または main のみが許可されます。環境ごとの差分は locals で定義された設定マップにより解決されます。
| 設定 | develop | main |
|---|---|---|
| S3 ライフサイクル | 7 日 | 30 日 |
| ログ保持期間 | 7 日 | 14 日 |
| CloudTrail | 無効 | 有効(90 日) |
ワークスペースの確認
環境の取り違えを防ぐため、操作前にワークスペースの確認が推奨されます。
ステート管理
S3 バックエンド
Terraform ステートは S3 バケットに保存され、use_lockfile = true による S3 lockfile でステートの同時変更を防止します。tracked の backend.tf は partial backend として backend "s3" {} だけを持ち、bucket や region は backend.local.hcl から terraform init に渡します。
| 項目 | 設定 |
|---|---|
| ステートバケット | <TERRAFORM_STATE_BUCKET>(backend.local.hcl で注入) |
| ステートキー | terraform.tfstate |
| ロック方式 | S3 lockfile (use_lockfile = true) |
| リージョン | ap-northeast-1 など、環境値から生成 |
| 暗号化 | AES256 |
named workspace ごとに state path と lockfile path は分かれますが、同じ backend bucket と root module を共有し、bootstrap・共有リソース・環境別 prover runtime が同居しています。長期運用では state と lifecycle の粒度に課題が残ります。経緯と改善候補は 設計ふりかえり § 7 を参照してください。
認証方式
Terraform の実行は STS AssumeRole を前提とし、現行の標準フローでは terraform-admin assumed role で実行します。aws_account_id は AWS provider の allowed_account_ids に渡され、principal_guard.tf は実行 principal が assumed-role/terraform-admin/* に一致しない場合に fail-fast します。
flowchart LR EXEC["実行環境<br/>(ローカル/CI)"] --> STS["AWS STS<br/>AssumeRole"] STS --> ROLE["Terraform 実行ロール<br/>IAM ロール"] ROLE --> TF["Terraform 実行"]
| 項目 | 設定 |
|---|---|
| 認証方式 | STS AssumeRole |
| IAM ロール | terraform-admin assumed role |
| アカウント guard | aws_account_id + provider allowed_account_ids |
| principal guard | principal_guard.tf が assumed-role/terraform-admin/* を要求 |
| 資格情報の保護方式 | 組織の標準(SSO / aws-vault / Keychain / KMS など) |
| 権限 | 最小権限を原則とする |
plan / apply は scripts/terraform/terraform-guarded.sh 経由で実行します。この wrapper は AWS caller、account ID、Terraform workspace、*.local.tfvars の environment を確認してから Terraform を起動します。
主要な入力変数
Terraform の実行に必要な変数と、そのバリデーションルールの概要です。
必須変数
| 変数 | 説明 | バリデーション |
|---|---|---|
environment | デプロイ環境 | develop または main |
aws_account_id | 実行先 AWS アカウント ID | 12 桁。provider の account guard に使用 |
ecs_image_uri | プローバーイメージ URI | ダイジェスト固定形式(@sha256:<64-hex>) |
finalize_callback_lambda_arn | コールバック Lambda の ARN | 実 ARN を要求(placeholder 不可) |
ecr_signing_profile_arn | AWS Signer プロファイルの ARN | 実 ARN を要求(placeholder 不可) |
codestar_connection_arn | CodeStar Connections ARN(IAM ポリシーで参照) | 実 ARN を要求(placeholder 不可) |
codebuild_source_location | CodeBuild が clone する GitHub repository URL | 実 URL を要求(placeholder 不可) |
*_arn / *_location の各変数は、sanitized placeholder を弾くバリデーションが入っているため、実値の注入が前提です。現行の CodeBuild source は GITHUB タイプ(location 指定)で構成されています。
オプション変数
| 変数 | デフォルト | 説明 |
|---|---|---|
aws_region | ap-northeast-1 | デプロイリージョン |
project_name | stark-ballot-simulator | リソース命名プレフィックス |
ecs_cpu | 16384 | Fargate の CPU ユニット |
ecs_memory | 32768 | Fargate のメモリ(MiB) |
s3_proof_prefix | sessions/ | S3 パスプレフィックス |
s3_cors_allowed_origins | [] | CORS 許可オリジン(空のとき S3 CORS 設定は未作成。標準の local tfvars 生成ヘルパーは非空を要求) |
risc0_toolchain_codebuild_name | stark-ballot-simulator-risc0-toolchain-builder | 共有 toolchain builder のプロジェクト名 |
risc0_toolchain_source_version | refs/heads/main | 共有 toolchain builder の Git ref |
risc0_version | 3.0.5 | RISC Zero の pin |
risc0_commit | 8eb06ab020a92dc5b63ba6dd0836d432aba6d890 | risc0/risc0 の pin commit |
risc0_rust_version | 1.91.1 | host Rust toolchain の pin |
risc0_rust_toolchain_tag | r0.1.91.1 | ARM64 guest toolchain tag |
risc0_toolchain_image_retention_count | 5 | 共有 toolchain ECR の保持イメージ数 |
tracked の develop.tfvars / main.tfvars は値の形を示す placeholder です。実運用では .env.local または shell 環境から pnpm terraform:backend / pnpm terraform:tfvars:develop / pnpm terraform:tfvars:main で backend.local.hcl と *.local.tfvars を生成します。
出力値
Terraform の出力値は、Amplify 環境変数や運用ツールから参照されます。
| 出力 | 対応する値 / 参照元 | 用途 |
|---|---|---|
prover_state_machine_arn | PROVER_STATE_MACHINE_ARN | dispatch-proxy が SFN を起動 |
prover_work_queue_arn | PROVER_WORK_QUEUE_ARN | Amplify backend が SQS event source / IAM に使用 |
prover_work_queue_url | PROVER_WORK_QUEUE_URL | API が SQS にメッセージ送信 |
s3_bucket_name | S3_PROOF_BUCKET | Lambda が S3 にアクセス |
ecr_repository_url | 運用者 / CLI | プローバーイメージの push 先確認 |
risc0_toolchain_repository_url | 運用者 / CLI | 共有 toolchain イメージの push 先確認 |
prover_image_metadata_bucket_name | 運用者 / CodeBuild | prover image metadata の保存先確認 |
prover_current_image_metadata_parameter_name | 運用者 / CodeBuild | 現行 metadata 候補を指す SSM Parameter 確認 |
IAM 設計
最小権限の原則に基づき、各コンポーネントに専用の IAM ロールが割り当てられています。
flowchart TD
subgraph "信頼されるサービス (Service Principal)"
ECSSVC["ecs-tasks.amazonaws.com"]
STATESVC["states.${aws_region}.amazonaws.com"]
CBSVC["codebuild.amazonaws.com"]
LAMSVC["lambda.amazonaws.com"]
end
subgraph "IAM ロール"
ETE["ecs_task_execution"]
ET["ecs_task"]
SFN["step_functions"]
CB["codebuild"]
CBT["codebuild_risc0_toolchain"]
CIS["check_image_signature"]
CTL["cloudtrail_logs<br/>(main only)"]
end
ECSSVC --> ETE
ECSSVC --> ET
STATESVC --> SFN
CBSVC --> CB
CBSVC --> CBT
LAMSVC --> CIS
CTSVC["cloudtrail.amazonaws.com"] --> CTL
| ロール | 信頼サービス | 主要権限 |
|---|---|---|
ecs_task_execution | ecs-tasks | ECR イメージ取得、CloudWatch Logs 書き込み |
ecs_task | ecs-tasks | S3 var.s3_proof_prefix 配下への読み書き(既定: sessions/*) |
step_functions | states.${aws_region}.amazonaws.com | ECS RunTask、Lambda Invoke、ログ、EventBridge managed rule |
codebuild | codebuild | 環境別 prover image の ECR 操作、AWS Signer、metadata S3/SSM、ログ |
codebuild_risc0_toolchain | codebuild | 共有 toolchain image の ECR 操作、AWS Signer、ログ |
check_image_signature | lambda | ECR 署名ステータス照会、ログ |
cloudtrail_logs(main のみ) | cloudtrail | CloudTrail から CloudWatch Logs への書き込み |
スコープの制限
- ECS タスクロールの S3 権限は
var.s3_proof_prefix配下に制限(既定:sessions/*) - Step Functions ロールの ECS 権限は特定クラスター ARN に制限
- Step Functions ロールの
iam:PassRoleは ECS 関連ロールのみに制限 - Step Functions ロールの EventBridge 権限は
ecs:runTask.syncの managed rule 操作用 - CodeBuild ロールの metadata 書き込みは prover image metadata バケット配下と現行 metadata SSM Parameter に制限
Amplify との連携ポイント
Terraform と Amplify は別系統で管理され、以下のポイントで手動同期を含む連携が残っています。
flowchart TB
subgraph TF["Terraform"]
SFN_ARN["Step Functions ARN"]
SQS_ARN["SQS キュー ARN"]
SQS_URL["SQS キュー URL"]
S3_NAME["S3 バケット名"]
CB_INPUT["入力変数<br/>finalize_callback_lambda_arn"]
end
subgraph AMP["Amplify"]
ENV["環境変数"]
CB_ARN["finalize-callback-runner<br/>Lambda ARN"]
end
SFN_ARN --> ENV
SQS_ARN --> ENV
SQS_URL --> ENV
S3_NAME --> ENV
CB_ARN -. "IaC input" .-> CB_INPUT
| 方向 | 情報 | 設定方法 |
|---|---|---|
| Terraform → Amplify | SFN ARN、SQS ARN、SQS URL、S3 バケット名 | Terraform 出力値 → Amplify 環境変数 |
| Amplify → Terraform | callback Lambda ARN | Terraform 入力変数 finalize_callback_lambda_arn |
この双方向の参照により、Amplify が管理する Lambda を Terraform が管理する Step Functions から呼び出します。Terraform 出力 → Amplify 環境変数は手動同期のため、出力を変更した場合は Amplify 側の app-level / branch override の実効値も合わせて確認してください。
バージョン制約
| ツール | バージョン |
|---|---|
| Terraform | >= 1.10.0 |
| AWS プロバイダー | ~> 6.0 |
| Archive プロバイダー | 2.x(terraform/.terraform.lock.hcl で解決) |
API リファレンス
この章では、公開ドキュメントとして扱うべき API(ブラウザクライアントと第三者検証で利用するエンドポイント)を、現行実装ベースで説明します。
この部に含まれる章
- エンドポイント一覧 — 外部クライアント向け API のリクエスト/レスポンス仕様
- セッションライフサイクル — クライアントとサーバーのセッション管理実装
想定読者と前提
- 想定読者: ブラウザクライアントや第三者検証ツールから API を呼び出す実装者
- 前提: HTTP / セッションヘッダーの基本と、本書 全体像 のフローを把握していること
本章で扱わないもの
- 内部運用/デバッグ向け API(
/api/debug/*、/api/finalize/callback)の詳細 - レート制限・Turnstile・capability TTL などの環境変数チューニング
- Amplify / Hono / Lambda 側の認可・ルーティング実装
関連する章
- 検証パイプライン —
/api/verifyが返す検証ペイロードの内訳 - 第三者検証ガイド —
bundle.zipの取得とローカル監査 - 用語集 — capability トークン・session-scoped API の用語定義
エンドポイント一覧
この文書は、外部クライアント向け API のレスポンス形状・要件を現行実装ベースで記載します。session-scoped / capability 保護 API も含むため、無認証公開 API ではありません。
対象外(内部向け)
以下は内部運用/デバッグ用途のため、この文書の詳細対象外です。
GET /api/debug/enablePOST /api/finalize/callback
デュアルランタイム構成
API ハンドラはフレームワーク非依存の共通実装で、Next.js と Hono(Lambda) の両方から利用されます。
| ランタイム | 用途 | 主な入口 |
|---|---|---|
| Next.js Route Handler | ローカル開発 / SSR | src/app/api/**/route.ts |
| Hono on Lambda | AWS デプロイ API | amplify/functions/hono-api/handler.ts |
外部クライアント向け API 一覧
| メソッド | パス | 主用途 | X-Session-ID | X-Session-Capability |
|---|---|---|---|---|
POST | /api/session | セッション作成 | 不要 | 不要 |
POST | /api/vote | 投票送信 | 必須 | 必須 |
GET | /api/progress | ボット投票進捗 | 必須 | 必須 |
POST | /api/finalize | 集計/証明生成 | 必須 | 必須 |
POST | /api/finalize/cancel | 非同期集計キャンセル | 必須 | 必須 |
GET | /api/sessions/:sessionId/status | 非同期集計ステータス | 不要 | 必須 |
GET | /api/verify | 検証ペイロード取得 | 必須 | 必須 |
POST | /api/verification/run | STARK 検証実行 | 必須 | 必須 |
GET | /api/verification/bundles/:sessionId/:executionId | バンドル ZIP 取得 | 不要 | 必須 |
GET | /api/verification/bundles/:sessionId/:executionId/report | 検証レポート取得 | 不要 | 必須 |
GET | /api/bulletin | 掲示板一覧(inspection) | 必須 | 必須 |
GET | /api/bulletin/:voteId/proof | 最小包含証明 | 必須 | 必須 |
GET | /api/bulletin/consistency-proof | 整合性証明(tooling) | 必須 | 必須 |
GET | /api/botdata/:id | ボット投票データ(tooling) | 必須 | 必須 |
GET | /api/bitmap-proof | ビットマップ証明材料 | 必須 | 必須 |
GET | /api/sth | STH スナップショット | 必須 | 必須 |
GET | /api/zkvm-input-hash | zkVM 入力コミットメント | 不要 | 必須 |
共通の実装上の注意
ミドルウェア構成
ミドルウェアは「全リクエスト共通の固定チェーン」ではなく、エンドポイント実装ごとに必要な検証を呼び出す方式です。セッションヘッダーの要否は上記一覧テーブルを参照してください。
| 区分 | 代表エンドポイント | Turnstile | レート制限 |
|---|---|---|---|
| セッション作成 | POST /api/session | 環境設定次第 | セッション作成用 |
| 投票 | POST /api/vote | あり | 投票用 |
| 集計 | POST /api/finalize | あり | zkVM 用 |
| 非同期集計キャンセル | POST /api/finalize/cancel | なし | キャンセル専用 |
| 検証実行 | POST /api/verification/run | なし | zkVM 用 |
| 読み取り(セッションスコープ) | GET /api/progress, GET /api/bulletin*, GET /api/botdata/:id, GET /api/bitmap-proof, GET /api/sth, GET /api/verify | なし | なし |
| 読み取り(path/query で session を指す) | GET /api/sessions/:sessionId/status, GET /api/verification/bundles/..., GET /api/zkvm-input-hash | なし | なし |
共通セッションエラー(ヘッダースコープ)
X-Session-ID / X-Session-Capability をヘッダーで受け取る session-scoped エンドポイントは、session/capability 失敗時に以下の共通エラーを返します。各エンドポイントの「主なエラー」欄ではこれらを省略し、固有エラーのみ記載します。
SESSION_ID_REQUIRED(400)SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)SESSION_NOT_FOUND(404)
パス/クエリで session を指定するエンドポイント(/api/sessions/:sessionId/status, /api/verification/bundles/..., /api/zkvm-input-hash)も capability 検証は共通ですが、session 特定エラーの形式が異なるため、各エンドポイントで個別に記載します。
ボディサイズ制限
共通 JSON パーサーを使うエンドポイントは API_REQUEST_BODY_LIMIT_BYTES(既定 16 KiB)を超えると PAYLOAD_TOO_LARGE (413) を返します。
例外:
POST /api/finalize/cancelは現状request.json()を直接使うため、この共通サイズ制限の対象外です。JSON 不正や payload 不正は handler 固有の400を返します。
エラーレスポンスの形式
多くのエンドポイントは errorResponse(...) を通して以下の形式を返します。
error(エラーコード)message(メッセージ)statusCode(HTTP ステータス)- 必要に応じて
detailsなど
以下のエンドポイントは、session/capability 失敗時は標準形式、handler 内部の個別検証では { error: "..." } 系の独自形式を返します。
POST /api/finalize/cancelGET /api/sessions/:sessionId/statusGET /api/bulletin/consistency-proofGET /api/bitmap-proofGET /api/zkvm-input-hash
現行 response で返さない legacy フィールド
以下は過去の public response に存在したものの、現行の POST /api/finalize(同期)および GET /api/verify のレスポンスには含まれません。旧クライアントや旧ドキュメントが参照している場合は更新してください。
- バンドル/レポート URL 系:
verificationBundleUrl,verificationReportUrl - S3 メタデータ系:
s3BundleUrl,s3BundleKey,s3UploadedAt,s3BundleExpiresAt - カウント互換エイリアス:
missingIndices,invalidIndices,countedIndices,excludedCount
バンドルとレポートの取得は verificationExecutionId を識別子に /api/verification/bundles/:sessionId/:executionId および同 /report を組み立てます。
基本フロー API
POST /api/session
新規セッションを作成します。X-Session-ID は不要です。
レスポンス(200):
data.sessionIddata.electionIddata.electionConfigHashdata.logIddata.contractGenerationdata.capabilityToken
備考:
SESSION_CREATE_TURNSTILE_REQUIRED=1の場合、turnstileTokenが必要です。
POST /api/vote
ユーザー投票を保存し、ボット投票を非同期開始します。
要件:
- ヘッダー:
X-Session-ID必須、X-Session-Capability必須 - ボディ:
commitment,vote,rand(turnstileTokenは開発設定により省略可) - Turnstile 検証あり
- 投票用レート制限あり
レスポンス(200):
data.voteIddata.commitmentdata.bulletinIndexdata.bulletinRootAtCastdata.timestamp
主なエラー:
CAPTCHA_FAILED(403)GLOBAL_LIMIT_EXCEEDED(503)ALREADY_VOTED(400)SESSION_FINALIZED(400)INVALID_REQUEST(400; リクエスト形式不正)INVALID_COMMITMENT(400)DUPLICATE_VOTE(409)PAYLOAD_TOO_LARGE(413)
GET /api/progress
投票進捗を取得します。
レスポンス(200):
data.countdata.totaldata.completeddata.userVoteddata.finalized
主なエラー: 共通セッションエラー(ヘッダースコープ)のみ。
POST /api/finalize
集計と証明生成を開始します。同期/非同期の 2 形態があります。
要件:
- ヘッダー:
X-Session-ID必須、X-Session-Capability必須 - ボディ:
scenarioId(S0-S5),turnstileToken(環境により必須) - Turnstile 検証あり
- zkVM レート制限あり
レスポンス(200, 同期):
data.sessionIddata.tallydata.bulletinRootdata.verifiedTallydata.voteReceiptdata.receiptdata.receiptPublication(保存時)data.imageIddata.userVotedata.missingSlotsdata.invalidPresentedSlotsdata.rejectedRecordsdata.totalExpecteddata.treeSizedata.excludedSlotsdata.sthDigestdata.seenBitmapRoot(条件付き)data.includedBitmapRootdata.inputCommitmentdata.seenIndicesCountdata.journaldata.verificationStatusdata.verificationReport(条件付き)data.verificationExecutionId(条件付き)data.tamperSummary(条件付き)
補足:
- バンドル/レポート取得の識別子は
verificationExecutionIdです。クライアントは自身のsessionIdとverificationExecutionIdから/api/verification/bundles/:sessionId/:executionIdおよび同/reportを構築します。
レスポンス(202, 非同期):
executionIdstatusUrlstatequeue(nullの場合あり)
主なエラー:
CAPTCHA_FAILED(403)INVALID_REQUEST(400)VERIFICATION_FAILED(400; CT proof unavailable)USER_NOT_VOTED(400)VOTING_NOT_COMPLETE(400)SESSION_ALREADY_FINALIZED(400)ZKVM_RATE_LIMIT_EXCEEDED(429)GLOBAL_LIMIT_EXCEEDED(503)PAYLOAD_TOO_LARGE(413)Invalid ImageID(400; 独自形式{ error: "Invalid ImageID", details: { expected, actual } })
POST /api/finalize/cancel
進行中の非同期集計をキャンセルします。
要件:
FINALIZE_ASYNC_MODE=trueのときのみ有効- ヘッダー:
X-Session-ID(x-session-idも受理) - ヘッダー:
X-Session-Capability必須 - ボディ:
executionId必須、reason任意 - キャンセル専用レート制限あり
レスポンス(200):
state
主なエラー:
GLOBAL_LIMIT_EXCEEDED(503)
handler 固有エラー(独自形式):
404: Async finalization disabled400: Invalid JSON body / payload 不正409: 現在状態ではキャンセル不可501: ストアが cancellation 非対応
GET /api/sessions/:sessionId/status
非同期集計の状態を返します。
要件:
- パスパラメータ
sessionId必須 - ヘッダー:
X-Session-Capability必須
レスポンス(200):
sessionIdfinalizationState(nullの場合あり)artifactState(unsupported/corrupt finalized artifact 時のみ)queue(nullの場合あり)progress(条件付き)finalizationResult(nullの場合あり)stepFunctions(nullの場合あり)asyncFinalizationMode(enabled/disabled)
主なエラー:
SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)SESSION_NOT_FOUND(404; 標準形式)
handler 固有エラー(独自形式):
400: Session ID is required
検証 API
GET /api/verify
検証画面向けの統合ペイロードを返します。
要件:
- クエリ:
includeJournal=1(任意)
レスポンス(200):
data.electionIddata.electionConfigHashdata.logIddata.tallydata.bulletinRootdata.scenarioIddata.verificationStatusdata.verificationReport(条件付き)data.verificationSteps/data.verificationChecksdata.imageIddata.tamperDetecteddata.verifiedTallydata.missingSlotsdata.invalidPresentedSlotsdata.rejectedRecordsdata.totalExpecteddata.treeSizedata.excludedSlotsdata.sthDigestdata.seenBitmapRoot(条件付き)data.includedBitmapRootdata.inputCommitmentdata.seenIndicesCount(条件付き)data.journalStatusdata.journal(includeJournal=1のとき)data.voteReceipt(条件付き)data.userVotedata.botVotesSummary(条件付き)data.verificationExecutionId(条件付き)data.tamperSummary(条件付き)
fail-closed 応答:
verificationStatusが許容セット外の場合、取得可能な current-generation finalized session では200の通常datapayload を返し、data.verificationStatusをfailedに正規化します。- unsupported/corrupt finalized artifact の場合は
200で{ error, message, artifactState }を返し、datapayload は含みません。
補足:
- 署名付き URL の再発行はこのエンドポイントでは扱わず、capability 保護された
/api/verification/bundles/...系ルートの責務です。
主なエラー:
SESSION_NOT_FINALIZED(400)USER_NOT_VOTED(400)
POST /api/verification/run
サーバー側で STARK レシート検証を実行します。
要件:
- ボディ: JSON オブジェクト(通常は空オブジェクト
{}) - zkVM レート制限あり
レスポンス(200):
data.verificationStatus(success/failed/dev_mode/not_run/running)data.verificationExecutionIddata.estimatedDurationMsdata.idempotent
挙動:
- 既存結果がある場合や実行中の場合は、再実行せず
idempotent: trueを返します。
主なエラー:
SESSION_NOT_FINALIZED(400)INVALID_REQUEST(400)ZKVM_RATE_LIMIT_EXCEEDED(429)GLOBAL_LIMIT_EXCEEDED(503)PAYLOAD_TOO_LARGE(413)INTERNAL_ERROR(500)
バンドル取得 API
両エンドポイントとも X-Session-Capability ヘッダーによる capability 保護が必須です。
S3 配信条件が揃っている場合(対象 artifact が S3 にアップロード済みで、USE_S3=true または Lambda ランタイム)、302 で短命な presigned URL にリダイレクトします。
GET /api/verification/bundles/:sessionId/:executionId
秘密データを含まない配布対象アーカイブ bundle.zip を返します。
レスポンス:
200: ZIP バイナリ302: S3 presigned URL へリダイレクト
主なエラー:
SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)400: パラメータ不正404: バンドル未検出500: ダウンロード URL 生成失敗 / 読み込み失敗
GET /api/verification/bundles/:sessionId/:executionId/report
検証レポート verification.json を返します。非公開レポートであり、配布対象アーカイブ bundle.zip には含まれません。
レスポンス:
200: JSON302: S3 presigned URL へリダイレクト
主なエラー:
SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)400: パラメータ不正404: レポート未検出500: ダウンロード URL 生成失敗 / 読み込み失敗
掲示板 API
GET /api/bulletin
掲示板一覧を返します。
要件:
- クエリ:
offset,limit(任意)
レスポンス(200):
commitmentsbulletinRoottreeSizetimestamprootHistory(条件付き)nextOffset/hasMore(ページング時)
主なエラー:
INVALID_OFFSET(400)INVALID_LIMIT(400)INVALID_REQUEST(400;details=BULLETIN_STATE_UNAVAILABLE)
GET /api/bulletin/:voteId/proof
最小形式の包含証明を返します。
要件:
- セッションは finalized 必須
- パス:
voteId(UUID v4 形式)
アクセス制御:
- 原則は「そのセッションのユーザー投票」のみ
- 例外としてシナリオ
S3/S4では対象ボット票を許可
レスポンス(200):
voteIdproof.leafIndexproof.merklePathproof.treeSizeproof.bulletinRootAtCast
キャッシュ:
Cache-Control: private, no-storeVary: X-Session-ID, X-Session-Capability
主なエラー:
INVALID_VOTE_ID(400)SESSION_NOT_FINALIZED(400)VOTE_NOT_FOUND(404)VERIFICATION_FAILED(400; CT proof unavailable)
GET /api/bulletin/consistency-proof
RFC6962 整合性証明を返します。
補足:
/verifyの最終判定は内部チェックパイプライン(recorded_consistency_proofを含む)で行うため、この HTTP エンドポイントは検証ツール・点検用途として扱います。
要件:
- クエリ:
oldSize,newSize必須
レスポンス(200):
oldSizenewSizerootAtOldSizerootAtNewSizeproofNodesoldSubtreeHashes/appendSubtreeHashes(条件付き)timestamp
主なエラー(handler 固有、独自形式):
400:oldSize/newSize欠落・不正・範囲外、掲示板未初期化、proof 生成失敗500: consistency proof 生成中の内部エラー
GET /api/botdata/:id
ボット投票(1..63)のデータと証明を返します。
補足:
- 将来の bot verification UI の building block であり、現行
/verifyページの最終判定には参加しません
要件:
- セッション finalized 必須
レスポンス(200):
data.iddata.votedata.randomdata.commitmentdata.voteIddata.timestampdata.proof(leafIndex,merklePath,treeSize,bulletinRootAtCast)
主なエラー:
INVALID_BOT_ID(400)SESSION_NOT_FINALIZED(400)BOT_DATA_NOT_FOUND(404)INTERNAL_ERROR(500; CT proof を組み立てられない場合を含む)
補助 API
GET /api/bitmap-proof
ビットマップ証明の材料を返します。
要件:
- クエリ:
i(0 以上整数)必須 - クエリ:
kind任意(included/seen、省略時はincluded)
備考:
- 全ストア実装で sessionId をキーに保存済み bitmap を参照します。
includedは counted された index、seenは prover に提示された index を対象にします。
レスポンス(200):
leafChunkauditPath
キャッシュ:
ETag対応If-None-Match一致時304Cache-Control: private, max-age=86400, stale-while-revalidate=3600, immutableVary: X-Session-ID, X-Session-Capability
主なエラー(handler 固有、独自形式):
INVALID_INDEX(400)INVALID_BITMAP_KIND(400)BITMAP_NOT_FOUND(404)INTERNAL_ERROR(500)
GET /api/sth
STH スナップショットを返します。
要件:
- finalized セッションのみ
レスポンス(200):
sth.sthDigeststh.bulletinRootsth.treeSizesth.timestampsth.logId
備考:
- 現行実装の
sth.timestampはジャーナル内時刻ではなく、session.lastActivityを返します。
主なエラー:
SESSION_NOT_FINALIZED(404; このエンドポイント固有の扱い)INTERNAL_ERROR(500; 例: finalized 済みだが journal が欠落している場合)
GET /api/zkvm-input-hash
セッション由来の zkVM 入力コミットメントを返します。
要件:
- クエリ:
sessionId必須 - ヘッダー:
X-Session-Capability必須 - クエリ:
includeData任意(true/1/yesで有効) includeData=trueは debug authorization が必要
レスポンス(200):
inputCommitmentdata(includeData有効時のみ)
主なエラー:
SESSION_CAPABILITY_REQUIRED(401)SESSION_CAPABILITY_INVALID(401)SESSION_CAPABILITY_EXPIRED(401)
handler 固有エラー(独自形式):
INVALID_REQUEST(400)SESSION_NOT_FOUND(404)SESSION_NOT_FINALIZED(400)CT_PROOF_UNAVAILABLE(400)INCLUDE_DATA_FORBIDDEN(403)INTERNAL_ERROR(500)
関連する章
- セッションライフサイクル — セッション・capability・finalize の状態遷移
- チェック一覧 — 各 API レスポンスがどの検証チェックに使われるか
- 用語集 —
voteReceipt、bundle.zip、STH、capability などの用語定義
セッションライフサイクル
この文書は、セッション管理の実装を クライアント側とサーバー側に分けて説明します。
1. 管理責務の分離
| 管理面 | 主な保存先 | 主な責務 |
|---|---|---|
| クライアント共有 | localStorage (starkBallotSession, starkBallotSessionSchemaVersion) | 画面遷移フェーズ、クライアント TTL、検証継続状態、UI 復元、スキーマ無効化 |
| クライアントタブ単位 | sessionStorage (starkBallotSessionLock) | タブごとの session identity lock、stale tab の fail-closed |
| サーバー | VoteStore 実装(Mock/File/Amplify) | 投票データ、掲示板、集計結果、検証結果 |
クライアントとサーバーのセッション対応付けには sessionId と X-Session-Capability(署名トークン)が使われます。
ヘッダー / path / query の使い分けはエンドポイント一覧を参照してください。
補足:
ensureClientStorageSchema()はstarkBallotSessionSchemaVersionを確認し、不一致時はstarkBallotSession、stark-ballot-knowledge、starkBallotSessionLockをまとめてクリアします。
2. クライアント側フェーズ
クライアントセッション(src/lib/session/client.ts)のフェーズは以下の 3 つです。
votingfinalizingverifying
ここでの「canonical な finalizeResult」とは、現行契約で受理可能な集計スナップショットを指します。
主な遷移トリガー:
POST /api/session後にgenerateSessionId(sessionId, capabilityToken, contractGeneration)でvoting開始aggregate画面で非同期集計のpending/runningを検知すると identity-scoped helper でphase: 'finalizing'を保存aggregateまたはresult画面で canonical なfinalizeResultを保存できると identity-scoped helper でphase: 'verifying'へ進む/resultから/verifyへ進む時はverificationRequestedAtを保存し、POST /api/verification/runを必要に応じて先行起動する/verifyの継続判定:verificationRequestedAtと canonical なfinalizeResultの両方がそろっていれば継続扱い(hasContinuationAuthority)- 上記がなくても、サーバー返却の STARK 状態が
not_run以外なら進行できる hasContinuationAuthority不成立かつ STARK がnot_runの場合はブロックする
3. クライアント TTL 実装
SESSION_PHASE_TIMEOUTS_MS:
voting: 30 分finalizing: 30 分verifying: 24 時間
TTL 更新の実装ポイント:
generateSessionId(...): 新規作成saveSessionData(...)/saveSessionDataForIdentity(...): フェーズを加味してexpiresAtを再計算updateLastActivity(...)/updateLastActivityForIdentity(...): 現在フェーズでexpiresAtを再延長
期限切れ判定:
checkTimeout()、getSessionData*()、saveSessionData*()、updateLastActivity*()は有効期限超過を検出するとclearSession()を実行
補足:
- 専用の heartbeat API はなく、verify 画面でクライアントが 60 秒間隔で
updateLastActivityForIdentity()を呼びローカル TTL を延長します。 sanitizeSessionData()は非 canonical なfinalizeResultを保存しません。phase: 'verifying'なのに有効なfinalizeResultがない場合はverificationRequestedAtを削除し、phaseをvotingに巻き戻します。
4. サーバー側セッション状態
サーバーは SessionData に以下を保持します。
- 投票(
votes,userVoteIndex,botCount) - 集計状態(
finalizationState) - 集計結果(
finalizationResult) - 最終活動時刻(
lastActivity)
finalizationState.status は以下を取り得ます。
pendingrunningsucceededfailedtimeout
5. サーバー側 TTL / 失効の実装差分
サーバー側の失効挙動はストア実装で異なります。
| ストア | 失効/TTL の実装 |
|---|---|
MockSessionStore | getActiveSessionCount() 呼び出し時に lastActivity から 5 分超を掃除 |
FileMockSessionStore | getActiveSessionCount() 呼び出し時に同様に 5 分超を掃除 |
AmplifySessionStore | TTL 属性を保存。初期は AMPLIFY_DATA_TTL_SECONDS(既定 1800 秒)、実効状態が finalized の保存では AMPLIFY_DATA_VERIFICATION_TTL_SECONDS(既定 86400 秒)へ延長 |
補足:
- Amplify の TTL は保存時点の実効 finalized 状態で決まります。finalized 到達後の
finalizationResult更新は検証 TTL を維持し、finalized 前の queue/running 更新は通常 TTL で保存されます。
重要事項:
/api/sessionはMAX_SESSIONSを参照し、上限到達時はSESSION_LIMIT_EXCEEDEDを返します。
6. セッションヘッダースコープ
エンドポイントごとの X-Session-ID / X-Session-Capability の要否はエンドポイント一覧を参照してください。
POST /api/session のみヘッダー不要で、それ以外の外部クライアント向け API は少なくとも一方が必須です。
7. マルチタブ時の実務上の注意
localStorage は同一オリジンで共有されますが、現行実装は sessionStorage の tab lock を併用し、別タブがセッションを差し替えたら stale tab を fail-closed にします。
代表的な結果:
- 片方のタブで投票済み後、別タブで再投票すると
ALREADY_VOTED - 片方のタブで集計完了後、別タブで再集計すると
SESSION_ALREADY_FINALIZED - 別タブでセッションが差し替えられた場合、aggregate / result / verify / bot progress を開いている stale tab は進行を停止する
- セッション作成を並行すると
starkBallotSession自体は共有更新されますが、先に開いていたタブはstarkBallotSessionLockと不一致になり継続利用できません
第三者検証ガイド
公開 snapshot に関する注意
bundle.zipの展開とjournal.json完全性チェック(Step 3, 6)はダウンロード済み ZIP のみで実行できます。verifier-serviceビルド・imageId-mapping.json参照・公開アーティファクト整合・inputCommitment再計算(Step 2, 4, 7, 8)は、検証対象リリースと対応する公開 repository snapshot が必要です。
この章は、検証ページでダウンロードした bundle.zip を使って、第三者がローカルで行える配布対象アーカイブ単体の最小監査手順をまとめたものです。/verify 画面の最終判定の完全再現ではなく、下表の不変条件を確認することがゴールです。
bundle.zip 単体では揃わないもの(上の callout は手順実行に必要な前提、ここは検証材料そのものとして ZIP に入らないものです):
/api/verifyが返す claimed tally とverificationChecks/verificationSteps- 投票者端末に残る投票意図・乱数・投票レシート
- 掲示板の包含証明 / 整合性証明
- 自票 inclusion 用のビットマップ証明
- 有効化されている場合の第三者 STH ソース照合
これは PoC の設計意図です(配布対象アーカイブ の構成も参照)。
この部に含まれる章
- ZIP ローカル検証(Ubuntu) —
bundle.zipを取得した第三者が Ubuntu 上で実行できる最小監査手順
想定読者と前提
- 想定読者: 配布された
bundle.zipを独立にローカル監査したい第三者 - 前提: Ubuntu 系 Linux と
jq/unzipなどの基本 CLI、対応する公開リポジトリ snapshot へのアクセス。詳細は はじめに を参照
本章で扱わないもの
/verifyUI が表示する最終判定の完全再現(包含証明・整合性証明・第三者 STH 照合などはサーバー側でのみ評価される)- 投票者端末のローカル証跡(投票意図・乱数・投票レシート)を使った Cast-as-Intended 検証
- AWS インフラのデプロイ・運用手順
- 上で扱わない検証材料の一覧は冒頭の「
bundle.zip単体では揃わないもの」も参照
関連する章
この章は bundle.zip のローカル監査に絞ります。範囲外の作業は次のページを参照してください。
- チェック一覧 — チェック ID と判定ロジック
- API エンドポイント一覧 — 手動検証に使う API 契約
- 検出メカニズム — 改ざんシナリオごとの失敗パターン
- 非同期プローバー — 非同期 finalize の処理と障害調査導線
- バンドル構造 — 配布対象アーカイブの公開可能・非公開アーティファクト
- 用語集 — 「検証」と「監査」の使い分けほか
最低限確認する不変条件
| 項目 | 合格条件 |
|---|---|
| STARK レシート | verifier-service verify が status: "success" |
| 投票の除外有無 | excludedSlots == 0 かつ missingSlots == 0 かつ invalidPresentedSlots == 0 |
| 期待投票数整合 | totalExpected == treeSize |
| 集計合計整合 | journal.json の verifiedTally の合計が validVotes と一致 |
| 公開入力の基本整合性 | public-input.json が現行 contract に沿い、入力数・root・重複検査が成立 |
| 公開監査アーティファクト | election-manifest.json と close-statement.json の自己整合・相互整合が成立 |
| 入力整合性 | inputCommitment の再計算値が journal.json と一致 |
ZIP ローカル検証(Ubuntu)
この手順は、検証ページでダウンロードした bundle.zip を対象に、Ubuntu 上で第三者が行える最小監査のガイドです。
0. 前提
- 検証ページから
bundle.zipをダウンロード済みであること - Ubuntu 22.04 / 24.04
- このリポジトリ(
stark-ballot-simulator)のソースを取得済みであること - Node.js 24 と Corepack 経由の pnpm 11.x が利用可能で、
$REPO_ROOTでcorepack enableとpnpm install --frozen-lockfileを実行済みであること(Step 7–8 のみ必要)
手順の前提(ソース取得やビルドが必要なステップ)は、リポジトリが公開されるまで実行できません。詳細は 第三者検証ガイド を参照してください。
ここで扱う public は「秘密データを含まない配布対象」を指し、無認証取得を意味しません。取得経路(capability エンドポイントと短命な presigned URL)と、現行レスポンスに含まれない旧 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
生成物:
verifier-service/target/release/verifier-service
3. bundle.zip を展開
mkdir -p "$AUDIT_ROOT"
cp ~/Downloads/stark-ballot-verification-*.zip "$AUDIT_ROOT/bundle.zip"
cd "$AUDIT_ROOT"
unzip -o bundle.zip -d bundle
ls -1 bundle
最低限、以下のファイルが必要です。
bundle/receipt.jsonbundle/journal.jsonbundle/public-input.jsonbundle/election-manifest.jsonbundle/close-statement.json
metadata.json は同期モードでのみ含まれる場合があります。
4. 期待 Image ID を決定
Step 4 では receipt.json の image_id が public/imageId-mapping.json のどの variant に該当するかを判定し、verifier-service に渡す Image ID を決めます。アプリ側と同様に methodVersion が CURRENT_METHOD_VERSION と一致しない場合は fail-closed で停止します(アプリ内では EXPECTED_IMAGE_ID または EXPECTED_IMAGE_ID_VARIANT=default|x86_64 で variant を選択します)。
METHOD_VERSION="$(jq -r '.methodVersion' bundle/journal.json)"
CURRENT_METHOD_VERSION="$(awk -F'= ' '/export const CURRENT_METHOD_VERSION/ {print $2; exit}' "$REPO_ROOT/src/lib/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
RECEIPT_IMAGE_ID="$(jq -r '.image_id // .imageId // .receipt.image_id // .receipt.imageId // empty' bundle/receipt.json | tr '[:upper:]' '[:lower:]')"
ARM_IMAGE_ID="$(jq -r --arg v "$METHOD_VERSION" '.mappings[$v].expectedImageID // empty' "$REPO_ROOT/public/imageId-mapping.json" | tr '[:upper:]' '[:lower:]')"
X86_IMAGE_ID="$(jq -r --arg v "$METHOD_VERSION" '.mappings[$v].expectedImageID_x86_64 // empty' "$REPO_ROOT/public/imageId-mapping.json" | tr '[:upper:]' '[:lower:]')"
case "$RECEIPT_IMAGE_ID" in
"$ARM_IMAGE_ID")
EXPECTED_IMAGE_ID="$ARM_IMAGE_ID"
;;
"$X86_IMAGE_ID")
EXPECTED_IMAGE_ID="$X86_IMAGE_ID"
;;
"")
echo "receipt_image_id is missing; choose the expected Image ID manually"
exit 1
;;
*)
echo "receipt_image_id is not present in imageId-mapping.json for methodVersion=$METHOD_VERSION"
exit 1
;;
esac
echo "methodVersion=$METHOD_VERSION"
echo "receiptImageId=$RECEIPT_IMAGE_ID"
echo "expectedImageId=$EXPECTED_IMAGE_ID"
通常の本番 bundle では expectedImageID(ARM64)が選ばれ、ローカル x86_64 で生成した receipt では expectedImageID_x86_64 が選ばれます。
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": フェイクレシート(本番検証としては不合格)exit_code=3またはstatus="failed": 不合格
6. journal.json の完全性チェック
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 '(.verifiedTally | add) == .validVotes' bundle/journal.json >/dev/null \
&& echo 'tally_sum=ok' \
|| echo 'tally_sum=ng'
excludedSlots > 0 または missingSlots > 0 または invalidPresentedSlots > 0 は、検証失敗として扱います。加えて totalExpected != treeSize も、現行の必須チェックでは検証失敗です。
7. 公開監査アーティファクトの整合性チェック
public-input.json、election-manifest.json、close-statement.json は bundle.zip に含まれる Counted 段階の必須チェック対象です。次の 4 点を確認します(フィールド単位の詳細はスクリプト内の checks 参照)。
public-input.jsonが現行 contract に沿い、vote entry / 重複 index / commitment /journal.json各フィールドと矛盾しないelection-manifest.jsonのelectionConfigHash再計算値が宣言値・public-input.json/journal.jsonと一致するclose-statement.jsonのsthDigest再計算値が宣言値・public-input.json/journal.jsonと一致するjournal.jsonとpublic-input.jsonのmethodVersionが現行 contract と一致する
cd "$REPO_ROOT"
pnpm tsx -e "
import fs from 'node:fs';
import { buildCloseStatement, recomputeElectionManifestHash } from './src/lib/verification/public-audit-artifacts';
import { parsePublicInputArtifact } from './src/lib/verification/public-input-contract';
import { CURRENT_METHOD_VERSION } from './src/lib/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_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: Counted 段階の input sanity / unique index・commitment / election-manifest / close-statement 整合チェック、または public input authority の整合性失敗(チェック ID の対応は チェック一覧 参照)
8. inputCommitment 再計算
public-input.json から再計算した値が journal.json の inputCommitment と一致することを確認します。Step 0 の Node.js / pnpm 前提を満たしてから実行してください。
RECALC="$(cd "$REPO_ROOT" && pnpm tsx -e "import fs from 'node:fs'; import { computeInputCommitmentFromPublicInput } from './src/lib/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 4 で
EXPECTED_IMAGE_IDを決定できる - Step 5–8 の判定がすべて緑
いずれかが失敗した場合、Counted / STARK 段階の必須チェックを満たしていないため Verified にはなりません。範囲外や bundle.zip 単体では揃わない検証材料は 第三者検証ガイド を参照してください。
設計判断
PoC として何を割り切り、構築を通じて何を学んだかを記録する部です。
「何を PoC 都合で割り切ったか」「構築を通じて何がわかったか」を明文化し、実装・運用・監査の前提を共有します。
判断の記録方針
各章は目的に応じて、記録軸を使い分けます。
| 章 | 記録軸 |
|---|---|
| PoC の意図的な制約 | 制約の内容 / 受け入れた理由 / 影響範囲 |
| 設計ふりかえり | 背景 / 知見 / 改善候補 |
2 章の役割分担:
- PoC の意図的な制約 — 「今は意図的に変えない割り切り」を記録します。固定票数 64、ビットマップチャンク漏洩、非 GPU 前提のように、PoC スコープ内で受け入れた制約が対象です。
- 設計ふりかえり — 「構築を通じて見えた構造課題(カテゴリ A)」と「制約が見えた後に整理し直した設計判断(カテゴリ B)」の 2 カテゴリで記録します。改善候補は確定した移行計画ではなく、次に検討しうる選択肢として扱います。カテゴリの定義は 設計ふりかえり に集約しています。
この部に含まれる章
- PoC の意図的な制約 — 公開版で明示する 3 つの制約
- 設計ふりかえり — 構築を通じて得た構造上の知見
想定読者と前提
本章で扱わないもの
- 一般的な投票システムの脅威モデル比較
- 本番運用に向けた具体的な実装ロードマップ
- 採用判断のためのコスト試算や ROI 評価
関連する章
- 暗号プロトコル — 各プリミティブの仕様と安全性
- AWS アーキテクチャ — インフラ構成の詳細
- 検証パイプライン — 検証ロジックとゲーティング
- 参考文献 — 設計背景の一次資料一覧
PoC の意図的な制約
本章では、公開版 PoC で意図的に受け入れている制約を 3 点に絞って説明します。
「バグではなく設計上の割り切り」であることを明確化します。
制約の全体像
| カテゴリ | 制約 |
|---|---|
| スケーラビリティ | 固定票数 64(1 ユーザー + 63 ボット) |
| プライバシー | ビットマップチャンク漏洩 |
| 実行基盤 | 非 GPU 前提の証明実行(ECS Fargate) |
1. 固定票数 64(1 ユーザー + 63 ボット)
| 項目 | 内容 |
|---|---|
| 制約の内容 | BOT_COUNT=63、MERKLE_TREE_DEPTH=6(2^6 = 64 リーフ)で固定 |
| 受け入れた理由 | 単一ユーザーでも掲示板・Merkle・zkVM・検証 UI までの E2E フローを再現できる最小構成 |
| 影響範囲 | 入力サイズ、証明時間、ツリー深度、検証表示 |
PoC の主目的は「検証可能投票の成立条件を end-to-end で示すこと」であり、まずは 64 票固定でパイプライン全体の再現性を優先しています。
2. ビットマップチャンク漏洩
| 項目 | 内容 |
|---|---|
| 制約の内容 | ビットマップ Merkle で 32 バイト(256 ビット)チャンク全体を返すため、対象ビット以外の counted 状態も同時に見える |
| 受け入れた理由 | 64 票が 1 チャンクに収まり、63 票がボットのため情報価値が限定的 |
| 漏洩する情報 | 現行の 64 票構成では、全 64 票が集計に含まれたか否か |
本来はチャンクを小さくし、木を深くすることで漏洩を抑制できます。PoC では zkVM 実行時間とのトレードオフを優先し、現在のチャンク設計を採用しています。
詳細: ビットマップ Merkle
3. 非 GPU 前提の証明実行(ECS Fargate)
| 項目 | 内容 |
|---|---|
| 制約の内容 | STARK 証明生成を CPU 前提で実行(ECS Fargate 16 vCPU / 32 GB) |
| 受け入れた理由 | GPU インスタンスは高価かつ起動に時間がかかるため、非同期キュー + Fargate が現実的だった |
| 影響範囲 | 公開 AWS 構成では FINALIZE_ASYNC_MODE=true を前提に POST /api/finalize が非同期化され、証明完了まで数分待機が発生 |
コードベース自体は同期パスも保持しており、FINALIZE_ASYNC_MODE=false のローカル・テスト構成では同期レスポンスを返せます。
詳細: AWS アーキテクチャ、非同期プローバー
設計ふりかえり
本章は、実装・運用を経て見えた構造課題と、制約が見えた後に整理し直した設計判断を記録します。
記録方針
各項目は背景 / 知見 / 改善候補の 3 軸で記述します。改善候補は確定計画ではなく、ふりかえりを通じて見えた検討中の選択肢です。
項目はカテゴリ A / B に分けて並べます。
| カテゴリ | 意味 |
|---|---|
| A | 実装を通じて今も残る構造的課題 |
| B | 制約が見えた後に整理し直した設計判断 |
知見の全体像
| # | 項目 | カテゴリ |
|---|---|---|
| 1 | Store インターフェースの肥大化 | A. 永続化層 |
| 2 | SessionData の責務混在 | A. 型設計 |
| 3 | 設定の組み合わせ爆発 | A. 構成管理 |
| 4 | Verified 判定ロジックの分散 | A. 検証パイプライン |
| 5 | 検証ドメインへの I/O 混入 | A. アーキテクチャ境界 |
| 6 | Amplify 単独構成からハイブリッド構成への移行 | B. クラウド境界 |
| 7 | Terraform root module と lifecycle の分離不足 | B. IaC 運用 |
| 8 | proof-bound authority と表示用 cache の分離 | B. 型・データ権威 |
| 9 | 公開可能 artifact の allowlist 化 | B. セキュリティ境界 |
A. 実装を通じて残る構造的課題
1. Store インターフェースの肥大化
背景
VoteStore インターフェース(src/types/voteStore.ts)は 19 メソッド(うち optional 3)を持ち、3 つの大きな実装(Mock / FileMock / Amplify)が存在します。
知見
セッション管理・投票操作・ファイナライズ状態遷移・成果物保存の 4 責務が 1 インターフェースに混在しています。特にファイナライズ状態遷移の 5 メソッド(markFinalizationQueued 〜 markFinalizationTimedOut)は全実装で類似のバリデーションロジックを個別に持ちます。
改善候補
インターフェース分離原則(ISP)を適用し、Session / Vote / FinalizationState / Artifact の 4 インターフェースに分ける案が考えられます。さらに「永続化 I/O」と「状態遷移ポリシー」を分離し、ファイナライズ状態遷移を共通のステートマシンとして抽出できれば、各 Store は永続化に集中しやすくなります。
詳細: セッションライフサイクル
2. SessionData の責務混在
背景
SessionData 型(src/types/server.ts)は 20 フィールド(うち 14 が optional)を持ちます。ネストされた finalizationResult だけで 30 サブフィールドを含みます。
知見
永続データ(sessionId、electionId)、実行時状態(votes、bulletin、lastActivity)、検証結果(finalizationResult、finalizationState)が 1 型に同居しています。optional フィールドが 14 ある事実は、「ライフサイクルの各段階で存在するはずだが、型では保証されないフィールド」が多いことを意味します。
改善候補
Session(最小の identity)、VoteLog(append-only の投票記録)、FinalizationJob(非同期ジョブの状態)、VerificationArtifact(検証結果と成果物)に型を分ける案が考えられます。各段階で必要なフィールドを required として扱える構造に寄せることで、ライフサイクル上の前提を型で表しやすくなります。
詳細: セッションライフサイクル
3. 設定の組み合わせ爆発
背景
.env.local.example に 57 変数が定義されており、USE_MOCK_ZKVM、RISC0_DEV_MODE、FINALIZE_ASYNC_MODE などの切り替えフラグが独立して存在します。
知見
問題の本質は変数の数ではなく「プロファイル化されていない構成契約」です。Amplify / Terraform / Hono / S3 / zkVM の境界をまたいで個別フラグが増え、フラグの組み合わせによって意図が曖昧になる状態が生まれます(例: USE_MOCK_ZKVM=true と RISC0_DEV_MODE=1 の同時有効)。zkvm-mode.ts で production 時の不正モードは検出できますが、起動時に全組み合わせを網羅的に検証していません。
改善候補
プロファイルベースの設定体系(local / dev / staging / prod)を導入し、個別フラグを段階的に減らす案が考えられます。プロファイルが各フラグの値を決め、シークレットのみを環境変数として残す形に寄せられれば、起動時バリデーションで無効な組み合わせを fail-fast に検出しやすくなります。
4. Verified 判定ロジックの分散
背景
「Verified を表示してよいか」の判定ロジックが複数箇所に分散しています。主系は verification-summary.ts の deriveVerificationSummary(チェック群から総合判定)と page.tsx の overallStatusOverride(UI 表示制御)で、補助系として consistency-verifier.ts の validateVotingIntegrity がフォールバック経路を持ちます。さらに verify.ts ハンドラにも verificationStatus の許可ステータス判定が存在します。
知見
判定基準の責務境界が明確ではなく、チェック評価(engine)、総合判定(summary)、最終表示 override(UI)、API レスポンス、第三者検証、bundle 監査が同じ判定根拠を別々に参照しています。フォールバック経路(integrityStatus)も残るため、仕様の二重管理が起きやすい状態です。
改善候補
VerificationPolicy のような単一モジュールに「Verified を出す条件」を集約する案が考えられます。API、UI、CLI、第三者検証、bundle 監査が同じ判定根拠を参照できれば、判定基準の分散を減らせます。
詳細: ゲーティングロジック
5. 検証ドメインへの I/O 混入
背景
consistency-verifier.ts は検証ドメインロジックを担いますが、内部で fetch() を直接呼び出しています(整合性証明の取得および STH の第三者検証)。
知見
ドメインロジックが HTTP 可用性に依存し、テストではモックが必須になります。また、fetch 失敗時のエラー伝播が暗黙的で、呼び出し元の useVerificationPipeline は例外を捕捉して状態を null にするのみです。本プロジェクトの方針「ドメイン層は Result パターン、境界でのみ例外捕捉」に反します。
改善候補
検証関数を純関数に寄せ、必要なデータ(整合性証明、STH レスポンス等)を全て引数で受け取る構造が候補になります。I/O をアプリケーション層(hooks または handler)の adapter に移せれば、ドメイン層を HTTP 非依存かつ fixture テストしやすい状態にできます。エラーも Result 型で明示伝播させる余地があります。
詳細: 検証パイプライン
B. 後から見えた設計判断
6. Amplify 単独構成からハイブリッド構成への移行
背景
当初は Amplify Gen 2 のみで Web、API、データ、認証、証明生成まわりまで完結できると想定していました。しかし STARK 証明生成は 16 vCPU / 32 GB で約 6 分を要し、Lambda の 60 秒タイムアウトには載りません。実装が進むにつれて ECS Fargate、Step Functions、SQS、ECR、イメージ署名、S3 artifact 配布を組み合わせた非同期実行基盤が必要になり、結果として Terraform 管理の領域を後追いで切り出しました。
知見
境界を後から切ったことで、Terraform → Amplify への環境変数同期(SFN ARN、SQS ARN、SQS URL、S3 bucket 名)と、Amplify → Terraform への callback Lambda ARN 注入という双方向の手動同期契約が残りました。動作はしますが、デプロイ順序、設定ドリフト、Amplify branch override の実効値確認といった運用負荷を生みます。
改善候補
今後この構成を発展させるなら、Amplify と Terraform の境界を整理するだけでなく、Amplify 依存を段階的に下げ、最終的には Web / API / データ / 非同期プローバー基盤を一貫した IaC とデプロイフローで管理する案が考えられます。
- 短期的には Terraform output と Amplify environment の対応表を contract として生成可能にする
- 必須 ARN や bucket 名が欠落した場合に deploy 時点で検出できるようにする
- 手動同期が残る値を runbook と CI check の確認対象にする
- 中長期的には Amplify 管理領域を別の IaC / hosting / API 実行基盤へ移す選択肢を検討する
- SSM Parameter Store などを介した参照に寄せ、コピー & ペーストの同期点を減らす
cross-ref: 現行構成とサービス一覧
7. Terraform root module と lifecycle の分離不足
背景
現行構成では、develop と main を Terraform workspace と git 管理外の *.local.tfvars の組み合わせで分離しています。S3 remote backend は named workspace ごとに state path と lockfile path が分かれますが、同じ backend bucket と root module を共有しているため、長期運用には粒度不足が残ります。
一方で、bootstrap(state bucket・共通 IAM)、共有リソース(RISC Zero toolchain ECR、共有 CodeBuild、署名プロファイル)、環境別 prover runtime(ECS / Step Functions / SQS / S3 / CloudWatch)が同じ root module 内に同居しています。
知見
PoC としては workspace 分離と terraform-guarded.sh の principal guard で取り違えは抑止できましたが、長期運用には粒度不足です。root module を共有してしまうと、共有リソースの変更が環境別 plan/apply の影響範囲に紛れ込み、ライフサイクル・アクセス制御・レビュー粒度を分けるのが難しくなります。
改善候補
長期運用を前提にするなら、Terraform を以下のような 3 階層に分ける案が考えられます。
terraform/
bootstrap/ # state bucket、lock、共通 IAM
shared/ # toolchain ECR、共有 CodeBuild、signing profile
envs/develop/ # develop の prover runtime(ECS / SFN / SQS / S3 / CloudWatch)
envs/main/ # main の prover runtime
このように分けられれば、環境ごとの plan/apply の影響範囲を小さくし、main と develop のアクセス制御、レビュー粒度、ロールバック判断を分離しやすくなります。
cross-ref: Terraform
8. proof-bound authority と表示用 cache の分離
背景
検証パイプラインで扱う情報には、journal、public-input.json、election-manifest.json、close-statement.json のように証明に束縛された権威データと、UI 表示用の tally や tamperDetected のように派生値として組み立てた表示 cache が混在します。
知見
これらを同列に扱うと「画面では正しそうに見えるが、検証側では根拠が示せない」状態を作りやすくなります。データの権威性は型レベルで区別すべきで、UI が表示 cache を消費する経路と、検証エンジンが authority を消費する経路は別であるべきです。
改善候補
authority と presentation cache を別の型として扱う案が考えられます。UI → authority への参照を一方向に保ち、表示用派生値を authority から純関数で導出した結果として扱えれば、画面状態のために authority を書き換えるリスクを減らせます。
cross-ref: 4 段階検証モデル, 入力コミットメント
9. 公開可能 artifact の allowlist 化
背景
検証用に配布する bundle.zip には、receipt.json、journal.json、public-input.json、election-manifest.json、close-statement.json を含めます。一方、input.json(証拠)、verification.json(検証レポート)、included-bitmap.json、seen-bitmap.json は公開対象外です。
知見
セキュリティ設計として重要なのは「秘密を隠すこと」だけではなく、「第三者検証に必要な最小限を allowlist として明示的に公開する」設計です。non-public artifact を都度判断するブロックリスト方式では、新しい artifact を追加した時に取りこぼしが起きます。
改善候補
bundle.zip の内容は明示的な allowlist として 1 箇所で管理するのが望ましい方向です。生成側(sync の verification-bundle.ts、async の docker/entrypoint.sh)と配信側(verificationBundles.ts)の 3 点を契約として照合できれば、新しい artifact を追加した時の取りこぼしを減らせます。詳細仕様と allowlist は バンドル構造 に集約しています。
用語集
本書で使用する主要な用語の定義です。暗号・検証の基礎用語と実装・運用の主要用語に分けて掲載しています。
暗号プリミティブ
コミットメント(総称)
本書で「コミットメント」と書いた場合、文脈に応じて次のいずれかを指します。両者は対象とドメイン分離タグが異なるため、明示が必要な箇所では下位用語(投票コミットメント / 入力コミットメント)を使います。
| 用語 | 対象 | ドメイン分離タグ |
|---|---|---|
| 投票コミットメント | 個々の投票(選挙 ID・選択肢・乱数)の束縛 | stark-ballot:commit|v1.0 |
| 入力コミットメント | zkVM の公開可能入力(掲示板状態と投票一覧)の束縛 | stark-ballot:input|v1.0 |
投票コミットメント(Vote Commitment)
ドメイン分離タグ、選挙 ID、投票選択肢、乱数を結合して SHA-256 でハッシュした値。投票内容を秘匿しつつ(隠蔽性)、後から変更できないことを保証する(束縛性)。投票の Cast-as-Intended 検証の起点となる。
詳細: コミットメントスキーム 本文での使用: ゲストプログラム、4 段階検証モデル、チェック一覧
Merkle ルート(Bulletin Root)
掲示板上の全投票コミットメントから RFC 6962 の規則に従って計算されるハッシュ値。掲示板の特定時点における状態を一意に表現する。新しい投票が追加されるたびに更新される。
Merkle パス(Audit Path)
特定のリーフ(投票コミットメント)からルートまでを再構成するために必要な兄弟ノードのハッシュ列。包含証明の構成要素であり、対数オーダーの検証コストを実現する。
包含証明(Inclusion Proof)
特定の投票コミットメントが掲示板に含まれていることを暗号学的に証明するデータ。リーフインデックス、監査パス、ツリーサイズから構成される。RFC 6962 のハッシュ規則に従い、リーフとパスからルートを再計算して期待値と照合する。
詳細: CT Merkle ツリー 本文での使用: 4 段階検証モデル、ゲストプログラム
整合性証明(Consistency Proof)
RFC 6962 で定義された、2 つの時点のツリーが追記関係にあることを暗号学的に証明するデータ。古いツリーが新しいツリーのプレフィックスであること(投票の削除・並べ替えが行われていないこと)を保証する。
詳細: CT Merkle ツリー 本文での使用: 4 段階検証モデル、チェック一覧
入力コミットメント(Input Commitment)
zkVM が処理した公開可能な入力フィールドの一部を、固定のドメインタグと version を含む正準エンコーディングで SHA-256 ハッシュした値。現行実装では electionId、bulletinRoot、treeSize、totalExpected、votesCount、各投票の index・コミットメント・Merkle パスを束縛し、public-input.json より狭い部分集合を対象とする。
詳細: 入力コミットメント 本文での使用: ゲストプログラム、4 段階検証モデル、チェック一覧
STH ダイジェスト(Signed Tree Head Digest)
掲示板のログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを結合して SHA-256 でハッシュした値。特定の時点における掲示板の状態を一意に識別し、複数の独立した監視者間で掲示板の一貫性を検証するために使用する。
詳細: STH ダイジェスト 本文での使用: 4 段階検証モデル、ゲストプログラム、チェック一覧
包含ビットマップルート(Included Bitmap Root)
zkVM ゲストが生成するビットマップ(各投票インデックスが集計に含まれたか否か)の Merkle ルート。投票者は自分のインデックスに対応するビットが 1 であることを Merkle 証明で確認できる。
詳細: ビットマップ Merkle
提示ビットマップルート(Seen Bitmap Root)
zkVM ゲストに提示された投票インデックスを表すビットマップの Merkle ルート。includedBitmapRoot と組み合わせることで、自票が「counted された」「提示されたが無効だった」「そもそも prover に提示されなかった」のどれかを説明できる。
詳細: ビットマップ Merkle
正準エンコーディング(Canonical Encoding)
固定のドメインタグ・バージョン番号・フィールド順を含む決定論的なバイト列表現。同一の入力から常に同一のバイト列が得られることを保証する。本システムではコミットメントと入力コミットメントの計算に使用する。
ドメイン分離タグ(Domain Separation Tag)
ハッシュ計算において異なる用途のデータが衝突しないように付与するプレフィックス文字列。本システムでは、コミットメント、入力コミットメント、CT Merkle のリーフ・ノードハッシュにそれぞれ固有のタグを使用する。
投票レシート(Vote Receipt)
投票受理時にサーバーが返す応答データ。voteId、commitment、bulletinIndex、bulletinRootAtCast を含む。検証では voteReceipt として参照される。投票者がローカルに保持する投票時データ(選挙 ID、選択肢、乱数)とは別物であり、zkVM が生成する STARK レシート(Receipt)とも別物。Cast-as-Intended 検証では、投票時データからコミットメントを再計算し、投票レシートのコミットメント値と照合する。
詳細: コミットメントスキーム、4 段階検証モデル 本文での使用: チェック一覧、セッションライフサイクル
STARK 証明
STARK(Scalable Transparent ARgument of Knowledge)
Trusted setup(信頼されたセットアップ)を必要としない暗号証明方式。ハッシュベースの構成により耐量子計算機性に優位がある。本システムでは RISC Zero zkVM によって投票集計の正当性を証明するために使用する。
RISC Zero zkVM
RISC-V アーキテクチャ上で通常の Rust コードを実行し、その実行が正しく行われたことの STARK 証明を生成するゼロ知識仮想マシン。ゲストプログラムとホストプログラムから構成される。
レシート(Receipt)
zkVM が生成する暗号証明オブジェクト。内部に Seal(STARK 証明本体)とジャーナル(公開出力)を含む。Receipt::verify(image_id) によって、特定のゲストプログラムが正しく実行されたことを第三者が検証できる。
ジャーナル(Journal)
zkVM ゲストプログラムの公開出力。現行契約は methodVersion=14 で、検証済み集計結果、missingSlots / invalidPresentedSlots / excludedSlots、inputCommitment、includedBitmapRoot、seenBitmapRoot などを含む。レシートに暗号学的に束縛されており、改ざんできない。
Image ID
コンパイル済みのゲストプログラムバイナリを一意に識別するハッシュ値。レシート検証時に期待される Image ID と照合することで、意図したゲストプログラムによって生成された証明であることを確認する。プローバーイメージの更新時に同期して更新が必要。
詳細: Image ID 本文での使用: チェック一覧、検証サービス、イメージ署名
ゲストプログラム(Guest Program)
zkVM 内部で実行される Rust プログラム。投票データの検証と集計を行い、結果をジャーナルとして出力する。ゲストの実行内容は STARK 証明によって保証される。
ホストプログラム(Host Program)
zkVM の外部で動作し、ゲストプログラムの実行と証明生成を制御する Rust プログラム。入力データの読み込み、zkVM の起動、レシートとジャーナルの出力を担う。
検証サービス(Verifier Service)
Rust で実装された独立した STARK レシート検証プログラム。Receipt::verify(expected_image_id) を実行し、レシートの暗号学的正当性を確認する。結果は verification.json として保存される。
詳細: 検証サービス
フェイクレシート(Fake Receipt)
RISC0_DEV_MODE=1 で生成される暗号学的保証のないレシート。開発効率のためのモックであり、検証サービスは InnerReceipt::Fake を自動検出して dev_mode ステータスを返す。本番環境では使用してはならない。
ジャーナル契約(Journal Contract)
methodVersion で識別されるジャーナル出力構造の仕様。ゲストプログラムが出力するフィールドの集合と意味を定義する。現行契約は methodVersion=14(v1.4)。ゲストプログラムの変更は新しい Image ID の生成を伴い、検証時には期待 Image ID との一致が確認される。
レシートラッパー JSON
ホストバイナリが出力する { "receipt": ..., "image_id": "0x..." } 形式のラッパー JSON。STARK レシート本体を image_id と一緒に運ぶための受け渡し形式で、検証サービスはこの形式を読み込んで Receipt::verify(expected_image_id) を実行する。配布対象アーカイブ内のファイル名は receipt.json。本書では「レシート」「STARK レシート」「receipt.json」「レシートラッパー JSON」を次のように使い分ける:
| 表記 | 指すもの |
|---|---|
投票レシート(voteReceipt) | サーバーが投票受理時に返す応答データ |
| STARK レシート(Receipt) | zkVM が生成する暗号証明オブジェクト |
receipt.json | レシートラッパー JSON のファイル名 |
| レシートラッパー JSON | { receipt, image_id } 構造のホスト出力 |
詳細: ホストと証明生成 本文での使用: 検証サービス、バンドル構造
検証パイプライン
「検証」と「監査」の使い分け 本書では、
/verify画面と内部パイプラインによる判定を 検証、第三者がbundle.zipをローカルに取得して独立に行う確認作業を 監査 と呼び分ける。reproducibility/章は主に「監査」の文脈で書かれており、verification/章は「検証」の文脈で書かれている。
fail-closed
/api/verify と検証パイプラインが、データ不在や未解決状態を成功側に倒さず、not_run / 失敗 / Warning 側へ確定させる方針。要求された証拠が揃わない限り Verified に到達させない設計姿勢を指す。### cast-time 証跡 の not_run 扱い、### ゲーティングロジック の不変条件 (ゲーティングロジック)、### 重要度 の required 扱いはいずれもこの方針の具体化。
本文での使用: ゲーティングロジック、設計と流れ、チェック一覧
E2E 検証可能投票(End-to-End Verifiable Voting)
投票者が自分の投票について「意図通りに投じた」「正しく記録された」「正しく集計された」の 3 段階を独立に検証できる投票方式。システム運営者を信頼せずとも投票の完全性を確認できることが目標。
Cast-as-Intended(意図通りの投票)
検証の第 1 段階。投票者がローカルに保持する投票時データ(選挙 ID、選択肢、乱数)からコミットメントを再計算し、投票レシート(voteReceipt)のコミットメント値と照合することで、投票時に意図した選択が正しくコミットメントに反映されたことを確認する。クライアント側で完結する。
詳細: 4 段階検証モデル 本文での使用: コミットメントスキーム、チェック一覧
Recorded-as-Cast(記録通りの保存)
検証の第 2 段階。コミットメントが掲示板に正しく記録されたことを、RFC 6962 の包含証明と整合性証明によって確認する。掲示板が追記専用であること(投票が削除・改変されていないこと)を暗号学的に保証する。
詳細: 4 段階検証モデル 本文での使用: CT Merkle ツリー、チェック一覧
cast-time 証跡(Cast-Time CT Artifact)
投票受理時に CT ツリーへ書き込んだ時点の証跡。具体的には voteReceipt(投票レシート)と userVote.proof(包含証明パラメータ: leafIndex、treeSize、auditPath)の 2 つを指す。Recorded-as-Cast の検証では両方が必要。Cast-as-Intended では voteReceipt のみを使用する。/api/verify は store から再構成できた場合にだけこれらを返し、再構成できない場合は関連チェックを not_run として fail-closed に扱う。
詳細: 4 段階検証モデル
Counted-as-Recorded(記録通りの集計)
検証の第 3 段階。掲示板に記録された全投票が zkVM の集計に過不足なく含まれたことを確認する。除外されたスロットがないこと(excludedSlots == 0)は最重要不変条件。
詳細: 4 段階検証モデル 本文での使用: 入力コミットメント、ビットマップ Merkle、チェック一覧
STARK 検証(STARK Verification)
検証の第 4 段階。STARK レシートが暗号学的に正当であること、および期待される Image ID で生成されたことを確認する。ジャーナルの内容が正しい実行結果であることの最終的な保証。
詳細: 4 段階検証モデル 本文での使用: 検証サービス、Image ID、チェック一覧
検証チェック(Verification Check)
検証パイプラインを構成する個別の原子的な検証項目。現行実装では 22 個のチェックがあり、それぞれ一意の ID、所属する検証段階、証拠種別、重要度を持つ。Counted-as-Recorded には counted_election_manifest_consistent と counted_close_statement_consistent も含まれ、公開監査アーティファクトとの整合も required 条件となっている。
詳細: チェック一覧 本文での使用: 設計と流れ、ゲーティングロジック
ゲーティングロジック(Gating Logic)
検証チェックの結果を集約し、「Verified」「Verification Failed」「Warning」のいずれを表示するかを決定するロジック。required 扱いのチェックが 1 つでも failed なら Verified は表示されず、not_run / pending / running や必須証拠欠落でも Verified にはならない。Stage のステータスも、その Stage で required 扱いになるチェック群全体から導出される。
詳細: ゲーティングロジック 本文での使用: 設計と流れ、チェック一覧
公開監査アーティファクト
election-manifest.json(選挙設定の公開監査用スナップショット)と close-statement.json(集計締切時点のログ境界を表す公開監査レコード)の総称。Counted-as-Recorded 段階の必須チェック(counted_election_manifest_consistent、counted_close_statement_consistent)で整合性が検証される。
公開可能アーティファクト
秘密データを含まず、第三者検証や監査に利用できるアーティファクトの機密性区分。ここでの「公開可能」は無認証で取得できることを意味しない。配布や取得は bundle.zip、capability 保護 API、短命な presigned URL など、個別のアクセス経路に従う。
詳細: バンドル構造
配布対象アーカイブ(bundle.zip)
公開許可リストに基づいて作成される ZIP アーカイブ。証明バンドル のうち公開可能アーティファクトだけを束ねた部分集合で、bundle.zip というファイル名で配布される。現行構成は public-input.json、election-manifest.json、close-statement.json、receipt.json、journal.json などを含み、input.json、verification.json、included-bitmap.json、seen-bitmap.json は含まれない。
詳細: バンドル構造 本文での使用: 第三者検証ガイド、ホストと証明生成
無認証公開
セッション 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(教育用シミュレーション由来データ)の 4 種別がある。
重要度(Criticality)
検証チェックの必須性を示す分類。required(失敗・未実行・未解決なら Verified をブロック)と optional(補助的で、単独では Verified をブロックしない)の 2 段階。なお recorded_sth_third_party のように、設定状況に応じて optional から blocking な required 相当に昇格するチェックもある。
詳細: ゲーティングロジック、チェック一覧
掲示板と透明性
掲示板(Public Bulletin Board)
全投票コミットメントを時系列で記録する追記専用のログ。RFC 6962 の Certificate Transparency モデルに基づき、包含証明と整合性証明によって第三者が監査可能な透明性を実現する。
詳細: CT Merkle ツリー 本文での使用: STH ダイジェスト、4 段階検証モデル、ゲストプログラム
RFC 6962
Certificate Transparency(証明書の透明性)の標準規格。追記専用の Merkle ツリー、リーフハッシュ(0x00 プレフィックス)とノードハッシュ(0x01 プレフィックス)のドメイン分離、包含証明、整合性証明の仕様を定義する。本システムの掲示板は、この規格のハッシュ規則と証明アルゴリズムを参照した CT スタイル実装を採用している。
STH(Signed Tree Head)
掲示板の特定時点における状態の要約。ログ ID、ツリーサイズ、タイムスタンプ、ルートハッシュを含む。複数の独立したソースからの STH を比較することで、サーバーが異なるクライアントに異なるツリーを提示するスプリットビュー攻撃を検出する。
スプリットビュー攻撃(Split-View Attack)
掲示板サーバーが異なるクライアントに異なるツリー状態を提示する攻撃。特定の投票者に対してのみ投票を除外したツリーを見せることで、不正を隠蔽しようとする。整合性証明と STH の第三者検証によって検出される。
ルート履歴(Root History)
掲示板のルートハッシュ、ツリーサイズ、タイムスタンプの時系列記録。投票時のルートが最終ツリーの有効なプレフィックスであることを、整合性証明で検証する際に参照する。
詳細: CT Merkle ツリー
改ざんシナリオ
改ざんシナリオ(Tamper Scenario)
検証システムが不正をどのように検出するかを教育的に示すシミュレーション。S0(正常)から S5(複合改ざん)まで 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 のサーバーレスコンテナ実行環境。本システムでは STARK 証明生成に必要な大量のメモリ(32 GB)と CPU(16 vCPU)を提供するために使用する。アイドル時のコストは 0。
詳細: 非同期プローバー
Step Functions
AWS のワークフローオーケストレーションサービス。イメージ署名検証 → ECS プローバー実行 → コールバックの一連のフローを管理する。
非同期証明モード(Async Proving)
SQS → Step Functions → ECS Fargate の経路で STARK 証明を非同期に生成するモード。集計リクエスト(POST /api/finalize)は 202 Accepted を返し、クライアントはステータスポーリングで完了を待つ。
同期証明モード(Sync Proving)
ローカルプロセスで zkVM ホストバイナリを直接実行し、STARK 証明を同期的に生成するモード。開発環境で使用される。
イメージ署名検証(Image Signing)
ECS タスクで使用するプローバーコンテナイメージが、信頼できるビルドパイプラインから生成されたことを検証する仕組み。AWS Signer を使用し、Step Functions のゲートとして機能する。
詳細: イメージ署名
証明バンドル(Proof Bundle)
zkVM の実行結果を検証可能な形で保存・配布するためのアーティファクト群を指す 上位概念。公開可能アーティファクト(public-input.json など)と非公開アーティファクト(input.json、verification.json、included-bitmap.json、seen-bitmap.json など)の両方を含む。public /「公開可能」は秘密情報を含まず検証に利用可能であるという機密性の分類であり、無認証公開を意味しない。
公開許可リストで取り出した部分集合が 配布対象アーカイブ であり、それを ZIP 化したファイル名が bundle.zip です。3 者の関係は次のとおり:
証明バンドル ⊃ 配布対象アーカイブ ⊃ bundle.zip(ファイル)
「証明バンドル」は文脈上、AWS / S3 上の隣接オブジェクトや非公開アーティファクトも含めて議論したい箇所で使うのが望ましい。単に公開可能な ZIP を指す場合は bundle.zip または「配布対象アーカイブ」を使う。
詳細: バンドル構造 本文での使用: ホストと証明生成、非同期プローバー、第三者検証ガイド
隣接オブジェクト(Sibling Object)
S3 上で bundle.zip と同じ prefix(sessions/{sessionId}/{executionId}/)に配置される非 bundle ファイル。included-bitmap.json、seen-bitmap.json、verification.json など。bundle.zip には含まれないが、コールバック復元や検証レポート配信で利用される。
詳細: バンドル構造
セッションと API
セッション(Session)
一連の投票フロー(セッション作成 → 投票 → 集計 → 検証)を管理する単位。一意の sessionId(UUID v4)で識別される。投票・集計中は 30 分、検証中は 24 時間の有効期限を持つ。
選挙(Election)
投票の論理的な単位。一意の electionId(UUID v4)で識別され、コミットメントのドメイン分離に使用される。選挙設定ハッシュ(electionConfigHash)が期待投票数などの設定を束縛する。
集計確定(Finalization)
全投票の収集後に zkVM 入力を構築し、STARK 証明を生成するプロセス。同期モード(ローカル実行)と非同期モード(ECS Fargate)の 2 つの実行パスがある。
X-Session-ID
多くの API リクエストで付与される HTTP ヘッダー。セッションスコーピングに使用され、異なるセッション間のデータアクセスを防止する。POST /api/session は新規セッション作成のため不要。
X-Session-Capability
POST /api/session のレスポンスで返る署名付きセッショントークンを運ぶ HTTP ヘッダー。このヘッダーが運ぶ値を capability トークンと呼ぶ。/api/vote、/api/progress、/api/finalize、/api/verify、/api/verification/run、/api/bulletin/*、/api/botdata/:id、/api/bitmap-proof、/api/sth、/api/sessions/:sessionId/status、/api/verification/bundles/...、/api/zkvm-input-hash など session-scoped / capability 保護 API で必須。
capability 保護 API
セッション capability の提示を要求する API。多くの場合は X-Session-Capability を使い、ヘッダースコープの API では X-Session-ID も併用する。capability 保護 API は外部クライアント向けに文書化されていても、無認証公開 API ではない。
Turnstile
Cloudflare が提供する CAPTCHA サービス。/api/vote と /api/finalize で Bot による不正アクセスを防止するために使用する。TURNSTILE_BYPASS=1 は fail-closed で、AWS_BRANCH などのランタイムマーカーが明示的に非本番(develop/dev)と判定できる場合のみ有効。
定数
| 定数名 | 値 | 説明 |
|---|---|---|
BOT_COUNT | 63 | サーバーが自動生成するボット投票数 |
MERKLE_TREE_DEPTH | 6 | Merkle ツリーの深度(2^6 = 64 リーフに対応) |
VOTE_CHOICES | A, B, C, D, E | 投票で選択可能な選択肢 |
| コミットドメインタグ | stark-ballot:commit|v1.0 | コミットメントハッシュのドメイン分離タグ |
| 入力ドメインタグ | stark-ballot:input|v1.0 | 入力コミットメントのドメイン分離タグ |
| リーフドメインタグ | stark-ballot:leaf|v1 | CT Merkle リーフハッシュのドメイン分離タグ |
参考文献
本システムの設計領域に関連する主要な文献を掲載しています。
[1] J. Benaloh, R. Rivest, P. Y. A. Ryan, P. Stark, V. Teague, P. Vora. “End-to-end verifiability,” arXiv:1504.03778, 2015. https://arxiv.org/abs/1504.03778
E2E 検証可能投票の基本モデル(Cast-as-Intended / Recorded-as-Cast / Counted-as-Recorded)を定義した文献。本システムの 4 段階検証モデルはこのフレームワークと同じ構造を採用している。
[2] M. Harrison, T. Haines. “On the Applicability of STARKs to Counted-as-Collected Verification in Existing Homomorphic E-Voting Systems,” in Financial Cryptography and Data Security: FC 2024 International Workshops, LNCS, Springer, 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, 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 ツリーの包含証明・整合性証明を定義した標準仕様。本システムの掲示板は、この仕様のハッシュ規則(0x00 リーフ / 0x01 ノード)と証明アルゴリズムに基づく CT スタイル実装を採用している。
ライセンス
STARK Ballot Simulator は Apache License 2.0 の下で提供されています。
- SPDX:
Apache-2.0 - 詳細: リポジトリルートの
LICENSE
依存ソフトウェアのライセンスは各パッケージ定義に従います。