BitRaffleDocs

Draw service

Verifying a draw

A proof lets anyone recompute a draw from scratch. Nothing in the check depends on trusting BitRaffle.

In the browser

Paste the proof (and, optionally, the list) into https://www.bitraffle.io/verify. The page does the whole check in the browser; the proof is never sent anywhere. It suits the people you announce winners to.

In your own code

import { createHash, createHmac } from "node:crypto";

const sha256 = (s) => createHash("sha256").update(s).digest("hex");

// Returns "ok", or the reason the proof fails.
function verifyDraw(proof, candidates) {
  if (proof.alg !== "fair-draw-v1") return "unknown algorithm";

  // 1. The seed we revealed is the one we committed to before you chose yours.
  if (sha256(proof.serverSeed) !== proof.serverSeedHash) return "seed does not match its commitment";

  // 2. Optional: the list drawn from is exactly yours.
  if (candidates) {
    const listHash = sha256(candidates.map((c) => `${c.length}:${c}`).join("|"));
    if (candidates.length !== proof.candidateCount || listHash !== proof.candidatesHash) return "candidate list differs";
  }

  // 3. Re-derive the winners: an HMAC-SHA256 stream keyed by the server seed...
  const clientHash = sha256(proof.clientSeed);
  let counter = 0, block = Buffer.alloc(0), offset = 0;
  const next = () => {
    if (offset + 4 > block.length) {
      block = createHmac("sha256", Buffer.from(proof.serverSeed, "hex"))
        .update(`${clientHash}:${proof.nonce}:${counter++}`)
        .digest();
      offset = 0;
    }
    const v = block.readUInt32BE(offset);
    offset += 4;
    return v;
  };
  // ...unbiased integers by rejection sampling...
  const below = (m) => {
    const limit = Math.floor(0x100000000 / m) * m;
    let v = next();
    while (v >= limit) v = next();
    return v % m;
  };
  // ...and a partial Fisher-Yates shuffle. Order matters: first drawn first.
  const idx = Array.from({ length: proof.candidateCount }, (_, i) => i);
  for (let i = 0; i < proof.pick; i++) {
    const j = i + below(proof.candidateCount - i);
    [idx[i], idx[j]] = [idx[j], idx[i]];
  }
  const winners = idx.slice(0, proof.pick);
  return winners.join() === proof.winnerIndices.join() ? "ok" : "winners do not match";
}
import hashlib, hmac

def sha256(s: str) -> str:
    return hashlib.sha256(s.encode()).hexdigest()

def js_len(s: str) -> int:
    # Candidate lengths are counted like JavaScript's string length (UTF-16 units).
    return len(s.encode("utf-16-le")) // 2

def verify_draw(proof: dict, candidates=None) -> str:
    if proof["alg"] != "fair-draw-v1":
        return "unknown algorithm"
    # 1. The seed we revealed is the one we committed to before you chose yours.
    if sha256(proof["serverSeed"]) != proof["serverSeedHash"]:
        return "seed does not match its commitment"
    # 2. Optional: the list drawn from is exactly yours.
    if candidates is not None:
        list_hash = sha256("|".join(f"{js_len(c)}:{c}" for c in candidates))
        if len(candidates) != proof["candidateCount"] or list_hash != proof["candidatesHash"]:
            return "candidate list differs"
    # 3. Re-derive the winners: an HMAC-SHA256 stream keyed by the server seed...
    key = bytes.fromhex(proof["serverSeed"])
    client_hash = sha256(proof["clientSeed"])
    def stream():
        counter = 0
        while True:
            block = hmac.new(key, f"{client_hash}:{proof['nonce']}:{counter}".encode(), hashlib.sha256).digest()
            counter += 1
            for o in range(0, 32, 4):
                yield int.from_bytes(block[o:o + 4], "big")
    nxt = stream()
    # ...unbiased integers by rejection sampling...
    def below(m: int) -> int:
        limit = (2**32 // m) * m
        v = next(nxt)
        while v >= limit:
            v = next(nxt)
        return v % m
    # ...and a partial Fisher-Yates shuffle. Order matters: first drawn first.
    idx = list(range(proof["candidateCount"]))
    for i in range(proof["pick"]):
        j = i + below(proof["candidateCount"] - i)
        idx[i], idx[j] = idx[j], idx[i]
    return "ok" if idx[:proof["pick"]] == proof["winnerIndices"] else "winners do not match"

The algorithm: fair-draw-v1

Precise enough to implement in any language:

  1. serverSeedHash is the SHA-256 (hex) of the serverSeed hex string's UTF-8 bytes.
  2. candidatesHash is the SHA-256 (hex) of each candidate written as <length>:<candidate>, joined with |. Length counts UTF-16 code units, as JavaScript does; for plain ASCII that is the number of characters.
  3. A stream of 32-bit numbers: block n is HMAC-SHA256 keyed with the bytes of serverSeed (hex-decoded) over <sha256 hex of clientSeed>:<nonce>:<n>, starting at n = 0. Each 32-byte block gives eight big-endian unsigned 32-bit integers, in order.
  4. An integer below m is drawn by rejection: take the next number v; if v is at least floor(2^32 / m) × m, discard it and take another; otherwise use v mod m.
  5. Start with the indices 0 … candidateCount − 1. For i from 0 to pick − 1, draw j = i + (integer below candidateCount − i) and swap positions i and j. The first pick indices, in order, are winnerIndices.