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>
171 lines
7.4 KiB
JavaScript
171 lines
7.4 KiB
JavaScript
#!/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);
|
||
});
|