検出メカニズム
各改ざんシナリオに対して、検証パイプラインがどのチェックで失敗するかを整理します。ここでは実装上の実際の判定ロジック(verification-checks と verification-summary)に合わせて説明します。
本章は実 API(src/server/api/handlers/verify.ts と src/lib/verification/*)の判定ロジックを基準にしています。NEXT_PUBLIC_USE_MOCK_API=true の mock API fixture では、本章と異なるチェック結果を返すことがあります。
/api/verify の counted_* チェックは STARK 状態でゲートされます。
verificationStatus=not_runの間、counted_*はnot_runになり得るverificationStatus=runningの間、counted_*はpendingになり得る- 本章で「失敗するチェック」と書く箇所は、STARK 検証が解決済み(
success/failed)でcounted_*が評価可能な局面を前提とする
検出の 2 つの原理
flowchart TB P1[原理1: 完全性違反\nexcludedCount > 0] --> C1[counted_missing_indices_zero が失敗] P2[原理2: 主張集計の不整合\nclaimed ≠ verified] --> C2[counted_tally_consistent が失敗]
- 原理1(完全性違反): 主に S1/S3/S5
- 原理2(主張集計の不整合): 主に S2/S4
シナリオ別の主な失敗チェック(STARK 解決後)
| シナリオ | 主に失敗するチェック | 説明 |
|---|---|---|
| S0 | なし | 正常系 |
| S1 | counted_missing_indices_zero | ユーザー票除外により excludedCount=1 |
| S2 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S3 | counted_missing_indices_zero | 現行実装では botId=1 のボット票除外により excludedCount=1 |
| S4 | counted_tally_consistent | claimed tally と verified tally が不一致 |
| S5 | counted_missing_indices_zero(再集計分岐では counted_tally_consistent も失敗) | excludedCount>0 が必ず発生し、再集計分岐では claimed と verified も不一致になる |
補足:
- S1 では、ビットマップ証明が利用可能な場合
counted_my_vote_includedも失敗し得ます - S2/S4 では、
cast_commitment_matchとcounted_input_commitment_matchは通常成功します(zkVM 入力を改変していないため) - STARK 未解決(
not_run/running)の間は、上表のcounted_*失敗はnot_run/pendingとして観測され得ます
4 段階検証モデルとの対応
| 検証段階 | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
| Cast-as-Intended | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Recorded-as-Cast | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Counted-as-Recorded | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| STARK Verification | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
この表は「シナリオ適用による典型挙動」を示します。not_run(証拠不足)や running(検証実行中)は運用状態により別途発生します。
チェックID(主要項目)マトリクス(STARK 解決後)
| チェック ID | S0 | S1 | S2 | S3 | S4 | S5 |
|---|---|---|---|---|---|---|
cast_commitment_match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
counted_tally_consistent | ✅ | ✅ | ❌ | ✅ | ❌ | 分岐依存(再集計時は ❌) |
counted_missing_indices_zero | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
counted_my_vote_included | ✅ | ❌ または not_run | ✅ | ✅ | ✅ | 分岐依存 |
counted_input_commitment_match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
S5 の counted_my_vote_included は、ランダム対象がユーザー票(除外または再集計)なら失敗し得ます。証拠不足時は not_run になります。
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"]
このため以下は通常発生しません。
cast_commitment_matchの失敗(ローカル証拠は改変されない)counted_input_commitment_matchの失敗(zkVM 入力は元票)
S5 の実装依存ポイント
S5 はランダムに除外または再集計を選びますが、実装上は常に tamperMode=input です。FINALIZE_ASYNC_MODE=true の非同期 finalize では、実行後オーバーライドは行わず zkVM ジャーナル値をそのまま使います。
- 除外分岐:
missingIndices=1系になり、counted_missing_indices_zeroが失敗 - 再集計分岐:
invalidIndices=1系になり、counted_missing_indices_zeroが失敗 - 再集計分岐では
counted_tally_consistentも併発して失敗し得る
ビットマップ証明の役割
counted_my_vote_included は、チェック定義上 required のユーザー包含チェックです。
- S1(ユーザー票除外)では、証明が利用可能なら失敗して「自分の票が未集計」であることを直接示せる
- 証拠不足で
not_runになる場合でも、最終判定は Verified にならない(missing_evidenceもしくはexcludedCountによる失敗)
最終判定(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: 完全性違反(S1/S3/S5)published_tally_mismatch: claimed と verified の不一致(S2/S4)counted_integrity_failed: Counted 系必須チェック失敗の一般ケース