## Summary
Two EIP-8037 state gas accounting fixes:
1. **CALL state gas ordering** — Charge new-account state gas (112 ×
cpsb = 131,488) **before** the 63/64 child gas allocation, not after
2. **Code-store OOG underflow** — Do not inflate `TotalStateGasCharged`
when `UseGas` fails for code deposit, preventing uint64 underflow in
`blockGasUsed()`
## Bug 1: CALL state gas ordering (`operations_acl.go`)
In `makeCallVariantGasCall`, the EIP-8037 state gas for new account
creation was returned in the `GasCosts` struct and charged by the
interpreter's `UseGas`/`Sub` **after** `callGas()` had already computed
the 63/64 child gas allocation using the full (pre-state-gas)
`contract.Gas.RegularGas`.
When the state gas reservoir is empty (common case — reservoir only has
gas when `tx.gasLimit` exceeds `TX_MAX_GAS_LIMIT - intrinsic`), state
gas spills to regular gas. The spill amount (131,488) far exceeds the
1/64 retained gas (~15,600 at 1M gas), causing an underflow/OOG on CALLs
that should succeed.
**Fix:** Charge state gas directly before `callGas()` so the 63/64
calculation uses the reduced regular gas, then return `StateGas: 0` to
avoid double-charging. This matches nethermind's implementation
(`EvmInstructions.Call.cs:187-213`) and besu's approach.
**Spec basis:** EIP-8037 says "State gas charges deduct from
`state_gas_reservoir` first; when the reservoir is exhausted, from
`gas_left`." The 63/64 rule applies to `gas_left`, so state gas
spillover into `gas_left` must happen before the 63/64 computation.
## Bug 2: Code-store OOG underflow (`evm.go`)
In `initNewContract`, when `UseGas` fails for code deposit (code-store
OOG on valid code), the upstream code at `evm.go:666-672` added
`createDataGas.StateGas` to `TotalStateGasCharged` without actually
consuming any gas. For a 14KB init code output, this adds ~17.3M phantom
state gas to TSC.
This inflated TSC propagates through `blockGasUsed()`:
```
execRegularUsed := totalExecUsed - execStateUsed // uint64 underflow when TSC > totalExecUsed
```
The underflow produces a massive `txRegular` value, causing
`ReturnGasAmsterdam` to reject the block with `gas limit reached`.
**Fix:** Remove the TSC inflation on `UseGas` failure (lines 666-672).
Also remove `ErrCodeStoreOutOfGas` from the `isCodeValidation` condition
(line 621-623), so code-store OOG follows the normal exceptional halt
path: all regular gas consumed, state gas reverted via
`RevertStateGas()`.
**Spec basis:** EIP-8037 §"Contract deployment cost calculation"
explicitly lists code-store OOG as a failure path:
> **Failure paths** (REVERT, OOG/invalid during initcode, **OOG during
code deposit**, or `L > MAX_CODE_SIZE`): Do NOT charge `GAS_CODE_DEPOSIT
* L` or `HASH_COST(L)`
And §"Transaction-level gas accounting":
> On child **exceptional halt**, all state gas consumed by the child,
both from the reservoir and any that spilled into `gas_left`, is
restored to the parent's reservoir.
## Test plan
- [x] geth+besu: 161 blocks (5+ epochs) with heavy spamoor load (eoatx
50 + evm-fuzz 25 + tx-fuzz 15), zero errors
- [x] Without Bug 2 fix: geth+besu chain-split at block ~30 under load —
`blockGasUsed` uint64 underflow
- [x] geth+nethermind: 86+ blocks no-load, 162 blocks eoatx load
(nethermind has separate BAL validation bug under fuzz)
- [x] geth+nimbus: 150+ blocks evm-fuzz with nimbus CALL fix applied —
gasUsed matches
- [x] Verified cross-client: nethermind and besu both charge CALL state
gas before 63/64
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
For bal-devnet-3 we need to update the EIP-8024 implementation to the
latest spec changes: https://github.com/ethereum/EIPs/pull/11306
> Note: I deleted tests not specified in the EIP bc maintaining them
through EIP changes is too error prone.
Return the Amsterdam instruction set from `LookupInstructionSet` when
`IsAmsterdam` is true, so Amsterdam rules no longer fall through to the
Osaka jump table.
---------
Co-authored-by: rjl493456442 <garyrong0905@gmail.com>
https://eips.ethereum.org/EIPS/eip-7928 spec:
> Precompiled contracts: Precompiles MUST be included when accessed.
If a precompile receives value, it is recorded with a balance change.
Otherwise, it is included with empty change lists.
The precompiled contracts are not explicitly touched when they are
invoked since Amsterdam fork.
In src/ethereum/forks/amsterdam/vm/interpreter.py:299-304, the caller
address is
only tracked for block level accessList when there's a value transfer:
```python
if message.should_transfer_value and message.value != 0:
# Track value transfer
sender_balance = get_account(state, message.caller).balance
recipient_balance = get_account(state, message.current_target).balance
track_address(message.state_changes, message.caller) # Line 304
```
Since system transactions have should_transfer_value=False and value=0,
this condition is never met, so the caller (SYSTEM_ADDRESS) is not
tracked.
This condition is applied for the syscall in the geth implementation,
aligning with the spec of EIP7928.
---------
Co-authored-by: Felix Lange <fjl@twurst.com>
This PR makes `TestEIP8024_Execution` verify explicit error types (e.g.,
`ErrStackUnderflow` vs `ErrInvalidOpCode`) rather than accepting any
error. It also fails fast on unexpected opcodes in the mini-interpreter
to avoid false positives from missing opcode handling.
I recently went on a longer flight and started profiling the geth block
production pipeline.
This PR contains a bunch of individual fixes split into separate
commits.
I can drop some if necessary.
Benchmarking is not super easy, the benchmark I wrote is a bit
non-deterministic.
I will try to write a better benchmark later
```
goos: linux
goarch: amd64
pkg: github.com/ethereum/go-ethereum/miner
cpu: Intel(R) Core(TM) Ultra 7 155U
│ /tmp/old.txt │ /tmp/new.txt │
│ sec/op │ sec/op vs base │
BuildPayload-14 141.5µ ± 3% 146.0µ ± 6% ~ (p=0.346 n=200)
│ /tmp/old.txt │ /tmp/new.txt │
│ B/op │ B/op vs base │
BuildPayload-14 188.2Ki ± 4% 177.4Ki ± 4% -5.71% (p=0.018 n=200)
│ /tmp/old.txt │ /tmp/new.txt │
│ allocs/op │ allocs/op vs base │
BuildPayload-14 2.703k ± 4% 2.453k ± 5% -9.25% (p=0.000 n=200)
```
This PR reverts a part of changes brought by https://github.com/ethereum/go-ethereum/pull/33281/changes
Specifically, read-only protection should always be enforced at the opcode level,
regardless of whether the check has already been performed during gas metering.
It should act as a gatekeeper, otherwise, it is easy to introduce errors by adding
new gas measurement logic without consistently applying the read-only protection.
The core part of this PR that we need to adopt is to move the code and
nonce change hook invocations to occur at tx finalization, instead of
when the selfdestruct opcode is called.
Additionally:
* remove `SelfDestruct6780` now that it is essentially the same as
`SelfDestruct` just gated by `is new contract`
* don't duplicate `BalanceIncreaseSelfdestruct` (transfer to recipient
of selfdestruct) in the hooked statedb and in the opcode handler for the
selfdestruct opcode.
* balance is burned immediately when the beneficiary of the selfdestruct
is the sender, and the contract was created in the same transaction.
Previously we emit two balance increases to the recipient (see above
point), and a balance decrease from the sender.
---------
Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
Co-authored-by: lightclient <lightclient@protonmail.com>
There's no need to perform the subsequent state access on the target if
we already know that we are out of gas.
This aligns the state access behavior of selfdestruct with EIP-7928
This PR causes execution to terminate at the gas handler in the case of
sstore/call if they are invoked in a static execution context.
This aligns the behavior with EIP 7928 by ensuring that we don't record
any state reads in the access list from an SSTORE/CALL in this
circumstance.
---------
Co-authored-by: lightclient <lightclient@protonmail.com>
In order to reduce the amount of code that is embedded into the keeper
binary, I am removing all the verkle code that uses go-verkle and
go-ipa. This will be followed by further PRs that are more like stubs to
replace code when the keeper build is detected.
I'm keeping the binary tree of course. This means that you will still
see `isVerkle` variables all over the codebase, but they will be renamed
when code is touched (i.e. this is not an invitation for 30+ AI slop
PRs).
---------
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
The EIP says to increment PC by 2 _instead of_ the standard increment by
1. The opcode handlers added in #33095 result in incrementing PC by 3,
because they ignored the increment already present in `interpreter.go`.
Does this need to be better specified in the EIP? I've added a [new test
case](https://github.com/ethereum/EIPs/pull/10859) for it anyway.
Found by @0xriptide.
Looks like (in some very EVM specific tests) we spent a lot of time
resizing memory. If the underlying array is big enough, we can speed it
up a bit by simply slicing the memory.
goos: linux
goarch: amd64
pkg: github.com/ethereum/go-ethereum/core/vm
cpu: Intel(R) Core(TM) Ultra 7 155U
│ /tmp/old.txt │ /tmp/new.txt │
│ sec/op │ sec/op vs base │
Resize-14 6.145n ± 9% 1.854n ± 14% -69.83% (p=0.000 n=10)
│ /tmp/old.txt │ /tmp/new.txt │
│ B/op │ B/op vs base │
Resize-14 5.000 ± 0% 5.000 ± 0% ~ (p=1.000 n=10)
│ /tmp/old.txt │ /tmp/new.txt │
│ allocs/op │ allocs/op vs base │
Resize-14 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
From the blocktest benchmark:
620ms 10.93s (flat, cum) 9.92% of Total
. . 80:func (m *Memory) Resize(size uint64) {
30ms 60ms 81: if uint64(m.Len()) < size {
590ms 10.87s 82: m.store = append(m.store, make([]byte, size-uint64(m.Len()))...)
. . 83: }
. . 84:}
---------
Co-authored-by: Felix Lange <fjl@twurst.com>
EIP-8024: Backward compatible SWAPN, DUPN, EXCHANGE
Introduces additional instructions for manipulating the stack which
allow accessing the stack at higher depths. This is an initial implementation
of the EIP, which is still in Review stage.
This pr implements https://github.com/ethereum/go-ethereum/issues/32733
to make StateProcessor more customisable.
## Compatibility notes
This introduces a breaking change to users using geth EVM as a library.
The `NewStateProcessor` function now takes one parameter which has the
chainConfig embedded instead of 2 parameters.
before:
go test -run=^$ -bench=. ./core/vm/... -timeout=1h 1841.87s user 40.96s
system 124% cpu 25:15.76 total
after:
go test -run=^$ -bench=. ./core/vm/... -timeout=1h 1588.65s user 33.79s
system 123% cpu 21:53.25 total
---------
Co-authored-by: lightclient <lightclient@protonmail.com>