The verifier, in the open.
This is the exact math your browser runs to re-check every box, Evolve, and Mystery open. It uses only public inputs and standard Web Crypto — no PackLab server, no trust in us. Read it, copy it, run it against the test vectors or any completed box.
Boxes
// packages/fairness/src/verify.ts — verifyLot(commitments, reveal)
// Re-derives a whole box in your browser. No PackLab server involved.
// 1. The published seed matches its pre-committed fingerprint
sha256hex(reveal.serverSeed) === commitments.seedCommitment
// 2. The published card list + nonce match the box fingerprint
// (manifest v2 binds beaconRef, so a swapped round breaks this hash)
sha256hex(canonicalJson(reveal.manifest)) === commitments.manifestCommitment
reveal.manifest.beaconRef === commitments.beaconRef
// 3. The beacon value is the real drand round it claims to be
(await drand.get(commitments.beaconRef)).randomness === reveal.beaconValue
// 4. Reproduce every pack, byte for byte
finalSeed = HMAC_SHA256(hexDecode(serverSeed), utf8(beaconValue))
order = shuffledOrder(finalSeed, packCount) // Fisher-Yates
// pack at draw position p = canonical pack order[p]; its cards are that
// pack's slots in manifest order → compare to what was minted.Evolve & Mystery Pack
// src/lib/evolveVerify.ts + src/lib/mysteryVerify.ts
// The SAME roll for Evolve and Mystery — recomputed from public inputs only.
const hashOk = sha256hex(serverSeed) === serverSeedHash;
const sig = HMAC_SHA256(hexDecode(serverSeed), utf8(drandRandomness));
const roll = parseInt(hex(sig).slice(0, 13), 16) / 2 ** 52; // 52 bits → [0,1)
const rollOk = Math.abs(roll - recordedRoll) < 1e-9;
// Evolve: you won iff roll < winProbBps / 10000
// Mystery: the winning set is a cumulative bucket-select of roll over the
// committed odds table (whose SHA-256 you also re-check):
const tableOk = sha256hex(JSON.stringify(weightTable)) === weightTableHash;
let acc = 0, target = roll * sum(weightTable.map(w => w.weight));
for (const w of weightTable) { acc += w.weight; if (target < acc) return w.setId; }The shipped source
The snippets above mirror these files in the public repository. The full, runnable verifier is a few hundred lines total.
packages/fairness/src/verify.tsverifyLot — the full box re-derivationpackages/fairness/src/shuffle.tsshuffledOrder — the Fisher-Yates permutationpackages/fairness/src/beacon.tsdrand resolution + round timingapps/web/src/lib/evolveVerify.tsverifyEvolveInBrowser — the Evolve rollapps/web/src/lib/mysteryVerify.tsverifyMysteryInBrowser — the Mystery roll + bucket-select