Adversarial robustness and conformance tests of the execution layer against a throwaway 3-node simnet on ports 27600+. Six scenarios, each a runnable command with a design-derived pass criterion and a measured result: malformed/boundary txs, nonce games across parallel blocks, RPC fuzz, pgas exhaustion, reorgs under execution, and developer-registry abuse. 98 checks, 0 failures, 0 node panics. Two findings filed in the bench-log entry: the mempool admits txs with gas_limit above B_e (low), and an over-pgas-budget tx is executed natively in full before being skipped for no fee (medium, griefing). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4.4 KiB
Execution-layer attacks (robustness and conformance)
Adversarial tests of the Igneum execution layer (docs/design/execution-layer.md, docs/spec/07-execution.md)
against a throwaway 3-node igneumd simnet. Each scenario is a runnable command with a pass criterion taken from
the design and a measured result. This is testing of our own private software.
Everything runs on ports 27600 and above under /tmp/igneum-exec-attacks. The live devnet (26610, 26611, 26640,
26641, 28640) and other agents' ports (up to 27599) are never touched.
Build
The node, the honest miner and the hostile injector are built in the worktree vendor/igneum-node-exec-attacks
(branch exec-attacks):
cd vendor/igneum-node-exec-attacks
export PATH="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin:$PATH"
CARGO_TARGET_DIR=target nice -n 19 cargo build --release -j 4 -p kaspad -p igneum-miner --features igneum-pow
This produces igneumd, igneum-miner and igneum-inject under target/release.
igneum-inject is the hostile miner: it fetches a block template over gRPC, replaces the EVM body with an
arbitrary set of raw EIP-2718 bytes (which the mempool would never hand out), recomputes hash_merkle_root and
resubmits, so transactions the mempool rejects reach consensus body validation and the executor directly. Several
blocks built off one template share a selected parent and land in parallel on the DAG.
Contracts
node compile.mjs compiles contracts/PgasBomb.sol (modexp/keccak loops, cheap in gas and heavy in pgas) and
contracts/RegistryAbuse.sol (a Worker and a Factory for the developer-registry tests) with solc 0.8.37.
Network
./net.sh start [1|3] # hub topology: 3 nodes, 1 or 3 honest stub miners
./net.sh start-split # partition P1={node1}, P2={node2,node3}, no link until heal (igneum-inject addpeer)
./net.sh stop
Nodes run --simnet --enable-unsynced-mining --unsaferpc (PoW skipped). eth JSON-RPC on 27690/27691/27692, gRPC on
27610/27620/27630, p2p on 27611/27621/27631. Node 1's miner pays the test miner account; nodes 2 and 3 pay the
test accounts B and C, so rewards are spendable by the harness whichever chain wins.
Scenarios (run in priority order 1, 2, 5, 3, 6, 4)
| # | Command | What it does | Criterion |
|---|---|---|---|
| 1 | node scenario1_malformed.mjs |
malformed and boundary txs over eth_sendRawTransaction and inside a hostile block (bad RLP, wrong chain id, oversized calldata, gas at/over the block limit, bad signature, nonce far ahead, nonce reuse, zero/max fee) |
state-free faults invalidate the block; state-dependent faults skip the tx with no receipt; no panic; RSS bounded |
| 2 | node scenario2_nonce.mjs |
one sender's nonces spread across parallel blocks in different orders, duplicates in several blocks, a conflicting same-nonce pair | exactly one execution per nonce; deterministic; state roots identical on all nodes |
| 5 | node scenario5_rpcfuzz.mjs |
every eth_*/igneum_* with junk params, huge arrays, deep nesting; 50x eth_call flood from one client |
errors not crashes; honest latency under 200 ms |
| 3 | node scenario3_pgas.mjs |
modexp loops cheap in gas, heavy in pgas, with growing loop counts | the per-block pgas budget B_p caps inclusion; no executed block exceeds B_p; execution time per block measured |
| 6 | ./run_scenario6.sh |
partition/heal reorgs of several depths with hostile miners while txs flow (uses start-split and igneum-inject addpeer) |
state root recomputed deterministically; displaced-tx receipts consistent on all nodes and canonical; no stuck mempool |
| 4 | node scenario4_registry.mjs |
register a payee for someone else's code; factory inheritance (CREATE, CREATE2, same-tx override, unregistered, EOA override); self-dealing (sender = payee = miner) | design 4.5: base fees burned, no positive-expectation loop; records the max share a self-dealer recovers |
run_all.sh runs every scenario in priority order (starting and stopping the right network for each) and prints a
one-line pass/fail per scenario. Per-scenario detail lands in results/*.json.
Notes on DAG semantics observed here: a selected-chain reorg does not orphan merged blocks (design 1.2/1.3), so a "displaced" transaction re-executes exactly once in the segment that merges its block rather than losing its receipt; scenario 6 checks for a consistent, canonical outcome across nodes rather than Ethereum-style eviction.