Provably Fair

This page documents the exact scheme (algorithm version v1) used to commit and verify deck order in rip events. It is written for people who want to check our work.

Commitment (before the event starts)

  1. Each card slot in the deck has a unique ID. The IDs are sorted lexicographically and joined with |, and the integrity hash is computed as SHA-256(id1|id2|…|idN). This fingerprints the set of cards in the deck.
  2. A random 128-bit seed is generated and kept secret while the event runs.
  3. The deck hash is computed as SHA-256(integrityHash + ":" + seed) and published on the event page before the first pull.
  4. The deck is shuffled with a Fisher–Yates shuffle driven by the mulberry32 PRNG, seeded with the first 32 bits (8 hex characters) of the seed. The resulting order is frozen in the database.

Dealing

Each pack opening deals the next unpulled card in the frozen sequence, in a single atomic database operation. Pull order is recorded and the most recent pulls are shown publicly on the event page.

Reveal & verification (after the event ends)

Once an event has ended, the seed is revealed on the event data. Every event also has its own fairness page, linked from the event itself, listing the sealed deck in sequence order alongside the order cards were actually pulled in. To verify an event:

  1. Recompute SHA-256(integrityHash + ":" + seed) and confirm it equals the deck hash that was published while the event was live. This proves the seed was fixed before the first pull.
  2. Re-run the shuffle: seed mulberry32 with parseInt(seed.slice(0, 8), 16) and apply Fisher–Yates to the sorted slot IDs. The resulting order must match the observed pull order.

Reference implementation

function mulberry32(seed) {
  return function () {
    let t = (seed = (seed + 0x6d2b79f5) | 0);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function seededShuffle(ids, seedHex) {
  const rng = mulberry32(parseInt(seedHex.slice(0, 8), 16) >>> 0);
  const a = ids.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}
Because the deck hash binds both the card set and the seed, changing either one after publication would change the hash — any tampering is detectable by anyone who saved the commitment while the event was live.