igneum/tools/harness/lib/sim.mjs
igneum-josh 394030c9da Harness: consensus attack catalogue runner and first results
tools/harness runs the standard consensus-attack catalogue against a private
test network of our own igneumd nodes (127.0.0.1 ports 27200+, /tmp/igneum-harness,
never the live devnet or the PC node), with a pass criterion per scenario from the
spec and a measured result each. Built on the node fork's own crates
(igneum-harness-sim on kaspa_utils::sim as simpa does; igneum-p2p-probe for the
wire). Scenarios: 1 withholding, 2 timestamp edges and drift, 3 partition and heal,
4 eclipse, 5 malformed and boundary inputs on every p2p and RPC surface, 6 resource
exhaustion, 7 fast-miner flood. Finality and difficulty-controller scenarios are
stubs with their criteria written.

bench-log: one dated entry, a row per scenario (criterion, measured, pass or fail).
First run: 19 of 20 measured rows pass. Findings recorded in the entry: scenario 5
reproduces ledger M15 on HEAD (bogus past-day or DAA headers build a 256 MiB cache
before rejection; the r3-fixes branch removes it); scenario 1 at 45% hash with
burst withholding shows a selfish-mining blue-share gain (50.7% of blues), the one
failing row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 22:17:37 +00:00

47 lines
2.5 KiB
JavaScript

// Runs the in-process simulator (vendor worktree crate igneum/harness-sim) and reads its JSON report.
import { spawnSync, spawn } from 'node:child_process';
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
import { SIM, TMP, log } from './net.mjs';
export function runSim(name, args, { timeoutMs = 60 * 60_000 } = {}) {
if (!existsSync(SIM)) throw new Error(`missing ${SIM}; build igneum-harness-sim in the harness worktree`);
mkdirSync(`${TMP}/sim`, { recursive: true });
const out = `${TMP}/sim/${name}.json`;
const argv = [...args, '--out', out];
log(`sim ${name}: igneum-harness-sim ${argv.join(' ')}`);
const t0 = Date.now();
const r = spawnSync('nice', ['-n', '19', SIM, ...argv], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
if (!existsSync(out)) throw new Error(`sim ${name} produced no report: ${(r.stderr || '').slice(-400)}`);
const report = JSON.parse(readFileSync(out, 'utf8'));
report.wall_s = (Date.now() - t0) / 1000;
log(`sim ${name}: ${report.end_time_s} virtual s in ${report.wall_s.toFixed(0)} s wall`);
return report;
}
/// Several simulations at once (each uses a couple of threads); returns reports in order.
export async function runSims(jobs, { parallel = 3 } = {}) {
const results = new Array(jobs.length);
let next = 0;
async function worker() {
while (next < jobs.length) {
const i = next++;
const { name, args } = jobs[i];
mkdirSync(`${TMP}/sim`, { recursive: true });
const out = `${TMP}/sim/${name}.json`;
const argv = [...args, '--out', out];
log(`sim ${name}: igneum-harness-sim ${argv.join(' ')}`);
const t0 = Date.now();
await new Promise((resolve) => {
const p = spawn('nice', ['-n', '19', SIM, ...argv], { stdio: ['ignore', 'ignore', 'pipe'] });
let err = ''; p.stderr.on('data', d => { err += d; if (err.length > 4000) err = err.slice(-4000); });
p.on('exit', () => { if (!existsSync(out)) log(`sim ${name} FAILED: ${err.slice(-300)}`); resolve(); });
});
if (existsSync(out)) { results[i] = JSON.parse(readFileSync(out, 'utf8')); results[i].wall_s = (Date.now() - t0) / 1000; log(`sim ${name}: ${results[i].end_time_s} virtual s in ${results[i].wall_s.toFixed(0)} s wall`); }
}
}
await Promise.all(Array.from({ length: Math.min(parallel, jobs.length) }, worker));
return results;
}
export function hist(h) { return Object.entries(h || {}).map(([d, n]) => `${d}:${n}`).join(' ') || 'none'; }
export function maxDepth(h) { return Math.max(0, ...Object.keys(h || {}).map(Number)); }