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

コミットメントスキーム

投票者の選択を公開データから秘匿しつつ束縛するコミットメントスキームの設計を解説します。

SHA-256 ベースのコミットメントにより、投票内容の hiding(秘匿性)と binding(束縛性)を実現します。ドメイン分離タグにより、他プロトコルとのコミットメント衝突を防止します。

概要

投票コミットメントは、投票者が選んだ選択肢を公開データからは隠したまま、その選択に束縛されることを可能にする暗号プリミティブです。投票時にはコミットメント値のみが掲示板に記録され、選択肢と乱数は掲示板や public-input.json を含む配布対象バンドルには現れません。投票者は Cast-as-Intended 検証のためにこれらをローカルに保持します。

ただし、本 PoC は operator に対する完全な秘匿性を目的としていません。現行実装ではクライアントから投票 API に opening(選択肢と乱数)も送信され、サーバー側ストアに保持されうるため、ここでいう hiding は主に公開観測者や公開配布物に対する性質を指します。

flowchart LR
  subgraph 入力
    E[選挙 ID<br/>16 バイト UUID]
    C[選択肢<br/>1 バイト]
    R[乱数<br/>32 バイト]
  end
  E --> H[SHA-256]
  C --> H
  R --> H
  T["ドメインタグ<br/>&quot;stark-ballot:commit|v1.0&quot;"] --> H
  H --> CM[コミットメント<br/>32 バイト]

コミットメントの正準フォーマット

コミットメント値は、以下の入力を連結し SHA-256 で圧縮して生成されます。

commitment = SHA-256(
    domain_tag  ||   ← "stark-ballot:commit|v1.0" (24 バイト, UTF-8)
    election_id ||   ← UUID v4 のバイナリ表現 (16 バイト)
    choice      ||   ← 選択肢の値 (1 バイト, 0〜4)
    random           ← 一様乱数 (32 バイト)
)

各フィールドの仕様

フィールドサイズエンコーディング説明
ドメインタグ24 バイトUTF-8 固定文字列"stark-ballot:commit|v1.0"
選挙 ID16 バイトUUID v4 からハイフンを除去し、16 進数をバイト列に変換選挙スコープの識別子
選択肢1 バイト符号なし整数 (0 = A, 1 = B, 2 = C, 3 = D, 4 = E)投票者の選択
乱数32 バイト暗号学的に安全な一様乱数hiding 性を保証

SHA-256 への入力は合計 73 バイト、出力は 32 バイト(16 進数表記で 64 文字、0x プレフィックス付きでは 66 文字)です。

ドメイン分離

ドメインタグ "stark-ballot:commit|v1.0" は、このコミットメントが他のプロトコルで使用されるハッシュ値と偶発的に衝突することを防ぐための仕組みです。

ドメイン分離は本システムの全暗号プリミティブに一貫して適用されています。

プリミティブドメインタグ
コミットメント"stark-ballot:commit|v1.0"
入力コミットメント"stark-ballot:input|v1.0"
Merkle リーフ0x00 || "stark-ballot:leaf|v1"
Merkle ノード0x01
ログ ID"stark-ballot:bulletin-log|v1.0"

安全性

Hiding(秘匿性)

乱数フィールドが 32 バイト(256 ビット)のエントロピーを持つため、公開されたコミットメント値から選択肢を推測することは計算量的に不可能です。

前提条件:

  • 乱数は暗号学的に安全な乱数生成器(CSPRNG)から生成される
  • 同じ乱数は決して再利用しない

乱数の再利用は hiding 性を破壊します。同一選挙で同一乱数を使用した場合、同じ選択肢であればコミットメント値が一致してしまい、情報が漏洩します。

Binding(束縛性)

SHA-256 の原像耐性(preimage resistance)と第二原像耐性(second-preimage resistance)により、一度コミットした値と異なる選択肢に対して同じコミットメント値を生成することは計算量的に不可能です。

つまり、投票者はコミットメント公開後に「別の選択肢に投票した」と主張を変えることができません。

TypeScript と Rust の実装同期

コミットメントは TypeScript(クライアント・サーバー)と Rust(zkVM ゲスト)の双方で計算されます。これら 2 つの実装は、バイトレベルで完全に同一の出力を生成する必要があります。

同期が必要な要素:

  • ドメインタグの文字列とエンコーディング(UTF-8)
  • UUID からバイト列への変換規則(ハイフン除去 → 16 進数デコード)
  • 選択肢の整数エンコーディング(1 バイト、符号なし)
  • 乱数の 16 進数デコード規則

ドメインタグやエンコーディング規則を変更する場合は、TypeScript と Rust の両実装を同時に更新する必要があります。どちらか一方のみの変更は、コミットメント照合の失敗を引き起こします。

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

コミットメントは、4 段階検証モデルの最初の 3 段階で中心的な役割を果たします。

検証段階コミットメントの役割
Cast-as-Intended投票者がローカルに保持する(選択肢, 乱数, 選挙 ID)からコミットメントを再計算し、投票レシートと照合する
Recorded-as-Cast掲示板上でコミットメントの包含証明を検証し、投票時点のツリー状態に対して正しく記録されたことを確認する
Counted-as-RecordedzkVM ゲストが prover から渡された各 vote opening でコミットメントを再計算し、掲示板上の値と整合する票だけを tally に含める

注意: Counted-as-Recorded は投票者ローカルの opening ではなく、prover に渡された opening を使います。Cast-as-Intended とは照合対象の出所が異なる点に注意してください。

現行実装の Recorded-as-Cast では、投票時点(cast-time)のツリー状態に対する包含証明を使います。投票時に投票時ルート(内部名 rootAtCast)を保存し、後続の証明取得や最終化では投票時点のツリーサイズ(bulletinIndex + 1)に対する包含証明を再導出します。保存済みの投票時ルートと再導出した証明のルートが一致しない場合、その票の公開証拠は fail-closed で拒否されます。一致した場合にのみ、レシートの bulletinRootAtCast として公開されます。

各チェックの判定ロジックは チェック一覧 > 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/>掲示板の値と照合