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:
serverSeedHashis the SHA-256 (hex) of theserverSeedhex string's UTF-8 bytes.candidatesHashis 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.- A stream of 32-bit numbers: block
nis HMAC-SHA256 keyed with the bytes ofserverSeed(hex-decoded) over<sha256 hex of clientSeed>:<nonce>:<n>, starting atn = 0. Each 32-byte block gives eight big-endian unsigned 32-bit integers, in order. - An integer below
mis drawn by rejection: take the next numberv; ifvis at leastfloor(2^32 / m) × m, discard it and take another; otherwise usev mod m. - Start with the indices
0 … candidateCount − 1. Forifrom0topick − 1, drawj = i + (integer below candidateCount − i)and swap positionsiandj. The firstpickindices, in order, arewinnerIndices.