deviant/scripts/chipnet-local-node-check.mjs
Local Dev 6130c0cab1 feat(asm): Potidaea swap-relay + tally refresh to 52,998
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>
2026-08-14 01:39:41 +02:00

203 lines
12 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-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); });