#!/usr/bin/env node /** * verify-draw.mjs — check a Hit Engine draw yourself. * * Zero dependencies. Node 18 or newer. Nothing to install, nothing to trust * beyond Node itself and, if you let it, the drand network. * * node verify-draw.mjs https://hitenginegaming.com/raffle//proof.json * node verify-draw.mjs ./proof.json * node verify-draw.mjs ./proof.json --offline (skip the drand relay check) * node verify-draw.mjs ./proof.json --json (machine-readable output) * * This is deliberately a SEPARATE implementation from the one the studio's site * runs. It was written to disagree if the site is wrong. If both agree, that is * evidence; if they differ, trust neither and say so publicly. * * What it cannot do: verify the drand BLS signature, which needs a pairing * library and would mean shipping a dependency you would then have to trust. * Instead it fetches the round from a drand relay and compares byte for byte — * which is a stronger check for most purposes, because it asks the source * rather than asking you to trust our arithmetic. Use --offline to skip it and * check the beacon by hand. * * Exit code 0 = every check passed. 1 = something failed. 2 = could not run. */ import { createHash, createHmac } from 'node:crypto'; import { readFile } from 'node:fs/promises'; const SPEC = 'he-draw-2'; const GENESIS = 1_692_803_367; const PERIOD = 3; const QUICKNET = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971'; const DEFAULT_RELAY = 'https://api.drand.sh'; const args = process.argv.slice(2); const flags = new Set(args.filter(a => a.startsWith('--'))); const source = args.find(a => !a.startsWith('--')); const OFFLINE = flags.has('--offline'); const AS_JSON = flags.has('--json'); const C = process.stdout.isTTY && !AS_JSON ? { ok: '\x1b[32m', bad: '\x1b[31m', dim: '\x1b[2m', b: '\x1b[1m', off: '\x1b[0m' } : { ok: '', bad: '', dim: '', b: '', off: '' }; const checks = []; const record = (n, title, pass, detail) => { checks.push({ n, title, pass, detail }); if (!AS_JSON) { const mark = pass ? `${C.ok}PASS${C.off}` : `${C.bad}FAIL${C.off}`; console.log(` ${mark} ${n}. ${title}`); if (detail) console.log(` ${C.dim}${detail}${C.off}`); } }; function die(msg) { console.error(`${C.bad}cannot run:${C.off} ${msg}`); process.exit(2); } // ------------------------------------------------------------------ helpers const hex = buf => Buffer.from(buf).toString('hex'); const sha256 = buf => createHash('sha256').update(buf).digest('hex'); const roundTime = r => new Date((GENESIS + (r - 1) * PERIOD) * 1000); function entryCommit(entries) { const body = entries.slice() .sort((a, b) => a.ticket_index - b.ticket_index) .map(e => `${e.ticket_index}:${e.bet_ref}:${String(e.username).toLowerCase()}`) .join('\n'); return sha256(Buffer.from(body, 'utf8')); } function pickSequence({ seed, randomness, commit, ticketCount, ordinal, count, nameAt, distinct }) { const key = `${SPEC}|${seed}|${randomness}`; const picks = []; const users = new Set(); const ceiling = Math.min(count, distinct, ticketCount); const cap = Math.min(ticketCount * 8 + 1000, 2_000_000); for (let i = 0; picks.length < ceiling && i < cap; i++) { const h = createHmac('sha256', key).update(`${commit}|${ordinal}|${i}`, 'utf8').digest('hex'); const idx = Number(BigInt('0x' + h) % BigInt(ticketCount)); if (picks.includes(idx)) continue; const u = String(nameAt(idx)).toLowerCase(); if (users.has(u)) continue; picks.push(idx); users.add(u); } return picks; } async function load(src) { if (!src) die('give me a proof.json path or URL.\n\n' + ' node verify-draw.mjs https://hitenginegaming.com/raffle//proof.json'); try { if (/^https?:\/\//i.test(src)) { const res = await fetch(src, { redirect: 'follow' }); if (!res.ok) die(`fetching ${src} returned HTTP ${res.status}`); return await res.json(); } return JSON.parse(await readFile(src, 'utf8')); } catch (e) { die(`could not read ${src}: ${e.message}`); } } // --------------------------------------------------------------------- main const p = await load(source); /* * Shape check before any verification runs. * * Exit codes are the contract this tool is published for: 0 verified, 1 the * draw does NOT verify, 2 could not run. Anyone scripting "check every Hit * Engine draw" acts on that distinction, and 1 is an accusation. * * Readable JSON that simply is not a proof used to fall through to the checks * and come out as 1 — so a monitoring script pointed at a URL that answered * with, say, {"error":"not found"} through a proxy would report the studio's * draw as invalid. That is the one wrong answer this tool must never give: * crying fraud when it was never handed a proof at all. */ for (const key of ['raffle', 'commitment', 'seal', 'beacon', 'draw', 'entries']) { if (p?.[key] == null) { die(`${source} is JSON, but not a he-draw proof — it has no "${key}". ` + `Fetch the proof.json linked from the round's page.`); } } if (!Array.isArray(p.entries)) die(`${source}: "entries" is not a list.`); if (!AS_JSON) { console.log(`\n${C.b}Hit Engine draw verification${C.off} ${C.dim}(${SPEC}, independent implementation)${C.off}`); console.log(`${C.dim}${'-'.repeat(64)}${C.off}`); console.log(` raffle ${p.raffle?.title ?? '(untitled)'} ${C.dim}${p.raffle?.slug ?? ''}${C.off}`); console.log(` state ${p.raffle?.state}`); if (p.is_demonstration) { console.log(` ${C.b}NOTE${C.off} this proof is a DEMONSTRATION round. No prize was awarded.`); } console.log(''); } if (p.spec !== SPEC) { record(0, `proof declares spec "${p.spec}"; this tool implements "${SPEC}"`, false, 'A different spec string means different rules. Do not compare the results.'); } else { record(0, `spec is ${SPEC}`, true); } // 1 — the revealed seed matches the commitment { const seed = p.reveal?.server_seed; const claimed = p.commitment?.server_seed_sha256; const pass = Boolean(seed && claimed) && sha256(Buffer.from(seed, 'hex')) === claimed; record(1, 'the revealed seed matches the commitment', pass, pass ? `sha256(seed) = ${claimed}` : 'The studio committed to one seed and revealed another. This is fatal.'); } // 2 — the commitment was published off-site before entries opened { const url = p.commitment?.publication_url; const at = p.commitment?.published_at; const opens = p.raffle?.opens_at; const pass = Boolean(url && at && opens) && new Date(at) <= new Date(opens); record(2, 'the commitment was published externally before entries opened', pass, pass ? `${at} ${url}` : 'A commitment that only ever existed in the studio\'s own database proves nothing.'); if (pass && !AS_JSON) { console.log(` ${C.dim}Open that URL and confirm its timestamp yourself — this tool cannot.${C.off}`); } } // 3 — the entry commitment recomputes { const recomputed = entryCommit(p.entries ?? []); const pass = recomputed === p.seal?.entry_commit; record(3, 'entry_commit recomputes from the published entry list', pass, pass ? recomputed : `published ${p.seal?.entry_commit}\n recomputed ${recomputed}`); } // 4 — ticket count and index integrity { const idx = (p.entries ?? []).map(e => e.ticket_index).sort((a, b) => a - b); const contiguous = idx.every((v, i) => v === i); const pass = idx.length === p.seal?.ticket_count && contiguous; record(4, 'ticket_count matches the list, and indices are 0..n-1 with no gaps', pass, pass ? `${idx.length} tickets` : `published ${p.seal?.ticket_count}, list has ${idx.length}` + (contiguous ? '' : ', and indices have gaps or duplicates')); } // 5 — THE ONE THAT MATTERS { const sealed = p.seal?.sealed_at ? new Date(p.seal.sealed_at) : null; const roundAt = p.beacon?.round ? roundTime(p.beacon.round) : null; const declared = p.beacon?.round_at ? new Date(p.beacon.round_at) : null; const scheduleOk = roundAt && declared && roundAt.getTime() === declared.getTime(); const pass = Boolean(sealed && roundAt) && sealed < roundAt && scheduleOk; record(5, 'THE SEAL PRECEDES THE BEACON — entries were frozen before the randomness existed', pass, pass ? `sealed ${sealed.toISOString()} < beacon ${roundAt.toISOString()}` : !scheduleOk ? `the declared beacon time does not match drand's schedule for round ${p.beacon?.round}` : 'The entry set was still changeable when the randomness became known. Fatal.'); } // 6 — the beacon, checked against drand itself if (p.beacon?.chain !== QUICKNET) { record(6, 'the beacon is on the drand quicknet chain', false, `proof names chain ${p.beacon?.chain}`); } else if (OFFLINE) { const derived = p.beacon?.signature ? sha256(Buffer.from(p.beacon.signature, 'hex')) : null; const pass = derived === p.beacon?.randomness; record(6, 'randomness == sha256(signature) [--offline: relay not contacted]', pass, pass ? 'Consistent, but NOT proof of authenticity. Check the round against a relay.' : 'The published randomness is not the hash of the published signature.'); } else { const relay = process.env.DRAND_RELAY || DEFAULT_RELAY; const url = `${relay}/${p.beacon.chain}/public/${p.beacon.round}`; try { const res = await fetch(url, { signal: AbortSignal.timeout(15_000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const live = await res.json(); const pass = live.randomness === p.beacon.randomness && live.signature === p.beacon.signature; record(6, 'the beacon matches what the drand network publishes for that round', pass, pass ? `${relay} agrees on round ${p.beacon.round}` : `relay says randomness ${live.randomness}\n proof says ${p.beacon.randomness}`); } catch (e) { record(6, 'the beacon matches the drand network', false, `could not reach ${relay} (${e.message}). Re-run with --offline, ` + 'or set DRAND_RELAY to a relay you prefer.'); } } // 7 — the draw recomputes { const byIndex = new Map((p.entries ?? []).map(e => [e.ticket_index, e.username])); const distinct = new Set((p.entries ?? []).map(e => String(e.username).toLowerCase())).size; const want = (p.raffle?.prize_count ?? 0) + (p.raffle?.reserve_count ?? 0); let pass = false, recomputed = []; if (p.reveal?.server_seed && p.beacon?.randomness && p.seal?.entry_commit && p.seal?.ticket_count) { recomputed = pickSequence({ seed: p.reveal.server_seed, randomness: p.beacon.randomness, commit: p.seal.entry_commit, ticketCount: p.seal.ticket_count, ordinal: p.draw?.ordinal ?? 1, count: want, nameAt: i => byIndex.get(i), distinct, }); const published = (p.draw?.picks ?? []).map(x => x.ticket_index); pass = recomputed.length === published.length && recomputed.every((v, i) => v === published[i]); } record(7, 'the winners and reserves recompute from the published inputs', pass, pass ? recomputed.map((i, rank) => `#${rank} ${byIndex.get(i)}${rank < p.raffle.prize_count ? '' : ' (reserve)'}`).join(' ') : `recomputed [${recomputed.join(', ')}]\n published [` + `${(p.draw?.picks ?? []).map(x => x.ticket_index).join(', ')}]`); } // ------------------------------------------------------------------- report const failed = checks.filter(c => !c.pass); if (AS_JSON) { console.log(JSON.stringify({ spec: SPEC, slug: p.raffle?.slug ?? null, is_demonstration: Boolean(p.is_demonstration), ok: failed.length === 0, checks, }, null, 2)); } else { console.log(''); if (failed.length === 0) { console.log(` ${C.ok}${C.b}All checks passed.${C.off}`); console.log(` ${C.dim}The winners follow from inputs fixed before the randomness existed.${C.off}`); if (OFFLINE) { console.log(` ${C.dim}Run without --offline, or check the beacon against a drand relay yourself.${C.off}`); } if (p.is_demonstration) { console.log(` ${C.dim}This was a demonstration round. No prize was awarded.${C.off}`); } } else { console.log(` ${C.bad}${C.b}${failed.length} check(s) FAILED.${C.off}`); console.log(` ${C.dim}Do not accept this draw. Please make the failure public —${C.off}`); console.log(` ${C.dim}a verifier that nobody acts on is decoration.${C.off}`); } console.log(''); } process.exit(failed.length === 0 ? 0 : 1);