deviant/scripts/spv-pool-multiconn-test.mts
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

88 lines
4.3 KiB
TypeScript

#!/usr/bin/env -S npx tsx
/**
* spv-pool-multiconn-test.mts — exercise the REAL ServerPool (shells/app/src/core/spv/pool.ts)
* against live endpoints to prove the multi-connection + failover model:
* "multiple connections simultaneously; if one is down, connect to another; keep our Fulcrum too."
*
* Endpoints: local Fulcrum (own), one DEAD (connection-refused), 3 public.
* targetLive=3. We then force-close one live (non-local) connection and confirm
* the pool rotates to a spare and returns to 3 — with the new backoff preventing
* the dead endpoint from spinning. A hard 22s watchdog guarantees exit.
*/
import { ServerPool, type SpvEndpoint } from '../shells/app/src/core/spv/pool.ts';
const endpoints: SpvEndpoint[] = [
{ url: 'ws://127.0.0.1:50003', label: 'local-fulcrum', own: true },
{ url: 'wss://127.0.0.1:59999', label: 'DEAD(test)' },
{ url: 'wss://chipnet.bch.ninja:50004', label: 'bch.ninja' },
{ url: 'wss://chipnet.imaginary.cash:50004', label: 'imaginary' },
{ url: 'wss://tbch4.loping.net:62002', label: 'loping' },
];
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const line = () => console.log('─'.repeat(64));
let pool: ServerPool;
// Hard watchdog: no matter what, print and exit — this can never run away again.
const watchdog = setTimeout(() => {
console.log('\n[watchdog] 22s elapsed — forcing exit');
try { pool?.stop(); } catch { /* */ }
process.exit(0);
}, 22_000);
async function main() {
let lastStatus = '';
pool = new ServerPool({
endpoints,
targetLive: 3,
quorum: 3,
connectTimeoutMs: 5000,
requestTimeoutMs: 8000,
onStatus: ({ live, dead }) => { // de-duped: log only when it changes
const s = `live=${live.length} dead=${dead.length}`;
if (s !== lastStatus) { console.log(` [status] ${s}`); lastStatus = s; }
},
onDivergence: ({ method }) => console.log(` [DIVERGENCE] ${method}`),
onTipSpread: ({ minHeight, maxHeight }) => console.log(` [tip-spread] Δ${maxHeight - minHeight}`),
});
line(); console.log('1) start() — open 3 simultaneous connections (skip dead, keep Fulcrum)'); line();
await pool.start();
await sleep(2500);
const live = pool.liveUrls();
console.log(`\nLive connections (${live.length}):`);
for (const [url, h] of Object.entries(pool.tips())) console.log(` ✓ ${url} tip=${h}`);
const localLive = live.some((u) => u.includes('127.0.0.1:50003'));
const deadLive = live.some((u) => u.includes('59999'));
console.log(` local Fulcrum present: ${localLive ? 'YES' : 'NO'} | dead skipped: ${!deadLive ? 'YES' : 'NO'}`);
line(); console.log('2) quorum read — blockchain.block.header @300000 (servers must agree)'); line();
try {
const hdr = await pool.request<string>('blockchain.block.header', [300000], { quorum: 3 });
console.log(` majority header ${hdr.length / 2}B: ${hdr.slice(0, 24)}…`);
} catch (e) { console.log(` quorum read failed: ${(e as Error).message}`); }
line(); console.log('3) FAILOVER — force-close one live (non-local) connection'); line();
const conns = (pool as unknown as { conns: Map<string, { ep: SpvEndpoint; ws: WebSocket; alive: boolean }> }).conns;
const victim = [...conns.values()].find((c) => c.alive && !c.ep.own);
if (victim) { console.log(` closing: ${victim.ep.url}`); victim.ws.close(); }
else console.log(' (no non-local live connection found)');
await sleep(6000); // allow drop() → fill() to rotate to the spare candidate
const live2 = pool.liveUrls();
console.log(`\nLive connections after failover (${live2.length}):`);
for (const [url, h] of Object.entries(pool.tips())) console.log(` ✓ ${url} tip=${h}`);
line(); console.log('=== SUMMARY ==='); line();
console.log(` simultaneous connections: ${live.length}/3`);
console.log(` own Fulcrum kept as a peer: ${localLive ? 'YES' : 'NO'}`);
console.log(` dead endpoint auto-skipped: ${!deadLive ? 'YES' : 'NO'}`);
console.log(` refilled to 3 after a live drop: ${live2.length >= 3 ? 'YES' : 'NO (' + live2.length + ')'}`);
console.log(` local Fulcrum still connected: ${live2.some((u) => u.includes('127.0.0.1:50003')) ? 'YES' : 'NO'}`);
clearTimeout(watchdog);
pool.stop();
}
main().then(() => process.exit(0)).catch((e) => { console.error('TEST ERROR:', e?.message ?? e); clearTimeout(watchdog); try { pool?.stop(); } catch {} process.exit(1); });