Superstake v6.2.0
  1. Docs
Docs
◆Jul 3

Lootbox Provably Fair Draws

How lootbox rewards are decided by a commit–reveal scheme combined with blockchain randomness — and how to re-run the math yourself to verify any draw.

Overview

Every lootbox uses a commit–reveal scheme combined with blockchain randomness to guarantee that:

  1. We can't pick your reward — the server commits to its secret seed before the randomness exists
  2. You can't predict your reward — the draw depends on a future blockchain block
  3. Anyone can verify the result — after the box resolves, all inputs are revealed and the draw can be recomputed in your browser

The Core Math

Each box's reward is decided by a draw seed:

drawSeed = SHA256(serverSeed || randaoValue || lootboxId);
ComponentDescription
serverSeed

A secret 256-bit value generated by our backend when the box is created — hidden until the box resolves

randaoValue

Randomness from the blockchain (e.g. Ethereum RANDAO) at the box's revealBlockNumber — unpredictable until that block is finalized

lootboxId

The box's unique id (UTF-8), so two boxes sealed against the same block still get independent draws

The seeds are concatenated as raw bytes (serverSeed and randaoValue are hex-decoded, lootboxId is UTF-8-encoded) and hashed with SHA-256.

The Commitment

When you buy a box, before any randomness exists, the server publishes:

FieldDescription
serverSeedHash

SHA256(serverSeed) — a commitment to the secret seed

revealBlockNumberThe future block whose randomness will be used
randomnessSourceThe chain the randomness comes from
items (with weights)The full drop table the draw will run against

This commitment is what makes cheating impossible:

  • We can't change the seed after seeing the randomness — serverSeedHash locks in our choice before block revealBlockNumber exists
  • We can't grind for a favorable seed — the seed is a 256-bit random value committed up front; picking a new one after seeing the RANDAO would break the hash
  • You can't snipe a good block — the seed you'd need to predict the outcome stays secret until after the block is finalized

The Weighted Draw

The draw seed picks one item from the box's drop table, proportionally to each item's weight:

function weightedDraw(items: { weight: number }[], drawSeed: string): number {
  const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
  // First 8 bytes of the draw seed, as an integer, modulo the total weight
  const pick = BigInt("0x" + drawSeed.slice(0, 16)) % BigInt(totalWeight);
  let running = 0n;
  for (let i = 0; i < items.length; i++) {
    running += BigInt(items[i].weight);
    if (pick < running) return i;
  }
  return items.length - 1;
}

An item with weight 700 out of a total of 1000 wins 70% of the time — exactly the odds shown in the drop table before you open.


What You'll See in the UI

Before the Box Resolves (Seed Hidden)

FieldDescription
serverSeedHashOur commitment to the secret seed
revealBlockNumberThe block whose randomness decides the draw
randomnessSourceWhere that block lives (with an explorer link)

After the Box Resolves (Full Transparency)

FieldDescription
serverSeedThe secret seed (now revealed)
randaoValueThe randomness taken from the reveal block
drawnItemIndexThe item the server says you won

How to Verify Fairness

In-App (One Click)

Open Lootbox History, pick any opened box, and press Verify draw. Your browser re-runs the full SHA-256 + weighted-draw math locally and checks both that the commit matches the reveal and that the drawn item follows from the seed. Nothing is trusted from the server except the revealed inputs.

Step 1: Verify the Randomness is Real

Check that the blockchain randomness wasn't fabricated:

  1. Open the block explorer link next to the commit (or find block revealBlockNumber yourself)
  2. Confirm the block's prevRandao matches the revealed randaoValue

Step 2: Verify We Didn't Change the Seed

Confirm the revealed seed matches the hash we committed to at purchase:

const crypto = require("crypto");
 
const seedHash = crypto
  .createHash("sha256")
  .update(Buffer.from(serverSeed, "hex"))
  .digest("hex");
 
console.log(seedHash === serverSeedHash); // must be true

Step 3: Recompute the Draw Seed

const crypto = require("crypto");
 
function computeDrawSeed(
  serverSeed: string,
  randaoValue: string,
  lootboxId: string,
): string {
  const combined = Buffer.concat([
    Buffer.from(serverSeed, "hex"),
    Buffer.from(randaoValue.replace(/^0x/, ""), "hex"),
    Buffer.from(lootboxId, "utf8"),
  ]);
  return crypto.createHash("sha256").update(combined).digest("hex");
}
 
const drawSeed = computeDrawSeed(serverSeed, randaoValue, lootboxId);

Step 4: Recompute the Winning Item

Run weightedDraw(items, drawSeed) (code above) against the box's item list, in the exact order the box lists them. The returned index must equal drawnItemIndex — the item you were awarded.


Summary

PhaseWhat's PublicWhat's Hidden
At purchase

serverSeedHash, revealBlockNumber, drop table + odds

serverSeed
While resolvingThe finalized block's randaoValue (on-chain)serverSeed
After resolveEverythingNothing

The combination of hash commitment + future blockchain randomness + client-side recomputation means you never have to trust the reward you were dealt — you can verify it.

Verify you're human

Complete the security check to continue.

Docs