deviant/scripts/chipnet-asm-throughput.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

131 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* chipnet-asm-throughput.mjs — MAX throughput for a SINGLE backed asset (one
* category), all addresses, ROUND-TRIP swaps so funding never exhausts.
*
* One category → K reservoir shards (K = number of lanes). Each address runs its
* own lane on its own shard with its own connection. A lane repeats CYCLES of:
* BATCH × claim (BCH → MBA) then 1 redeem-all (MBA → BCH), consolidating the
* recovered BCH + leftover change back into one funding coin. So each swap only
* costs founder-fee + miner-fee (a few thousand bits); the backing (gross) is
* recovered every cycle — the wallet does not drain. Runs for a time budget and
* counts total swaps. Chipnet only. Amounts in bits.
*/
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 LANES = (process.env.LANES ?? 'a,b,c,d,e,f,g').split(',').map((s) => s.trim().toLowerCase());
const FUNDER = (process.env.FUNDER ?? 'gmx').toLowerCase();
const BATCH = Number(process.env.BATCH ?? '5'); // claims per redeem-all
const BUDGET_MS = Number(process.env.BUDGET ?? '180') * 1000;
const RATE = 1000n, FEE_BPS = 100n, MINER = 3000n, RMINER = 1200n, AMOUNT = 60n;
const PER_SHARD_SUPPLY = 1_000_000_000n;
const CANONICAL_BLOCK_S = 600;
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; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function sendRetry(tb, tries = 5) {
for (let i = 0; ; i++) {
try { return await tb.send(); }
catch (e) { const m = e?.message ?? ''; if (i < tries - 1 && (m.includes('disconnected') || m.includes('timeout') || m.includes('closed'))) { await sleep(400); continue; } throw e; }
}
}
const txidOf = (s) => (typeof s === 'string' ? s : s?.txid ?? s?.txId);
async function main() {
const provider = new ElectrumNetworkProvider('chipnet');
const { wif } = JSON.parse(readFileSync(KEY(FUNDER), 'utf8'));
const funder = await TestNetWallet.fromWIF(wif);
const funderSig = new SignatureTemplate(funder.privateKey);
const funderLB = p2(funder.publicKeyHash);
const K = LANES.length;
const gross = AMOUNT * RATE, fee = (gross * FEE_BPS) / 10_000n;
const batchNeed = BigInt(BATCH) * (gross + fee + MINER) + 1000n; // to front one batch
// genesis one category → K shards
const ss = await funder.send([{ cashaddr: funder.cashaddr, value: 50000, unit: 'sat' }]);
const category = txidOf(ss);
await sleep(1500);
const gcontract = new Contract(ARTIFACT, [rev(category), RATE, FEE_BPS, funderLB], { provider });
const gtb = new TransactionBuilder({ provider });
gtb.addInput({ txid: category, vout: 0, satoshis: 50000n }, funderSig.unlockP2PKH());
for (let k = 0; k < K; k++) gtb.addOutput({ to: gcontract.tokenAddress, amount: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } });
gtb.addOutput({ to: funder.cashaddr, amount: 50000n - BigInt(K) * 1000n - 1500n });
const genTxid = txidOf(await gtb.send());
console.log(`\nSINGLE ASSET, ${K} shards (category ${category.slice(0, 12)}…), round-trip swaps (batch ${BATCH}+redeem), budget ${BUDGET_MS / 1000}s\n`);
await sleep(2000);
// build lanes (own provider each)
const lanes = [];
for (let k = 0; k < K; k++) {
const { wif: w } = JSON.parse(readFileSync(KEY(LANES[k]), 'utf8'));
const wal = await TestNetWallet.fromWIF(w);
const lp = new ElectrumNetworkProvider('chipnet');
const lc = new Contract(ARTIFACT, [rev(category), RATE, FEE_BPS, funderLB], { provider: lp });
const bch = (await lp.getUtxos(wal.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis))[0];
lanes.push({
label: LANES[k].toUpperCase(), sig: new SignatureTemplate(wal.privateKey), lb: p2(wal.publicKeyHash), tokenaddr: wal.tokenaddr, cashaddr: wal.cashaddr,
provider: lp, contract: lc, shard: { txid: genTxid, vout: k, satoshis: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } },
remaining: PER_SHARD_SUPPLY, funding: bch, startBits: bch ? BigInt(bch.satoshis) : 0n, claims: 0, redeems: 0, note: null,
});
}
const state = { t0: Date.now() };
await Promise.all(lanes.map(async (L) => {
if (!L.funding) { L.note = 'no BCH'; return; }
while (Date.now() - state.t0 < BUDGET_MS) {
if (BigInt(L.funding.satoshis) < batchNeed) { L.note = 'funding below one batch'; break; }
const mbas = [];
for (let c = 0; c < BATCH && Date.now() - state.t0 < BUDGET_MS; c++) {
const change = BigInt(L.funding.satoshis) - gross - fee - MINER;
const newRemaining = L.remaining - AMOUNT;
const tb = new TransactionBuilder({ provider: L.provider });
tb.addInput(L.shard, L.contract.unlock.claim(AMOUNT, L.lb));
tb.addInput(L.funding, L.sig.unlockP2PKH());
tb.addOutput({ to: L.contract.tokenAddress, amount: 1000n, token: { amount: newRemaining, category, nft: { capability: 'minting', commitment: '' } } });
tb.addOutput({ to: L.tokenaddr, amount: gross, token: { amount: AMOUNT, category } });
tb.addOutput({ to: funder.cashaddr, amount: fee });
tb.addOutput({ to: L.cashaddr, amount: change });
try {
const txid = txidOf(await sendRetry(tb));
L.shard = { txid, vout: 0, satoshis: 1000n, token: { amount: newRemaining, category, nft: { capability: 'minting', commitment: '' } } };
L.remaining = newRemaining; L.funding = { txid, vout: 3, satoshis: change };
mbas.push({ txid, vout: 1, satoshis: gross, token: { amount: AMOUNT, category } });
L.claims++;
} catch (e) { L.note = 'claim: ' + (e?.message ?? e).replace(/\s+/g, ' ').slice(0, 60); return; }
}
if (mbas.length) { // redeem-all: consolidate MBAs + leftover funding → one BCH coin
const inSats = mbas.reduce((s, u) => s + BigInt(u.satoshis), 0n) + BigInt(L.funding.satoshis);
const tb = new TransactionBuilder({ provider: L.provider, allowImplicitFungibleTokenBurn: true });
for (const u of mbas) tb.addInput(u, L.sig.unlockP2PKH());
tb.addInput(L.funding, L.sig.unlockP2PKH());
tb.addOutput({ to: L.cashaddr, amount: inSats - RMINER });
try { const txid = txidOf(await sendRetry(tb)); L.funding = { txid, vout: 0, satoshis: inSats - RMINER }; L.redeems++; }
catch (e) { L.note = 'redeem: ' + (e?.message ?? e).replace(/\s+/g, ' ').slice(0, 60); return; }
}
}
}));
const wall = (Date.now() - state.t0) / 1000;
const claims = lanes.reduce((s, L) => s + L.claims, 0);
const redeems = lanes.reduce((s, L) => s + L.redeems, 0);
const total = claims + redeems;
console.log('lane | claims | redeems | BCH start → end (bits) | note');
for (const L of lanes) console.log(` ${L.label} | ${L.claims} | ${L.redeems} | ${L.startBits} → ${L.funding ? BigInt(L.funding.satoshis) : 0n} | ${L.note ?? 'running'}`);
const rate = total / wall;
console.log(`\n=== SINGLE-ASSET ROUND-TRIP THROUGHPUT ===`);
console.log(` Total swaps: ${total} (${claims} BCH→MBA + ${redeems} MBA→BCH) on ONE category, ${K} shards, in ${wall.toFixed(0)}s`);
console.log(` Measured rate: ${rate.toFixed(1)} swaps/sec (client/latency bound — a FLOOR, not consensus)`);
console.log(` Projected onto a CANONICAL 10-min block at this rate: ~${Math.round(rate * CANONICAL_BLOCK_S).toLocaleString()} swaps/block`);
console.log(` No exhaustion: round-trips recover the backing; only fees drain (see BCH start→end above).`);
console.log(` Zero collisions: lanes are independent (own shard coin + own funding).`);
console.log(` Consensus ceiling (real limit): block SIZE — a 32 MB block holds ~${Math.round(32 * 1024 * 1024 / 450).toLocaleString()} such swaps for ONE asset.`);
}
main().then(() => process.exit(0)).catch((e) => { console.error('ERROR:', e?.message ?? e); if (e?.stack) console.error(e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });