OpenGiveaway
VerifyProtocolPricingSign in
OGP · version 2.0

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:

Sources: email, x, instagram, tiktok, youtube, facebook, threads, telegram, discord, reddit, linkedin, username.

Aliases accepted in uploads and lookups: twitterx, insta/iginstagram, ytyoutube, fbfacebook, tgtelegram, e-mail/mailemail, handle/otherusername.

sourcenormalization
emailNFC, trim, lowercase. Must match ^[^\s@]+@[^\s@]+\.[^\s@]+$. Gmail dots and +tags are kept.
all othersNFC, 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

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

fieldcontents
protocol_version"2.0"
giveaway_idgiveaway identifier
titlegiveaway title
entriescount, merkle_root, leaf_domain, shard_size, shard_count
winner_countnumber of winners
prizesarray of {rank_from, rank_to, label}
schedulescheduled_at, timezone, frozen_at
randomnesssource: "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

  1. SHA256(JCS(manifest)) equals the published commitment.
  2. The Merkle root folded from shard_roots.bin equals manifest.entries.merkle_root, and the file's layout matches the manifest.
  3. Public lists only: every shard re-hashes to its root, keys are strictly increasing, and id_commit = SHA256(tag||salt||id).
  4. The block hash at block_height matches at least two independent explorers.
  5. Recomputed winners and prizes match result.json, including id_commit when shards are available.
  6. giveaway.ots verifies against the commitment and is anchored below block_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.

Reference implementation
npx opengiveaway-verify <artifacts-url>

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.