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

134 lines
9.4 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-asm-localnode-throughput.mjs — single-asset throughput against the
* LOCAL BCHN node (RPC sendrawtransaction), bypassing public electrum.
*
* Txs are BUILT locally (cashscript, offline) and broadcast straight to the
* local node's RPC (127.0.0.1:48332) — no WSS round-trip. One category → K
* reservoir shards; each lane does round-trip swaps (BATCH claims + redeem-all)
* so funding never drains. Reports client rate + node mempool depth. Canonical
* 10-min blocks. Chipnet only. RPC creds are read from bitcoin.conf (never
* hardcoded — the doc's Rule 0). Amounts in bits.
*/
import http from 'node:http';
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');
const BUDGET_MS = Number(process.env.BUDGET ?? '120') * 1000;
const RATE = 1000n, FEE_BPS = 100n, MINER = 3000n, RMINER = 1200n, AMOUNT = 60n;
const PER_SHARD_SUPPLY = 1_000_000_000n, CANONICAL_BLOCK_S = 600;
// ── local BCHN RPC (creds from conf; never hardcode — LOCAL-NODE-CONNECT.md Rule 0) ──
function rpcCreds() {
if (process.env.RPC_USER && process.env.RPC_PASS) return { user: process.env.RPC_USER, pass: process.env.RPC_PASS };
for (const p of ['D:\\Program Files\\ChipnetData\\bitcoin.conf', 'D:\\ChipnetData\\bitcoin.conf']) {
try { const t = readFileSync(p, 'utf8'); const u = t.match(/^\s*rpcuser=(.+)$/m)?.[1]?.trim(); const pw = t.match(/^\s*rpcpassword=(.+)$/m)?.[1]?.trim(); if (u && pw) return { user: u, pass: pw }; } catch { /* next */ }
}
throw new Error('RPC creds not found — set RPC_USER/RPC_PASS or ensure bitcoin.conf has rpcuser/rpcpassword');
}
const { user, pass } = rpcCreds();
const AUTH = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
function rpc(method, params = []) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ jsonrpc: '1.0', id: 't', method, params });
const req = http.request({ hostname: '127.0.0.1', port: 48332, path: '/', method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), authorization: AUTH } },
(res) => { const c = []; res.on('data', (d) => c.push(d)); res.on('end', () => { try { const j = JSON.parse(Buffer.concat(c).toString()); j.error ? reject(new Error(j.error.message)) : resolve(j.result); } catch (e) { reject(e); } }); });
req.on('error', reject); req.end(body);
});
}
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 bcast = async (tb) => rpc('sendrawtransaction', [tb.build()]); // build offline, broadcast to local node
async function main() {
const bh = await rpc('getblockcount');
console.log(`local node height ${bh}`);
const provider = new ElectrumNetworkProvider('chipnet'); // reads only (setup); broadcasts go via RPC
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;
// ── setup: self-send (→ vout0) + genesis K shards, all via local RPC ──
const fUtxo = (await provider.getUtxos(funder.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis))[0];
const sstb = new TransactionBuilder({ provider });
sstb.addInput(fUtxo, funderSig.unlockP2PKH());
sstb.addOutput({ to: funder.cashaddr, amount: 60000n });
sstb.addOutput({ to: funder.cashaddr, amount: BigInt(fUtxo.satoshis) - 60000n - 500n });
const category = await bcast(sstb);
const contract = new Contract(ARTIFACT, [rev(category), RATE, FEE_BPS, funderLB], { provider });
const gtb = new TransactionBuilder({ provider });
gtb.addInput({ txid: category, vout: 0, satoshis: 60000n }, funderSig.unlockP2PKH());
for (let k = 0; k < K; k++) gtb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } });
gtb.addOutput({ to: funder.cashaddr, amount: 60000n - BigInt(K) * 1000n - 1500n });
const genTxid = await bcast(gtb);
console.log(`genesis ${genTxid.slice(0, 12)}… → ${K} shards; round-trip lanes, ${BUDGET_MS / 1000}s budget, broadcast via LOCAL RPC\n`);
// ── lanes ──
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 bch = (await provider.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, contract,
shard: { txid: genTxid, vout: k, satoshis: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } }, remaining: PER_SHARD_SUPPLY, funding: bch, claims: 0, redeems: 0, note: null });
}
const t0 = Date.now();
await Promise.all(lanes.map(async (L) => {
if (!L.funding) { L.note = 'no BCH'; return; }
while (Date.now() - 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() - t0 < BUDGET_MS; c++) {
const change = BigInt(L.funding.satoshis) - gross - fee - MINER;
const newRemaining = L.remaining - AMOUNT;
const tb = new TransactionBuilder({ 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 = await bcast(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).slice(0, 70); return; }
}
if (mbas.length) {
const inSats = mbas.reduce((s, u) => s + BigInt(u.satoshis), 0n) + BigInt(L.funding.satoshis);
const tb = new TransactionBuilder({ 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 = await bcast(tb); L.funding = { txid, vout: 0, satoshis: inSats - RMINER }; L.redeems++; }
catch (e) { L.note = 'redeem: ' + (e?.message ?? e).slice(0, 70); return; }
}
}
}));
const wall = (Date.now() - t0) / 1000;
const claims = lanes.reduce((s, L) => s + L.claims, 0), redeems = lanes.reduce((s, L) => s + L.redeems, 0), total = claims + redeems;
const mp = await rpc('getmempoolinfo').catch(() => ({}));
console.log('lane | claims | redeems | note');
for (const L of lanes) console.log(` ${L.label} | ${L.claims} | ${L.redeems} | ${L.note ?? 'hit time budget'}`);
const rate = total / wall;
console.log(`\n=== SINGLE-ASSET THROUGHPUT via LOCAL NODE ===`);
console.log(` Total swaps: ${total} (${claims} BCH→MBA + ${redeems} MBA→BCH) on ONE category, ${K} shards, in ${wall.toFixed(0)}s`);
console.log(` Rate: ${rate.toFixed(1)} swaps/sec (now build-bound: cashscript covenant tx construction, local RPC ingest is ~ms)`);
console.log(` Projected onto a CANONICAL 10-min block: ~${Math.round(rate * CANONICAL_BLOCK_S).toLocaleString()} swaps/block at this rate`);
console.log(` Node mempool now: ${mp.size ?? '?'} txs, ${mp.bytes ?? '?'} bytes (getmempoolinfo)`);
console.log(` Zero collisions. Consensus ceiling: block SIZE — ~${Math.round(32 * 1024 * 1024 / 450).toLocaleString()} such swaps / 32 MB block 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); });