Skip to content

Test reservoir refunded at top level when parent halts after child sp…#2644

Closed
daniellehrner wants to merge 1 commit intoethereum:devnets/bal/3from
daniellehrner:eip8037_state_gas_reservoir_intial_frame_oog
Closed

Test reservoir refunded at top level when parent halts after child sp…#2644
daniellehrner wants to merge 1 commit intoethereum:devnets/bal/3from
daniellehrner:eip8037_state_gas_reservoir_intial_frame_oog

Conversation

@daniellehrner
Copy link
Copy Markdown
Contributor

🗒️ Description

Adds a new test for EIP-8037 that checks that the state gas reservoir is refunded at top level when parent halts after child spills and reverts

🔗 Related Issues or PRs

N/A.

✅ Checklist

  • All: Ran fast static checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    just static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).
  • Tests: Ran mkdocs serve locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.
  • Tests: For PRs implementing a missed test case, update the post-mortem document to add an entry the list.
  • Ported Tests: All converted JSON/YML tests from ethereum/tests or tests/static have been assigned @ported_from marker.

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

…ills and reverts.

Signed-off-by: daniellehrner <daniel.lehrner@consensys.net>
Copy link
Copy Markdown
Member

@jochem-brouwer jochem-brouwer left a comment

Choose a reason for hiding this comment

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

Left some thoughts, mainly confused by the spec 😅

# restored from the child refund is preserved for top-level refund.
# block_regular = gas_limit - sstore_state_gas (reservoir kept out)
# block_state = 0 (nothing actually grown)
expected_gas_used = gas_limit_cap - sstore_state_gas
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am confused by this (and also by spec). The regular gas here for Op.SSTORE(0,1) will cost 2900 (EIP-8037) + 2100 (cold access, EIP-2929) = 5000 gas, and the state gas is 32 * 1174 = 37568. If this is fully consumed by reservoir (and thus not eating into regular gas) then we now get "free refunds" it seems. If I would spam storage creation and then invalidate it in the end I think I could even end up with negative gas here (?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Never mind, I misunderstood the EIP

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No wait, in this test the reservoir is initialized to 0, right? So there is no storage gas available, it will all directly eat in regular gas.

child = pre.deploy_contract(code=(Op.SSTORE(0, 1) + Op.REVERT(0, 0)))

parent = pre.deploy_contract(
code=(Op.POP(Op.CALL(gas=500_000, address=child)) + Op.INVALID),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since the child frame reverts it will not consume all gas so the gas= param is technically not necessary here (we can send all gas).

Also it seems that this test is almost equal to the other test, the only difference seems if either REVERT/INVALID is used in the child contract, I think we should parametrize that.


tx = Transaction(
to=parent,
gas_limit=gas_limit_cap,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
gas_limit=gas_limit_cap,
gas_limit=gas_limit_cap + sstore_state_gas,

This inits the reservoir to have state gas.
Note: this also means it can at most refund sstore_state_gas, so this might not cover all cases, I think we should add multiple gas limits, where we use sstore_state_gas - 1 and sstore_state_gas + 1 as extra gas on top of the max gas limit to take into account off-by-one errors.


tx = Transaction(
to=parent,
gas_limit=gas_limit_cap,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Gas limit should be changed here too to make gas from reservoir available!

@spencer-tb
Copy link
Copy Markdown
Contributor

CIFO: #2644

Incorperated your comments @jochem-brouwer. Will add you as co-author @daniellehrner before merging! Thx

@spencer-tb spencer-tb closed this Apr 12, 2026
@daniellehrner daniellehrner deleted the eip8037_state_gas_reservoir_intial_frame_oog branch April 14, 2026 07:54
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 19, 2026
Ports three tests from the closed PR ethereum#2639 that cover reservoir
behavior paths not exercised by the merged ethereum#2689/ethereum#2704/ethereum#2707
tests.

  test_top_level_halt_preserves_restored_reservoir (parametrized
    reservoir_delta in {-1, 0, 1} x child_termination in {revert,
    halt})
    Regression test for the bal-devnet-3 Besu bug (ethereum#2644). Child
    runs an SSTORE then fails, restoring state gas to the parent.
    Parent then INVALIDs, triggering the top-level failure
    refund. Expected `header.gas_used =
    gas_limit_cap + min(reservoir_delta, 0)` so the reservoir
    (including any spill-restore) is preserved across the halt.

  test_callcode_value_no_new_account_state_gas
    CALLCODE transfers value to the caller, not to the target,
    so no new-account state gas is ever charged regardless of
    whether the target exists. The reservoir stays intact for a
    subsequent SSTORE.

  test_create_oog_during_state_gas_charge
    Parent CALLs an inner with only 20k gas forwarded. The
    inner's CREATE charges GAS_NEW_ACCOUNT which exceeds the
    forwarded budget, OOGing before any state gas lands. Per
    PR ethereum#2704 the refund restores the parent's reservoir and the
    parent's subsequent SSTORE succeeds from it.
spencer-tb added a commit to spencer-tb/execution-specs that referenced this pull request Apr 20, 2026
Ports three tests from the closed PR ethereum#2639 that cover reservoir
behavior paths not exercised by the merged ethereum#2689/ethereum#2704/ethereum#2707
tests.

  test_top_level_halt_preserves_restored_reservoir (parametrized
    reservoir_delta in {-1, 0, 1} x child_termination in {revert,
    halt})
    Regression test for the bal-devnet-3 Besu bug (ethereum#2644). Child
    runs an SSTORE then fails, restoring state gas to the parent.
    Parent then INVALIDs, triggering the top-level failure
    refund. Expected `header.gas_used =
    gas_limit_cap + min(reservoir_delta, 0)` so the reservoir
    (including any spill-restore) is preserved across the halt.

  test_callcode_value_no_new_account_state_gas
    CALLCODE transfers value to the caller, not to the target,
    so no new-account state gas is ever charged regardless of
    whether the target exists. The reservoir stays intact for a
    subsequent SSTORE.

  test_create_oog_during_state_gas_charge
    Parent CALLs an inner with only 20k gas forwarded. The
    inner's CREATE charges GAS_NEW_ACCOUNT which exceeds the
    forwarded budget, OOGing before any state gas lands. Per
    PR ethereum#2704 the refund restores the parent's reservoir and the
    parent's subsequent SSTORE succeeds from it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants