tn-late-join harness: the join-after target in DAA (the unpruned block count plateaus once the pruning point moves, seen on the pod 13:4x UK) and a --resume mode that attaches to a live node A and stops the named pids before B joins

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 12:50:34 +00:00
parent 08d97a849c
commit 343109f42a

View file

@ -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();