diff --git a/infra/fast-time/tn-late-join.mjs b/infra/fast-time/tn-late-join.mjs index e3cb8aa94..d546f5bbe 100644 --- a/infra/fast-time/tn-late-join.mjs +++ b/infra/fast-time/tn-late-join.mjs @@ -44,6 +44,10 @@ const WINDOW = Number(sflag('window') || 150); // stalls on "proofs this peer did not deliver in 20 s". const PROVER = sflag('prover') || null; const JOIN_AFTER = Number(sflag('join-after') || 0); +// --resume: node A is already up on BASE's ports with its chain (a harness instance whose miner ran out before the join), and +// RESUME_PIDS names pid files of the processes to stop before B joins (the rescue miner, the prover); nothing is wiped +const RESUME = args.includes('--resume'); +const RESUME_PIDS = (sflag('resume-pids') || '').split(',').filter(Boolean); const MARGIN = flag('margin', 200), WATCH = flag('watch', 600), THREADS = flag('threads', 3), MAX_MINE = flag('max-mine', 5400); const OUT = sflag('out') || `${TMP}/late-join-expect-${EXPECT}.json`; if (!['join', 'stall'].includes(EXPECT)) { console.error('usage: --expect join|stall'); process.exit(2); } @@ -51,7 +55,7 @@ const started = []; const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); } -rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true }); +if (!RESUME) { rmSync(TMP, { recursive: true, force: true }); } mkdirSync(TMP, { recursive: true }); const baseText = readFileSync(FILE, 'utf8'); const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; }; @@ -75,7 +79,7 @@ export function redepth(text) { } const DAY_MS = field('pow_day_ms'); const override = `${TMP}/override.json`; -writeFileSync(override, mergeOverrideText(redepth(baseText), { +if (!RESUME) writeFileSync(override, mergeOverrideText(redepth(baseText), { genesis_bits: 0x1f010000, skip_proof_of_work: false, program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: '100000', program_class_v4_signal_window_daa: 120, // the testnet lane's late-join case (ledger N9's second half): consensus proof verification from genesis under the @@ -92,6 +96,14 @@ class Node { 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 attach() { + for (let i = 0; i < 20; i++) { + try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); await this.rpc.call('getBlockDagInfo'); break; } catch { this.rpc = null; await sleep(500); } + } + if (!this.rpc) throw new Error(`n${this.i}: no live node answers on ${this.jsonPort}`); + log(`n${this.i} attached on json ${this.jsonPort} (resume)`); + return this; + } async start() { mkdirSync(this.dir, { recursive: true }); const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', @@ -131,9 +143,9 @@ process.on('SIGINT', async () => { await stopAll(); process.exit(130); }); process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); process.on('uncaughtException', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); }); -const a = await new Node(0).start(); +const a = RESUME ? await new Node(0).attach() : await new Node(0).start(); const genesis = (await a.dag()).pruningPointHash; -const m = miner('a-miner', a.grpc, THREADS, MAX_MINE); +const m = RESUME ? null : 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) { @@ -146,7 +158,8 @@ if (PROVER) { const t0 = Date.now(); const since = () => ((Date.now() - t0) / 1000).toFixed(1); let lastReport = 0, pruningMovedAt = null, pruningPoint = null; -while (Date.now() - t0 < MAX_MINE * 1000) { +if (RESUME) { const d = await a.dag(); pruningMovedAt = { t: 0, daa: +d.virtualDaaScore, blocks: d.blockCount, resumed: true }; pruningPoint = d.pruningPointHash; log(`resume: A at ${d.blockCount} blocks, daa ${d.virtualDaaScore}, pruning point ${String(d.pruningPointHash).slice(0, 8)} (taken as moved)`); } +while (!RESUME && Date.now() - t0 < MAX_MINE * 1000) { await sleep(10000); const d = await a.dag(); if (pruningMovedAt == null && d.pruningPointHash !== genesis) { pruningMovedAt = { t: +since(), daa: +d.virtualDaaScore, blocks: d.blockCount }; pruningPoint = d.pruningPointHash; log(`A's pruning point left genesis at ${since()} s: ${String(d.pruningPointHash).slice(0, 8)} at DAA ${d.virtualDaaScore}, ${d.blockCount} blocks`); } @@ -156,16 +169,19 @@ while (Date.now() - t0 < MAX_MINE * 1000) { // --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`); + // past the pool's 600-chain-block record window, measured in DAA (one block a second here): the block count is the + // unpruned count and plateaus once the pruning point moves (seen on the pod, 7 October 2026, 13:4x UK) + const readyDaa = +(await a.dag()).virtualDaaScore; + const target = readyDaa + JOIN_AFTER; + log(`A ready at daa ${readyDaa}; mining on to daa ${target} (${JOIN_AFTER} past the ready point) before B joins`); for (;;) { const d = await a.rpc.call('getBlockDagInfo'); - if (d.blockCount >= target) break; + if (+d.virtualDaaScore >= target) break; await sleep(5000); } } -try { m.kill('SIGINT'); } catch { } +for (const f of RESUME_PIDS) { try { const pid = +readFileSync(f, 'utf8').trim(); if (pid > 1) { process.kill(pid, 'SIGINT'); log(`resume: stopped pid ${pid} from ${f}`); } } catch (e) { log(`resume: ${f}: ${e.message}`); } } +try { m?.kill('SIGINT'); } catch { } try { prover?.kill('SIGINT'); } catch { } await sleep(3000); const aAtJoin = await a.dag();