DEV Community

Rake
Rake

Posted on

HMAC-SHA256 in Practice: How Crypto Casinos Generate Provably Fair Game Outcomes

If you've ever used JWT tokens or API authentication, you've used HMAC-SHA256. But there's a fascinating real-world application of this algorithm that most developers don't know about: provably fair gambling.

Crypto casinos use HMAC-SHA256 to generate game outcomes in a way that's mathematically verifiable by players. The system is elegant, and the implementation teaches important concepts about commitment schemes and deterministic randomness.

The Commitment Scheme

The security model is simple:

Phase 1 (Before bet):
  Casino: serverSeed = random()
  Casino: publishes SHA256(serverSeed) → commitment hash
  Player: sets clientSeed

Phase 2 (During bet):
  result = Algorithm(HMAC-SHA256(serverSeed, clientSeed:nonce))

Phase 3 (After bet):
  Casino: reveals serverSeed
  Player: verifies SHA256(revealed) === commitment ✓
  Player: verifies Algorithm(HMAC(revealed, clientSeed:nonce)) === result ✓
Enter fullscreen mode Exit fullscreen mode

The commitment hash ensures the casino can't change the server seed after seeing the player's bet. The client seed ensures the casino can't predict the player's input. Together, they produce deterministic but unpredictable outcomes.

Implementation: Dice

The simplest game. Convert an HMAC hash to a number between 0.00 and 100.00:

const crypto = require('crypto');

function verifyDice(serverSeed, clientSeed, nonce) {
  const hash = crypto
    .createHmac('sha256', serverSeed)
    .update(`${clientSeed}:${nonce}`)
    .digest('hex');

  // Take first 8 hex chars (32 bits)
  const int = parseInt(hash.slice(0, 8), 16);

  // Map to 0.00 - 100.00
  return (int % 10001) / 100;
}
Enter fullscreen mode Exit fullscreen mode

The player bets on whether the result will be above or below a target number. The house edge is baked into the payout calculation, not the roll itself.

Implementation: Crash/Limbo

This one's more interesting. Convert a hash into a crash multiplier:

function verifyCrash(serverSeed, clientSeed, nonce) {
  const hash = crypto
    .createHmac('sha256', serverSeed)
    .update(`${clientSeed}:${nonce}`)
    .digest('hex');

  const int = parseInt(hash.slice(0, 8), 16);

  // House edge: 1 in 101 games crash at 1.00x
  if (int % 101 === 0) return 1.00;

  // Inverse distribution: higher multipliers are rarer
  const e = 2 ** 32;
  return Math.max(1, Math.floor((e / (int + 1)) * 0.99) / 100);
}
Enter fullscreen mode Exit fullscreen mode

The probability of reaching multiplier m is approximately 1/m. So 2x has ~49.5% chance, 10x has ~9.9%, 100x has ~0.99%. This creates the characteristic exponential distribution that makes Crash exciting.

Implementation: Keno

Keno requires generating 10 unique numbers from a pool of 40:

function verifyKeno(serverSeed, clientSeed, nonce) {
  const picks = [];
  let cursor = 0;

  while (picks.length < 10) {
    const hash = crypto
      .createHmac('sha256', serverSeed)
      .update(`${clientSeed}:${cursor}:${nonce}`)
      .digest('hex');

    for (let i = 0; i < hash.length && picks.length < 10; i += 8) {
      const value = parseInt(hash.slice(i, i + 8), 16);
      const tile = (value % 40) + 1;
      if (!picks.includes(tile)) picks.push(tile);
    }
    cursor++;
  }

  return picks;
}
Enter fullscreen mode Exit fullscreen mode

The interesting part is collision handling — if the modulo operation produces a number already picked, it's skipped and the next chunk of the hash is used. If the hash runs out, a new one is generated with an incremented cursor.

Implementation: Mines

A 5x5 grid where N tiles contain mines. The hash determines mine positions via a Fisher-Yates shuffle:

function verifyMines(serverSeed, clientSeed, nonce, mineCount) {
  const hash = crypto
    .createHmac('sha256', serverSeed)
    .update(`${clientSeed}:${nonce}`)
    .digest('hex');

  const tiles = Array.from({ length: 25 }, (_, i) => i);

  for (let i = tiles.length - 1; i > 0; i--) {
    const idx = ((tiles.length - 1 - i) * 2) % hash.length;
    const j = parseInt(hash.slice(idx, idx + 2), 16) % (i + 1);
    [tiles[i], tiles[j]] = [tiles[j], tiles[i]];
  }

  return tiles.slice(0, mineCount);
}
Enter fullscreen mode Exit fullscreen mode

Key insight: mine positions are fixed before the player clicks any tile. The server seed was committed before the bet, so the casino can't react to player choices.

Why This Matters Beyond Gambling

The commitment scheme pattern used here is applicable to:

  • Fair coin flips in distributed systems — two parties contribute randomness
  • Sealed-bid auctions — commit to a bid before revealing
  • Random selection in DAOs — verifiable random committee selection
  • Fair NFT minting — prove rarity was pre-determined

The core principle — "commit, then reveal, then verify" — is one of the most useful patterns in applied cryptography.

Try It

I built a browser-based tool that implements all these algorithms: rakestake.com/verify

Select a casino, paste your seeds, and it runs the verification client-side. No backend, no data transmission. The same code shown in this article.


This is part of Rakestake, a platform for crypto casino verification tools and rakeback rewards. The verification tools are free and open-source | Also available on GitHub.

Top comments (0)