EuroWin
Back to raffles

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.

01

Commit

Before the round starts we generate a secret seed and publish its SHA-256 hash. The seed itself stays hidden.

02

Reveal

After the round ends we fetch a signed random number from drand's public beacon. Nobody can predict it in advance.

03

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_count

We 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 seed
  • server_seed_hash — the hash published before the round
  • drand_round — the fixed drand round number
  • drand_randomness — the public randomness from the beacon
  • drand_signature — the BLS signature proving the beacon is authentic
  • winning_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

  1. Open the round you want to check and click Verify fairness.
  2. Copy the server_seed_hash shown at the top. After the draw, copy the revealed server_seed. Compute sha256(server_seed) on your own machine and confirm it equals the hash.
  3. Note the drand_round number. Open the public drand API to inspect the beacon:api.drand.sh . Replace /latest with /{drand_round}.
  4. Compare the randomness and signature fields from the API with the values shown in the fairness dialog. They must match exactly.
  5. Compute sha256(server_seed + ":" + randomness), take the first 15 hex characters, convert to an integer, then take mod ticket_count. The result must equal the winning_index shown.
  6. The winning ticket is at position winning_index mod ticket_count in the list of tickets ordered by purchase time.
Quick check in browser console
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);
Back to live raffle