Provably Fair
Every draw is mathematically impossible to manipulate. We use a commit-reveal scheme combined with the drand public randomness beacon (League of Entropy). You can verify every result independently — no trust required.
Commit
Before the round starts we generate a secret seed and publish its SHA-256 hash. The seed itself stays hidden.
Reveal
After the round ends we fetch a signed random number from drand's public beacon. Nobody can predict it in advance.
Verify
The winner is computed from the seed + drand output. You can re-run the exact same math and confirm the result.
The algorithm
1. Commit phase (before the round)
When a new round is created, the server generates a cryptographically secure 32-byte random string called the server seed. We immediately compute and publish its SHA-256 hash — the server seed hash. Because SHA-256 is one-way, you cannot derive the seed from the hash, but once the seed is revealed you can easily confirm it matches the published hash.
server_seed_hash = sha256(server_seed)2. Locking the drand round
At round creation we also calculate which drand round will be the first beacon published after the raffle ends. drand produces a new signed beacon every 3 seconds, so the exact round number is deterministic based on the raffle end time. This means we cannot cherry-pick a convenient beacon after seeing the tickets.
3. Reveal & draw (after the round ends)
Once the raffle closes, the server fetches the signed beacon from drand for the locked round number. The beacon contains two public values: randomness and signature. We then combine our previously hidden server seed with drand's randomness and hash them together.
digest = sha256(server_seed + ":" + drand_randomness)
winning_index = int(digest[0:15], 16) mod ticket_countWe take the first 15 hexadecimal characters of the digest (≈60 bits) and interpret them as a large integer. The winning ticket position is that integer modulo the total number of tickets, ordered by purchase time. This gives a uniform distribution over all tickets.
4. On-chain publication
After the draw the following values are stored permanently in the round record and visible to everyone:
server_seed— the originally hidden seedserver_seed_hash— the hash published before the rounddrand_round— the fixed drand round numberdrand_randomness— the public randomness from the beacondrand_signature— the BLS signature proving the beacon is authenticwinning_index— the raw index computed from the formula above
Why this is safe
We can't change the seed
The hash is public before any ticket is sold. If we changed the seed after the fact, the hash wouldn't match.
We can't pick the beacon
The drand round is locked when the round starts. Rounds are published every 3 seconds — we can't influence which one appears.
drand is decentralized
The beacon is produced by the League of Entropy: Cloudflare, Protocol Labs, EPFL, Kudelski Security, and others. No single party controls it.
You can recompute everything
All inputs are public after the draw. You can run the exact same SHA-256 calculation and arrive at the same winning_index.
How to verify a round yourself
- Open the round you want to check and click Verify fairness.
- Copy the
server_seed_hashshown at the top. After the draw, copy the revealedserver_seed. Computesha256(server_seed)on your own machine and confirm it equals the hash. - Note the
drand_roundnumber. Open the public drand API to inspect the beacon:api.drand.sh . Replace/latestwith/{drand_round}. - Compare the
randomnessandsignaturefields from the API with the values shown in the fairness dialog. They must match exactly. - Compute
sha256(server_seed + ":" + randomness), take the first 15 hex characters, convert to an integer, then takemod ticket_count. The result must equal thewinning_indexshown. - The winning ticket is at position
winning_index mod ticket_countin the list of tickets ordered by purchase time.
async function sha256(text) {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
}
// Example (replace with real values from the round)
const seed = "abc123...";
const rand = "def456...";
const tickets = 47;
const digest = await sha256(seed + ":" + rand);
const index = parseInt(digest.slice(0, 15), 16) % tickets;
console.log("winning_index:", index);