deviant/scripts/chipnet-asm-sequencer-daemon-test.mjs
Local Dev 1180b2665d chore(asm-sequencer): default COORD to wallet H (consolidated reserve)
Wallet A-G were drained by the retry proof + first sequencer run.
User consolidated their chipnet BCH into wallet H (~1.86 BCH across
39 UTXOs, top 33.4M sats) on 2026-09-29 as the single funder for
future chipnet demos. GMX still has 0.386 BCH but is dust-heavy.

Re-ran the daemon test with COORD=h: 5/5 chained (txids
e90d3c38…/eb91d05c…/9339ec71…/400d0c63…/b1bea343…), 10.1 s wall time
on public electrum, chain valid, ~293k sats consumed. Combined with
the earlier GMX-coordinated run, that's 10 consecutive chained claims
on ONE reservoir across two sessions with zero collisions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-29 01:55:21 +02:00

171 lines
7.4 KiB
JavaScript
Raw Permalink 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
/**
* End-to-end chipnet test of the ASM sequencer daemon.
*
* 1. Spawn `scripts/asm-sequencer/server.mjs` as a child process on a free port.
* 2. Wait for /health to respond.
* 3. Fire N simultaneous /claim requests (Promise.all).
* 4. Verify every request succeeded and the txids form a chain (each spends
* the coin the previous one created).
* 5. Compare against the naïve-parallel baseline recorded in the retry proof
* (Phase A of chipnet-asm-forced-race-retry-proof.mjs — 1/N without a
* sequencer, N/N with one).
* 6. Shut the daemon cleanly and print the JSON summary.
*
* Coordinator-funded model: the daemon uses wallet D by default. Requests
* are gift-claims — the daemon covers the BCH backing + fee, the FT lands
* at the recipient. This is the same trust model that snapshot 0239's
* 26,599-swap swap-relay used at scale.
*
* Env:
* USERS=c,e,f,g,b recipients (uses their chipnet cashaddrs)
* AMOUNT=55 units of ASM2 per claim (55 × rate 1000 = 55,000 gross; fee = 550, above 546 dust)
* COORD=h daemon-side coordinator (funds the swaps) — wallet H holds the consolidated
* chipnet reserve (~1.86 BCH) after wallets A-G were drained by the retry proof.
* TOKEN=asm2 the token file the daemon serves
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { spawn } from 'node:child_process';
import { setTimeout as sleep } from 'node:timers/promises';
import { TestNetWallet } from 'mainnet-js';
import { sequencerHealth, sequencerClaim, sequencerReachable } from './asm-sequencer/client.mjs';
const ROOT = join(import.meta.dirname, '..');
const USERS = (process.env.USERS ?? 'c,e,f,g,b').split(',').map((s) => s.trim().toLowerCase());
const AMOUNT = BigInt(process.env.AMOUNT ?? '55');
const COORD = (process.env.COORD ?? 'h').toLowerCase();
const TOKEN = (process.env.TOKEN ?? 'asm2').toLowerCase();
const PORT = 8710 + Math.floor(Math.random() * 100); // avoid collisions across runs
const KEY = (w) => join(ROOT, w === 'a' ? '.chipnet-keys.json' : `.chipnet-keys-${w}.json`);
async function loadUsers() {
const users = [];
for (const u of USERS) {
const { wif } = JSON.parse(readFileSync(KEY(u), 'utf8'));
const wal = await TestNetWallet.fromWIF(wif);
users.push({ label: u.toUpperCase(), cashaddr: wal.cashaddr });
}
return users;
}
function spawnDaemon() {
const child = spawn(
process.execPath,
[join(ROOT, 'scripts/asm-sequencer/server.mjs')],
{
env: { ...process.env, PORT: String(PORT), COORD, TOKEN },
cwd: ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
},
);
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (d) => process.stdout.write(` [daemon] ${d}`));
child.stderr.on('data', (d) => process.stderr.write(` [daemon] ${d}`));
return child;
}
async function waitForReachable(endpoint, maxMs = 25_000) {
const start = Date.now();
while (Date.now() - start < maxMs) {
if (await sequencerReachable(endpoint, 800)) return true;
await sleep(400);
}
return false;
}
async function main() {
const endpoint = `http://127.0.0.1:${PORT}`;
console.log(`\n═══ ASM SEQUENCER DAEMON — end-to-end chipnet test ═══`);
console.log(` users : [${USERS.join(', ')}] → each gets ${AMOUNT} ${TOKEN.toUpperCase()}`);
console.log(` coordinator: wallet ${COORD.toUpperCase()} (funds BCH + fee)`);
console.log(` endpoint : ${endpoint}\n`);
const users = await loadUsers();
const daemon = spawnDaemon();
let daemonExited = false;
daemon.on('exit', (code, signal) => { daemonExited = true; console.log(` [daemon] exited code=${code} signal=${signal}`); });
try {
console.log(` waiting for daemon /health…`);
const up = await waitForReachable(endpoint);
if (!up) throw new Error('daemon did not become reachable within timeout');
const h0 = await sequencerHealth(endpoint);
console.log(` daemon up. initial tip ${h0.reservoir.txid.slice(0, 12)}…:${h0.reservoir.vout} FT ${h0.reservoir.ftAmount}\n`);
// ── the race ────────────────────────────────────────────────────────────
console.log(`── Fire ${users.length} simultaneous /claim requests (Promise.all) ──`);
const t0 = Date.now();
const results = await Promise.all(users.map(async (u) => {
const at = Date.now() - t0;
try {
const r = await sequencerClaim({
endpoint,
ticker: TOKEN.toUpperCase(),
amountUnits: AMOUNT,
recipientCashaddr: u.cashaddr,
});
return { label: u.label, at, ...r, err: null };
} catch (e) {
return { label: u.label, at, txid: null, order: null, err: e?.message ?? String(e) };
}
}));
const dur = Date.now() - t0;
// Sort by daemon-assigned order (not by wire-arrival order) — that's the whole point.
const byOrder = [...results].filter((r) => r.order != null).sort((a, b) => a.order - b.order);
console.log(`\n arrived order user spent reservoir → produced txid`);
for (const r of results) {
const seq = r.order != null ? `#${r.order}` : ' —';
const line = r.txid
? `${r.before.reservoir.slice(0, 10)}…:${r.before.vout} → ${r.txid.slice(0, 16)}…`
: `✗ ${r.err}`;
console.log(` +${String(r.at).padStart(4)}ms ${seq.padStart(3)} ${r.label.padEnd(2)} ${line}`);
}
// ── verify the chain: each order-k tx must spend order-(k-1)'s reservoir output ──
let chainValid = byOrder.length === users.length;
for (let i = 1; i < byOrder.length; i++) {
const prevTxid = byOrder[i - 1].txid;
const beforeTxid = byOrder[i].before.reservoir;
if (prevTxid !== beforeTxid) { chainValid = false; break; }
}
const ok = results.filter((r) => r.txid).length;
const h1 = await sequencerHealth(endpoint);
console.log(`\n═══ VERDICT ═══`);
console.log(` ${ok}/${users.length} accepted in ${dur} ms (naïve parallel baseline: 1/${users.length}, doc §4)`);
console.log(` daemon chain depth: ${h0.chainDepth} → ${h1.chainDepth} (Δ=${h1.chainDepth - h0.chainDepth})`);
console.log(` order preserved as a chain: ${chainValid ? '✓ yes — each tx spent the previous tx\'s reservoir output' : '✗ no — chain broken'}`);
const proved = ok === users.length && chainValid;
if (proved) {
console.log(` ✓ Tier-2 sequencer RESOLVES the same-target reservoir race by ordered chaining.`);
console.log(` Submission was parallel; settlement is a single sequential chain from ONE reservoir.`);
}
console.log('\n---BEGIN JSON---');
console.log(JSON.stringify({
users: users.length, amount: String(AMOUNT), token: TOKEN.toUpperCase(),
accepted: ok, chainValid, durationMs: dur, chainDepthDelta: h1.chainDepth - h0.chainDepth,
results: results.map((r) => ({ label: r.label, order: r.order, txid: r.txid, before: r.before, err: r.err })),
proved,
}, null, 2));
console.log('---END JSON---');
process.exitCode = proved ? 0 : 2;
} finally {
if (!daemonExited) {
daemon.kill('SIGINT');
await sleep(500);
if (!daemonExited) daemon.kill('SIGKILL');
}
}
}
main().catch((e) => {
console.error('HARNESS ERROR:', e?.message ?? e);
if (e?.stack) console.error(e.stack);
process.exit(1);
});