tools/harness: IGNEUM_HARNESS_BASE_PORT and IGNEUM_HARNESS_TMP move the test network's ports and data directory so two agents can run it at once; the u64 sentinel round-trip in overrideParams is fixed with the BigInt reviver from tools/finality-attacks (ledger F25); s6 records rss_start, rss_delta and cache_builds per load and reports per-load growth (the old row subtracted one baseline taken before all three loads, which is how the mempool flood was read as +270 MB); s7 counts "PoW cache built" lines beside every RSS sample; --live-only skips the s7 simulator part. docs/bench-log.md: the 4 October 2026 (night) entry: the floods' growth was one 256 MiB PoW cache per epoch roll (the engine kept a cache per (epoch, day) pair, KEEP 4), measured before and after the fork fix (fork branch fud-memory, 796f758d): submit load +263/+257 MB with 1/1 builds before, +9/+2 MB with 0/0 after; block flood 302 to 1,085 MB with 3/3 builds before, 300 to 315 MB with 0/0 after. Result JSON under docs/benchmarks/memory-floods-2026-10-04/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
128 lines
6.9 KiB
JavaScript
128 lines
6.9 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'),
|
|
};
|
|
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); });
|