Provably Fair & ZK Proofs
How Superstake ensures every game is unpredictable while live, checkable before you play, and fully verifiable after it ends.
Overview
Superstake on games like Cash or Crash and Moonshot 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
- You can check the randomness before you play, and cancel if it looks wrong
- Anyone can verify the result after the game ends
- We cannot manipulate the outcome — our secret is fixed before you choose when to play, and the rest is the newest blockchain block at that moment
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 | The newest block when you start the game (Ethereum RANDAO or Solana blockhash), made by the network, not by us |
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 fit our secret to the randomness — its hash is published before you choose when to start, and the block used is always newer than
proofGameIdCommitBlock - You can't brute-force a favorable seed — the
proofGameIdhas 122 random bits, so you'd need to try ~2^122 values, which is 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.
Why the Latest Block Is Enough
We use the newest block already made when you press play, so there is no waiting. This is safe because:
- Your
proofGameIdhash is published before you play (see your profile). - You choose when to start, so we can't know which block your game will use.
- The block is always newer than
proofGameIdCommitBlock, the block when yourproofGameIdwas created. - Each block has exactly one value, made by the network. There is nothing to search through.
- You can check the block before you play and cancel if it's wrong (see Step 1).
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)
As soon as the game is created, you can check its randomness (see Step 1) and cancel if it's wrong:
| 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.proofGameIdCommitBlock | Block when our secret ID was created — always lower than
|
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
Do this right after the game is created, before you start it:
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(explorers show base58,randaoValueis hex)
For both:
- The block's time should be just before the game's creation time (a few seconds on Solana).
proof.randaoBlockNumbershould be greater thanproof.proofGameIdCommitBlock(when set).- Optionally wait for
proof.zkProofStatusto be verified; the proof tiesrandaoValueto your game's seed hash. - If anything is wrong, cancel the game before you start it. Nothing is at stake yet.
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 | 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 games are provably fair — you don't have to trust us, you can verify.
