// Step 2: trait-blind deal of every reserve chair we control, seeded by the hash of a FUTURE Robinhood block. // This file does not import the art or trait table, and must never do so. Inputs are token ids and owners only. // // node shuffle.mjs --block // N must not exist yet when you run this (announce N publicly first; the chain makes ~10 blocks a second, so // N = current + 6000 is about 10 minutes out). The script freezes the input list right away // (shuffle-commit-.json), waits for block N plus 20, reads its hash, deals, writes shuffle-result.json. // node shuffle.mjs --verify // anyone can re-run the deal from shuffle-result.json and the chain's block hash; prints MATCH or MISMATCH. // node shuffle.mjs --dummy-seed <0xhash> (rehearsal only; writes shuffle-dummy.json, never shuffle-result.json) // // The deal: input ids ascending; for i = n-1 down to 1, j = uint256(keccak256(abi.encode(seedHash, i))) mod (i+1), // swap a[i], a[j]. Then pool = first (n - 100), Hood Street = next 50, stacc = last 50; each list printed ascending. import { context, reserveHoldings, fisherYates, split, readJson, writeJson, existsSync, sleep, log, ethers, A } from './reshuffle-lib.mjs'; const arg = (k) => { const i = process.argv.indexOf(k); return i >= 0 ? process.argv[i + 1] : null; }; const ctx = await context({ live: false, fork: false }); // read-only mainnet RPC, no key, no fork const P = ctx.provider; const rerun = 'node shuffle.mjs --verify'; if (process.argv.includes('--verify')) { const r = readJson('shuffle-result.json'); const b = await P.getBlock(r.seedBlock); const hash = b.hash; const again = split(fisherYates(r.input, hash)); const same = hash === r.seedHash && ['pool', 'hoodStreet', 'stacc'].every((k) => JSON.stringify(again[k]) === JSON.stringify(r.split[k])); log(same ? `MATCH: block ${r.seedBlock} hash ${hash} reproduces every list` : `MISMATCH (chain hash ${hash}, recorded ${r.seedHash})`); process.exit(same ? 0 : 1); } const dummy = arg('--dummy-seed'); const blockArg = arg('--block'); if (!dummy && !blockArg) { log('usage: node shuffle.mjs --block | --verify | --dummy-seed '); process.exit(2); } const N0 = blockArg ? Number(blockArg) : null; if (N0 && !existsSync(new URL(`shuffle-commit-${N0}.json`, import.meta.url).pathname) && N0 <= (await P.getBlockNumber())) { log(`REFUSED: block ${N0} already exists. Pick a block in the future and announce it first.`); process.exit(1); } const h = await reserveHoldings(ctx); if (h.elsewhere.length) log(`note: ${h.elsewhere.length} reserve ids are no longer ours (sold or given): ${h.elsewhere.map((x) => x.id).join(',')}`); const input = [...h.launcher, ...h.inVault].sort((a, b) => a - b); if (dummy) { const res = { DUMMY: true, seedHash: dummy, input, split: split(fisherYates(input, dummy)) }; writeJson('shuffle-dummy.json', res); log(`DUMMY deal from ${dummy}: pool ${res.split.pool.length}, Hood Street ${res.split.hoodStreet.length}, stacc ${res.split.stacc.length} (shuffle-dummy.json)`); process.exit(0); } const N = Number(blockArg); // No re-rolls: once a real deal exists, a second deal from another block is refused. The first announced block wins. if (existsSync(new URL('shuffle-result.json', import.meta.url).pathname)) { const prev = readJson('shuffle-result.json'); if (prev.seedBlock !== N) { log(`REFUSED: shuffle-result.json already holds the deal from block ${prev.seedBlock}. Re-dealing from another block would be a re-roll.`); process.exit(1); } } const commitFile = `shuffle-commit-${N}.json`; const now = await P.getBlockNumber(); let commit; if (existsSync(new URL(commitFile, import.meta.url).pathname)) { commit = readJson(commitFile); if (JSON.stringify(commit.input) !== JSON.stringify(input)) log('WARNING: reserve holdings changed since the commit; the committed input list is used.'); } else { if (N <= now) { log(`REFUSED: block ${N} was produced while the input was being read (latest ${now}). Pick a block further out.`); process.exit(1); } commit = { seedBlock: N, committedAtBlock: now, committedAt: new Date().toISOString(), input, inputSha256: ethers.sha256(ethers.toUtf8Bytes(JSON.stringify(input))) }; writeJson(commitFile, commit); log(`committed input (${input.length} ids, sha256 ${commit.inputSha256}) at block ${now}; seed block ${N} is ~${Math.round((N - now) / 10)} s away`); } if (commit.committedAtBlock >= N) { log('REFUSED: commit was not made before the seed block'); process.exit(1); } while ((await P.getBlockNumber()) < N + 20) { await sleep(3000); } const b = await P.getBlock(N); const seedHash = b.hash; const res = { seedBlock: N, seedHash, seedBlockTime: new Date(b.timestamp * 1000).toISOString(), committedAtBlock: commit.committedAtBlock, input: commit.input, inputSha256: commit.inputSha256, method: 'ids ascending; for i=n-1..1: j = uint256(keccak256(abi.encode(bytes32 seedHash, uint256 i))) % (i+1); swap; pool = first n-100, hoodStreet = next 50, stacc = last 50', split: split(fisherYates(commit.input, seedHash)), rerun, traitBlind: 'shuffle.mjs and reshuffle-lib.mjs import no art or trait data' }; writeJson('shuffle-result.json', res); log(`seed block ${N} hash ${seedHash}`); log(`pool (${res.split.pool.length}): ${res.split.pool.join(',')}`); log(`Hood Street (${res.split.hoodStreet.length}): ${res.split.hoodStreet.join(',')}`); log(`stacc (${res.split.stacc.length}): ${res.split.stacc.join(',')}`); log(`wrote shuffle-result.json. Anyone can check it: ${rerun}`); process.exit(0);