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>
121 lines
6.1 KiB
JavaScript
121 lines
6.1 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]
|
|
// 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.
|
|
//
|
|
// See tools/harness/README.md.
|
|
|
|
import { stopAll, assertBinaries } 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 { }
|
|
return `Machine: ${cpu}, ${mem}, load ${load.trim()}. Private test network of igneumd (release, skip_proof_of_work devnet) on 127.0.0.1 ports 27200+, data /tmp/igneum-harness; the live devnet and the PC node were not touched. Harness: tools/harness/, node fork worktree vendor/igneum-node-harness.`;
|
|
}
|
|
|
|
// 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');
|
|
|
|
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/igneum-harness/results and /tmp/igneum-harness/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)' : ''} ====`);
|
|
try {
|
|
const mod = await SCENARIOS[key]();
|
|
const { rows } = await mod.run({ quick });
|
|
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)' : ''} on the ordering-layer node`,
|
|
machine: machineLine(),
|
|
rows: allRows,
|
|
notes: [
|
|
'Full JSON per scenario under /tmp/igneum-harness/results and /tmp/igneum-harness/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); });
|