deviant/scripts/chipnet-sortition-beacon-test.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
Snapshot of accumulated work-in-progress on top of the baseline commit.

This session — retire Tier B from the wallet shell (single-tier model, glossary
"Tiers & trust"):
- SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5
  raw-BCH co-display, and the ADR-0013 friction ladder.
- ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis
  flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted.
- QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction
  thresholds" + "Tier explainer" rows removed; registry-trust note reworded.
- Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors.

Pre-existing uncommitted work also folded in:
- USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records)
- ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests)
- snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045
- mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:01:37 +02:00

170 lines
9.9 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-sortition-beacon-test.mjs — proves the Athenae SortitionBeacon
* (commit-reveal randomness, the BCH-native core of "hybrid E") on chipnet.
*
* Flow (cohort N=3, buyer/funder = B):
* 1. genesis beacon NFT commitment = 0x01 ++ tag (phase COMMIT, root=tag)
* 2. commit(H(s1)), commit(H(s2)), commit(H(s3)) (hash-chain root advances)
* 3. NEGATIVE: finalize(s1, s2, WRONG) must be REJECTED (binding: wrong reveal fails)
* 4. finalize(s1, s2, s3) with locktime >= commitDeadline (CLTV gate) -> seed
* 5. read seed on-chain, verify == sha256(s1++s2++s3), derive sortition draw
* indices juror_k = int(sha256(seed ++ k)) mod poolSize (streaming draw)
*
* Proves: trustless on-chain entropy (one honest committer => unbiasable),
* binding commits (wrong reveal rejected), CLTV timing anchor, and a deterministic
* seed consumable by the sortition draw. Writes .chipnet-sortition-beacon-test.json
* Run: node --import tsx scripts/chipnet-sortition-beacon-test.mjs --yes
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { createHash, randomBytes } from 'node:crypto';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const STATE_FILE = join(ROOT, 'ChipnetTests', '.chipnet-sortition-beacon-test.json');
const ARTIFACT_PATH = join(ROOT, 'contracts/dist/SortitionBeacon.json');
const KEYS = join(ROOT, '.chipnet-keys-b.json');
const POSITION_VALUE = 1000n, POOL_SIZE = 29;
const hexToBin = (h) => { const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16); return o; };
const binToHex = (b) => Buffer.from(b).toString('hex');
const sha256 = (buf) => new Uint8Array(createHash('sha256').update(Buffer.from(buf)).digest());
const cat = (...arrs) => { const t = arrs.reduce((n, a) => n + a.length, 0); const o = new Uint8Array(t); let p = 0; for (const a of arrs) { o.set(a, p); p += a.length; } return o; };
const txidToConsensusBytes = (h) => { const d = hexToBin(h); const o = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) o[i] = d[d.length - 1 - i]; return o; };
const p2pkh = (pkh) => { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function ensureEligibleUtxo(w) {
let u = await w.getUtxos(); let e = u.filter((x) => x.vout === 0 && !x.token);
if (e.length) return e[0];
const bad = u.filter((x) => !(x.vout === 0 && !x.token));
const r = await w.send([{ cashaddr: w.cashaddr, value: 5000n, unit: 'sat' }], bad.length ? { utxoIds: bad } : undefined);
console.log(` splitter ${r?.txId ?? r?.txid}`); await sleep(12_000);
u = await w.getUtxos(); e = u.filter((x) => x.vout === 0 && !x.token);
if (!e.length) throw new Error('no eligible vout-0'); return e[0];
}
async function getBeacon(provider, contract, categoryId) {
const ru = await provider.getUtxos(contract.tokenAddress);
const pos = ru.find((u) => u.token?.nft && u.token?.category === categoryId);
if (!pos) throw new Error('beacon not found'); return pos;
}
async function spend(provider, w, sig, contract, pos, unlocker, newCommitmentHex, { locktime } = {}) {
const cash = (await provider.getUtxos(w.cashaddr)).filter((u) => !u.token).sort((a, b) => Number(BigInt(b.satoshis) - BigInt(a.satoshis)));
const minerFee = 2000n;
const picked = []; let tot = 0n;
for (const u of cash) { picked.push(u); tot += BigInt(u.satoshis); if (tot >= POSITION_VALUE + minerFee + 1000n) break; }
const change = BigInt(pos.satoshis) + tot - POSITION_VALUE - minerFee;
const tb = new TransactionBuilder({ provider });
if (locktime !== undefined) tb.setLocktime(locktime);
tb.addInput(pos, unlocker, { sequence: 0xfffffffe });
for (const u of picked) tb.addInput(u, sig.unlockP2PKH(), { sequence: 0xfffffffe });
tb.addOutput({ to: contract.tokenAddress, amount: POSITION_VALUE,
token: { amount: 0n, category: pos.token.category, nft: { capability: 'mutable', commitment: newCommitmentHex } } });
if (change >= 546n) tb.addOutput({ to: w.cashaddr, amount: change });
const sent = await tb.send();
return typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
}
async function main() {
if (!process.argv.includes('--yes')) { console.log('Re-run with --yes.'); return; }
const artifact = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8'));
const w = await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS, 'utf8')).wif);
const provider = new ElectrumNetworkProvider('chipnet');
const sig = new SignatureTemplate(w.privateKey);
const state = existsSync(STATE_FILE) ? JSON.parse(readFileSync(STATE_FILE, 'utf8')) : {};
const save = () => writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
// Secrets + tag (deterministic across resume via state)
if (!state.secrets) {
state.tag = binToHex(sha256(Buffer.from('DeviantSortitionBeacon:round-1')));
state.secrets = [0, 1, 2].map(() => binToHex(randomBytes(32)));
save();
}
const tag = hexToBin(state.tag);
const secrets = state.secrets.map(hexToBin);
const hashes = secrets.map(sha256);
// off-chain hash-chain root after k commits
const rootAfter = [tag];
for (let i = 0; i < 3; i++) rootAfter.push(sha256(cat(rootAfter[i], hashes[i])));
const seedExpected = sha256(cat(secrets[0], secrets[1], secrets[2]));
const height = await provider.getBlockHeight();
if (!state.commitDeadline) { state.commitDeadline = height - 2; save(); } // CLTV gate in the recent past
const ctorArgs = [tag, BigInt(state.commitDeadline)];
const contract = new Contract(artifact, ctorArgs, { provider });
// 1. genesis
if (!state.categoryId) {
const utxo = await ensureEligibleUtxo(w);
const c = utxo.txid;
const ct = new Contract(artifact, [tag, BigInt(state.commitDeadline)], { provider });
console.log(`beacon ${ct.tokenAddress}\ncategory ${c} · tag ${state.tag.slice(0, 16)}… · commitDeadline ${state.commitDeadline}`);
const genCommit = binToHex(cat(new Uint8Array([0x01]), tag)); // phase COMMIT, root = tag
const r = await w.tokenGenesis({ cashaddr: ct.tokenAddress, amount: 0n, value: POSITION_VALUE,
nft: { capability: 'mutable', commitment: genCommit } }, [], { utxoIds: [utxo] });
console.log(`genesis ${r?.txId ?? r?.txid}`);
state.categoryId = c; state.commits = []; save(); await sleep(10_000);
}
// 2. three commits
for (let i = state.commits.length; i < 3; i++) {
const pos = await getBeacon(provider, contract, state.categoryId);
const newCommit = binToHex(cat(new Uint8Array([0x01]), rootAfter[i + 1]));
const txid = await spend(provider, w, sig, contract, pos, contract.unlock.commit(hashes[i]), newCommit);
console.log(`commit ${i + 1}: H(s${i + 1})=${binToHex(hashes[i]).slice(0, 12)}… → root ${binToHex(rootAfter[i + 1]).slice(0, 12)}… · ${txid}`);
state.commits.push(txid); save(); await sleep(9_000);
}
// 3. NEGATIVE — wrong reveal must be rejected
if (!state.negativeChecked) {
const pos = await getBeacon(provider, contract, state.categoryId);
const wrong = sha256(Buffer.from('not-the-secret'));
const fakeSeed = binToHex(cat(new Uint8Array([0x02]), sha256(cat(secrets[0], secrets[1], wrong))));
try {
await spend(provider, w, sig, contract, pos, contract.unlock.finalize(secrets[0], secrets[1], wrong), fakeSeed, { locktime: state.commitDeadline });
console.log(' ✗ UNEXPECTED: wrong-reveal finalize succeeded — binding NOT enforced!');
state.negativeChecked = 'FAILED';
} catch (e) {
console.log(` ✓ wrong-reveal finalize correctly REJECTED: ${String(e?.message ?? e).split('\n')[0].slice(0, 80)}`);
state.negativeChecked = true;
}
save(); await sleep(2_000);
}
// 4. finalize (correct, past CLTV gate)
if (!state.finalizeTxid) {
const pos = await getBeacon(provider, contract, state.categoryId);
const finalCommit = binToHex(cat(new Uint8Array([0x02]), seedExpected));
const txid = await spend(provider, w, sig, contract, pos, contract.unlock.finalize(secrets[0], secrets[1], secrets[2]), finalCommit, { locktime: state.commitDeadline });
console.log(`finalize: seed = sha256(s1++s2++s3) = ${binToHex(seedExpected).slice(0, 16)}… · locktime ${state.commitDeadline} · ${txid}`);
state.finalizeTxid = txid; save(); await sleep(9_000);
}
// 5. read seed on-chain, verify, derive sortition draw
const pos = await getBeacon(provider, contract, state.categoryId);
const onchain = pos.token.nft.commitment; // 0x02 ++ seed
const onchainSeed = onchain.slice(2); // strip phase byte
state.seedOnchain = onchainSeed;
state.seedMatches = onchainSeed === binToHex(seedExpected);
const draw = [];
for (let k = 0; k < 7; k++) {
const d = sha256(cat(seedExpected, new Uint8Array([k])));
draw.push(Number(BigInt('0x' + binToHex(d)) % BigInt(POOL_SIZE)));
}
state.draw = draw; save();
console.log(`\n=== RESULT — SortitionBeacon (commit-reveal, hybrid-E core) ===`);
console.log(` commits: ${state.commits.length} (hash-chain root binding all three secrets)`);
console.log(` wrong-reveal rejected: ${state.negativeChecked === true ? '✓' : state.negativeChecked}`);
console.log(` finalize past CLTV gate (h≥${state.commitDeadline}): ${state.finalizeTxid}`);
console.log(` on-chain seed: ${onchainSeed.slice(0, 24)}…`);
console.log(` off-chain seed: ${binToHex(seedExpected).slice(0, 24)}…`);
console.log(` SEED MATCH: ${state.seedMatches ? '✓ beacon produced the committed-reveal seed on-chain' : '✗ MISMATCH'}`);
console.log(` sortition draw (juror indices mod ${POOL_SIZE}, streaming k=0..6): [${draw.join(', ')}]`);
}
main().then(() => process.exit(0)).catch((e) => { console.error('FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack); process.exit(1); });