Nobody, including the commissioner and GridAction, can choose or re-roll a board’s numbers.
- At lock, the server picks a secret 256-bit seed and shows only its fingerprint (the commitment):
SHA-256("gridaction/draw/v1|commit|<boardId>|<seed>"). - At the draw, the seed is revealed. The digits come from it by a fixed recipe, so anyone can recompute them.
Since the commitment was visible before the draw, the seed can’t have been swapped for a luckier one.
The recipe
- A byte stream: block n is
SHA-256("gridaction/draw/v1|stream|<boardId>|<seed>|<n>"), for n = 0, 1, 2, …, read byte by byte. - A uniform number below k: take the next byte b; if
b < 256 − (256 mod k)useb mod k, otherwise discard it and take another (no bias). - Shuffle
[0…9]with Fisher–Yates: for i = 9 down to 1, swap position i with a uniform number below i + 1. - Row digits are the first shuffle, column digits the second, from the same stream.
- Smaller boards use the same ten digits per side, in the same order: on a 25-square board the first column covers the first two column digits, and so on.
Check it yourself
The board’s Rules tab has a “Sealed number draw” panel that runs this check for you. To check independently, take the board ID (from its address), the commitment, seed and digits from that panel. Paste this into any browser’s developer console (or Node 20+), then call verify(...):
async function verify({ boardId, seed, commitment, rowDigits, colDigits }) {
const D = "gridaction/draw/v1";
const sha = async (s) =>
new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)));
const hex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
let block = new Uint8Array(0), at = 0, n = 0;
const byte = async () => {
if (at >= block.length) { block = await sha(`${D}|stream|${boardId}|${seed}|${n++}`); at = 0; }
return block[at++];
};
const below = async (k) => {
for (;;) { const b = await byte(); if (b < 256 - (256 % k)) return b % k; }
};
const shuffle = async () => {
const d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for (let i = 9; i > 0; i--) { const j = await below(i + 1); [d[i], d[j]] = [d[j], d[i]]; }
return d;
};
const rows = await shuffle();
const cols = await shuffle();
return {
commitmentMatches: hex(await sha(`${D}|commit|${boardId}|${seed}`)) === commitment,
digitsMatch: rows.join() === rowDigits.join() && cols.join() === colDigits.join(),
};
}
Test it on this known example; both answers should be true:
await verify({
boardId: "board-1",
seed: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
commitment: "e24df9374ed70b5799bda3e69cd47aafdac4ed7e2a5c39377c095970843fcf89",
rowDigits: [2, 8, 6, 5, 4, 1, 7, 0, 3, 9],
colDigits: [3, 9, 8, 1, 7, 5, 4, 0, 6, 2],
});
Boards whose numbers were imported from a paper photo are marked as such and have no seed to check.