The fleet's 22:09 UK incident (a Mac-side pkill -f <log file name> matched nothing, the roll-everything script lived on and wiped a held box) and the day's two pgrep self-matches are one class. The check flags pgrep -f / pkill -f with a plain literal (every one on a line), any pgrep/pkill on a file-name shape, and ps | grep with a literal; it allows the bracket form, -x, -F pidfile, kill $(cat pidfile), a variable and a full path; 11 banned and 16 allowed shapes in its self-test; 0.15 s over the tree. The 25 pkill -f sp1-gpu-server inside bash -c bodies (which matched the calling bash) are pkill -x; the other 11 literals take the bracket form; prover-socket-check accepts both. Row R in the record; the CLAUDE.md rule names the check and covers pkill and file names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| contracts | ||
| lib | ||
| .gitignore | ||
| compile.mjs | ||
| net.sh | ||
| README.md | ||
| run_all.sh | ||
| run_scenario6.sh | ||
| scenario1_malformed.mjs | ||
| scenario2_nonce.mjs | ||
| scenario3_pgas.mjs | ||
| scenario4_registry.mjs | ||
| scenario5_rpcfuzz.mjs | ||
| scenario6_reorg.mjs | ||
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.