deviant/scripts/chipnet-asm-sequencer.mjs
Local Dev ced2eda48d @
chore: checkpoint in-flight work across domains

Sweep of accumulated uncommitted work on design/dex-p2p-architecture into
one checkpoint (multiple sessions share this tree). Main areas:

- anthemus: covenant-wiring follow-ups (engine, ExchangePanel), AgoraScreen seam
- spv: ServerPool + p2p header transport (pool.ts, core/spv/*), wallet.ts wiring
- walletconnect: responder/useConnect updates + connect-responder-compat test
- wallet: send-tier-a rework + swap.ts, plus send-tier-a unit tests
- asm: chipnet throughput/stress harness (scripts/chipnet-asm-*) + ASM_Native docs
- app UI: screens/* and shell.css touch-ups; sat -> bits copy migration
- docs: new snapshots (INDEX regenerated), Parameters (WalletConnect/SSH/ASM),
  KnowledgeBase, WALLET-COORDINATION changelog

Checkpoint only; not a release. Secrets (Keys/, .chipnet-keys*, .env) remain
gitignored and were verified absent from the staged set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-08-14 02:00:43 +02:00

92 lines
6 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-asm-sequencer.mjs — prototype the QUEUE/SEQUENCER for one backed asset,
* one category, one reservoir. N swap requests from different addresses "arrive
* simultaneously"; instead of each building against the same confirmed reservoir
* (→ 1 wins, N-1 collide), the sequencer processes them in order and CHAINS them:
* request k spends the reservoir coin request k-1 just created. Result: N/N
* accepted, zero collisions, on one category.
*
* Prototype funding model: a coordinator wallet funds each claim's miner fee and
* the backed asset (ASM2) is delivered to the requesting address (gift-claim,
* recipient ≠ payer). In production each user funds their own input and signs it;
* the sequencer only orders + relays, and the covenant still enforces the peg —
* the sequencer can order but cannot cheat. Chipnet only.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const ARTIFACT = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/DeviantOpenMintFT.json'), 'utf8'));
const TOK = JSON.parse(readFileSync(join(ROOT, 'ChipnetTests', '.chipnet-asm2-token.json'), 'utf8')); // one backed asset
const COORD = (process.env.COORD ?? 'd').toLowerCase();
const USERS = (process.env.USERS ?? 'c,e,f,g,b').split(',').map((s) => s.trim().toLowerCase());
const AMOUNT = 60n, RATE = BigInt(TOK.rateSats), FEE_BPS = BigInt(TOK.founderFeeBps), MINER = 3000n;
const KEY = (w) => join(ROOT, w === 'a' ? '.chipnet-keys.json' : `.chipnet-keys-${w}.json`);
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 rev = (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 p2 = (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; };
async function main() {
const provider = new ElectrumNetworkProvider('chipnet');
const contract = new Contract(ARTIFACT, [rev(TOK.categoryId), RATE, FEE_BPS, hexToBin(TOK.founderLockingBytecodeHex)], { provider });
const { wif } = JSON.parse(readFileSync(KEY(COORD), 'utf8'));
const coord = await TestNetWallet.fromWIF(wif);
const coordSig = new SignatureTemplate(coord.privateKey);
// Load the requesting users (recipients of the backed asset).
const users = [];
for (const u of USERS) {
const { wif: w } = JSON.parse(readFileSync(KEY(u), 'utf8'));
const wal = await TestNetWallet.fromWIF(w);
users.push({ label: u.toUpperCase(), lb: p2(wal.publicKeyHash), tokenaddr: wal.tokenaddr });
}
const gross = AMOUNT * RATE, fee = (gross * FEE_BPS) / 10_000n;
let reservoir = (await provider.getUtxos(contract.tokenAddress)).find((x) => x.token?.nft?.capability === 'minting' && x.token?.category === TOK.categoryId && x.token?.amount > 0n);
if (!reservoir) throw new Error('reservoir not found');
let coordBch = (await provider.getUtxos(coord.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis))[0];
if (!coordBch) throw new Error('coordinator has no BCH');
const arriveISO = new Date().toISOString();
console.log(`\nSEQUENCER — one backed asset (${TOK.name}, 1 category), one reservoir ${reservoir.txid.slice(0, 12)}…:${reservoir.vout}`);
console.log(`${users.length} swap requests ARRIVE SIMULTANEOUSLY @ ${arriveISO}: [${users.map((u) => u.label).join(', ')}]`);
console.log(`(naive parallel would be 1/${users.length}; the sequencer queues + chains them)\n`);
const results = [];
for (let i = 0; i < users.length; i++) {
const u = users[i];
const change = BigInt(coordBch.satoshis) - gross - fee - MINER;
if (change < 546n) { results.push({ label: u.label, txid: null, err: 'coordinator out of BCH' }); break; }
const tb = new TransactionBuilder({ provider });
tb.addInput(reservoir, contract.unlock.claim(AMOUNT, u.lb));
tb.addInput(coordBch, coordSig.unlockP2PKH());
tb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: reservoir.token.amount - AMOUNT, category: TOK.categoryId, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: u.tokenaddr, amount: gross, token: { amount: AMOUNT, category: TOK.categoryId } });
tb.addOutput({ to: TOK.founderAddress, amount: fee });
tb.addOutput({ to: coord.cashaddr, amount: change });
try {
const spent = `${reservoir.txid.slice(0, 10)}…:${reservoir.vout}`;
const sent = await tb.send();
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
results.push({ label: u.label, txid, spent });
// advance the queue state (chain onto this tx's outputs)
reservoir = { txid, vout: 0, satoshis: 1000n, token: { amount: reservoir.token.amount - AMOUNT, category: TOK.categoryId, nft: { capability: 'minting', commitment: '' } } };
coordBch = { txid, vout: 3, satoshis: change };
} catch (e) {
results.push({ label: u.label, txid: null, err: (e?.message ?? String(e)).replace(/\s+/g, ' ').slice(0, 120) });
}
}
console.log('order | user | spent reservoir state | delivered');
results.forEach((r, i) => console.log(` ${i + 1} | ${r.label} | ${r.spent ?? '—'} | ${r.txid ? '✓ ' + AMOUNT + ' ' + TOK.name + ' ' + r.txid.slice(0, 16) + '…' : '✗ ' + r.err}`));
const ok = results.filter((r) => r.txid).length;
console.log(`\n=== SEQUENCER RESULT ===`);
console.log(` ${ok}/${users.length} requests settled — one category, one reservoir, ZERO collisions.`);
console.log(` Each request spent the reservoir coin the previous one created (a chain), so`);
console.log(` simultaneous arrival → ordered settlement. Submission is parallel; settlement is a queue.`);
}
main().then(() => process.exit(0)).catch((e) => { console.error('ERROR:', e?.message ?? e); process.exit(1); });