抽選の仕組み — 実例
1つの実際の抽選を最初から最後まで辿ります: 1,000件の対象コメント、3名の当選者、9名の補欠。専門用語なし、ごまかしなし。
シナリオ
コンテストが終了したと想像してください。コメントが取得され、コンテストのフィルターが適用され、1,000件のエントリーが対象となっています。賞品は3名の当選者と9名の補欠(当選者に連絡が取れない場合に備えて)を設けています。以下は各エントリーの確率です — コメントが投稿された時刻やコメント者の名前の長さに関係なく、1,000件すべてのコメントで同一です。
| 結果 | 各エントリーの確率 |
|---|---|
| メイン当選者として選出(1位〜3位) | 1,000分の3 — 0.30% |
| 補欠として選出(4位〜12位) | 1,000分の9 — 0.90% |
| いずれかの当選枠に選出 | 1,000分の12 — 1.20% |
これはコンテストのコメントが444件でも444,444件でも変わりません。アルゴリズムは同一です。変わるのは分母だけです。
ステップ1 — エントリーの収集とフィルタリング
アプリは接続したFacebookまたはInstagramの投稿にあるすべてのコメントを取得し、コンテストのフィルター(重複、禁止ワード、アカウント年齢ルールなど)を適用します。残ったものが対象セットになります。このシナリオでは1,000件のコメントです。それぞれに固有の数値IDを持つデータベース行があり、抽選の残りの処理はこの1,000個のIDに対して行われます。
ステップ2 — 正準順にソート
ランダムな処理が始まる前に、アプリは1,000個のIDを厳密に昇順でソートします — 17, 142, 203, 1058, 9941, … — そして到着順を破棄します。なぜか?Facebookはページネーション、時刻、バックエンドの変更によって異なる順序でコメントを返す可能性があるためです。抽選がFacebookの順序を使用すると、同じランダムシードから2回の再取得で異なる当選者が生まれる可能性があります。最初にソートすることで入力を1つの正準リストに固定し、シードだけで結果が決まるようになります。
ステップ3 — SHA-256ハッシュで入力リストをロック
ソートされたリストをSHA-256(暗号ハッシュ関数)に入力し、64文字のフィンガープリントを生成します。フィンガープリントはリスト本体とともにコンテストレコードに保存されます。この時点以降、コンテストを調査する誰でも対象リストが改ざんされていないことを確認できます: 公開されたリストを再ハッシュし、保存されたフィンガープリントと比較すれば、1つのIDが変更されるだけでまったく異なるハッシュになります。このステップ以降にコメントを静かに追加または削除する方法はありません。
ステップ4 — 256ビットのランダムシードを生成
アプリはオペレーティングシステムの暗号ランダムソース(SecureRandom.hex(32))に32バイトの予測不可能な乱数を要求し、結果を64文字の16進数文字列として保存します(例: a3f1…ce)。256ビットは2²⁵⁶ ≈ 10⁷⁷通りの可能なシードを意味します。参考までに、観測可能な宇宙には約10⁸⁰個の原子があります。コンテスト作成者も運営者も、外部から監視する攻撃者も、このシードが生成される前に予測または推測することはできません。
ステップ5 — 1,000個のIDをシャッフルして最初の12個を取得
シードが決定論的シャッフルエンジンを初期化し、1,000個のソートされたIDに対してフィッシャー–イェーツシャッフルを実行します。フィッシャー–イェーツは標準的な公平シャッフルアルゴリズムです — 1,000件のエントリーのそれぞれが、すべての位置に同等のチャンスで位置する可能性があります。シャッフル後、アプリは上位12件を取得します。これが選択ステップのすべてです: シャッフルして12件をスライス。
ステップ6 — 順位の割り当て
選ばれた12件のエントリーを順番に処理します。最初の3件が当選者1〜3位となり、次の9件が補欠1〜9位となります。残り988件の対象エントリーは未選択として記録されます。以前のフィルタリングで除外されたコメントは、資格外となった具体的な理由とともに失格としてマークされます。
ステップ7 — すべてを公開
最後に、アプリはコンテストレコードに5つの証拠を保存します — すべて公開読み取り可能です。これらがなければ、結果はブラックボックスになります。これらがあれば、誰でも自宅で抽選全体を再現し、公開された当選者がそのシードが生み出しうる唯一の当選者であることを確認できます。
各抽選で公開されるもの
これらはすべてコンテストに保存され、公開検証ページで公開されます。これらを組み合わせることで抽選は完全に再現可能になります — 同じ入力、同じ出力、常に。
- 256ビットのランダムシード — ステップ4の64文字の16進数文字列。
- ソートされた対象IDリスト — ステップ2のすべての1,000個のIDを正準順に。
- IDのSHA-256フィンガープリント — ステップ3の64文字のハッシュ。公開リストが改ざんされていないことを確認します。
- アルゴリズムバージョン — 現在
v1。正確な選択アルゴリズムを固定するため、将来の改善が古い結果を遡及的に変更することはありません。 - Rubyバージョン — 言語ランタイムを固定します。シャッフルの正確な順序がそれに依存するためです。
5行で自分で計算を再現する
Rubyがインストールされている場合(macOSとほとんどのLinuxディストリビューションに付属)、自宅で正確な抽選を再現できます。各コンテストの検証ページ(URLパターン: /results/<token>/verify)に独自のeligible-ids.txtが公開されています — 検証ガイドがダウンロード方法をステップごとに説明しています。公開されたシードをコピーしてirbに貼り付けてください:
require "digest"
seed = "PASTE_THE_PUBLISHED_SEED_HERE"
ids = File.read("eligible-ids.txt").split("\n").map(&:to_i).sort
# (1) Sanity check the input: must equal the published SHA-256.
Digest::SHA256.hexdigest(ids.join("\n"))
# (2) Re-run the shuffle. First 3 items are winners 1..3 (in order);
# the next 9 items are reserves 1..9.
ids.shuffle(random: Random.new(seed.to_i(16))).first(12)
12個のIDがコンテストが公開したのと完全に同じ順序で出力されます。3名の当選者、9名の補欠、ビット単位で同一。これがPick a Winnerの抽選を単なる約束ではなく検証可能にするものです — 数学が仕事をするのであり、運営者の言葉ではありません。
これがリグを困難にする理由
- シードは推測不可能です。2²⁵⁶通りの可能性があり、対象リストが確定した後にのみ生成されます。シードが好むIDを事前計算することは誰にもできません。なぜならシードは抽選の瞬間まで存在しないからです。
- 入力はロックされています。SHA-256フィンガープリントにより、シードを見た後で友人のコメントを静かに追加することはできません — フィンガープリントが変わり、抽選は再現できなくなります。
- シャッフルは機械的です。シードとソートされたリストが揃えば、当選者は決定されます。その間に人間の判断は介在しません。事後的な調整は、誰かが計算を再実行した瞬間に不一致として検出されます。
- レシートは公開されています。シード、リスト、ハッシュ、アルゴリズムバージョン、Rubyバージョン — 5つすべてが公開されています。運営者が誰にも気づかれずに変更できるプライベートな抽選の要素は存在しません。