igneum/tools/harness/run.mjs
igneum-labs 3bafccf103 Harness s8 steady state and the bench-log paragraph: RSS per 1,000 blocks before and after the M30 fix
s8-steady.mjs: two nodes, one honest vmine miner at 1 block/s, no flood,
RSS and cache-build count every 60 s, vmmap -summary at 0, 500, 1,000 and
1,500 blocks. Both builds ran 1,500 blocks on the 60x profile: before
41 to 1,342 MB by 514 blocks (9 cache builds, five 256 MiB chunks resident:
KEEP 4 plus one evicted chunk the allocator keeps) then flat, 27 builds in
1,529 blocks; after 319 MB at 510 blocks (1 build), 589 at 1,029 (the second
day's cache, by design), 603 at 1,526, 2 builds. Residual 30 MB per 1,000
blocks on both builds, read as the consensus database and caches filling,
not the PoW cache. JSON and vmmap files under
docs/benchmarks/memory-floods-2026-10-04/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 01:49:46 +00:00

129 lines
7 KiB
JavaScript

#!/usr/bin/env node
// Igneum consensus attack harness. Runs the catalogue against a private test network of our own nodes on
// 127.0.0.1 ports 27200+ and /tmp/igneum-harness, writes a results table per run to docs/bench-log.md, and leaves
// the test network stopped. The live devnet (26610/26611, 26640/26641, 28640) and the PC node are never touched.
//
// node tools/harness/run.mjs [scenario ...] [--quick] [--no-bench-log] [--fast-time] [--live-only]
// --live-only skips the simulator part of s7 (the vmine flood against real nodes needs only igneumd).
// IGNEUM_HARNESS_BASE_PORT and IGNEUM_HARNESS_TMP move the ports and the data directory (lib/net.mjs).
// scenarios: s5 s2 s1 s3 s6 s4 s7 (default: priority order 5,2,1,3,6,4,7)
// --quick runs shorter block counts and durations for a smoke run.
// --fast-time runs the network and the simulator on infra/fast-time/override-60x.json (every clock-like consensus
// parameter divided by 60: merge depth 60, finality window 120 DAA, 60-block epochs) with the devnet-v4 binaries
// from vendor/igneum-node/target-integration; the merge-depth scenarios (3, 4) scale their cuts with it.
//
// See tools/harness/README.md.
import { stopAll, assertBinaries, FAST_TIME, TARGET, BASE_PORT, TMP } from './lib/net.mjs';
import { benchLogEntry, appendBenchLog } from './lib/report.mjs';
import { stubRows } from './scenarios/stubs.mjs';
import { execSync } from 'node:child_process';
const SCENARIOS = {
s1: () => import('./scenarios/s1-withhold.mjs'),
s2: () => import('./scenarios/s2-timestamp.mjs'),
s3: () => import('./scenarios/s3-partition.mjs'),
s4: () => import('./scenarios/s4-eclipse.mjs'),
s5: () => import('./scenarios/s5-malformed.mjs'),
s6: () => import('./scenarios/s6-exhaustion.mjs'),
s7: () => import('./scenarios/s7-flood.mjs'),
s8: () => import('./scenarios/s8-steady.mjs'), // steady state, not in the priority order (25 min, no criterion)
};
const PRIORITY = ['s5', 's2', 's1', 's3', 's6', 's4', 's7'];
function machineLine() {
let cpu = 'unknown', mem = '';
try { cpu = execSync('sysctl -n machdep.cpu.brand_string').toString().trim(); } catch { }
try { mem = (parseInt(execSync('sysctl -n hw.memsize').toString().trim(), 10) / 2 ** 30).toFixed(0) + ' GB'; } catch { }
let load = ''; try { load = execSync('uptime').toString().match(/load averages?: ([\d. ]+)/)?.[1] || ''; } catch { }
const profile = FAST_TIME ? 'skip_proof_of_work devnet on the 60x fast-time profile, infra/fast-time/override-60x.json' : 'skip_proof_of_work devnet';
return `Machine: ${cpu}, ${mem}, load ${load.trim()}. Private test network of igneumd (release, ${profile}) on 127.0.0.1 ports ${BASE_PORT}+, data ${TMP}; the live devnet and the PC node were not touched. Harness: tools/harness/, binaries ${TARGET}.`;
}
// Assemble one bench-log entry from result JSONs already written under /tmp/igneum-harness/results, without
// re-running. Each scenario file writes either { rows: [...] } or a scenario-specific shape; this reads the rows.
async function assembleFromResults() {
const { readFileSync, existsSync } = await import('node:fs');
const { RESULTS } = await import('./lib/report.mjs');
const files = { s5: 's5-malformed', s2: 's2-timestamp', s1: 's1-withhold', s3: 's3-partition', s6: 's6-exhaustion', s4: 's4-eclipse', s7: 's7-flood' };
const rows = [];
for (const key of PRIORITY) {
const f = `${RESULTS}/${files[key]}.json`;
if (!existsSync(f)) { console.error(`no result for ${key} at ${f}`); continue; }
const d = JSON.parse(readFileSync(f, 'utf8'));
const rs = d.rows || (d.summary_row ? [d.summary_row] : []);
rows.push(...rs);
}
return rows;
}
async function main() {
const args = process.argv.slice(2);
const quick = args.includes('--quick');
const noBench = args.includes('--no-bench-log');
const assemble = args.includes('--assemble');
const liveOnly = args.includes('--live-only');
if (assemble) {
const allRows = await assembleFromResults();
allRows.push(...stubRows());
const date = new Date().toISOString().slice(0, 10);
const entry = benchLogEntry({
date: `${date}, consensus attack harness (consensus-engineer)`,
title: 'catalogue run on the ordering-layer node',
machine: machineLine(),
rows: allRows,
notes: [
`Full JSON per scenario under ${TMP}/results and ${TMP}/sim. The simulator (igneum/harness-sim in the fork worktree) runs real consensus code in virtual time with PoW skipped, as rusty-kaspa simpa does; the live scenarios (5, 6, 7 Part B) drive real igneumd processes over wRPC and the fork's own p2p (igneum/p2p-probe).`,
'Finality and difficulty-controller scenarios are stubs here: their criteria are written and they run against those branches once merged into the harness worktree (see tools/harness/scenarios/stubs.mjs).',
],
});
const fp = appendBenchLog(entry);
console.log(`appended ${allRows.length} rows to ${fp}`);
process.exit(0);
}
let picks = args.filter(a => !a.startsWith('--'));
if (!picks.length) picks = PRIORITY;
assertBinaries();
const allRows = [];
for (const key of picks) {
if (!SCENARIOS[key]) { console.error(`unknown scenario ${key}`); continue; }
console.log(`\n==== scenario ${key}${quick ? ' (quick)' : ''}${FAST_TIME ? ' (fast-time 60x)' : ''} ====`);
try {
const mod = await SCENARIOS[key]();
const { rows } = await mod.run({ quick, liveOnly });
allRows.push(...rows);
} catch (e) {
console.error(`scenario ${key} threw: ${e.stack || e}`);
allRows.push({ scenario: key, criterion: 'see tools/harness', result: `harness error: ${String(e.message).slice(0, 160)}`, pass: false });
await stopAll();
}
}
allRows.push(...stubRows());
console.log('\n==== results ====');
for (const r of allRows) console.log(`[${r.pass === true ? 'PASS' : r.pass === false ? 'FAIL' : 'STUB'}] ${r.scenario}: ${r.result}`);
if (!noBench) {
const date = new Date().toISOString().slice(0, 10);
const entry = benchLogEntry({
date: `${date}, consensus attack harness (consensus-engineer)`,
title: `catalogue run${quick ? ' (quick)' : ''}${FAST_TIME ? ' (fast-time 60x)' : ''} on the ordering-layer node`,
machine: machineLine(),
rows: allRows,
notes: [
`Full JSON per scenario under ${TMP}/results and ${TMP}/sim. The simulator (igneum/harness-sim in the fork worktree) runs real consensus code in virtual time with PoW skipped, as rusty-kaspa simpa does; the live scenarios (5, 6, 7 Part B) drive real igneumd processes over wRPC and the fork's own p2p (igneum/p2p-probe).`,
'Finality and difficulty-controller scenarios are stubs here: their criteria are written and they run against those branches once merged into the harness worktree (see tools/harness/scenarios/stubs.mjs).',
],
});
const f = appendBenchLog(entry);
console.log(`\nappended results to ${f}`);
}
await stopAll();
const failed = allRows.filter(r => r.pass === false);
process.exit(failed.length ? 1 : 0);
}
main().catch(async (e) => { console.error(e); await stopAll(); process.exit(1); });