Skip to content

execution: fix EIP-8037 state gas reservoir restore & CREATE state gas ordering#20290

Merged
yperbasis merged 18 commits into
mainfrom
yperbasis/bal_v5.6.0_a
Apr 8, 2026
Merged

execution: fix EIP-8037 state gas reservoir restore & CREATE state gas ordering#20290
yperbasis merged 18 commits into
mainfrom
yperbasis/bal_v5.6.0_a

Conversation

@yperbasis

@yperbasis yperbasis commented Apr 2, 2026

Copy link
Copy Markdown
Member
  • Fix two uint64 underflow bugs in EIP-8037 multidimensional gas accounting that caused incorrect receipt gasUsed when child frames reverted with a state gas reservoir that grew beyond its initial value (via sub-child reverts)
  • Move CREATE/CREATE2 state gas charge (account creation) from the dynamic gas functions into opCreate/opCreate2, after the static-context and initcode-size checks, so state gas is not consumed on early failures where no state is created (aligns with feat(spec-specs): EIP-8037 - move CREATE state gas charge after initcode size validation ethereum/execution-specs#2608)
  • Update execution-spec-tests submodule to bal@v5.6.1
  • Skip 2 BAL tests where coinbase==target exposes a separate TxIn bug in exec3_parallel finalize (TODO)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis force-pushed the yperbasis/bal_v5.6.0_a branch from 81d9d7a to 5566932 Compare April 2, 2026 14:51
…ild revert

Two uint64 underflow bugs in the EIP-8037 multidimensional gas accounting
caused incorrect receipt gasUsed when child frames reverted with a state
gas reservoir that grew beyond its initial value (via sub-child reverts):

1. handleFrameRevert: `reservoirUsed = initialChildState - gas.State`
   underflowed when the reservoir grew (gas.State > initialChildState),
   preventing state gas from cascading up through nested reverts.
   Fix: guard the subtraction; use `gas.State += childStateConsumed`
   to preserve sub-child restorations already in the reservoir.

2. txn_executor.mdGasUsed().Total(): per-component Minus() underflowed
   when gasRemaining.State > initialGas.State.
   Fix: compute receipt gasUsed as `initialGas.Total() - gasRemaining.Total()`
   matching the EIP-8037 formula `tx.gas - gas_left - reservoir`.

Also skip 2 BAL tests where coinbase==target exposes a separate TxIn bug
in exec3_parallel finalize (TODO).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Apr 2, 2026
@yperbasis yperbasis force-pushed the yperbasis/bal_v5.6.0_a branch from 5566932 to 64fc310 Compare April 2, 2026 14:52
@yperbasis yperbasis changed the title evm, txn_executor: fix EIP-8037 state gas reservoir restoration on child revert execution: fix EIP-8037 state gas reservoir restoration on child revert Apr 2, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis

Copy link
Copy Markdown
Member Author

The scenario that triggers both bugs

EIP-8037 says:

On child revert or 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.

"Spill" is state gas that was charged from gas_left (regular gas) because the reservoir ran dry. When a child reverts, that spill gets moved into the parent's reservoir. This means a frame's reservoir can grow beyond its initial value after a sub-child revert.

Concrete example with three nested frames:

  1. Frame A calls Frame B with reservoir = 100
  2. Frame B calls Frame C with reservoir = 100
  3. Frame C charges 120 state gas: 100 from reservoir, 20 from regular (spill). Reservoir = 0.
  4. Frame C reverts. Per the EIP quote above, all 120 goes back to B's reservoir. B's reservoir is now 120 — above the 100 it started with. The 20 regular gas spent on spill has been converted into reservoir gas.
  5. Frame B charges 10 state gas from reservoir. Reservoir = 110.
  6. Frame B reverts.

Bug 1: handleFrameRevert (evm.go:215)

At step 6, handleFrameRevert runs for Frame B with initialChildState = 100 and gas.State = 110.

The old code:

reservoirUsed := initialChildState - gas.State   // 100 - 110 → uint64 underflow!

This wraps to ~2^64, which poisons the spill calculation and the subsequent reservoir restoration:

gas.State = initialChildState + spill            // overwrites to ~100, losing the 20 from C's revert

The fix guards the subtraction and changes the restoration to preserve sub-child contributions:

var reservoirUsed uint64
if initialChildState > gas.State {
    reservoirUsed = initialChildState - gas.State
}
// ...
gas.State += childStateConsumed                  // 110 + 10 = 120, preserving C's 20

Bug 2: txn_executor.go receipt gasUsed

EIP-8037 defines receipt gas:

tx_gas_used_before_refund = tx.gas - tx_output.gas_left - tx_output.state_gas_reservoir

The old code computed this via MdGas.Minus():

func (g MdGas) Minus(other MdGas) MdGas {
    return MdGas{
        Regular: g.Regular - other.Regular,
        State:   g.State - other.State,      // underflows when reservoir grew
    }
}

When gasRemaining.State > initialGas.State (same reservoir-growth scenario, at the top-level frame), the .State component underflows, and .Total() returns garbage.

The fix implements the EIP formula directly on the combined sum:

st.initialGas.Total() - st.gasRemaining.Total()

This works because spill transfers gas between dimensions but preserves the total: if State grew by 20, Regular shrank by 20 (the spill came from regular gas). So gasRemaining.Total() ≤ initialGas.Total() always holds, even when individual components don't.

@yperbasis yperbasis requested a review from taratorio April 2, 2026 15:17
yperbasis and others added 2 commits April 2, 2026 17:20
Per-component subtraction is unsound when spill can transfer gas
between dimensions (reservoir grows above initial value on child
revert). Remove the footgun; inline Regular subtraction at the
two pre-Amsterdam call sites that only need it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move reservoirUsed/spill into the only branch that uses them
(depth == 0, REVERT). Inline regularGasUsed assignments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis

Copy link
Copy Markdown
Member Author

Observation on depth-0 spill computation

The spill computation at depth 0 can produce a value that doesn't correspond to the actual spill in the depth-0 frame's own execution. For example, if depth-0 consumed state gas entirely from the reservoir but a sub-child's spill-and-revert caused the reservoir to grow, the computed "spill" includes gas that wasn't really spilled at this depth.

This is not a bug because RevertedSpillGas exactly cancels the spill added to gas.Regular in the final receipt formula (initialGas.Total() - gasRemaining.Total() + RevertedSpillGas()). But it means gas.Regular and RevertedSpillGas individually carry inexact intermediate values — worth keeping in mind if they're ever used independently in the future.

yperbasis and others added 4 commits April 2, 2026 18:09
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bal@v5.6.0 execution-spec-tests fixtures have deeply nested paths
that exceed Windows' 260-char MAX_PATH limit, causing submodule checkout
to fail with "Filename too long".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The new execution-spec-tests submodule nests devnet LFS fixtures one
level deeper (e.g. eip7934_block_rlp_limit/max_block_rlp_size/*.json).
Use ** globs so patterns match regardless of fork directory and nesting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes EIP-8037 multi-dimensional gas accounting on reverts so state gas reservoir restoration doesn’t trigger uint64 underflows and receipt gasUsed remains correct when child-frame reverts cause the reservoir to grow.

Changes:

  • Adjust handleFrameRevert to restore child-consumed state gas by incrementing the existing reservoir (preserving sub-child restorations) and compute spill safely when the reservoir grows.
  • Update transaction receipt gas used computation to avoid per-dimension subtraction underflow by subtracting at Total() level.
  • Misc: remove unsafe MdGas.Minus, fix tracer test gas used math, tweak EEST devnet test skips, and CI setup (Windows longpaths + broader LFS fixture include globs).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
execution/vm/interpreter.go Updates comment describing revert-path reservoir restoration behavior.
execution/vm/evm.go Fixes EIP-8037 revert accounting: spill computation and reservoir restoration on child revert.
execution/tracing/tracers/js/tracer_test.go Avoids MdGas.Minus/underflow by computing gas used via total subtraction.
execution/tests/eest_devnet/block_test.go Skips a BAL devnet test due to a known recording bug.
execution/protocol/txn_executor.go Computes receipt gasUsed using Total()-level subtraction; removes mdGasUsed() helper.
execution/protocol/mdgas/md_gas.go Removes MdGas.Minus (unsafe under uint64 underflow).
.github/actions/setup-erigon/action.yml Enables git long paths on Windows; broadens LFS fixture include patterns for devnet blobs/RLP-limit tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread execution/tests/eest_devnet/block_test.go
Comment thread execution/protocol/txn_executor.go
Move the EIP-8037 state gas charge for account creation
(StateBytesNewAccount × CostPerStateByte) from the dynamic gas
functions into opCreate/opCreate2, after the static-context and
initcode-size checks. This ensures state gas is not consumed on
early failures where no state is created.

Aligns with execution-specs#2608 (bal@v5.6.1 test fixtures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis changed the title execution: fix EIP-8037 state gas reservoir restoration on child revert execution: fix EIP-8037 state gas reservoir restore & CREATE state gas ordering Apr 7, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yperbasis yperbasis marked this pull request as ready for review April 7, 2026 14:57
@yperbasis yperbasis requested a review from Copilot April 7, 2026 15:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread execution/vm/instructions.go
Comment thread execution/vm/instructions.go
Comment thread execution/tests/eest_devnet/block_test.go
@mh0lt mh0lt added this pull request to the merge queue Apr 7, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 7, 2026
@yperbasis yperbasis enabled auto-merge April 8, 2026 07:33
@yperbasis yperbasis added this pull request to the merge queue Apr 8, 2026
Merged via the queue into main with commit 76b9940 Apr 8, 2026
35 checks passed
@yperbasis yperbasis deleted the yperbasis/bal_v5.6.0_a branch April 8, 2026 11:50
github-merge-queue Bot pushed a commit that referenced this pull request Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Glamsterdam https://eips.ethereum.org/EIPS/eip-7773

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants