The OpenGiveaway Protocol
How a giveaway is committed, drawn and checked. This page is the whole specification — enough to write an independent verifier without reading our source code, which is the only way a verifiable draw means anything.
What the protocol guarantees
Three properties, each checkable by anyone holding only public data.
The list was fixed first
Entries, prizes, winner count and the draw block collapse to one hash, timestamped on Bitcoin before the draw block exists.
Nobody chose the randomness
The seed comes from a Bitcoin block named in advance. Its hash cannot be known — by anyone — until it is mined.
The draw is reproducible
Same list plus same block always yields the same winners. A single changed entry changes the commitment.
Protocol v2 extends v1 in three ways: entry lists scale to tens of millions, identifiers are salted so a list can stay private, and prize tiers are committed. The Merkle construction and the winner selection are unchanged from v1.
1 Identifiers
Each entry is an account on a source. The canonical identifier is <source>:<account> — for example x:alice, instagram:alice.art or email:[email protected]. That string is what gets salted and committed, so the source is part of the entry:
x:aliceandinstagram:aliceare two different entries.- To prove inclusion, a participant must give the same source.
Sources: email, x, instagram, tiktok, youtube, facebook, threads, telegram, discord, reddit, linkedin, username.
Aliases accepted in uploads and lookups: twitter→x, insta/ig→instagram, yt→youtube, fb→facebook, tg→telegram, e-mail/mail→email, handle/other→username.
| source | normalization |
|---|---|
email | NFC, trim, lowercase. Must match ^[^\s@]+@[^\s@]+\.[^\s@]+$. Gmail dots and +tags are kept. |
| all others | NFC, trim. A profile URL (https://x.com/Alice?s=20) becomes its first path segment. Leading @ stripped, lowercase, no whitespace. |
When no source is given, input matching the email pattern is email and anything else is username.
2 Entries
Every giveaway has a 32-byte secret that never leaves the server. From it:
saltKey = HMAC-SHA256(secret, "ogv/salt/v2")
lookupKey = HMAC-SHA256(secret, "ogv/lookup/v2")
salt = HMAC-SHA256(saltKey, id)
id_commit = SHA256("ogv-id-v2" || salt || id)
lookup_key = HMAC-SHA256(lookupKey, id) -- routing only, not committed
leaf = SHA256(0x00 || JCS({"domain":"giveaway-entry-v2",
"giveaway_id":G,
"id_commit":hex(id_commit)}))Entries are de-duplicated by id, then sorted by lookup_key, in unsigned bytewise order. An entry's position is its 0-based index in that order.
3 Merkle tree and shards
node = SHA256(0x01 || left || right). An odd node at the end of a level is promoted unchanged, never duplicated.- Leaves are cut into shards of
shard_sizeleaves, a power of two (4096 in production). Every shard except the last is a complete subtree, so the root over the shard roots — using the same promotion rule — equals the root over all leaves.
shard_roots.bin = "OGR2" | u32 shard_size | u32 shard_count
| f64 entry_count | 32 bytes × shard_count
shard file = "OGS2" | u32 n
| n × (lookup_key[32] salt[32] id_commit[32] u16 len id[len])Inclusion proof: {position, shard_index, local_index, salt, shard_path[], top_path[]}. Each path lists only the sibling hashes; sides and promoted levels follow from the index and the leaf count, so every proof is bound to its position. A verifier recomputes leaf from the identifier the participant typed plus salt, folds it to the shard root, then to the committed root.
4 Manifest and commitment
| field | contents |
|---|---|
protocol_version | "2.0" |
giveaway_id | giveaway identifier |
title | giveaway title |
entries | count, merkle_root, leaf_domain, shard_size, shard_count |
winner_count | number of winners |
prizes | array of {rank_from, rank_to, label} |
schedule | scheduled_at, timezone, frozen_at |
randomness | source: "bitcoin", selection_rule: "predetermined-block-height", block_height, min_confirmations |
commitment = SHA256(JCS(manifest)), where JCS is RFC 8785 canonical JSON. It is OpenTimestamped at freeze as giveaway.ots. Prize tiers are contiguous, non-overlapping, and cover ranks 1..winner_count exactly.
5 Block selection
The organizer picks block_height when scheduling. The app suggests the block expected at a chosen date and time — tip + ceil((t − now) / 600s) — and the number can be edited. The height must be at least 150 blocks ahead when scheduling.
Entries freeze manually, or automatically once the block is ≤ 144 blocks (~24h) away. A freeze needs at least 12 blocks of headroom so the OpenTimestamps anchor lands before the randomness block; otherwise the giveaway is marked invalid.
The draw runs once tip ≥ block_height + min_confirmations − 1. It also requires the OTS anchor height to be below block_height, unless the owner overrides — ots_override: true in result.json records that publicly.
6 Winners
Identical to protocol v1.
seed = SHA256(commitment_bytes || block_hash_bytes) -- block hash in explorer (display) hex block_i = SHA256(seed || u64be(i)); read as big-endian uint32 values nextBelow(n): v = next uint32 reject v >= floor(2^32 / n) * n -- rejection sampling: no modulo bias return v % n for i in 0 .. winner_count-1: j = i + nextBelow(count - i) swap(pool[i], pool[j]) winner[i] = pool[i]
Rank i+1 receives the prize whose tier contains that rank. result.json publishes {commitment, block{height,hash}, seed, winners[{rank, position, prize, id_commit}], drawn_at}.
7 Verification checklist
SHA256(JCS(manifest))equals the published commitment.- The Merkle root folded from
shard_roots.binequalsmanifest.entries.merkle_root, and the file's layout matches the manifest. - Public lists only: every shard re-hashes to its root, keys are strictly increasing, and
id_commit = SHA256(tag||salt||id). - The block hash at
block_heightmatches at least two independent explorers. - Recomputed winners and prizes match
result.json, includingid_commitwhen shards are available. giveaway.otsverifies against the commitment and is anchored belowblock_height.
Steps 1–5 run in any browser. Step 6 needs a Bitcoin node or the opentimestamps client, so it is done by the CLI.
Check it yourself
No account, no sign-in, no trust in us required.
The protocol and verifier packages are open source and carry the canonical copy of this spec in docs/PROTOCOL.md. Where this page and that file disagree, the file wins.