Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

入力コミットメント

zkVM 入力の公開可能フィールドに対する正準エンコーディングとコミットメントの設計を解説します。

入力コミットメントが束縛するのは zkVM 入力全体ではなく、現行実装で公開検証に使うフィールド群です。

入力コミットメントにより、「証明されたデータセット」と「主張されたデータセット」の一致を検証可能にします。バイトレベルの正準化により、TypeScript と Rust の間で決定的な一致を保証します。

概要

入力コミットメントは、zkVM 入力のうち現行実装で束縛する公開可能な検証フィールドを単一のハッシュ値に集約するプロトコルです。可変部分として electionIdbulletinRoottreeSizetotalExpectedvotesCount と、各投票の index・コミットメント値・Merkle パスを含みます。厳密な計算では、これに固定のドメインタグ stark-ballot:input|v1.0 と version 10(v1.0)も加わります。

このハッシュ値は 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 証明は「ゲストプログラムが正しく実行された」ことを証明しますが、「どの入力に対して実行されたか」は証明のスコープ外です。入力コミットメントがなければ、悪意あるサーバーは以下の攻撃が可能になります:

  1. 投票を除外した入力で zkVM を実行し、有効な STARK 証明を取得する
  2. 公開用の入力データには除外されていない投票を含めて提示する
  3. 第三者は STARK 証明が有効であることを確認できるが、実際に処理された入力は異なる

入力コミットメントをジャーナルに含めることで、第三者は「公開データから再計算した入力コミットメント」と「ジャーナルに記録された入力コミットメント」を照合し、不一致を検出できます。

正準エンコーディング

入力コミットメントの計算には、すべてのフィールドを決定的な順序・エンコーディングで連結する正準化が不可欠です。

バイトレイアウト

input_commitment = SHA-256(
    domain_tag       ← "stark-ballot:input|v1.0" (23 バイト, UTF-8)
    || version       ← u32 リトルエンディアン (4 バイト) = 10
    || election_id   ← UUID v4 バイナリ (16 バイト)
    || bulletin_root ← 32 バイト
    || tree_size     ← u32 リトルエンディアン (4 バイト)
    || total_expected← u32 リトルエンディアン (4 バイト)
    || votes_count   ← u32 リトルエンディアン (4 バイト)
    || [投票データ]  ← インデックス昇順でソートされた各投票
)

各投票のエンコーディング

投票配列の各要素は以下の形式でエンコードされます:

vote_entry =
    index            ← u32 リトルエンディアン (4 バイト)
    || commitment_len← u16 リトルエンディアン (2 バイト) = 32 (固定)
    || commitment    ← 32 バイト
    || path_len      ← u16 リトルエンディアン (2 バイト)
    || path_nodes    ← path_len × 32 バイト

フィールド一覧

フィールドサイズエンコーディング説明
ドメインタグ23 バイトUTF-8 固定文字列"stark-ballot:input|v1.0"
バージョン4 バイトu32 LEv1.0 = 10
選挙 ID16 バイトUUID バイナリ選挙スコープの識別子
掲示板ルート32 バイトハッシュ値最終的な Merkle ルート
ツリーサイズ4 バイトu32 LE掲示板のリーフ数
期待投票数4 バイトu32 LE想定される総投票数
投票数4 バイトu32 LE実際に含まれる投票数
各投票インデックス4 バイトu32 LE掲示板上の位置
コミットメント長2 バイトu16 LE固定値 32
コミットメント32 バイトハッシュ値投票コミットメント
パス長2 バイトu16 LEMerkle パスのノード数
パスノード各 32 バイトハッシュ値包含証明の兄弟ハッシュ

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

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

ただし、public-input.json に含まれる全フィールドが入力コミットメントの計算対象に入るわけではありません。現行実装で直接束縛されるのは、概要で示した対象フィールドに固定のドメインタグと version を加えた部分です。electionConfigHashlogIdtimestampmethodVersion は入力コミットメントには直接含まれません。

現行実装では、公開パラメータの照合は public-input.json 単体では完結せず、proof bundle に含まれる election-manifest.jsonclose-statement.json も組み合わせて検証されます。入力コミットメント対象外のフィールドは、次のように別経路で照合されます。

  • electionConfigHashcounted_election_manifest_consistent(manifest と journal 等を照合)
  • logIdtimestampcounted_close_statement_consistent(close statement と journal 等を照合)
  • methodVersion → 入力コミットメントには含まれない。journal 互換性チェックや Image ID 解決に使用

したがって、counted_input_commitment_match は公開パラメータ照合の中核ですが、唯一のクロスチェックではありません。残りのパラメータは上記チェックで補完的に検証されます。

正準化規則

エンコーディングの決定性を保証するために、以下の規則が厳守されます。

ソート規則

投票はインデックスの昇順にソートしてからエンコードする必要があります。

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

前提となる不変条件:

  • 各投票の index は一意である(重複インデックスは不正入力)

重複インデックスが存在する入力はプロトコル違反であり、正常系の正準化対象ではありません。したがって、正準順序の主キーは index の昇順として定義されます。

実装上は、重複インデックスという異常入力に対して Rust 側で追加の tie-break(commitmentmerklePath)を行いますが、これはあくまで異常系の決定性補助であり、正常系仕様を変更するものではありません。

エンディアン規則

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

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

16 進数正規化

コミットメント値やパスノードなどの 16 進数表現は、0x プレフィックスを除去した上でバイト列にデコードされます。16 進数文字列のまま連結するのではなく、常にバイナリ表現を使用します。

TypeScript と Rust の同期

入力コミットメントは TypeScript(サーバー側)と Rust(zkVM ゲスト内)の双方で独立に計算され、結果が一致する必要があります。

flowchart LR
  subgraph TypeScript
    TS_IN[public-input.json から抽出した<br/>入力コミットメント対象フィールド] --> TS_CALC[正準エンコーディング<br/>+ SHA-256]
    TS_CALC --> TS_IC[入力コミットメント A]
  end

  subgraph "Rust (zkVM ゲスト)"
    RS_IN[ゲスト入力] --> RS_CALC[正準エンコーディング<br/>+ SHA-256]
    RS_CALC --> RS_IC[入力コミットメント B]
  end

  TS_IC --> CMP{A = B ?}
  RS_IC --> CMP
  CMP -->|一致| OK[検証成功]
  CMP -->|不一致| NG[検証失敗:<br/>入力データが異なる]

同期が破壊される典型的な原因:

  • ソート順序の不一致
  • エンディアンの不一致
  • ドメインタグの文字列差異
  • バージョン番号の不一致
  • 16 進数正規化規則の差異(大文字/小文字、0x プレフィックスの有無)

検証パイプラインにおける役割

入力コミットメントは、Counted-as-Recorded 段階での検証チェック counted_input_commitment_match として使用されます。

現行の Counted-as-Recorded 段階では、これに加えて counted_election_manifest_consistentcounted_close_statement_consistent も必須チェックとして動作します(対象フィールドと対応関係は上記を参照)。

チェック ID検証内容
counted_input_commitment_match公開可能な検証データから再計算した入力コミットメントがジャーナルの値と一致するか

このチェックが失敗する場合、zkVM が処理した入力データと公開可能な検証データから再構成される入力コミットメント対象フィールドが異なることを意味し、結果の信頼性が根本的に損なわれます。

各チェックの判定ロジックは チェック一覧 > Counted-as-Recorded を参照してください。

注意事項

入力コミットメントには投票者の秘密データ(選択肢や乱数)は含まれません。束縛対象は概要で示した公開可能なフィールドのみであるため、入力コミットメントの公開は投票の秘密性を損ないません。