#!/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); });