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>
96 lines
6.9 KiB
Markdown
96 lines
6.9 KiB
Markdown
# 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). `IGNEUM_HARNESS_BASE_PORT=29500 IGNEUM_HARNESS_TMP=/tmp/my-harness` moves the ports (node
|
|
`i` takes base + 10i, proxies base + 900 + i) and the data and results directories, so two agents can run the
|
|
harness at once (4 October 2026). 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
|
|
node tools/harness/run.mjs s7 --quick --live-only # s7 Part B only (the vmine flood against real nodes; no simulator binary needed)
|
|
node tools/harness/run.mjs s3 s4 --fast-time # the 60x fast-time profile (infra/fast-time): merge depth 60 s, so the
|
|
# partition and eclipse cuts are 10, 30 and 62 s instead of 600, 1,800 and
|
|
# 3,700; nodes and the simulator come from vendor/igneum-node/target-integration
|
|
```
|
|
|
|
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 |
|
|
| 8 | Steady state: one honest miner at 1 block/s, no flood, 1,500 blocks (ledger M30 follow-up) | live | recorded only: RSS of the mining node and the follower at 0, 500, 1,000 and 1,500 blocks, cache builds, `vmmap -summary` at each milestone (`IGNEUM_STEADY_BLOCKS`, `IGNEUM_STEADY_MAX_MIN`) |
|
|
|
|
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.
|