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>
170 lines
9.9 KiB
JavaScript
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); });
|