Superstake v6.23.0
  1. Docs
Docs
◆Jan 13

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:

  1. No one can predict the outcome while the game is live
  2. You can check the randomness before you play, and cancel if it looks wrong
  3. Anyone can verify the result after the game ends
  4. 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);
ComponentDescription
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 proofGameId has 122 random bits, so you'd need to try ~2^122 values, which is 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.

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:

  1. Your proofGameId hash is published before you play (see your profile).
  2. You choose when to start, so we can't know which block your game will use.
  3. The block is always newer than proofGameIdCommitBlock, the block when your proofGameId was created.
  4. Each block has exactly one value, made by the network. There is nothing to search through.
  5. 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 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)

As soon as the game is created, you can check its randomness (see Step 1) and cancel if it's wrong:

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.proofGameIdCommitBlock

Block when our secret ID was created — always lower than randaoBlockNumber (empty on older games)

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

Do this right after the game is created, before you start it:

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 (explorers show base58, randaoValue is hex)

For both:

  • The block's time should be just before the game's creation time (a few seconds on Solana).
  • proof.randaoBlockNumber should be greater than proof.proofGameIdCommitBlock (when set).
  • Optionally wait for proof.zkProofStatus to be verified; the proof ties randaoValue to 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); // 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

Quick Verification (Recommended)

  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 gameproofGameIdHashproofGameId, gameSeed
During gameZK proof, RANDAO valueproofGameId, gameSeed
After gameEverythingNothing

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.

HomeQuestsProfile

Verify you're human

Complete the security check to continue.

Docs