Every live draw's winner comes from an on-chain ECVRF proof (RFC 9381) under a key we froze on-chain before any draw ran — so the operator cannot bias which holder wins. Verify the proof in your browser or on Collector Crypt. Older draws used commit-reveal; the verifier below auto-detects and checks either.
FOUR STEPS - ONE VIEWPORT
Freeze > Snapshot > Prove > Roll
1
Freeze
We froze our VRF key on-chain before any draw
Our ECVRF public key is registered + FROZEN on-chain (Collector Crypt VRF program) under a fixed owner + label. Once frozen it cannot be swapped — every proof verifies against this one key.
vrf_authority: frozen(pk, owner, label)
2
Snapshot
Every holder balance is recorded at the close slot
When the window closes we record a Merkle root of every eligible $GACHA balance at that exact Solana slot. Nobody touches your balance — we just take a tamper-evident snapshot the weighted pick runs over.
The proof input alpha = sha256(slotHash ‖ drawId) binds the FUTURE slot blockhash — nobody (including us) can precompute it. We produce the ECVRF proof and commit sha256(proof) on-chain.
alpha = sha256(slotHash ‖ drawId)
4
Roll
beta[0:32] picks the winner
The roll is the first 32 bytes of the VRF output (beta). We walk the prefix-sum of weights — first holder whose slice covers the offset wins. verifyVRF confirms the proof; the roll is forced by it.
roll = beta[0:32] (beta = ECVRF output)
SECTION 02
Recompute the roll. Right here. In your browser.
Paste any draw id. For a VRF draw the widget recomputes the alpha binding, verifies the ECVRF proof itself (verifyVRF, RFC 9381) against the operator key, and confirms the roll IS beta[0:32] - all in your browser. For an older commit-reveal draw it re-runs the same HMAC-SHA-256 recompute. If our published roll does not match, the badge turns red.
● ON-CHAIN VERIFIER
Paste a draw id. Recompute the roll in your browser.
Try #1674, #1664, #1663
Enter a draw id to verify.
Want the full story of a single draw — which cards were in the pot, who won at what odds, and this proof, all on one page? Every draw has one: open any row in history and hit its FAIRNESS link, or go to /raffle/fairness/<draw id> directly.
REFERENCE IMPLEMENTATION
Copy-paste. Run it locally. Same answer.
fairness.tsTypeScript
import { verifyVRF, vrfProofToHash } from '@collectorcrypt/vrf-client';
import { sha256 } from '@noble/hashes/sha2';
// alpha binds the resolved slot hash + draw id (32B slot hash || utf8 id)
const alpha = sha256(concat(slotHashBytes, utf8(String(drawId))));
// 1. the ECVRF proof must verify under our FROZEN operator key
const ok = verifyVRF(operatorPubkey, alpha, proof); // RFC 9381 -> true
// 2. the published roll must be the first 32 bytes of the VRF output
const roll = vrfProofToHash(proof).slice(0, 32); // === published rollHex
Node ≥ 20 · no dependencies · same output as the browser widget.
SECTION 03
What we cannot do
×Swap the VRF key after it is frozen on-chain. Freeze is irreversible — verifyVRF checks that one frozen public key.
×Pick which Solana slot lands at the close. Validators choose that, and alpha binds its blockhash so the roll cannot be precomputed.
×Forge a proof. A wrong proof fails verifyVRF (RFC 9381 ECVRF) under the frozen key — the badge turns red.
×Substitute a roll silently. sha256(proof) is committed on-chain, so any swap is detectable.