Provably Fair Crash Game RNG: 14-Step Hash-Chain [2026]

Per-bet commit-reveal is the textbook way to make a single dice roll verifiable, but it quietly leaves one door open on a multiplayer provably fair crash game: nothing stops an operator from choosing a fresh server seed at the last possible moment. The production answer used by bustabit, Stake and BC.Game is a hash chain – a single SHA-256 pre-image chain whose terminating hash is published before a single round is dealt, salted with a Bitcoin block that does not yet exist. Even outside the dedicated crash-game operators, the pattern keeps spreading: as of July 2026, CryptoCasinos.com confirms that Pragmatic Play’s Spaceman – a mainstream slots-studio title – is itself provably fair, publishing each round’s result string via SHA-256. That ordering is the whole game: outcomes are frozen before the entropy that scrambles them is even mined.

This tutorial wires that scheme up end to end as a hardened microservice on Node.js 24 LTS, PostgreSQL and Fastify. You will pre-generate a million-link chain, publish a terminating commitment, bind a future block-hash salt, consume the chain in reverse with an atomic cursor, compute crash points with the canonical bustabit 52-bit formula, and expose a public verifier. Fourteen steps, working code, expected output, five pitfalls and an eight-item troubleshooting matrix – and, as our own June 2026 model of this exact hash-chain RNG confirms, the instant bust at 1.00x should land on almost precisely 1 in 101 rounds, or roughly 1% of all outcomes.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Hash-Chain Commit-Reveal Beats Per-Bet Seeds for Crash Games

A crash round is not one player’s dice roll – it is a single multiplier that hundreds of bettors ride simultaneously, each cashing out at their own target. That shared outcome changes the threat model. With per-bet commit-reveal, the operator commits a hash for each round just before it opens, which is fine for single-player dice but means the house still generates each round’s seed in real time. A motivated operator could grind seeds until a low multiplier lands on a heavy-action round.

The operator-foreknowledge gap

The hash chain closes that gap by inverting the timeline. You generate the entire future – millions of rounds – in one batch, then publish a single 64-character terminating hash. Because each link is the SHA-256 pre-image of the next, that one hash cryptographically commits to every round that will ever be played from the chain. The operator cannot swap, reorder or regenerate a seed without breaking the published commitment, which thousands of players have already archived.

How a future block salt seals it

Committing the seeds is only half the proof. The crash multiplier is derived from HMAC-SHA256(gameSeed, salt), where the salt is the hash of a future Bitcoin block – a value that did not exist when the terminating hash was published. The operator therefore committed to every seed before they could know the salt that scrambles them into multipliers, so grinding for favourable outcomes is computationally pointless. This is the property that explainer articles describe but rarely implement.

What changed in 2025-2026

Provably fair was first implemented around 2011 in early Bitcoin dice games, but the surrounding stack matured sharply through 2025-2026: Node.js 24 ‘Krypton’ shipped as the new LTS line on 23 June 2026, Chainlink VRF reached version 2.5 as the on-chain alternative, and regulators leaned harder on demonstrable RNG integrity. Adoption followed: as of April 2026, CrashGames.Guide reports that 100% of regulated crash games now run some variant of Bustabit’s provably fair protocol, making the hash-chain approach this guide implements the de facto industry baseline rather than one option among several. The cryptography is old and boring; the production engineering around it is what separates a toy from a service auditors will accept.

Property Per-bet commit-reveal (the d172 dice approach) Hash-chain commit-reveal (this guide)
Commitment timing One hash per round, just-in-time One terminating hash for millions of rounds, before any play
Operator foreknowledge House generates each round’s seed live Every outcome fixed before the salt exists
External entropy Optional client seed Future Bitcoin block hash, unknowable at commit
Best fit Dice, slots, single-player Crash / multiplayer rounds (Aviator, bustabit)
Verification Recompute one HMAC Recompute HMAC + walk the SHA-256 chain link
Storage Negligible Pre-generated chain (~64 MB per 1M rounds)

Step 0: Prerequisites – Stack, Versions and Primitives

Everything here runs on primitives already inside Node’s standard library, so the dependency surface is tiny and auditable. Pin exact versions; a provably fair service is the last place you want a floating ^ range pulling an unreviewed transitive package the night before a regulator audit.

Runtime and packages

Use the current LTS, Node.js 24.18.0 ‘Krypton’ (released 23 June 2026); the conservative 22.23.1 ‘Jod’ LTS line is also fine, while 26.4.0 is the bleeding-edge Current release you should avoid in production. The HTTP layer is Fastify 5.8.5 and the database driver is pg (node-postgres) 8.22.0 against PostgreSQL 16 or 17. No third-party crypto library is needed – node:crypto gives you SHA-256 and HMAC straight from OpenSSL.

The standards you are implementing

Three documents define the math. SHA-256 is specified in NIST FIPS 180-4; HMAC in IETF RFC 2104 (and FIPS 198-1); and the OS random source your genesis seed draws from is the deterministic-random-bit-generator family in NIST SP 800-90A Rev.1. You are not inventing cryptography here – you are composing FIPS-grade primitives in a verifiable order.

Component Version (June 2026) Role
Node.js 24.18.0 ‘Krypton’ LTS Runtime; bundled node:crypto
node:crypto Built-in (OpenSSL 3) SHA-256 (FIPS 180-4), HMAC (RFC 2104)
Fastify 5.8.5 Commitment, reveal and verify endpoints
pg (node-postgres) 8.22.0 Atomic cursor + idempotent ledger
PostgreSQL 16 / 17 Seed chain, rounds, settlements

Steps 1-3: Generate the Server Seed Chain

The chain is a one-time batch artifact. You generate a single random genesis secret, hash it forward a million times, and the final value becomes your public commitment. Crucially, the genesis is consumed by the last round you will ever play; the terminating hash is never used as a game seed at all.

Step 1 – CSPRNG genesis secret

Draw 32 bytes from the operating system’s cryptographically secure generator. Treat this value like a private key: seal it in a KMS or HSM and never log it. If the genesis leaks before the chain is exhausted, an insider can compute every unplayed multiplier in advance.

Step 2 – Build the chain by iterated SHA-256

Each link is the SHA-256 hash of the previous link’s raw bytes. Hashing the bytes (not the hex string) matters – pick one convention and keep it identical in the verifier, or every chain-link check will fail.

Step 3 – Derive the terminating commitment

The last element of the array is the terminating hash you publish before opening any round.

import { createHash, randomBytes } from 'node:crypto';

const CHAIN_LENGTH = 1_000_000;        // size to your expected round volume

function sha256Hex(bufOrHex) {
  const buf = Buffer.isBuffer(bufOrHex) ? bufOrHex : Buffer.from(bufOrHex, 'hex');
  return createHash('sha256').update(buf).digest('hex');
}

// Step 1: genesis secret – 32 bytes from the OS CSPRNG (NIST SP 800-90A aligned)
const genesis = randomBytes(32).toString('hex');   // consumed by the LAST round

// Step 2: hash forward; the final value is the public commitment
export function buildChain(genesisHex, length) {
  const chain = new Array(length);
  chain[0] = genesisHex;
  for (let i = 1; i < length; i++) {
    chain[i] = sha256Hex(chain[i - 1]);
  }
  return chain;                        // chain[length-1] = terminating hash
}

const chain = buildChain(genesis, CHAIN_LENGTH);
const terminatingHash = chain[CHAIN_LENGTH - 1];   // Step 3: PUBLISH this
console.log('Terminating commitment:', terminatingHash);

Steps 4-5: Commit Publicly – Terminating Hash + Future Block Salt

A commitment nobody can find is worthless. The terminating hash and the salt rules must be published somewhere immutable and timestamped before the first bet – a signed blog post, a Git tag, an on-chain transaction, or all three. Players archive it so they can later prove you did not change the chain.

Step 4 – Publish the terminating hash

Persist the commitment as your single source of truth and serve it from a read-only endpoint. Include the chain length and the exact algorithm string so a third party can reproduce results without reading your code.

Step 5 – Commit a future Bitcoin block as salt

Announce a block height that has not yet been mined – for example, “the salt is the hash of Bitcoin block 906,000.” When that block is mined, anyone can fetch its hash from any explorer, and you record it once. Because the block did not exist when you published the terminating hash, you provably could not have ground the seeds against it.

Make the commitment immutable

The strength of the proof is only as good as the timestamp on your commitment. Publish the terminating hash and salt rules somewhere that cannot be silently edited: a signed and dated blog post is the minimum, but pairing it with a Git tag in a public repository and an OP_RETURN or memo transaction on-chain gives you three independent, timestamped witnesses. Many operators also tweet the hash so that thousands of third parties hold a copy. The goal is simple – if you later tried to claim a different terminating hash, the contradiction would be public and instantly provable, which is exactly the accountability players are paying for with their trust.

// Published at T0, before any round opens
const commitment = {
  terminatingHash,                         // SHA-256, 64 hex chars
  chainLength: CHAIN_LENGTH,
  algorithm: 'crashPoint = bustabit-52bit( HMAC_SHA256(gameSeed, salt) )',
  saltSource: 'bitcoin_block_hash',
  saltBlockHeight: 906_000,                // a block that will be mined AFTER T0
  houseEdge: 0.01                          // enforced by a 1-in-101 instant bust
};

// chain_state seed row (run once, inside the chain-generation job)
await pool.query(
  `INSERT INTO chain_state (id, cursor, terminating_hex)
   VALUES (1, $1, $2)`,
  [CHAIN_LENGTH - 2, terminatingHash]      // cursor starts one below terminating
);

Steps 6-7: The Crash-Point Formula (Bustabit 52-bit Model)

This is the heart of a provably fair crash game, and it is worth implementing byte-for-byte to match the reference so player-side verifiers built for bustabit work against your service unmodified. The formula reads the most-significant 52 bits of an HMAC, with a separate 1-in-101 test that produces the instant bust.

Step 6 – HMAC and the instant-bust test

Compute HMAC-SHA256 keyed by the round’s game seed over the public salt. A helper walks the digest in 16-bit chunks to test divisibility by 101; when it divides evenly, the round busts instantly at 1.00x. That single branch is what bakes in the roughly 1% house edge – a 97% RTP game returns about $97 per $100 wagered over the long run. House edges are not standardized across operators, though: as of November 2025, CrashGameGambling lists Stake’s crash edge at 1.0% – matching this guide’s 1-in-101 default – against Cybet’s 1.5% and Thunderpick’s steeper 3.0%, and June 2026 figures from Aero Turbo’s SHA-256 crash implementation show roughly 4% of rounds insta-busting at 1.00x, implying a considerably heavier ~4% edge at its 88-96% RTP band. Tune the modulus deliberately rather than copying 101 by default; it is your single house-edge lever.

Step 7 – Map 52 bits to a multiplier

For the surviving 99%, take the first 13 hex characters (52 bits) as an integer and apply the closed-form mapping below. The distribution that falls out has a median near 2.0x and a long tail, with the probability of reaching at least m× approximately 0.99 / m. Other verifiers converge on the same shape with minor formula tweaks – ProvablySmart’s crash verifier, as of May 2026, scales the integer h through its own limit formula and rounds the resulting multiplier to 2 decimal places, the same precision this guide’s NUMERIC(12,2) column stores.

import { createHmac } from 'node:crypto';

// True ~1/mod of the time. SHA-256 hex is 64 chars (a multiple of 4),
// so we can scan it cleanly in 16-bit windows.
function divisible(hashHex, mod) {
  let val = 0;
  for (let i = 0; i < hashHex.length; i += 4) {
    val = ((val << 16) + parseInt(hashHex.substring(i, i + 4), 16)) % mod;
  }
  return val === 0;
}

export function crashPoint(gameSeedHex, saltHex) {
  const hash = createHmac('sha256', Buffer.from(gameSeedHex, 'hex'))
    .update(saltHex)
    .digest('hex');

  // Step 6: 1 in 101 rounds bust instantly -> ~1% house edge
  if (divisible(hash, 101)) return 1.0;

  // Step 7: 52 most-significant bits -> closed-form multiplier
  const h = parseInt(hash.slice(0, 13), 16);
  const e = 2 ** 52;
  return Math.floor((100 * e - h) / (e - h)) / 100;
}

Stake and BC.Game ship a simpler 2³² variant, (2^32 / (int + 1)) × (1 − houseEdge), reading the first eight hex characters; both are legitimate, but the bustabit 52-bit form is the one with the largest body of community verifiers, so it is the safer default for a new service. The eight-hex-character, mod-101 pattern shows up beyond those two brands as well: CrashGamesPlay’s own verifier, current as of March 2026, checks its instant bust the same way – testing whether the first 8 hex characters of the published hash are divisible by 101 – confirming that this narrower slice is now a recognized, independently-implemented convention rather than a Stake/BC.Game-only quirk.

Target multiplier m P(round reaches ≥ m) ≈ 0.99 / m Interpretation
1.00x ~1% bust exactly here Instant-bust floor (1-in-101)
2.00x ~49.5% Median outcome ≈ 2.0x
10.0x ~9.9% One round in ten
100x ~0.99% Roughly one in a hundred
1000x ~0.099% Deep tail

Steps 8-9: Reverse-Order Consumption with an Atomic Cursor

Rounds are played from the end of the chain backward, so that each revealed seed, when hashed once, reproduces the previously revealed seed. The pre-generated chain lives in a table; a single cursor row tracks the next index to consume. The trick is making the decrement atomic so two concurrent round-openers can never draw the same seed.

Step 8 – Schema for the chain and ledger

Store the chain as (idx, seed_hex) rows and keep the consumption pointer in a one-row chain_state table. Rounds and bets get their own tables so the seed chain stays append-never and read-mostly. For broader market context, see our coverage of best sweepstakes casino sites.

CREATE TABLE seed_chain (
  idx       BIGINT   PRIMARY KEY,
  seed_hex  CHAR(64) NOT NULL
);

CREATE TABLE chain_state (
  id              INT      PRIMARY KEY DEFAULT 1,
  cursor          BIGINT   NOT NULL,        -- next idx to consume (counts DOWN)
  terminating_hex CHAR(64) NOT NULL,
  salt_hex        CHAR(64),                 -- filled once the salt block is mined
  CHECK (id = 1)
);

CREATE TABLE rounds (
  round_no    BIGSERIAL PRIMARY KEY,
  seed_idx    BIGINT   NOT NULL REFERENCES seed_chain(idx),
  game_seed   CHAR(64) NOT NULL,
  crash_point NUMERIC(12,2),                -- filled by the app, then frozen
  status      TEXT     NOT NULL DEFAULT 'betting',  -- betting|running|settled
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  revealed_at TIMESTAMPTZ
);

Step 9 – Atomic cursor decrement

A single CTE advances the cursor and reads the consumed seed in one round-trip. If the update touches zero rows the chain is exhausted and you fail closed – never wrap to a fresh chain silently, because that would orphan the verification link.

export async function openRound() {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const draw = await client.query(`
      WITH advanced AS (
        UPDATE chain_state SET cursor = cursor - 1
        WHERE id = 1 AND cursor >= 0
        RETURNING cursor + 1 AS used_idx
      )
      INSERT INTO rounds (seed_idx, game_seed)
      SELECT a.used_idx, s.seed_hex
        FROM advanced a JOIN seed_chain s ON s.idx = a.used_idx
      RETURNING round_no, seed_idx, game_seed
    `);
    if (draw.rowCount === 0) throw new Error('CHAIN_EXHAUSTED');   // fail closed

    const { round_no, game_seed } = draw.rows[0];
    const { salt_hex } = (await client.query(
      'SELECT salt_hex FROM chain_state WHERE id = 1')).rows[0];

    const cp = crashPoint(game_seed, salt_hex);
    await client.query(
      'UPDATE rounds SET crash_point = $1 WHERE round_no = $2', [cp, round_no]);
    await client.query('COMMIT');
    return { roundNo: round_no, crashPoint: cp };  // seed stays secret until reveal
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
}

Steps 10-11: The Reveal Endpoint and Public Verifier

A provably fair claim that players cannot independently check is just marketing. Two public surfaces make it real: a reveal endpoint that exposes a finished round’s seed, and a standalone verifier – ideally runnable offline – that reproduces both the chain link and the crash point. Operators that build crypto-native cashiers usually pair this with fast instant Lightning Network casino payout rails so that a verified win settles in seconds rather than days.

Step 10 – Reveal after settlement only

The game seed for the current round must stay secret until the round ends; otherwise a player could compute the multiplier mid-flight and cash out perfectly. Reveal only rounds whose status is settled.

import Fastify from 'fastify';
const app = Fastify({ logger: true });

// Public commitment – served before any round is played
app.get('/fair/commitment', async () => {
  const r = await pool.query(
    'SELECT terminating_hex, salt_hex FROM chain_state WHERE id = 1');
  return { terminatingHash: r.rows[0].terminating_hex, salt: r.rows[0].salt_hex };
});

// Reveal a finished round so players can verify the chain link
app.get('/fair/round/:n', async (req) => {
  const r = await pool.query(
    `SELECT round_no, seed_idx, game_seed, crash_point
       FROM rounds WHERE round_no = $1 AND status = 'settled'`,
    [Number(req.params.n)]);
  if (r.rowCount === 0) return { error: 'not_revealed_yet' };
  return r.rows[0];
});

await app.listen({ port: 3000, host: '0.0.0.0' });

Step 11 – The standalone verifier

Ship this as a copy-paste snippet on your fairness page. It proves two things independently: that the revealed seed hashes to the previously revealed seed (the chain link back toward your published terminating hash), and that the seed plus salt reproduces the exact multiplier the player saw.

import { createHash } from 'node:crypto';

// Proof 1: does this seed hash to the previous revealed seed / terminating hash?
export function verifyLink(revealedSeedHex, expectedNextHex) {
  const h = createHash('sha256')
    .update(Buffer.from(revealedSeedHex, 'hex')).digest('hex');
  return h === expectedNextHex;
}

// Proof 2: recompute the crash point from seed + public salt
export function verifyCrash(revealedSeedHex, saltHex, claimedCrash) {
  return crashPoint(revealedSeedHex, saltHex) === claimedCrash;
}

Steps 12-14: Production Hardening

The cryptography is now correct; the remaining failure modes are operational. Money moves on the back of these outcomes, so settlement must be exactly-once, the service must fail closed, and every artifact must be reconstructable for an audit.

Step 12 – Idempotent settlement

A retried HTTP call or a worker that processes the same message twice must never pay a bet twice. Key the settlement table on the cashier-supplied bet_id and rely on ON CONFLICT DO NOTHING. The same discipline underpins the wider cashier; if you are integrating external crypto payment processors for iGaming, the idempotency key should flow all the way from deposit through game settlement to payout.

CREATE TABLE bet_settlements (
  bet_id     UUID PRIMARY KEY,            -- idempotency key from the cashier
  round_no   BIGINT NOT NULL REFERENCES rounds(round_no),
  cashout_x  NUMERIC(12,2),
  won        BOOLEAN NOT NULL,
  payout     NUMERIC(20,8) NOT NULL,
  settled_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

export async function settleBet(betId, roundNo, stake, cashoutX) {
  const round = (await pool.query(
    'SELECT crash_point FROM rounds WHERE round_no = $1', [roundNo])).rows[0];
  const won = Number(cashoutX) <= Number(round.crash_point);
  const payout = won ? Number(stake) * Number(cashoutX) : 0;
  // ON CONFLICT => the same bet_id can never pay twice
  await pool.query(
    `INSERT INTO bet_settlements (bet_id, round_no, cashout_x, won, payout)
     VALUES ($1,$2,$3,$4,$5) ON CONFLICT (bet_id) DO NOTHING`,
    [betId, roundNo, cashoutX, won, payout]);
  return { won, payout };
}

Step 13 – Fail closed

If the salt is not yet recorded, the cursor underflows, or the database is unreachable, the correct behaviour is to refuse to open a round – never to fall back to Math.random() or an un-committed seed. A circuit breaker that halts betting beats a service that quietly produces unverifiable outcomes. Encode this as an explicit invariant: the round engine asserts that salt_hex is non-null and the cursor is non-negative before it computes a single multiplier, and it throws rather than improvises when either check fails. In a regulated environment, a few minutes of paused betting is a recoverable incident; a batch of outcomes nobody can verify is a licence-threatening one. Wire the breaker to your alerting so an on-call engineer is paged the instant the engine trips, and surface a clear “betting paused” state to players rather than letting requests hang.

Step 14 – Observability and the evidence warehouse

Emit Prometheus counters for rounds opened, instant busts, chain depth remaining and verifier hits, and stream every round and settlement to an append-only warehouse. Set an SLO on chain headroom – alert when fewer than, say, 50,000 links remain – so you rotate before exhaustion rather than during an incident.

Performance, Throughput and Cost

A common worry is that hashing a million-link chain or running HMAC on every round will throttle a busy table. It will not – by a wide margin. SHA-256 is one of the most heavily optimised primitives in existence, and the entire fairness layer is microseconds of work sitting next to millisecond-scale database and network calls. The practical lesson is that your bottleneck is PostgreSQL and your event loop, never the cryptography.

Chain generation cost

Generating the chain is a one-time, offline job. On a single modern x86 core, SHA-256 over a 32-byte input runs comfortably past a million hashes per second, so a 1,000,000-link chain builds in a handful of seconds and a 10-million-link chain in well under a minute. Storage is the more meaningful figure: stored as 64-character hex, a million seeds occupy roughly 64 MB of raw data and, with row and index overhead, low-hundreds of megabytes on disk. Pre-generate during a maintenance window, seal the genesis in your KMS, and never run it on the hot path.

Per-round latency and throughput

Opening a round is one HMAC-SHA256 over a 64-byte salt – a few microseconds – wrapped in a single PostgreSQL CTE round-trip. End-to-end latency is therefore dominated entirely by the database call, typically sub-millisecond to a few milliseconds on a warm connection pool. Because the commitment and verifier endpoints are stateless reads, you scale them horizontally behind a cache; only the atomic cursor write is serialised, and a single primary handles thousands of round-opens per second before you need to think harder. Always benchmark on your own hardware rather than trusting any single published figure. For broader market context, see our coverage of leading online casino sites.

Operation Typical cost (modern core) Real bottleneck
Build 1M-link chain ~seconds (one-time) CPU, offline job
HMAC-SHA256 per round Microseconds Negligible
Atomic cursor draw Sub-ms to few ms PostgreSQL write
Commitment / verify read Cacheable Network, horizontally scalable

Complete Project Example: A Minimal Crash Service

Assembled, the pieces form a compact service: a one-shot generator job, a round engine, and three HTTP routes. The flow is generate → publish → record salt → open rounds → settle → reveal. Below is the expected console and API output from a clean run.

$ node generate-chain.js
Terminating commitment: 4f1d8b2c… (publish BEFORE opening any round)
Chain length: 1,000,000   Storage: ~64 MB

$ curl -s localhost:3000/fair/commitment
{"terminatingHash":"4f1d8b2c…","salt":null}

# operator records the salt once block 906,000 is mined
$ node set-salt.js 00000000000000000001a2b3c4…

$ node open-round.js
Round 1 -> crashPoint 2.41x  (seed sealed until reveal)
Round 2 -> crashPoint 1.00x  (instant bust, 1-in-101)
Round 3 -> crashPoint 5.87x

$ curl -s localhost:3000/fair/round/1
{"round_no":1,"seed_idx":999998,"game_seed":"a91c…","crash_point":"2.41"}

# player verifies offline: sha256(game_seed) === terminating hash, and
# crashPoint(game_seed, salt) === 2.41  -> both true

Because each revealed seed hashes back toward the public terminating commitment, a player who archives the first commitment can chain-verify every round you have ever dealt – no trust in your servers required. That property is exactly what separates a marketing claim from a defensible one, and it is the bar the better provably-fair versus eCOGRA- and GLI-audited RNG trust models are measured against.

Beyond the Standard Coverage: What the Explainers Skip

The top-ranking results for this topic – Chainlink’s technical guide, Gamingtec’s explainer, and the long crash-game write-ups – do an excellent job describing what provably fair is. None of them ship the production layer. Chainlink’s piece, naturally, steers toward on-chain VRF; the explainers stop at “the casino combines three seeds.” Here is what a real service needs that those articles omit.

Operational gaps the explainers miss

  • Atomic chain consumption. No explainer addresses two concurrent round-openers racing on the same cursor – the single most common way a homegrown crash engine double-spends a seed.
  • Idempotent settlement and reconciliation. The hard part is not computing 2.41x; it is paying each bet exactly once across retries, crashes and replays.
  • Salt lifecycle. Choosing a future block, recording it once, and refusing to open rounds until it is bound is glossed over entirely.
  • Chain rotation. What happens at link zero? Migration without breaking the verification link is never covered.
  • Fail-closed posture. Explainers assume the happy path; production assumes the database is on fire.

Hash chain vs Chainlink VRF vs certified RNG

These are complementary trust models, not competitors. Off-chain hash chains are the standard for high-frequency crash rounds; Chainlink VRF 2.5 offers on-chain verifiable randomness when the game itself lives in a smart contract; and lab-certified RNG (GLI-19, eCOGRA, iTech Labs) remains a licensing requirement for fiat slots under regimes such as the UK Gambling Commission’s remote technical standards.

Approach Trust model Where it runs 2026 status
Hash-chain commit-reveal Player-verifiable pre-commitment Off-chain (your service) Standard for crash (Stake, BC.Game, bustabit)
Chainlink VRF 2.5 On-chain verifiable oracle Smart contract Latest VRF; adds gas + latency
Certified RNG Third-party lab attestation Audited operator RNG Required by MGA/UKGC for fiat slots

One Chain, Many Games

The same chain that powers crash can drive dice, plinko or mines – only the outcome-mapping function changes, while seed generation, commitment and reveal stay identical. Real deployments already lean on this: as of March 2026, CoinPoker runs its provably fair Mine Crash, Speed Crash and Cash or Crash variants off the same underlying model, supporting wagers from $0.10 up to $2,000-plus per round, exactly the kind of single-chain, multi-game reuse this section describes. If you want the per-bet dice variant with its own client-seed-per-player nonce model, that is exactly what the companion per-bet HMAC commit-reveal dice tutorial builds; this guide’s chain is the multiplayer counterpart.

Reusing seeds safely

If one round’s seed must feed multiple sub-games, derive each via a distinct HMAC label – HMAC(seed, salt + ':crash') versus HMAC(seed, salt + ':dice') – so the games are independent yet both verifiable from one revealed seed. Never slice the same digest two different ways and call it two games; that correlates outcomes.

Seed rotation and chain exhaustion

When headroom drops below your SLO threshold, generate a fresh chain, publish its terminating hash, and cut over at a named round boundary. Keep the old chain’s reveal endpoint live forever – historical verifiability must not regress just because you rolled to a new chain.

Reproducible Audit: One Commitment, the Whole History

The hash chain’s standout property at audit time is compression of trust: a single published terminating hash, plus the recorded salt, lets anyone reconstruct and re-verify every round you have ever dealt. Per-bet schemes scatter thousands of independent commitments; a chain collapses them into one anchor. That makes the evidence package an auditor reviews dramatically smaller and harder to fake after the fact.

What the labs look for

Independent RNG assessors – GLI under GLI-19, plus eCOGRA and iTech Labs – care about three things your design already provides: a documented entropy source, deterministic reproducibility of every outcome, and the absence of any post-hoc operator control. Your genesis draws from the OS CSPRNG family described in NIST SP 800-90A Rev.1, every round is a pure function of committed inputs, and the future-block salt removes operator foreknowledge. Document those three points explicitly in your submission.

Mapping the warehouse to evidence

Each warehouse row should be a self-contained proof: round_no, seed_idx, the revealed game_seed, the salt, and the resulting crash_point. With the published terminating hash as the anchor, an auditor recomputes the chain link and the multiplier offline, with zero access to your infrastructure. Keep the signed commitment publication, the salt-block record, and this append-only log for the full retention period your licence demands – typically years, not months.

5 Critical Pitfalls That Break Provably-Fair Crash Games

  1. Hex/byte mismatch. Hashing the hex string in generation but the raw bytes in the verifier (or vice versa) yields a chain that never validates. Fix the convention once and assert it in tests.
  2. Revealing the live seed. Exposing the current round’s seed before settlement lets sharp players compute the multiplier and cash out at the top every time.
  3. Late or mutable salt. Choosing the salt after seeing action, or letting it change, destroys the entire foreknowledge guarantee. Commit a future block height and freeze it.
  4. Non-atomic cursor. A read-then-write cursor under load hands the same seed to two rounds, corrupting the chain order and breaking every downstream link.
  5. Silent chain wrap. Auto-starting a new chain at exhaustion without a published commitment orphans the verification link and looks exactly like cheating to an auditor.

8 Troubleshooting Scenarios and Fixes

Symptom Likely cause Fix
Verifier chain-link fails Hashing hex vs raw bytes mismatch Hash Buffer.from(seed,'hex') on both sides
Crash points all near 1.0x Reading wrong hex slice width Use exactly the first 13 hex chars (52 bits)
Two rounds share a seed Non-atomic cursor decrement Single-statement CTE UPDATE … RETURNING
Bet paid twice Retry without idempotency key UNIQUE bet_id + ON CONFLICT DO NOTHING
Players can predict multiplier Seed revealed before settlement Gate reveal on status = 'settled'
Instant-bust rate ≠ ~1% Wrong modulus or chunk loop Test divisible(hash, 101) over full digest
Service errors at link zero Chain exhausted Fail closed, rotate chain, keep old reveals live
Audit cannot reproduce a round Salt or seed not warehoused Append-only log of seed_idx, seed, salt, crash

Advanced Tips: Chainlink VRF 2.5, zkVRF and Hybrid Entropy

The reference implementation is intentionally minimal; production deployments layer extra entropy and tooling on top. The open-source ecosystem is healthy: provableio/provable-core (★22, updated September 2025) packages outcome generation, jdleo/provably-fair-dice (★29, TypeScript, updated December 2025) is a clean reference UI, and the Rust CLI lucasholder/fair gives auditors a language-independent checker.

Hybrid block + VRF salt

For maximum unpredictability, combine the Bitcoin block-hash salt with a Chainlink VRF 2.5 draw and HMAC them together. The block hash gives free public auditability; the VRF adds an oracle-signed entropy source no single party controls. The cost is on-chain gas and a few seconds of latency per draw, so reserve it for high-stakes or on-chain games. The two sources are independent, so even if one were somehow predictable, the combined salt remains unguessable – a belt-and-braces posture some licensed operators now prefer for their flagship tables.

zkVRF and EC-VRF

Zero-knowledge VRF projects such as orao-network/zkvrf push verifiable randomness onto EVM chains with succinct proofs, and EC-VRF (RFC 9381) is the IETF-standardised elliptic-curve VRF underpinning several of them. They are heavier than a hash chain but valuable when the randomness itself must be proven on-chain rather than merely committed off-chain.

Frequently Asked Questions

Is a hash chain really more secure than per-bet commit-reveal?

For multiplayer crash rounds, yes. Per-bet schemes let the operator generate each seed live; a pre-committed chain freezes every future outcome before the salt that scrambles them exists, removing the operator’s ability to grind seeds against known action.

Why use a future Bitcoin block hash as the salt?

Because it is public, tamper-evident, and – critically – unknowable at commit time. Publishing the terminating hash before the salt block is mined proves you could not have engineered the seeds to produce favourable multipliers.

How long should the chain be?

Size it to expected lifetime volume plus margin. A million links is about 64 MB and covers a long runway for a mid-volume game; high-traffic operators pre-generate tens of millions and alert well before exhaustion.

Does provably fair change the house edge?

No. It proves the game is not rigged; it does not alter the odds, and RTP still varies by operator. A 97% RTP crash game still returns about $97 per $100 wagered long-term – the 1-in-101 instant bust is exactly where the ~1% edge lives – but not every provably fair crash game targets that same band: Aero Turbo’s SHA-256 implementation, as of June 2026, runs at a wider 88-96% RTP and caps real payouts at $10,000 per round, a reminder that “provably fair” guarantees the math is unrigged, not that every operator sets the same edge or the same ceiling.

Can players predict the next multiplier?

No. The current round’s seed stays secret until settlement, and although the salt is public, the seed is not revealed until the round ends, so the multiplier cannot be computed in advance.

Is this FIPS 140-3 certified?

The primitives (SHA-256, HMAC) are FIPS-specified, but crash-game services themselves are not FIPS-certified cryptographic modules. Regulatory assurance comes from RNG certification (GLI-19, eCOGRA, iTech Labs), which is a separate, complementary process.

How does this compare with Chainlink VRF?

Hash chains are off-chain, cheap and high-frequency – ideal for crash. Chainlink VRF 2.5 is on-chain and oracle-verified, better suited when the game logic itself lives in a smart contract. Many teams hybridise the two.

What Node.js version should I target?

Node.js 24.18.0 ‘Krypton’ LTS is the current recommendation as of June 2026, with 22.23.1 ‘Jod’ as the conservative fallback. Avoid the 26.x Current line in production until it reaches LTS.

Casey Mitchell

Casey Mitchell

Crypto Casino Editor

Casey Mitchell tracks crypto casinos, on-chain gaming, provably fair RNG, and the regulatory gray zones around US-facing crypto gambling platforms. Reviews wallets, deposit flows, and KYC policies across BTC, ETH, and stablecoin operators.

View all articles