igneum/tools/harness/README.md
igneum-labs 87df9eac9d 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

5.9 KiB

Igneum consensus attack harness

Plays the standard consensus-attack catalogue against a private test network of our own Igneum nodes, with a pass criterion per scenario taken from the spec and a measured result for each. This is robustness and conformance testing of our own devnet software, the practice upstream Kaspa (simpa, testing/integration) and the Ethereum clients follow.

What it is built on

  • The in-process network simulator from rusty-kaspa (kaspa_utils::sim, the engine simpa uses): one real Consensus per node in virtual time. The harness adds hash-share miners, a withholder, timestamp policies, a cut-and-heal topology and reorg records. Source: vendor/igneum-node-harness/igneum/harness-sim.
  • Real igneumd processes on 127.0.0.1, driven over wRPC JSON, for the live scenarios (5, 6, 7 Part B).
  • A p2p probe that speaks the fork's own protocol (handshake, InvRelayBlock, RequestRelayBlocks, Block) to deliver malformed blocks on the wire. Source: vendor/igneum-node-harness/igneum/p2p-probe.

All nodes run with skip_proof_of_work (the harness never hashes), so the harness controls each miner's hash share exactly. Every other consensus rule (timestamps, DAA, GHOSTDAG, merge depth, mass, coinbase) runs unchanged, so the harness exercises the ordering layer, not a weakened copy of it.

Ports and isolation

The test network uses 127.0.0.1 ports 27200 and up and data under /tmp/igneum-harness. It never touches the live devnet (gRPC 26610, P2P 26611, observer 26640/26641/28640), the PC node at 192.168.68.67, or any port other agents use (up to 27199). Loopback peers are never gossiped (components/addressmanager/src/lib.rs), so no link forms that a scenario did not ask for. Everything the harness starts is stopped at the end, including on SIGINT.

Build

The harness binaries live in the harness worktree of the node fork:

cd vendor/igneum-node/ && git worktree add -b harness ../igneum-node-harness HEAD   # once
cd ../igneum-node-harness
CARGO_TARGET_DIR=target nice -n 19 ~/.cargo/bin/cargo build --release -j 4 \
  -p kaspad -p igneum-miner --features kaspad/igneum-pow
CARGO_TARGET_DIR=target nice -n 19 ~/.cargo/bin/cargo build --release -j 4 \
  -p igneum-p2p-probe -p igneum-harness-sim

This produces target/release/igneumd, igneum-miner, igneum-p2p-probe and igneum-harness-sim. The run scripts find them there; override with IGNEUMD, IGNEUM_P2P_PROBE, IGNEUM_HARNESS_SIM.

Run

node tools/harness/run.mjs                # the full catalogue, priority order 5,2,1,3,6,4,7
node tools/harness/run.mjs s5 s2 --quick  # named scenarios, short durations
node tools/harness/run.mjs --no-bench-log # do not append to docs/bench-log.md
node tools/harness/scenarios/s1-withhold.mjs --quick   # one scenario on its own

Each run appends one dated entry to docs/bench-log.md with a row per scenario (criterion, measured result, pass or fail), writes full JSON per scenario under /tmp/igneum-harness/results and /tmp/igneum-harness/sim, and leaves the test network stopped. Exit code is non-zero if any scenario failed.

The catalogue

# Scenario Where Criterion (spec)
1 Withheld-block mining (10, 25, 33, 45% share, release every 5 and 20) sim spec 02 2.1: attacker blue-block share within 2 sigma of hash share over 2,000 blocks; reorg depth distribution recorded
2 Timestamp manipulation: past-median and future-time edges; a 33% miner stretching inside the rules live + sim spec 02 2.3: rejected exactly at the boundary; block-rate drift of the controller measured
3 Partition and heal (2, 10, 30 min, plus one beyond merge depth) sim spec 02 2.1: one chain after the merge-depth rule; reorg depth and time to heal recorded
4 Eclipse of one node (victim fed a slower side chain) sim spec 02 2.1: rejoins the honest chain on reconnection within the merge-depth bound
5 Malformed and boundary inputs on every p2p message and RPC method the fork touches live + p2p fork-divergence header and RPC rows; ledger M15: rejected without a crash or a cache build
6 Resource exhaustion (50x template, submit and mempool floods from one peer) live honest template p95 under 200 ms, node under its memory bound; numbers recorded
7 Fast-miner flood (50x joins at once, the devnet event) live + sim node stays responsive; controller trajectory recorded for the difficulty branch

Scenario 5 overlaps the r3-fixes branch (ledger M15): that branch runs the cheap checks before the lottery engine, caps cache builds and bans the peer. On this node branch (HEAD, d62708a8, before r3-fixes) the engine keeps three caches, so a stream of bogus past-day headers can still force a build; scenario 5 records whether any case built a 256 MiB cache, which is the quantity r3-fixes drives to zero.

Stubs (finality and difficulty-controller branches)

Finality and the difficulty controller live on their own branches. Their scenarios are stubs here, with criteria written and a one-line plan for running them once the branch is merged into the harness worktree. See scenarios/stubs.mjs. In short: 1b (withhold vs vote weight), 3b (partition vs lock revocation, ledger F16), 4b (eclipse vs presence window, ledger F2), 2b (timestamp stretch on the dual-lane controller), 7b (the 50x flood on the dual-lane controller, against the kaspa-sampled baseline this harness records).

Files

  • run.mjs scenario runner and bench-log writer.
  • scenarios/s1..s7 one file per scenario; each exports run({ quick }) and runs standalone.
  • scenarios/stubs.mjs the finality and controller stubs.
  • lib/net.mjs node processes, topology, TCP proxy for a cuttable link, cleanup.
  • lib/rpc.mjs wRPC JSON client. lib/miner.mjs virtual miner and latency probe. lib/address.mjs devnet address encoder. lib/sim.mjs runs igneum-harness-sim. lib/report.mjs results and bench-log entry.