The operator cannot bias whether this spin wins. The roll is an RFC 9381 ECVRF output keyed by our on-chain frozen key, with its input bound to the resolved slot hash so it cannot be precomputed. sha256(proof) is committed on-chain. Recompute AND verify everything — the binding, the ECVRF proof, the roll, the outcome — right here in your browser.
The widget recomputes the VRF input alpha = sha256(slotHash ‖ spinId), verifies the ECVRF proof itself (verifyVRF, RFC 9381) against the operator key, confirms the published roll IS beta[0:32], and decides WON/LOST locally — the entire chain, in your browser. Collector Crypt is an independent cross-check that the operator key is the one frozen on-chain.
import { sha256 } from '@noble/hashes/sha256';
// alpha = sha256(slotHash(32B) || utf8(spinId)) — the public VRF input.
export function vrfAlphaHex(slotHashHex: string, spinId: number): string {
const slot = Buffer.from(slotHashHex.replace(/^0x/, ''), 'hex');
const id = new TextEncoder().encode(String(spinId));
const msg = new Uint8Array(slot.length + id.length);
msg.set(slot, 0); msg.set(id, slot.length);
return Buffer.from(sha256(msg)).toString('hex');
}
// The published roll IS the VRF output beta[0:32].
export function rollFromBeta(betaHex: string): string {
return '0x' + betaHex.replace(/^0x/, '').slice(0, 64);
}
// Same [0,1) map + chance compare as the worker.
export function isWin(rollHex: string, chanceBps: number): boolean {
const n = BigInt(rollHex);
const unit = Number((n * 1_000_000_000_000n) / 2n ** 256n) / 1e12;
return unit < chanceBps / 10_000;
}
// Verify the ECVRF proof itself in-browser (no server, no CC dependency):
// import { verifyVRF, hexToBytes } from '@collectorcrypt/ecvrf';
// verifyVRF(hexToBytes(operatorKey), hexToBytes(alpha), hexToBytes(proof)) === true
// Cross-check operatorKey is the on-chain frozen one at
// https://vrf.collectorcrypt.com/#/verify.Node ≥ 20 · no dependencies · same output as the browser widget.