From 1bb91045a109780d2c9395f8994a4375ea0bc5da Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:47:41 +0000 Subject: [PATCH] fast-time: the join harness's late-join hook for the testnet lane (--prover beside A's miner, --join-after past the pool's record window, per-node exec ports) Co-Authored-By: Claude Fable 5.1 --- infra/fast-time/headers-proof-join.mjs | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/infra/fast-time/headers-proof-join.mjs b/infra/fast-time/headers-proof-join.mjs index 807fef4e9..db83e5d60 100644 --- a/infra/fast-time/headers-proof-join.mjs +++ b/infra/fast-time/headers-proof-join.mjs @@ -32,6 +32,14 @@ const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; }; const EXPECT = sflag('expect') || 'join'; const WINDOW = Number(sflag('window') || 150); +// The testnet lane's late-join case (ledger N9's second half, 7 October 2026): --prover "" starts a prover +// beside A's miner (its records land in A's blocks through the ordinary record flows; the command sees IGNEUM_GRPC +// and IGNEUM_EVM_RPC for node A), and --join-after holds B's start until A has mined that many blocks past +// its ready point, so B joins after the pool's 600-chain-block window has passed and every proof it asks for must +// come from the peers' archives. PASS is the same join; the failed shape is the node before the archive, which +// stalls on "proofs this peer did not deliver in 20 s". +const PROVER = sflag('prover') || null; +const JOIN_AFTER = Number(sflag('join-after') || 0); const MARGIN = flag('margin', 200), WATCH = flag('watch', 600), THREADS = flag('threads', 3), MAX_MINE = flag('max-mine', 5400); const OUT = sflag('out') || `${ROOT}docs/plans/counter-asic-3-gate/headers-proof-join-expect-${EXPECT}-w${WINDOW}.json`; if (!['join', 'stall'].includes(EXPECT)) { console.error('usage: --expect join|stall'); process.exit(2); } @@ -73,14 +81,14 @@ log(`expect ${EXPECT}: pruning depth 4600 (finality 120, merge 60, k 18), the wi class Node { constructor(i, connect = null) { - this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; + this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; this.evmPort = BASE + i * 10 + 3; this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`; } get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; } async start() { mkdirSync(this.dir, { recursive: true }); const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', - `--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`, + `--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`, `--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes']; if (this.connect) a.push(`--addpeer=127.0.0.1:${this.connect}`); else a.push('--outpeers=0'); const out = openSync(this.logFile, 'a'); @@ -116,6 +124,13 @@ process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); const a = await new Node(0).start(); const genesis = (await a.dag()).pruningPointHash; const m = miner('a-miner', a.grpc, THREADS, MAX_MINE); +// the testnet lane's prover beside A's miner (its records reach A's blocks through the record flows) +let prover = null; +if (PROVER) { + const out = openSync(`${TMP}/prover.log`, 'a'); + prover = spawn('/bin/sh', ['-c', PROVER], { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_GRPC: a.grpc, IGNEUM_EVM_RPC: `http://127.0.0.1:${a.evmPort}` } }); + log(`prover up pid ${prover.pid}: ${PROVER}`); +} const t0 = Date.now(); const since = () => ((Date.now() - t0) / 1000).toFixed(1); let lastReport = 0, pruningMovedAt = null, pruningPoint = null; @@ -126,7 +141,20 @@ while (Date.now() - t0 < MAX_MINE * 1000) { if (Date.now() - lastReport > 120000) { lastReport = Date.now(); log(`t=${since()} s A ${d.blockCount} blocks daa ${d.virtualDaaScore} pruning ${String(d.pruningPointHash).slice(0, 8)}${pruningMovedAt ? ' (moved)' : ''}`); } if (pruningMovedAt && d.blockCount >= pruningMovedAt.blocks + MARGIN) break; } +// --join-after: A's miner runs on this many blocks past the ready point (past the pool's 600-chain-block record +// window, so every proof B asks for must come from the peers' archives), then stops as before so B joins a still sink +if (JOIN_AFTER > 0) { + const ready = (await a.dag()).blockCount; + const target = ready + JOIN_AFTER; + log(`A ready at ${ready} blocks; mining on to ${target} (${JOIN_AFTER} past the ready point) before B joins`); + for (;;) { + const d = await a.rpc.call('getBlockDagInfo'); + if (d.blockCount >= target) break; + await sleep(5000); + } +} try { m.kill('SIGINT'); } catch { } +try { prover?.kill('SIGINT'); } catch { } await sleep(3000); const aAtJoin = await a.dag(); const aSinkHeader = await a.headerOf(aAtJoin.sink);