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:
- No one can predict the outcome while the game is live
- Anyone can verify the result after the game ends
- 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)
| Component | Description |
|---|---|
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
proofGameIdis 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
proofGameIdand you can verifySHA256(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
proofGameIdorgameSeeduntil 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:
| Field | Description |
|---|---|
proof.zkProofStatus | Status: |
proof.zkProofLink | Link to the ZK proof on the explorer |
proof.randaoValueProvider | Randomness source: ethereum / solana |
proof.randaoBlockNumber | The block/slot used for randomness |
proof.randaoValue | The actual randomness value (hex) |
proof.proofGameIdHash | Our commitment to the secret ID |
After the Game Ends (Full Transparency)
Once the game concludes, we reveal everything:
| Field | Description |
|---|---|
proof.proofGameId | The secret ID (now revealed) |
gameSeed | The 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:
- Go to a block explorer (e.g., Etherscan)
- Find block number
proof.randaoBlockNumber - Confirm the block's
prevRandaomatchesproof.randaoValue
For Solana:
- Go to a Solana explorer (e.g., Solscan)
- Find slot
proof.randaoBlockNumber - 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); // trueStep 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); // trueStep 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 tilesVerify the ZK Proof
Quick Verification (Recommended)
- Click the
proof.zkProofLinkto open the ZK proof explorer - 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 explorerpublicSignals.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.jsonIf 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"
- Check the block/slot on a blockchain explorer
- Confirm the RANDAO/blockhash matches what we claim
Standard — Verify the ZK Proof
"I want cryptographic proof, but I'll trust the explorer"
- Open
proof.zkProofLink - Confirm it shows "Verified"
Advanced — Full Local Verification
"I don't trust anyone — let me verify everything myself"
- Download proof artifacts
- Run
snarkjs groth16 verifylocally - Recompute the seed derivation manually
- Regenerate the game outcome from the seed
Summary
| Phase | What's Public | What's Hidden |
|---|---|---|
| Before game | proofGameIdHash, upcoming RANDAO block | proofGameId, gameSeed |
| During game | ZK proof, RANDAO value | proofGameId, gameSeed |
| After game | Everything | Nothing |
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.