#!/usr/bin/env node // Proof of the 60x fast-time profile: a 3-node private network on 127.0.0.1 ports 29500 and up, data under // /tmp/igneum-fast-time, network id igneum-devnet-950, every node on infra/fast-time/override-60x.json with // skip_proof_of_work. Three voting vmine miners (the fin-attacks igneum-miner, one per node) share 1 block/s, the // devnet rate, so DAA score advances one per second as on the devnet. One real CPU miner (devnet-v4 igneum-miner, // --engine igneum-pow, 1 thread) follows node 0's templates and prints its program swap at every epoch boundary; // at the devnet genesis difficulty a CPU thread finds no block, so it adds nothing to the chain. // // Records, in wall seconds from the first node's start: the first finality lock (getFinalityCheckpoints) and the // first hourly program swap (template epoch index 0 -> 1 at DAA 60, and the CPU miner's "program and cache ready" // line for the new seed). Expected: lock within about 3 minutes (window 120 DAA + checkpoint depth), swap within // about 2 minutes. Never touches the live devnet (26610/26611, 26640/26641, 28640). // // node infra/fast-time/simnet.mjs [--secs 300] [--no-cpu-miner] import { spawn } from 'node:child_process'; import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs'; import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs'; import { devAddress } from '../../tools/harness/lib/address.mjs'; const ROOT = new URL('../../', import.meta.url).pathname; const FILE = `${ROOT}infra/fast-time/override-60x.json`; const IGNEUMD = process.env.IGNEUMD || `${ROOT}vendor/igneum-node/target-integration/release/igneumd`; const VMINE = process.env.IGNEUM_VMINE || `${ROOT}vendor/igneum-node-fin-attacks/target/release/igneum-miner`; const CPU_MINER = process.env.IGNEUM_MINER || `${ROOT}vendor/igneum-node/target-integration/release/igneum-miner`; const TMP = '/tmp/igneum-fast-time'; const BASE = 29500, SUFFIX = 950; const args = process.argv.slice(2); const SECS = +(args[args.indexOf('--secs') + 1] || 300); const CPU = !args.includes('--no-cpu-miner'); 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, VMINE]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); } rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true }); const override = `${TMP}/override.json`; // merged as text, not through JSON.parse: a `never` height (18446744073709551615) does not survive a JavaScript number // (the class-v3.mjs gate run of 5 October 2026 21:30Z: "invalid type: floating point 1.8446744073709552e+19") writeFileSync(override, readFileSync(FILE, 'utf8').replace(/\s*"skip_proof_of_work":\s*[^,}\n]+,?/, '').replace(/,?\s*}\s*$/, ',\n "skip_proof_of_work": true\n}\n')); class Node { constructor(i, connect = []) { this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; 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}`, `--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes']; if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0'); const out = openSync(this.logFile, 'a'); this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] }); started.push(this.proc); await sleep(800); this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}`); return this; } grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } } } function miner(bin, argv, name, env = {}) { const out = openSync(`${TMP}/${name}.log`, 'a'); const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } }); started.push(p); return p; } async function stopAll() { for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } } await sleep(1500); for (const p of started) { try { p.kill('SIGKILL'); } catch { } } } process.on('SIGINT', async () => { await stopAll(); process.exit(130); }); const t0 = Date.now(); const since = () => ((Date.now() - t0) / 1000).toFixed(1); const n0 = await new Node(0).start(); const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start(); const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start(); const nodes = [n0, n1, n2]; log(`override: ${readFileSync(override, 'utf8').length} bytes; node log says: ${n0.grepLog(/PoW schedule/).join(' | ') || '(no PoW schedule line)'}`); // three voters sharing 1 block/s (the devnet rate): DAA advances 1 per second nodes.forEach((n, i) => miner(VMINE, ['vmine', n.grpc, String(SECS), '--share', String(1 / 3), '--bps', '1', '--label', `v${i}`], `vmine-v${i}`)); if (CPU) miner(CPU_MINER, ['mine', n0.grpc, '1', String(SECS), 'cpu0', '--engine', 'igneum-pow', '--status-secs', '30'], 'cpu-miner', { IGNEUM_POW_DAY_MS: '1440000' }); const pay = devAddress('fast-time'); let firstLock = null, firstSwap = null, firstPrepare = null, cpuSwap = null, lastDaa = 0, lastEpoch = 0, lastReport = 0; const samples = []; while (Date.now() - t0 < SECS * 1000) { await sleep(1000); let daa = null, epoch = null, nextSeed = null, cps = null; try { const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] }); const pe = t.powEpoch || t.pow_epoch || {}; daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; nextSeed = pe.nextEpochSeed; } catch (e) { log(`template: ${e.message}`); } try { cps = await n0.rpc.call('getFinalityCheckpoints', { last: 20 }); } catch { } const locked = (cps?.checkpoints || []).filter(c => c.state === 'locked'); if (firstPrepare == null && nextSeed) { firstPrepare = +since(); log(`next epoch seed known at ${firstPrepare} s (daa ${daa})`); } if (firstSwap == null && epoch >= 1) { firstSwap = +since(); log(`PROGRAM SWAP: template epoch index ${epoch} at ${firstSwap} s wall (daa ${daa})`); } if (firstLock == null && locked.length) { firstLock = +since(); const c = locked[0]; log(`FIRST LOCK: checkpoint ${c.index} (blue score ${c.blueScore ?? c.blue_score}, daa ${c.daaScore ?? c.daa_score}) locked at ${firstLock} s wall`); } if (cpuSwap == null && CPU) { const lines = (() => { try { return readFileSync(`${TMP}/cpu-miner.log`, 'utf8').split('\n').filter(l => /program and 256 MiB cache ready/.test(l)); } catch { return []; } })(); if (lines.length >= 2) { cpuSwap = +since(); log(`CPU MINER SWAP at ${cpuSwap} s wall: ${lines[1].trim()}`); } } if (epoch != null && epoch !== lastEpoch) { log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s`); lastEpoch = epoch; } lastDaa = daa ?? lastDaa; if (Date.now() - lastReport > 15000) { lastReport = Date.now(); const lockedPer = await Promise.all(nodes.map(async n => { try { const c = await n.rpc.call('getFinalityCheckpoints', { last: 400 }); return (c.checkpoints || []).filter(x => x.state === 'locked').length; } catch { return '?'; } })); log(`t=${since()} s daa ${daa} epoch ${epoch} nextSeed ${nextSeed ? 'known' : 'no'} locked per node ${lockedPer.join('/')}`); samples.push({ t: +since(), daa, epoch, locked: lockedPer }); } if (firstLock != null && firstSwap != null && (cpuSwap != null || !CPU) && Date.now() - t0 > 150_000) break; } const sinks = await Promise.all(nodes.map(async n => { try { return (await n.rpc.call('getBlockDagInfo')).sink.slice(0, 12); } catch { return '?'; } })); const summary = { first_lock_s: firstLock, first_swap_s: firstSwap, next_seed_known_s: firstPrepare, cpu_miner_swap_s: cpuSwap, final_daa: lastDaa, sinks_agree: new Set(sinks).size === 1, sinks, samples, secs: SECS, node: IGNEUMD }; writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2)); log(`SUMMARY first lock ${firstLock} s, first program swap ${firstSwap} s (next seed known at ${firstPrepare} s), CPU miner swap ${cpuSwap} s, final daa ${lastDaa}, sinks agree ${summary.sinks_agree}`); await stopAll(); process.exit(firstLock != null && firstSwap != null ? 0 : 1);