infra/fast-time/override-60x.json is the devnet with every clock-like consensus parameter divided by 60 and every block count unchanged (finality window, ban and min_daa 120 DAA; merge depth 60; Kaspa finality depth 720; pruning depth at the anticone bound 13,838; coinbase maturity 2; the hourly program epoch 60 blocks with a 10-block lead; the dataset day 24 minutes). The epoch length, lead and day are consensus parameters of the node since devnet-v4 a5ef8b07, carried by the override file. README lists each field, why it scales or not, the flags and the numbers. Measured (simnet.mjs, three devnet-v4 nodes, three vmine voters at 1 block/s, one real-hash CPU miner): next epoch seed in the template at 56.1 s, program swap at 65.1 s wall (DAA 60), first finality lock at 185.5 s wall (checkpoint 5, DAA 149). Both harnesses take --fast-time: finality-attacks s3 PASS in 113 s wall with 16 locks per node (the devnet rule needs 20 min of warm-up at 6 blocks/s before any lock); harness s3 partition and heal 43 s wall for three cuts against 983 s for four on the devnet profile with the same binary. Bench-log entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
126 lines
8.2 KiB
JavaScript
126 lines
8.2 KiB
JavaScript
#!/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`;
|
|
writeFileSync(override, JSON.stringify({ ...JSON.parse(readFileSync(FILE, 'utf8')), skip_proof_of_work: true }));
|
|
|
|
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);
|