diff --git a/docs/bench-log.md b/docs/bench-log.md index 38a953f2..da52ab1b 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -386,3 +386,30 @@ Six scenarios x 5 seeds (`sim/economy/results.md`): no backlog, no window miss, Sensitivities on b (2 seeds, `sim/economy/levers.md`): traffic 3 / 30 / 100 / 300 shards per block gives hash trough 0.81 / 0.82 / 0.63 / 0.06, oldest unproven age 0 / 0 / 85 / permanent, worst day within 60 s 1.000 / 1.000 / 0.994 / 0.825, hours under 50% hash 0 / 0 / 0 / 22. Observation window 20 min to 24 h: score 0.939 to 0.952, churn only. Lever study on b at 100 shards per block (2 seeds): window 5 / 10 / 20 / 30 s gives age max 565 / 325 / 0 / 0 s, hash trough 0.53 / 0.62 / 0.77 / 0.77, score 0.592 / 0.765 / 0.948 / 0.948; pool 0.1 / 0.2 / 0.3 / 0.4 gives age 168 / 325 / 16 / 0 and cards off 0.05 / 0.07 / 0.07 / 0.10; burn 0 to 0.5 and claim timeout 60 to 600 s leave the age at 325 s in every row. Proposal (not applied): window = p90 shard time plus one swap, 25 s at today's targets (O-5.1); `B_p` tied to the live proving fleet rather than a launch calibration. Not done: DAG and network latency, pool protocol, bonds on jobs beyond a class filter, price feedback from burns, the launch ramp; the age column of the 300-shard sensitivity row predates the age-formula fix. + +## 2026-10-04 execution layer attack fixes: F-exec-A (mempool gas-limit bound) and F-exec-B (pgas abort rule, spec 7.5) (execution-engineer) + +Machine: Apple M5 Max (18 cores), shared with other agents' builds (load 13 to 18). Worktree `vendor/igneum-node-exec`, branch `execution-layer` (fix commit on top of fb330692); built release with `CARGO_TARGET_DIR=target nice -n 19 cargo build --release -j 4 -p kaspad -p igneum-miner --features igneum-pow` (stable-aarch64 toolchain), unit tests with `cargo test --release -j 4 -p igneum-exec -p igneum-evm-types`. Network: 3 `igneumd --simnet --enable-unsynced-mining --unsaferpc --disable-upnp` nodes from this worktree, one honest stub miner, eth RPC 27990/27991/27992, gRPC 27910/27920/27930, p2p 27911/27921/27931, appdir `/tmp/igneum-exec-fix`; hostile blocks through the attack suite's `igneum-inject` (`vendor/igneum-node-exec-attacks/target/release`, same wire protocol). The attack scripts of `tools/exec-attacks` ran unchanged against this network with `IGNEUM_RPCS` and `IGNEUM_GRPC1` pointed at it, from copies outside the repository so `results/*.json` of the 3 October run stay as recorded; the after-fix reproduction is a separate script (session scratchpad, `scenario3_after.mjs`, 25 checks) written against the new rule. The live devnet and the attack suite's 276xx ports were not touched; every process was stopped at the end; 0 panics in the three node logs. + +What changed (spec 7.5, design 10 note): the inspector meters pgas against the including block's remaining `B_p` and halts the transaction before the opcode or precompile that would cross it (a precompile over the cap is answered with a revert that spends none of the forwarded gas, and the parent halts at its next instruction); the executor charges an aborted transaction as out of gas for the gas and pgas consumed to the abort, status 0, nonce advanced, receipt `pgasAborted`; the proving charge never takes a sender past the signed budget. The mempool refuses `gas_limit > B_e` (F-exec-A) and an estimated pgas above `B_p` (estimate = simulation at the tip under the cap), the template packs by the estimate, `eth_estimateGas` and `eth_call` fail naming the pgas when the cap is hit, `igneum_estimateGas` returns both dimensions. Simulations now read the state through `DatabaseRef` instead of cloning it per call. `igneum-exec-diff` treats a `pgasAborted` transaction as an Igneum-only flow (plain revm would run it to its own end). + +Unit tests (new, all pass): `pool::gas_limit_is_bounded_by_the_block_execution_limit`, `pool::estimated_proving_gas_is_bounded_by_the_block_proving_limit`, `pool::template_never_exceeds_the_remaining_proving_budget`, `executor::over_budget_pgas_is_aborted_charged_and_the_nonce_advances` (an SLOAD-loop bomb with a 30 M gas limit, about 54 M pgas if run out, is cut under `B_p`, charged exactly `gas_used x price + pgas_used x f_p`, nonce advanced; a second inclusion skips with `NonceTooLow` in under 100 ms; the next nonce executes), `executor::the_cap_is_the_remaining_block_budget` (two bombs in one block fill it to within 1,000 pgas of `B_p`; a third copy skips at its intrinsic pgas), `executor::estimate_reports_the_cap`. 6 of 6 in `igneum-exec`, 3 of 3 in `igneum-evm-types`. + +Scenario 3 (pgas exhaustion), before (3 October run, `tools/exec-attacks/results/scenario3.json`) and after, `B_p` = 30,000,000, modexp loops from one sender: + +| Loop | Before: outcome | Before: pgas, time | After: mempool | After: hostile inclusion (igneum-inject) | +|---|---|---|---|---| +| 1,000 | executed | 3,449,475 pgas, 1.34 ms | executed, 3,449,475 pgas, 1.46 ms | not needed | +| 3,000 | executed | 10,327,475 pgas, 4.56 ms | executed, 10,327,475 pgas, 3.94 ms | not needed | +| 6,000 | executed | 20,644,475 pgas, 7.54 ms | executed, 20,644,475 pgas, 8.57 ms; two of them in one go land in separate chain blocks (70, 72), neither aborted | not needed | +| 9,000 | skipped `BlockProvingBudget { would_be: 30,961,475 }` after 10.85 ms of execution, nothing charged, nonce stuck | 200 pgas charged to the block | refused: "proving gas above the block proving limit: at least 29,998,593 pgas metered before the abort, limit 30,000,000" | executed with status 0, `pgasAborted`, 29,998,593 pgas, 4,680,437 gas (limit 29,000,000), 11.37 ms, block pgas 29,998,593; sender charged 39,359,467,000,000,000 wei = 0.0394 IGN (gas x 2 gwei + pgas x 1 gwei), nonce 1 to 2; a second hostile inclusion skipped `NonceTooLow { expected: 2, got: 1 }` in 35 us, no second charge | +| 14,000 | never included (behind the stuck 9,000) | none | refused, same message | not run | +| 20,000 | never included | none | refused, same message | not run | + +After-fix checks: F-exec-A (`gas_limit` 30,000,001 refused: "gas limit 30000001 above the block execution gas limit 30000000"); `eth_estimateGas` for the 9,000 loop fails naming 29,998,593 pgas, `igneum_estimateGas` returns `exceedsProvingLimit: true`, and for the 6,000 loop `pgas` 20,644,475 with folded `gas` 27,465,126; the sender's next nonce (a transfer) executed four blocks after the abort; node RSS 322 MiB to 328 MiB (x1.02); blocks kept coming. 25 of 25. The unchanged `scenario3_pgas.mjs` now reports 3 of 4: its check "a heavy transaction is skipped with `BlockProvingBudget`" asserts the old rule and fails by design (the 9,000, 14,000 and 20,000 loops are refused at the mempool), the other three pass (max executed block pgas 20,644,475). + +Scenario 1 (malformed and boundary, unchanged script): 30 of 30; the `single-gas-limit-over-block` case now records `mempoolAdmitted: false` (the observation that filed F-exec-A is gone); the script's RSS probe looks for the attack worktree's binary path and found no process here, so that check was trivial in this run (the after-fix script measured RSS itself, above). + +Differential: `igneum-exec-diff` over the test network's export, segments 0 to 176, 17 executed transactions compared (one `pgasAborted`), 6 skipped copies confirmed, 11 accounts compared, 0 mismatches. + +Not changed: the pgas table magnitudes (prototype), `B_p` = 30 M (prototype). Open: the admission estimate runs under the RPC's state read lock, so a flood of heavy `eth_sendRawTransaction` calls delays the follower by up to `B_p` of simulation each (same shape as the `eth_call` flood of scenario 5, which stayed under 34 ms p95); a per-sender or per-second cap on estimates is the next step if the devnet shows it. diff --git a/docs/design/execution-layer.md b/docs/design/execution-layer.md index b3bce637..dbcbd553 100644 --- a/docs/design/execution-layer.md +++ b/docs/design/execution-layer.md @@ -450,6 +450,8 @@ Branch `execution-layer` in `vendor/igneum-node-exec` (a git worktree of `vendor | 8.2 RPC | Implemented | `rpc.rs` | `eth_chainId, blockNumber, gasPrice, maxPriorityFeePerGas, feeHistory, getBalance, getTransactionCount, getCode, getStorageAt, getBlockByNumber, getBlockByHash, getBlockTransactionCountByNumber, getBlockReceipts, getTransactionByHash, getTransactionReceipt, getTransactionByBlockNumberAndIndex, getLogs, sendRawTransaction, call, estimateGas, syncing, accounts, net_version, net_listening, web3_clientVersion`; `igneum_getTransactionStatus, igneum_getSegment, igneum_getBudgets, igneum_exportSegments`. HTTP JSON-RPC 2.0 with batches, port 26790 on devnet and simnet. viem 2.57 drove the whole smoke test through the stock paths (`deployContract`, `writeContract`, `readContract`, fee estimation). Missing: `eth_getProof`, `eth_subscribe`, `debug_*`, `trace_*`, `eth_getUncle*` | | 8.5 differential | Implemented, first row only in spirit | `igneum/exec/src/bin/diff.rs` | `igneum-exec-diff` replays the node's exported sequence through plain revm (no inspector, no pgas, no split) and compares status, gas used and logs per transaction, confirms every skipped copy is rejected by plain revm at its position, and compares every account's balance after adding back the Igneum-only flows the export records (proving charges, developer shares). 0 mismatches over 79 segments, 57 executed, 19 skipped, 10 accounts | +Rule change, 4 October 2026 (findings F-exec-A and F-exec-B of the attack suite, spec 7.5). The execution-time skip of 4.3 for a transaction's own running pgas is withdrawn: the inspector meters pgas against the including block's remaining `B_p` and halts the transaction before the opcode or precompile that would cross it, and the executor charges it as out of gas for the gas and pgas consumed to that point, status 0, nonce advanced (`pgas.rs` cap, `executor.rs`). The mempool refuses `gas_limit > B_e` and an estimated pgas above `B_p`, the template packs by that estimate (`pool.rs`), `eth_estimateGas` names the pgas when the cap is hit and `igneum_estimateGas` returns both dimensions (`rpc.rs`). Numbers: `docs/bench-log.md`, 4 October 2026, "execution layer attack fixes". + ### 10.2 Devnet rules fixed here (not in the design text) | Rule | Value | Why | diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index f46a15df..0d1a14b4 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -1436,3 +1436,23 @@ Status: Open, design and measurement scheduled (O-8.2, O-8.3); update control an Answer: Update control and the seed are decided (spec 8.2 item 4: no silent updates, the user accepts each release; 8.5: seed confirmed, hardware wallet). The litepaper promises earnings "in IGN and in your currency" and M13 promised projected earnings before mining starts; neither nets off power. New requirements for the official client and the phone monitor: (a) net earnings per card after an electricity tariff the user enters, the gross beside it; (b) mining income and proving income reported separately, per card and per day; (c) hardware compatibility and power limits per card, with mining and proving capability stated separately as the benchmark reports them (P16); (d) every failed or retried job visible with its reason; (e) the running release, its hash and any pending release the user has not accepted; (f) isolation: proving jobs run guest programs supplied by strangers, so the prover process holds no wallet key, no seed and no vote key, runs under a separate OS user or sandbox, and reaches the signer only through a local socket that signs payout claims and nothing else, designed and reviewed before the first external job runs. The phone app carries (a), (b), (d) and (e) as display rules (phone-app 4.1). Evidence: spec 8.2, 8.5; `site/litepaper.html` For miners; M13; `proto-cuda/windows-app/README.txt`. Experiment: O-8.2 (display rules measured against the pool protocol's `stats` on the phase 4 devnet), O-8.3 (isolation design reviewed, then an escape test with a hostile guest program). Review: external, point 7. + +## Execution attack findings (4 October 2026): fixed + +### P18. The mempool queues transactions no block can carry +"Send a transaction with a gas limit above the block limit and the node says thank you and keeps it. It can never be mined. Fill the queue with them." + +Status: Fixed (4 October 2026). Spec 7.5 item 3; `igneum/exec/src/pool.rs` `EvmPool::add`. + +Answer: Correct, and low: the queue slot was reserved against the sender's funds, so it was self-limited. The mempool now refuses `gas_limit > B_e` at admission with the error "gas limit N above the block execution gas limit B_e", as geth refuses `gas > block gas limit`. Measured: `gas_limit` 30,000,001 refused; 30,000,000 admitted and executed; unit test `pool::gas_limit_is_bounded_by_the_block_execution_limit`. + +Evidence: `docs/bench-log.md`, 4 October 2026 "execution layer attack fixes"; the 3 October attack entry, F-exec-A. + +### P19. An over-budget proving transaction runs for free, every time, and blocks its sender +"One big modexp costs more proving gas than a block allows. The node executes it in full, then skips it unpaid because it does not fit. Every node does that on every inclusion, and the sender's next nonces sit behind it forever. Free CPU on the whole network for the price of a signature." + +Status: Fixed (4 October 2026). Spec 7.5 items 1 to 4; `igneum/exec/src/pgas.rs`, `executor.rs`, `pool.rs`, `rpc.rs`. + +Answer: Correct, medium. The 3 October attack run showed it: a 9,000-call modexp loop ran 10.85 ms of native work, was skipped with `BlockProvingBudget`, paid nothing, and the sender's 14,000 and 20,000 loops were never includable behind it. Four rules now hold. (1) pgas is metered incrementally against the including block's remaining `B_p` and the transaction halts before the opcode or precompile that would cross it, so native work is bounded by `B_p`. (2) An aborted transaction is executed, not skipped: status 0, charged for the gas and pgas consumed to the abort, nonce advanced, so a later copy skips by the nonce rule at one account read and the sender's later nonces are free. (3) The mempool refuses a transaction whose estimated pgas exceeds `B_p` and the template packs by the estimate. (4) `eth_estimateGas` names the pgas when the cap is hit and `igneum_estimateGas` returns both dimensions. Measured after the fix: the same loop is refused by the mempool; a hostile miner's inclusion is cut at 29,998,593 pgas after 11.4 ms, the sender pays 0.0394 IGN, the nonce advances, a second inclusion costs 35 us, the next nonce executes; `igneum-exec-diff` 0 mismatches. What remains open is node policy, not consensus: the admission estimate costs up to `B_p` of simulation per heavy submission, under the RPC's state lock. + +Evidence: `docs/bench-log.md`, 4 October 2026 "execution layer attack fixes" (before and after table); the 3 October attack entry, F-exec-B; unit tests `executor::over_budget_pgas_is_aborted_charged_and_the_nonce_advances`, `executor::the_cap_is_the_remaining_block_budget`, `executor::estimate_reports_the_cap`, `pool::estimated_proving_gas_is_bounded_by_the_block_proving_limit`, `pool::template_never_exceeds_the_remaining_proving_budget`. diff --git a/docs/spec/07-execution.md b/docs/spec/07-execution.md index cb288a0b..d78a7138 100644 --- a/docs/spec/07-execution.md +++ b/docs/spec/07-execution.md @@ -68,3 +68,17 @@ Nothing in consensus changes for any of this: the segment claim already commits | Proof-record validity | relative to the carrying block's own selected-parent chain | Decided 3 October 2026 (ledger P11) | | Bridged stablecoins at genesis | none | Decided (3 October 2026) | | Official bridge | none | Decided (3 October 2026) | +| Per-transaction pgas cap | the including block's remaining `B_p`, metered incrementally, halt before the crossing opcode or precompile | Decided 4 October 2026 (ledger P19), 7.5 | +| Over-cap transaction | executed as out of gas: charged for gas and pgas consumed to the abort, status 0, nonce advanced, never skipped | Decided 4 October 2026 (ledger P19), 7.5 | +| Mempool bounds | `gas_limit <= B_e`; estimated pgas `<= B_p`; template packs by the estimate | Designed, node policy (ledger P18, P19), 7.5 | + +## 7.5 Proving gas per transaction: the cap and the abort + +Decided 4 October 2026 (ledger P18 and P19, closed by this section; `docs/bench-log.md`, "execution layer attack fixes"). Implemented on branch `execution-layer` and measured there. This section supersedes the execution-time skip of design 4.3 for a transaction's own running pgas: a transaction is never skipped for its proving cost after it has run, because a skip is free and the attack of P19 is exactly a transaction that runs for free. The intrinsic-only skip of design 1.5 (a copy whose intrinsic pgas alone does not fit the block) stays: it costs one comparison and no execution. + +1. **Incremental metering under a cap.** The executor meters pgas opcode by opcode and precompile by precompile while the native execution runs. Before an opcode executes, or a precompile is entered, its pgas is added to the transaction's running total. If the total would exceed the cap, the opcode or precompile does not run and the transaction halts there. The cap is the including block's remaining proving budget: `B_p` minus the pgas already charged to the block, which includes the intrinsic pgas of every copy included before it and of the transaction itself. A transaction carries no pgas limit field of its own (7.1, design 4.1); its own limit is the signed wei budget, which halts it as out of gas under 7.1 the moment the running charge would cross it, so the binding limit is whichever of the two comes first. A halt propagates: every frame still open halts at its next instruction, so a contract cannot catch the failed call and go on. The native work of one transaction is therefore bounded by `B_p` of proving gas and its own gas limit of execution gas, whatever its input, and no block ever carries more than `B_p`. +2. **An aborted transaction pays, and its nonce advances.** It is not skipped. It is an executed transaction with status 0, no logs and its state changes reverted, as a transaction that ran out of gas; its nonce advances. The sender pays `gas_consumed x (f_e + tip) + pgas_consumed x f_p`: `gas_consumed` is the execution gas spent up to the abort, intrinsic included and never the whole limit; `pgas_consumed` is the pgas metered up to the abort plus the intrinsic pgas. The proving part never takes the charge past the signed budget. Both base fees burn and the tip splits as design 4.4. The receipt carries `pgasAborted: true`. A later copy of the same transaction is skipped by the nonce rule of design 1.3 at one account read, so it is never executed twice, and the sender's later nonces are not held behind it. +3. **Admission and the template.** A node's mempool does not admit a transaction whose `gas_limit` exceeds `B_e`, nor one whose estimated pgas exceeds `B_p`; the estimate is a simulation at the tip under the cap of item 1, so it costs at most `B_p` of pgas, and the rejection names the pgas metered and `B_p`. The template builder packs by that estimate: it never includes a transaction whose estimated pgas exceeds the block's remaining `B_p`, and it stops at the first transaction that does not fit a budget, so a sender's later nonces never jump ahead. These are node policy (design 1.4), not consensus; item 1 is the consensus rule and holds whatever a miner includes. +4. **The pgas dimension is visible.** `eth_estimateGas` folds the proving charge into the quoted gas limit (7.1) and fails, naming the pgas metered and `B_p`, for a call that hits the cap; `eth_call` fails the same way. `igneum_estimateGas` returns both dimensions for the same simulation: `gasUsed`, `pgas`, the folded `gas`, `provingGasLimit`, both base fees and `exceedsProvingLimit`, so a wallet or tool can show the proving side. + +What the rule gives, measured (bench-log, 4 October 2026, `B_p` 30 M, modexp loop of 9,000 calls): before, 10.85 ms of native execution per inclusion, nothing charged, the nonce stuck and the sender's later transactions never includable; after, the mempool refuses it, a hostile miner's inclusion is cut at 29,998,593 pgas after 4,680,437 gas in 11.4 ms, the sender pays 0.0394 IGN, the nonce advances, a second inclusion is skipped in 35 us, and the next nonce executes in the following blocks.