How it works

Fair because of the order of events, checkable because everything is public.

Every draw follows the same published protocol, drawproof/1. The promise is simple: neither the organizer nor Drawproof can choose the winners after seeing the randomness, and anyone can re-check the result.

1. The list and rules are locked

When an organizer creates a draw, each entrant becomes a salted fingerprint (a SHA-256 hash). The fingerprints are sorted and combined into a single Merkle root. The rules (title, organizer, number of winners and alternates, entry count, the root, and the algorithm) are written as canonical JSON and hashed into one commitment.

2. Independent authorities timestamp it

The commitment is sent to two RFC 3161 timestamp authorities (FreeTSA and DigiCert, with Sectigo as a backup), which sign it with the current time. A draw only goes ahead with two valid signatures. The commitment is also submitted to OpenTimestamps calendars, which anchor it in the Bitcoin blockchain within a few hours. Every commitment is added to a public, hash-chained log, and the log head is timestamped every hour.

3. The randomness round is fixed in advance

Winners are seeded by drand, a randomness beacon run by the League of Entropy, an independent group of organizations. The draw uses the first quicknet round published at least 60 seconds after the latest timestamp on the commitment. That round did not exist when the list was locked, and no single party can predict or bias it.

4. Winners are computed, then re-computable

The selection is deterministic: a seed is derived from the commitment and the drand randomness, and a SHA-256 stream drives a partial Fisher-Yates shuffle over the sorted fingerprints, with rejection sampling so every entry has the same chance. After the draw, the salt is revealed so entrants can check their own entry and anyone can match winner names to their fingerprints.

What your browser checks on every proof page

  1. The rules hash to the commitment that was timestamped.
  2. The published fingerprints rebuild the locked Merkle root.
  3. The timestamp tokens are validly signed, chained to pinned authority root keys, and cover the commitment.
  4. The randomness round follows the rule above.
  5. The drand round's BLS signature verifies against the League of Entropy key.
  6. Re-running the algorithm gives the published winners and alternates.
  7. Each winner name hashes to its winning fingerprint under the revealed salt.

Entries are never published as plain text, but the fingerprints are not encryption: once the salt is revealed, anyone who already knows a handle can check whether it was in the list. Email addresses among the winners are shown masked.

You can also download the rules, timestamp tokens and OpenTimestamps proof from any proof page and verify them with standard tools, such as openssl ts -verify and the ots client.

What it does not prove

  • That this was the organizer's only draw. Nothing stops an organizer from creating several draws. If they reuse the same or a nearly identical list (90% or more of the same entries), every one of those proof pages lists the others, so a re-roll is visible. A substantially different list counts as a separate draw.
  • That the uploaded list is complete. The organizer provides the list. Each entrant can check their own entry, so leaving people out is detectable by them, but adding fake entries before locking is not prevented.
  • Who the organizer is. The organizer name is entered by whoever creates the draw. Trust the link the real organizer posts.
  • That the timestamp authorities are honest. The design relies on them not backdating; the Bitcoin anchor and the public log are independent checks.

Coming next

Draws from private lists (emails, customer lists) where the full list stays hidden and a zero-knowledge proof shows the winners were picked fairly from it, and anchoring on Nockchain, a proof-of-work network built around zero-knowledge proofs.

The algorithm, exactly

leaf_i     = SHA-256("drawproof/1/entry" || 0x00 || salt || utf8(entry_i))
leaves     = sort ascending, unique
root       = Merkle(leaves), leaf node = SHA-256(0x00 || leaf),
             inner node = SHA-256(0x01 || left || right), odd node promoted
commitment = SHA-256(canonical JSON of the rules)
round      = first drand quicknet round with time >= max(TSA times) + 60 s
randomness = SHA-256(drand BLS signature for that round)
seed       = SHA-256("drawproof/1/seed" || 0x00 || commitment || randomness)
block_i    = SHA-256(seed || u64be(i)), read as big-endian u64 values
uniform(n) = rejection sampling: draw x until x < 2^64 - (2^64 mod n), return x mod n
for i in 0 .. winners + alternates - 1:
    j = i + uniform(count - i); swap(leaves[i], leaves[j])
winners    = leaves[0 .. winners-1], alternates = the next ones in order