Prove the Potidaea platform-fee revenue model live on chipnet with two end-to-end swap-relay tests, then sweep the cumulative chipnet-swap tally from "17,000+" to the exact figure 52,998 across every referencing doc. Snapshot 0239 (four revisions) captures the full arc — GMX Potidaea (9,296 swaps · 5.47 M platform bits) → OpenMint Potidaea (26,599 swaps · 15.02 M platform bits · 30.03 M founder bits · 44 swaps/sec peak · 2:1 founder/platform ratio matching ADR-0053 spec exactly). Client-added platform-fee output at tx.outputs[3] — no covenant change required. Ships the multi-source connectivity layer used throughout: scripts/lib/chipnet-multisource.mjs (race local Fulcrum + BCHN RPC + public electrums, chipnet-only endpoint list, wrong-network tip-guard, belt-and-braces broadcast) and scripts/lib/chipnet-keystore.mjs (single Keys/chipnet-keystore.json for all wallets, legacy per-file fallback). Keys/ added to .gitignore. Also: GMX/Game.X zero-bet-fee policy — chipnet-gmx-game-loop-test.mjs now returns 0n from escrowFee() by default (feeAmount=0 on the escrow covenant; already covered by the "if feeAmount > 0" gate at GamePoolEscrowFlat.cash:54, no covenant change needed). Old formula kept behind ARBITER_FEE_LEGACY=1 env flag. Canonical tally table lives in Parameters/ASM_Native/ASM-single-asset-throughput.md §4a; update it first when the next throughput runs land, then grep-replace the exact number across dependent files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
203 lines
12 KiB
JavaScript
203 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* chipnet-local-node-check.mjs — deep dive on the local chipnet stack itself.
|
||
*
|
||
* Complements chipnet-connectivity-check.mjs (which surveys ALL sources). This
|
||
* one drills into the local BCHN + Fulcrum + BNS stack:
|
||
*
|
||
* 1. BCHN networkinfo: version, protocol, subver, relay policy, IPv4/IPv6, local addresses
|
||
* 2. BCHN listening: port 48333 (P2P) actually accepting inbound?
|
||
* 3. BCHN peers deep: per-peer version, direction, AS/network family, ping distribution,
|
||
* headers/blocks alignment, banscore, whitelisted
|
||
* 4. BCHN bandwidth: getnettotals (bytes in/out), uptime, upload-target status
|
||
* 5. BCHN mining/mempool: getchaintxstats, getmempoolinfo (fee floor, growth, min-txfee)
|
||
* 6. BCHN banlist: peers we've banned
|
||
* 7. Fulcrum health: process alive, port 50003 open, log tail (indexing progress)
|
||
* 8. BNS Indexer: process alive, port 50010 open, log tail (indexing progress)
|
||
* 9. Local⇄Fulcrum sync: does Fulcrum's tip match BCHN's tip? (once Fulcrum is up)
|
||
*
|
||
* Chipnet only. Reads BCHN RPC creds from bitcoin.conf. Read-only checks.
|
||
*/
|
||
import http from 'node:http';
|
||
import { readFileSync, existsSync, statSync } from 'node:fs';
|
||
import { execSync } from 'node:child_process';
|
||
import WebSocket from 'ws';
|
||
import { rpcCreds } from './lib/chipnet-multisource.mjs';
|
||
|
||
const { user, pass } = rpcCreds();
|
||
const AUTH = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
||
const OK = '[32m✓[0m', BAD = '[31m✗[0m', WARN = '[33m•[0m';
|
||
const pad = (s, w) => String(s).padEnd(w);
|
||
const bits = (n) => Number(n).toLocaleString('en-US');
|
||
|
||
function rpc(method, params = [], timeoutMs = 30000) {
|
||
return new Promise((resolve, reject) => {
|
||
const body = JSON.stringify({ jsonrpc: '1.0', id: 't', method, params });
|
||
const t0 = performance.now();
|
||
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(Object.assign(new Error(j.error.message), { code: j.error.code })) : resolve({ result: j.result, ms: performance.now() - t0 }); }
|
||
catch (e) { reject(e); }
|
||
}); });
|
||
req.setTimeout(timeoutMs, () => req.destroy(new Error(`RPC timeout ${timeoutMs}ms`)));
|
||
req.on('error', reject);
|
||
req.end(body);
|
||
});
|
||
}
|
||
|
||
function portListening(port) {
|
||
try {
|
||
const out = execSync(`netstat -an`, { encoding: 'utf8', windowsHide: true });
|
||
return out.split('\n').some((l) => l.match(new RegExp(`:${port}\\s.*LISTENING`)));
|
||
} catch { return false; }
|
||
}
|
||
|
||
function processAlive(imageName) {
|
||
try {
|
||
const out = execSync(`tasklist /FI "IMAGENAME eq ${imageName}"`, { encoding: 'utf8', windowsHide: true });
|
||
return out.includes(imageName);
|
||
} catch { return false; }
|
||
}
|
||
|
||
async function electrumProbe(url, method = 'server.version', params = ['probe', '1.4'], timeoutMs = 6000) {
|
||
return new Promise((resolve) => {
|
||
const t0 = performance.now();
|
||
let done = false, ws;
|
||
const finish = (ok, val) => { if (done) return; done = true; try { ws?.close(); } catch {} resolve({ ok, val, ms: performance.now() - t0 }); };
|
||
try { ws = new WebSocket(url, { handshakeTimeout: timeoutMs }); }
|
||
catch (e) { return finish(false, e); }
|
||
const timer = setTimeout(() => finish(false, new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
|
||
ws.on('open', () => ws.send(JSON.stringify({ id: 1, method, params }) + '\n'));
|
||
ws.on('message', (b) => { clearTimeout(timer); try { const j = JSON.parse(b.toString().trim()); j.error ? finish(false, new Error(j.error.message)) : finish(true, j.result); } catch (e) { finish(false, e); } });
|
||
ws.on('error', (e) => { clearTimeout(timer); finish(false, e); });
|
||
});
|
||
}
|
||
|
||
async function main() {
|
||
// ── 1. networkinfo ──
|
||
console.log('\n═══ 1. BCHN getnetworkinfo ═══');
|
||
const ni = (await rpc('getnetworkinfo')).result;
|
||
console.log(` version: ${ni.version} · subver ${ni.subversion}`);
|
||
console.log(` protocol: ${ni.protocolversion} · minrelay ${ni.relayfee} BCH/kB · incrementalfee ${ni.incrementalfee}`);
|
||
console.log(` connections: ${ni.connections} total (${ni.connections_in} in / ${ni.connections_out} out)`);
|
||
console.log(` reachable: IPv4=${ni.networks.find((n) => n.name === 'ipv4')?.reachable} · IPv6=${ni.networks.find((n) => n.name === 'ipv6')?.reachable} · Tor=${ni.networks.find((n) => n.name === 'onion')?.reachable}`);
|
||
console.log(` localrelay: ${ni.localrelay} · timeoffset ${ni.timeoffset}s · warnings: ${ni.warnings || '(none)'}`);
|
||
if (ni.localaddresses?.length) for (const la of ni.localaddresses) console.log(` local addr: ${la.address}:${la.port} score=${la.score}`);
|
||
else console.log(` ${WARN} no localaddresses discovered — inbound may not be reachable from Internet`);
|
||
|
||
// ── 2. listening ports ──
|
||
console.log('\n═══ 2. BCHN listening ports ═══');
|
||
const p2p = portListening(48333);
|
||
const rpcP = portListening(48332);
|
||
console.log(` ${rpcP ? OK : BAD} RPC 48332 (127.0.0.1) LISTENING`);
|
||
console.log(` ${p2p ? OK : BAD} P2P 48333 LISTENING (inbound-accept surface)`);
|
||
|
||
// ── 3. peers deep ──
|
||
console.log('\n═══ 3. BCHN peers ═══');
|
||
const peers = (await rpc('getpeerinfo')).result;
|
||
const outbound = peers.filter((p) => !p.inbound);
|
||
const inbound = peers.filter((p) => p.inbound);
|
||
const ipv4 = peers.filter((p) => !p.addr.startsWith('['));
|
||
const ipv6 = peers.filter((p) => p.addr.startsWith('['));
|
||
const versions = new Map();
|
||
for (const p of peers) { const v = (p.subver || '').replace(/^\/|\/$/g, ''); versions.set(v, (versions.get(v) || 0) + 1); }
|
||
const pings = peers.map((p) => p.pingtime).filter((x) => typeof x === 'number');
|
||
const pMin = Math.min(...pings) * 1000, pMax = Math.max(...pings) * 1000, pAvg = pings.reduce((a, b) => a + b, 0) / pings.length * 1000;
|
||
console.log(` total: ${peers.length} outbound: ${outbound.length} inbound: ${inbound.length}`);
|
||
console.log(` IPv4: ${ipv4.length} IPv6/NAT64: ${ipv6.length}`);
|
||
console.log(` ping: min ${pMin.toFixed(0)}ms · avg ${pAvg.toFixed(0)}ms · max ${pMax.toFixed(0)}ms`);
|
||
console.log(` version diversity:`);
|
||
for (const [v, n] of [...versions.entries()].sort((a, b) => b[1] - a[1])) console.log(` ${n}× ${v.slice(0, 50)}`);
|
||
const ourTip = (await rpc('getblockcount')).result;
|
||
const behind = peers.filter((p) => (p.synced_headers ?? ourTip) < ourTip);
|
||
const ahead = peers.filter((p) => (p.synced_headers ?? 0) > ourTip);
|
||
console.log(` header alignment vs local tip ${ourTip}: aligned=${peers.length - behind.length - ahead.length} · behind=${behind.length} · ahead=${ahead.length}`);
|
||
const flagged = peers.filter((p) => (p.banscore ?? 0) > 0);
|
||
console.log(` ${flagged.length === 0 ? OK : WARN} banscore>0 peers: ${flagged.length}`);
|
||
|
||
// ── 4. bandwidth ──
|
||
console.log('\n═══ 4. BCHN bandwidth & uptime ═══');
|
||
const nt = (await rpc('getnettotals')).result;
|
||
const up = (await rpc('uptime')).result;
|
||
const hr = (s) => `${Math.floor(s/3600)}h${Math.floor(s%3600/60)}m`;
|
||
console.log(` uptime: ${hr(up)} (${up}s)`);
|
||
console.log(` bytes: ⬇ ${(nt.totalbytesrecv/1024/1024).toFixed(1)} MB · ⬆ ${(nt.totalbytessent/1024/1024).toFixed(1)} MB`);
|
||
const upTarget = nt.uploadtarget;
|
||
console.log(` upload target: target ${(upTarget.target/1024/1024).toFixed(0)}MB · used ${((upTarget.target - upTarget.target_reached ? 0 : upTarget.serve_historical_blocks) || 0)} · target_reached=${upTarget.target_reached}`);
|
||
|
||
// ── 5. chain / mempool ──
|
||
console.log('\n═══ 5. BCHN chain + mempool ═══');
|
||
const bci = (await rpc('getblockchaininfo')).result;
|
||
const mp = (await rpc('getmempoolinfo')).result;
|
||
const stats = await rpc('getchaintxstats', [144]).catch(() => ({ result: {} }));
|
||
const s = stats.result;
|
||
console.log(` chain=${bci.chain} height=${bci.blocks}/${bci.headers} progress=${(bci.verificationprogress * 100).toFixed(4)}% IBD=${bci.initialblockdownload}`);
|
||
console.log(` chainwork: ${bci.chainwork.slice(-16)}… · size on disk ${(bci.size_on_disk/1024/1024/1024).toFixed(2)} GB · pruned=${bci.pruned}`);
|
||
console.log(` last 144 blocks: window=${s.window_final_block_height ?? '?'} txs=${s.window_tx_count ?? '?'} txrate=${(s.txrate ?? 0).toFixed(3)} tx/s`);
|
||
console.log(` mempool: ${mp.size} txs · ${bits(mp.bytes)} B · usage ${(mp.usage/1024/1024).toFixed(1)}/${(mp.maxmempool/1024/1024).toFixed(0)} MB · minfee ${mp.mempoolminfee} BCH/kB`);
|
||
|
||
// ── 6. banlist ──
|
||
console.log('\n═══ 6. BCHN banlist ═══');
|
||
const bans = (await rpc('listbanned')).result;
|
||
console.log(` ${bans.length === 0 ? OK : WARN} banned: ${bans.length}`);
|
||
for (const b of bans.slice(0, 5)) console.log(` ${b.address} · until ${new Date(b.banned_until * 1000).toISOString()}`);
|
||
|
||
// ── 7. Fulcrum ──
|
||
console.log('\n═══ 7. Fulcrum health ═══');
|
||
const fulcAlive = processAlive('Fulcrum.exe');
|
||
const fulcListen = portListening(50003);
|
||
console.log(` ${fulcAlive ? OK : BAD} process Fulcrum.exe alive`);
|
||
console.log(` ${fulcListen ? OK : WARN} port 50003 LISTENING (${fulcListen ? 'clients can connect' : 'still indexing OR not started'})`);
|
||
const fulcLog = 'D:/Program Files/FulcrumData/fulcrum-out.log';
|
||
if (existsSync(fulcLog)) {
|
||
const st = statSync(fulcLog);
|
||
const ageMin = (Date.now() - st.mtimeMs) / 60000;
|
||
console.log(` log: ${fulcLog} last-modified ${ageMin.toFixed(1)}m ago`);
|
||
try {
|
||
const content = readFileSync(fulcLog, 'utf8');
|
||
const lines = content.trim().split('\n').slice(-4);
|
||
for (const l of lines) console.log(` ${l.slice(-160)}`);
|
||
} catch {}
|
||
}
|
||
if (fulcListen) {
|
||
const ver = await electrumProbe('ws://127.0.0.1:50003');
|
||
if (ver.ok) {
|
||
const tip = await electrumProbe('ws://127.0.0.1:50003', 'blockchain.headers.subscribe', []);
|
||
console.log(` ${OK} responds server.version=${JSON.stringify(ver.val)} · tip h=${tip.ok ? tip.val.height : '?'} (${ver.ms.toFixed(0)}ms)`);
|
||
if (tip.ok) console.log(` ${tip.val.height === bci.blocks ? OK + ' Fulcrum-BCHN tip agreement' : WARN + ` Fulcrum tip ${tip.val.height} vs BCHN ${bci.blocks} — Fulcrum behind by ${bci.blocks - tip.val.height}`}`);
|
||
} else console.log(` ${BAD} probe failed: ${ver.val?.message ?? ver.val}`);
|
||
}
|
||
|
||
// ── 8. BNS Indexer ──
|
||
console.log('\n═══ 8. BNS Indexer health ═══');
|
||
const bnsListen = portListening(50010);
|
||
console.log(` ${bnsListen ? OK : BAD} port 50010 LISTENING`);
|
||
const bnsLog = 'D:/Program Files/ChipnetData/bns-indexer.log';
|
||
if (existsSync(bnsLog)) {
|
||
const st = statSync(bnsLog);
|
||
console.log(` log: ${bnsLog} last-modified ${((Date.now() - st.mtimeMs)/60000).toFixed(1)}m ago`);
|
||
try {
|
||
const lines = readFileSync(bnsLog, 'utf8').trim().split('\n').slice(-4);
|
||
for (const l of lines) console.log(` ${l.slice(-160)}`);
|
||
} catch {}
|
||
} else console.log(` ${WARN} no BNS log at ${bnsLog}`);
|
||
if (bnsListen) {
|
||
const ver = await electrumProbe('ws://127.0.0.1:50010');
|
||
console.log(` ${ver.ok ? OK : BAD} probe: ${ver.ok ? JSON.stringify(ver.val) : ver.val?.message ?? ver.val} (${ver.ms.toFixed(0)}ms)`);
|
||
}
|
||
|
||
// ── Summary line ──
|
||
console.log('\n═══ SUMMARY ═══');
|
||
const bad = [];
|
||
if (!rpcP) bad.push('BCHN RPC not listening');
|
||
if (!p2p) bad.push('BCHN P2P not listening');
|
||
if (peers.length < 4) bad.push(`only ${peers.length} P2P peers`);
|
||
if (outbound.length < 2) bad.push('too few outbound peers');
|
||
if (behind.length > peers.length / 2) bad.push('most peers behind our tip');
|
||
if (!fulcAlive) bad.push('Fulcrum process dead');
|
||
if (bad.length === 0) console.log(` ${OK} local chipnet stack healthy · BCHN ${ni.connections} P2P peers · tip h=${bci.blocks} · uptime ${hr(up)}`);
|
||
else { console.log(` ${WARN} local stack has ${bad.length} issue(s):`); for (const b of bad) console.log(` · ${b}`); }
|
||
}
|
||
main().catch((e) => { console.error('LOCAL CHECK FAILED:', e?.message ?? e); if (e?.stack) console.error(e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });
|