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> @
131 lines
8.6 KiB
JavaScript
131 lines
8.6 KiB
JavaScript
#!/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); });
|