Provably Fair & ZK Proofs

How Cash or Crash ensures every game is unpredictable while live and fully verifiable after it ends.

Jan 13

Overview

Cash or Crash (CoC) uses a provably fair system powered by zero-knowledge proofs (ZK proofs) to guarantee that:

  1. No one can predict the outcome while the game is live
  2. Anyone can verify the result after the game ends
  3. We cannot manipulate the outcome — the randomness comes from the blockchain itself

The Core Math

Each game's outcome is determined by a game seed, which is derived from two components:

gameSeed = SHA256(randaoValue || proofGameId)
ComponentDescription
randaoValue

Future blockchain randomness (Ethereum RANDAO or Solana blockhash) — unpredictable until the block is mined

proofGameId

A secret per-game identifier generated by our backend

Why the Proof Game ID Hash is Necessary

Before the game starts, we publish a commitment to the proofGameId:

function computeProofGameIdHash(proofGameId: string): string {
  const buffer = hexToBuffer(proofGameId);
  return crypto.createHash("sha256").update(buffer).digest("hex");
}

This commitment (proofGameIdHash) is critical for preventing manipulation:

  • We can't change the secret after seeing the randomness — The hash locks in our choice before the blockchain randomness is revealed
  • You can't brute-force to find a favorable seed — The proofGameId is a 256-bit random value. Even if you knew the upcoming RANDAO value, you'd need to try ~2^256 values to find one that produces a desired outcome — computationally impossible
  • Full accountability — After the game, we reveal proofGameId and you can verify SHA256(proofGameId) === proofGameIdHash

Without this commitment, we could theoretically wait to see the blockchain randomness and then pick a proofGameId that produces a favorable (for us) outcome. The hash commitment makes this impossible.


The ZK Proof

We generate a Groth16 zero-knowledge proof (using snarkjs) that proves the following without revealing secrets during the game:

gameSeed     = SHA256(randaoValue || proofGameId)
gameSeedHash = SHA256(gameSeed)

The ZK proof allows us to:

  • Commit to the game outcome before it's revealed
  • Prove the math is correct without exposing the proofGameId or gameSeed until the game ends
  • Verify on-chain that the derivation followed the rules

Public Signals

The proof exposes these values publicly:

  • The RANDAO value (blockchain randomness)
  • The gameSeedHash (commitment to the seed)

The proofGameId and gameSeed remain hidden until after the game.


What You'll See in the UI

During the Game (Seed Hidden)

While the game is live, you can already verify fairness is in place:

FieldDescription
proof.zkProofStatus

Status: pending / processing / verified / failed

proof.zkProofLinkLink to the ZK proof on the explorer
proof.randaoValueProviderRandomness source: ethereum / solana
proof.randaoBlockNumberThe block/slot used for randomness
proof.randaoValueThe actual randomness value (hex)
proof.proofGameIdHashOur commitment to the secret ID

After the Game Ends (Full Transparency)

Once the game concludes, we reveal everything:

FieldDescription
proof.proofGameIdThe secret ID (now revealed)
gameSeedThe full game seed

Now you can verify the entire chain of derivation yourself.


How to Verify Fairness

Step 1: Verify the Randomness is Real

Check that the blockchain randomness wasn't fabricated:

For Ethereum:

  1. Go to a block explorer (e.g., Etherscan)
  2. Find block number proof.randaoBlockNumber
  3. Confirm the block's prevRandao matches proof.randaoValue

For Solana:

  1. Go to a Solana explorer (e.g., Solscan)
  2. Find slot proof.randaoBlockNumber
  3. Confirm the blockhash matches proof.randaoValue

Step 2: Verify We Didn't Change the Secret ID

After the game ends, confirm we used the same proofGameId we committed to:

const crypto = require("crypto");
 
const proofGameId = "..."; // revealed after game
const expectedHash = crypto
  .createHash("sha256")
  .update(Buffer.from(proofGameId, "hex"))
  .digest("hex");
 
// This should equal proof.proofGameIdHash
console.log(expectedHash === proofGameIdHash); // true

Step 3: Verify the Seed Derivation

Confirm the game seed was derived correctly:

const crypto = require("crypto");
 
function deriveGameSeed(randaoValue: string, proofGameId: string): string {
  const randaoBuffer = Buffer.from(randaoValue, "hex");
  const proofGameIdBuffer = Buffer.from(proofGameId, "hex");
  const combined = Buffer.concat([randaoBuffer, proofGameIdBuffer]);
  return crypto.createHash("sha256").update(combined).digest("hex");
}
 
const expectedSeed = deriveGameSeed(proof.randaoValue, proof.proofGameId);
 
console.log(expectedSeed === gameSeed); // true

Step 4: Verify the Game Outcome

Recompute the tile generation from the gameSeed:

const crypto = require("crypto");
 
function generateCrashTiles(
  seed: string,
  rows: number,
  tilesPerRow: number,
): number[] {
  const crashTiles: number[] = [];
 
  for (let row = 0; row < rows; row++) {
    const rowSeed = `${seed}-row-${row}`;
    const hash = crypto.createHash("sha256").update(rowSeed).digest("hex");
    const hashNumber = parseInt(hash.slice(0, 8), 16);
    const crashTile = hashNumber % tilesPerRow;
    crashTiles.push(crashTile);
  }
 
  return crashTiles;
}
 
const computedTiles = generateCrashTiles(gameSeed, totalRows, tilesPerRow);
// Compare with the actual revealed crash tiles

Verify the ZK Proof

  1. Click the proof.zkProofLink to open the ZK proof explorer
  2. Confirm the page shows "Verified"

This proves the seed derivation was correct without needing to trust us.

Advanced: Local Verification (No Trust Required)

For maximum trustlessness, verify the Groth16 proof locally:

Requirements:

  • verification_key.prod.json — the circuit's verification key (download this)
  • proof.json — the Groth16 proof (pi_a, pi_b, pi_c) from the zkVerify explorer
  • publicSignals.json — the public signals array from the zkVerify explorer

Steps:

# 1. Install snarkjs
npm install -g snarkjs
 
# 2. Download the verification key
curl -O https://superstake.fun/verification_key.prod.json
 
# 3. Download proof.json and publicSignals.json from the zkVerify explorer
# (Click on the proof link in the game details)
 
# 4. Verify the proof
snarkjs groth16 verify verification_key.prod.json publicSignals.json proof.json

If verification succeeds, you'll see:

[INFO]  snarkJS: OK!

What Local Verification Proves

  • The proof is mathematically valid against the verification key
  • The public signals match the claimed values
  • The seed derivation followed the circuit rules

Important: You should also verify that the public signals match the game:

  • The RANDAO bytes in public signals match proof.randaoValue
  • The packed seed hash matches SHA256(gameSeed) after reveal

Trust Levels

Choose your level of verification:

Basic — Verify Randomness Source

"I trust the ZK proof system, just show me the randomness is real"

  1. Check the block/slot on a blockchain explorer
  2. Confirm the RANDAO/blockhash matches what we claim

Standard — Verify the ZK Proof

"I want cryptographic proof, but I'll trust the explorer"

  1. Open proof.zkProofLink
  2. Confirm it shows "Verified"

Advanced — Full Local Verification

"I don't trust anyone — let me verify everything myself"

  1. Download proof artifacts
  2. Run snarkjs groth16 verify locally
  3. Recompute the seed derivation manually
  4. Regenerate the game outcome from the seed

Summary

PhaseWhat's PublicWhat's Hidden
Before gameproofGameIdHash, upcoming RANDAO blockproofGameId, gameSeed
During gameZK proof, RANDAO valueproofGameId, gameSeed
After gameEverythingNothing

The combination of blockchain randomness + hash commitments + ZK proofs ensures that Cash or Crash games are provably fair — you don't have to trust us, you can verify.