Skip to content

fix(evm): restore frame gas after inspector callback to preserve rescue_gas#170

Closed
krabat-l wants to merge 1 commit intomainfrom
fix/inspector-rescue-gas
Closed

fix(evm): restore frame gas after inspector callback to preserve rescue_gas#170
krabat-l wants to merge 1 commit intomainfrom
fix/inspector-rescue-gas

Conversation

@krabat-l
Copy link
Contributor

Summary

When an inspector is attached (e.g. TracerEip3155), frame_end is called after each subcall frame completes, which triggers GasInspector::call_endgas.spend_all() for any error result (including OutOfGas).
This happens in both inspect_frame_run and inspect_frame_init, before frame_return_result has a chance to call rescue_gas().

MegaETH's rescue_gas mechanism saves gas.remaining() from frames halted by additional limits (state growth, compute gas, etc.) to refund the sender in last_frame_result.
With the inspector path, spend_all() zeroes gas.remaining() first, so rescue_gas() always reads 0 — the refund is lost.
This causes different gas_used values compared to the non-inspector path, producing a receipts root mismatch when validating blocks with an inspector attached.

Fix: in both inspect_frame_init and inspect_frame_run, save gas.remaining() before calling frame_end, then restore it if the inspector callback reduced it.

Root cause trace

inspect_frame_run / inspect_frame_init
  └─ frame_end(ctx, inspector, ..., frame_result)
       └─ TracerEip3155::call_end
            └─ GasInspector::call_end
                 └─ if is_error() { gas.spend_all() }   ← zeroes remaining gas

(later, in the exec loop)
frame_return_result(result)
  └─ before_frame_return_result
       └─ if check_limit().exceeded_limit() { rescue_gas(result.gas()) }
            └─ self.rescued_gas += gas.remaining()      ← reads 0, should be > 0

Verification

Validated against block 3021764 on MegaETH mainnet:

  • Without inspector: block validates correctly
  • With TracerEip3155 before fix: receipts root mismatch
  • With TracerEip3155 after fix: block validates correctly

…ue_gas

GasInspector::call_end (revm) calls gas.spend_all() on error results
such as OutOfGas. This is called from frame_end in inspect_frame_run
and inspect_frame_init, before frame_return_result has a chance to
call rescue_gas(). The rescue_gas mechanism saves the remaining gas
of a frame halted by MegaETH's additional limits (e.g. state growth
limit) so it can be refunded to the sender in last_frame_result.

With the inspector path, spend_all() zeroes gas.remaining() first,
so rescue_gas() reads 0 and the refund is lost. This causes different
gas_used values compared to the non-inspector path, resulting in a
receipts root mismatch when validating blocks.

Fix: save gas.remaining() before calling frame_end, then restore it
afterwards if the inspector callback reduced it. The inspector still
receives the unmodified (pre-spend_all) gas value for its callbacks,
which is fine since GasInspector computes last_gas_cost from the
delta between step() and step_end() readings rather than from the
final remaining value.
@krabat-l krabat-l requested review from Troublor and flyq February 27, 2026 10:14
@krabat-l krabat-l closed this Feb 27, 2026
@krabat-l krabat-l deleted the fix/inspector-rescue-gas branch February 27, 2026 10:16
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.

1 participant