Skip to content

fix(levm): use post-refund gas for check_gas_limit in sequential execution#6309

Merged
edg-l merged 7 commits into
mainfrom
fix/check-gas-limit-post-refund
Mar 5, 2026
Merged

fix(levm): use post-refund gas for check_gas_limit in sequential execution#6309
edg-l merged 7 commits into
mainfrom
fix/check-gas-limit-post-refund

Conversation

@edg-l

@edg-l edg-l commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Use post-refund gas (report.gas_spent) for the check_gas_limit accumulator in both sequential execution paths (execute_block and execute_block_pipeline)
  • Keep pre-refund gas (report.gas_used) for block_gas_used in BlockExecutionResult (used for header validation via validate_gas_used)

Pre-Amsterdam, both values are identical so there is no behavior change.

Post-mortem

Observed error on bal-devnet-2:

Error: Invalid Block: Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Caused by:
    Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Location:
    /ethrex/cmd/ethrex/initializers.rs:629:9

Root cause: When a node executes Amsterdam+ blocks sequentially (e.g. full sync, engine API catch-up, or any path without a BAL), check_gas_limit accumulates gas to verify each transaction fits within the block gas limit. EIP-7778 introduced separate gas tracking: gas_used (pre-refund, higher) for block-level accounting and gas_spent (post-refund, lower) for what the user actually pays. The check_gas_limit accumulator was using pre-refund gas, which inflates the running total.

Why it failed: The accumulated pre-refund gas (79.3M) + next tx's gas limit (1M) = 80.3M, exceeding the block gas limit (80M). With post-refund gas the accumulated value is lower (accounting for refunds from SSTORE clears, etc.), keeping the total within the 80M limit. The block is valid — it's at 99.2% capacity and the refund difference is just enough to push the pre-refund accumulator over.

Fix: Use post-refund gas (report.gas_spent) for the check_gas_limit accumulator, matching the pre-Amsterdam behavior where both values were identical. Pre-refund gas (report.gas_used) is still used for block_gas_used in the BlockExecutionResult, which is validated against header.gas_used.

Why EF tests didn't catch this: The existing EIP-7778 EF test (test_multi_transaction_gas_accounting) has gasUsed == gasLimit (fully packed block), but each transaction's pre-refund gas is bounded by its own gas_limit. The check is accumulated_gas + next_tx.gas_limit <= block_gas_limit — since a tx's actual gas can never exceed its own limit, the accumulated pre-refund gas from prior txs never pushes the check over. The bug only manifests with real-world blocks where large refunds (e.g. SSTORE clears) create a significant gap between pre-refund and post-refund gas AND the block is packed tightly enough that the inflated accumulator + next tx's gas limit overflows the block limit.

image

Test plan

  • cargo build passes
  • cargo test passes
  • EIP-7778 EF tests pass (test_multi_transaction_gas_accounting)
  • Manual devnet test on bal-devnet-2

…ution

The check_gas_limit accumulator was using pre-refund gas (report.gas_used),
which for Amsterdam+ blocks is higher than post-refund gas. This causes
valid tightly-packed blocks to be rejected with "Gas allowance exceeded"
when executing sequentially without a BAL (e.g. syncing from a peer that
doesn't support eth/71). Use post-refund gas (report.gas_spent) for the
limit check while keeping pre-refund gas for block_gas_used (header
validation). Pre-Amsterdam both values are identical, so no behavior change.
@github-actions github-actions Bot added the levm Lambda EVM implementation label Mar 4, 2026
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

The PR correctly fixes the gas limit check issue by using post-refund gas (gas_spent) instead of pre-refund gas (gas_used) when validating transaction gas limits. This prevents valid Amsterdam blocks from being rejected during sequential execution.

Issues Found

  1. Code duplication - The same gas tracking logic is implemented twice in both execute_block() and execute_block_with_precompiles(). Consider extracting this into a shared helper function.

  2. Comment inconsistency - Line 134-135 in execute_block() and lines 311-312 in execute_block_with_precompiles() have outdated comments. The comments state "gas_spent (POST-REFUND) for receipt cumulative_gas_used and limit check" but the actual implementation uses gas_spent for both cumulative_gas_used and limit check, while the comment suggests it might be used for something else.

  3. Variable naming - The variable name gas_used_for_limit_check is slightly confusing since it actually tracks gas_spent (post-refund), not gas_used. Consider renaming to gas_spent_for_limit_check for clarity.

Security Assessment

No security vulnerabilities found - The change correctly implements EIP-7778 gas accounting rules and prevents DoS attacks that could occur from incorrectly rejecting valid blocks.

Performance Impact

Minimal impact - Only adds one additional accumulator variable per transaction, negligible overhead.

Suggested Changes

// In both functions, update the comment to be more accurate:
// EIP-7778: Separate gas tracking
// - gas_spent (POST-REFUND) for receipt cumulative_gas_used and gas limit validation
// - gas_used (PRE-REFUND for Amsterdam+) for block accounting / header validation

Consider extracting the gas tracking logic into a shared function to reduce code duplication between the two execution paths.


Automated review by Kimi (Moonshot AI) · custom prompt

@edg-l edg-l marked this pull request as ready for review March 4, 2026 16:21
@edg-l edg-l requested a review from a team as a code owner March 4, 2026 16:21
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

The PR correctly implements EIP-7778's gas accounting changes by separating pre-refund and post-refund gas tracking. However, there are some issues that need attention:

1. Potential Bug - Gas Limit Check Logic

File: crates/vm/backends/levm/mod.rs

The gas limit check now uses post-refund gas (gas_spent) instead of pre-refund gas (gas_used). This change appears intentional per the EIP-7778 implementation, but the comment on line 105-107 and 270-272 is misleading:

// Post-refund accumulator for check_gas_limit (pre-refund would reject valid
// tightly-packed Amsterdam blocks when executing sequentially without BAL)

This comment suggests the change is to prevent rejecting valid blocks, but EIP-7778 explicitly changes the gas limit check to use post-refund values. The comment should be updated to reflect this is the intended EIP-7778 behavior, not a workaround.

2. Code Duplication

The same gas tracking logic is duplicated in both execute_block and execute_block_parallel. Consider extracting this into a shared helper function to maintain consistency.

3. Missing Validation

There's no validation that gas_spent <= gas_used which should always be true (since refunds can't exceed gas used). While not strictly necessary, an assertion could help catch implementation bugs.

4. Minor - Comment Inconsistency

The comment on lines 134-135 and 311-312 states:

// - gas_spent (POST-REFUND) for receipt cumulative_gas_used and limit check

This is partially incorrect - gas_spent is used for receipts and limit check, but the comment says "and limit check" twice. This appears to be a copy-paste error from the previous line.

Recommendation

Update the misleading comments to accurately reflect EIP-7778's specification that gas limit checks should use post-refund values. The implementation itself appears correct for the EIP-7778 changes.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

I now have a thorough understanding of the change. Here's my review:


Review: fix(levm): use post-refund gas for check_gas_limit in sequential execution

Summary

This PR fixes a bug in sequential block execution where the pre-refund gas accumulator (block_gas_used) was incorrectly used to check whether a transaction fits within the block gas limit. For Amsterdam+ blocks (EIP-7778), pre-refund gas is higher than post-refund gas, causing valid tightly-packed blocks to be incorrectly rejected.


Correctness

The fix is logically sound. The key insight is:

  • check_gas_limit checks whether the next transaction's gas limit fits within the remaining space. That remaining space should be computed from gas actually consumed (post-refund), not from the pre-refund accounting figure, which EIP-7778 introduced solely for block-level header validation.
  • block_gas_used (pre-refund) is correctly preserved for BlockExecutionResult and downstream validate_gas_used against block.header.gas_used.
  • cumulative_gas_used (post-refund) for receipts is correctly left unchanged.

Pre-Amsterdam, gas_spent == gas_used per the ExecutionReport documentation (crates/vm/levm/src/errors.rs:238-245), so there is truly no behavior change for existing forks.

One observation worth noting: gas_used_for_limit_check ends up tracking exactly the same value as cumulative_gas_used. Both are initialized to zero and incremented by report.gas_spent on each transaction (mod.rs:136-137 and mod.rs:313-314). They are never diverged. This isn't a bug — both variables serve a clear, different semantic purpose (one for receipts, one for the gas limit guard) — but the duplication could be confusing to a future reader. It's worth considering whether to simply reuse cumulative_gas_used in the check_gas_limit call, or leave the separate variable with a comment explaining the intentional equivalence.


Coverage of Both Sequential Paths

The fix is correctly applied to both sequential execution paths:

  • execute_block (lines 108, 115, 137)
  • execute_block_pipeline sequential branch (lines 272, 278, 314)

The parallel path (execute_block_parallel) does not call check_gas_limit at all (it relies on the BAL being pre-validated), so it is correctly unaffected.


Minor Issues

  • Redundant variable: As noted above, gas_used_for_limit_check is always equal to cumulative_gas_used within both sequential loops. Reusing cumulative_gas_used would eliminate the extra variable. If the decision is to keep them separate for clarity, a brief comment in each sequential loop noting the equivalence would preempt future confusion.

  • check_gas_limit parameter name mismatch: The function signature at line 65 names the first parameter block_gas_used, but it now receives gas_used_for_limit_check (post-refund). The parameter name still implies pre-refund semantics. Consider renaming to gas_used_so_far or similar to avoid misleading readers of the function's body.


No Security Concerns

This is a validation-logic fix. The change loosens rejection criteria for block execution (allowing blocks that were previously incorrectly rejected), which is the correct direction. It does not weaken any security invariant: the validate_gas_used check against block.header.gas_used remains intact and uses the pre-refund block_gas_used.


Verdict

The fix is correct and well-scoped. The test coverage (EF tests for test_multi_transaction_gas_accounting) is appropriate. The main suggestions are cosmetic: either eliminate gas_used_for_limit_check in favour of cumulative_gas_used, or rename the check_gas_limit parameter to avoid the naming confusion introduced by this change.


Automated review by Claude (Anthropic) · custom prompt

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have a complete picture of the PR. Let me write the review.


PR Review: fix(levm): use post-refund gas for check_gas_limit in sequential execution

Summary

The fix is correct and well-motivated. EIP-7778 (Amsterdam+) decouples gas_used (pre-refund, used for block header validation) from gas_spent (post-refund, used for receipt cumulative gas). Using pre-refund gas in check_gas_limit was an over-conservative gate that could reject valid, tightly-packed blocks during sequential execution without a BAL. Using gas_spent here is consistent with how tightly packed blocks are evaluated under the new accounting rules.


Correctness

The logic split is sound:

  • gas_used_for_limit_check accumulates report.gas_spent (post-refund) — used to gate whether the next transaction fits within the block gas limit.
  • block_gas_used accumulates report.gas_used (pre-refund) — written to BlockExecutionResult and validated against block_header.gas_used via validate_gas_used (validation.rs:67).
  • cumulative_gas_used accumulates report.gas_spent (post-refund) — used for receipt root, which is correct per EIP-7778.

The change is applied symmetrically to both execute_block (line 115) and the sequential path inside execute_block_pipeline (line 278), covering all non-parallel execution paths.

The parallel path (execute_block_parallel, entered when header_bal is Some) is unaffected. That path already does not call check_gas_limit on a sequential accumulator, so no change is needed there.


Minor Observations

Redundant variable (gas_used_for_limit_check duplicates cumulative_gas_used).

After the fix, both cumulative_gas_used and gas_used_for_limit_check are incremented by report.gas_spent in lockstep with no intervening divergence. They will always be equal at the point check_gas_limit is called. The check could use cumulative_gas_used directly instead of introducing a new variable. This is a readability/cleanliness concern rather than a bug.

Affected lines: mod.rs:103-108, 136-137, 267-272, 313-314.

Variable naming.

gas_used_for_limit_check tracks gas_spent (post-refund), not gas_used (pre-refund). The name could mislead a future reader. gas_spent_for_limit_check or simply reusing cumulative_gas_used would be clearer.

Comment accuracy (minor).

The comment block at lines 133–135 (and 310–312) reads:

// - gas_spent (POST-REFUND) for receipt cumulative_gas_used and limit check
// - gas_used (PRE-REFUND for Amsterdam+) for block accounting / header validation

This is accurate. No issue here — it's an improvement over the previous comment which didn't mention the limit check.


Security / Consensus

No security regression. The check being loosened (post-refund < pre-refund) is correct by EIP-7778 semantics: a block that fits within the gas limit under post-refund accounting is genuinely valid and should not be rejected. The header validation (validate_gas_used) is untouched, so consensus correctness for the block's gas_used field is preserved.


Verdict

Approve with one non-blocking suggestion: consider removing gas_used_for_limit_check and reusing the already-tracked cumulative_gas_used directly in check_gas_limit, since both variables are always in sync. This simplifies the code without any behavioral change.


Automated review by Claude (Anthropic) · custom prompt

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 8
Total lines removed: 0
Total lines changed: 8

Detailed view
+---------------------------------------+-------+------+
| File                                  | Lines | Diff |
+---------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs | 1515  | +8   |
+---------------------------------------+-------+------+

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

No blocking bugs found in this diff. The change is internally consistent in both execution paths and aligns with the stated EIP-7778 split (gas_spent for receipt-style cumulative tracking/check gate, gas_used for block/header accounting).

Non-blocking review points:

  1. Potential spec-ambiguity risk (needs explicit test coverage)
    The pre-check now uses post-refund accumulation:

    • crates/vm/backends/levm/mod.rs:115
    • crates/vm/backends/levm/mod.rs:278
      This is probably intentional for Amsterdam, but it changes early rejection behavior. If consensus still expects strict per-tx gas-pool reservation semantics, this could admit blocks for full execution and only fail later at header gas-used validation. I recommend a regression test that proves intended validity/invalidity for:
    • tightly packed Amsterdam blocks with large refunds
    • sequential path (no BAL header) vs BAL path parity
  2. Naming/readability mismatch in helper contract
    check_gas_limit still documents/accepts block_gas_used, but callers now pass a post-refund accumulator (gas_used_for_limit_check):

Overall: change looks good, with the main gap being explicit tests around the altered gas-limit gate semantics.


Automated review by OpenAI Codex · custom prompt

@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

  1. Medium – unchecked addition in gas-limit guard can overflow
    In check_gas_limit, block_gas_used + tx_gas_limit is unchecked and can overflow on malformed inputs, which can panic in debug builds or wrap in release builds before comparison.
    Reference: crates/vm/backends/levm/mod.rs:70
    Suggestion: use checked_add and map None to the existing "Block gas used overflow" error path.

  2. Low – add regression tests for the new Amsterdam accounting split
    The behavior change to use gas_spent for pre-execution check_gas_limit is subtle and consensus-critical. I’d add tests that cover:

Overall, the PR direction looks correct and internally consistent with the gas_used/gas_spent split, and I don’t see an obvious consensus regression from the diff alone.


Automated review by OpenAI Codex · custom prompt

@greptile-apps

greptile-apps Bot commented Mar 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR correctly fixes a critical regression in sequential block execution for Amsterdam+ (EIP-7778) blocks, where valid tightly-packed blocks were incorrectly rejected with "Gas allowance exceeded" because the check_gas_limit accumulator was using PRE-REFUND gas (report.gas_used) rather than POST-REFUND gas (report.gas_spent).

Key changes:

  • Introduces gas_used_for_limit_check (POST-REFUND) in both execute_block and the sequential path of execute_block_pipeline to drive check_gas_limit
  • Preserves block_gas_used (PRE-REFUND) unchanged for block header validation via validate_gas_used
  • Pre-Amsterdam behavior is unaffected because gas_used == gas_spent before EIP-7778
  • The parallel execution path already handled this correctly and is not modified

The implementation is logically sound and symmetric across both sequential execution paths. EF tests (test_multi_transaction_gas_accounting) validate the fix.

Confidence Score: 5/5

  • This PR is safe to merge; it fixes a real block-rejection bug for Amsterdam+ with no behavior change for earlier forks and passes EF tests.
  • The implementation is correct and logically sound. Both sequential execution paths are now symmetric in their use of POST-REFUND gas for the limit check. The fix is validated by passing EIP-7778 EF tests (test_multi_transaction_gas_accounting). No correctness or logic issues were found.
  • No files require special attention.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Start execute_block / execute_block_pipeline sequential] --> B[Init accumulators\ncumulative_gas_used = 0\nblock_gas_used = 0\ngas_used_for_limit_check = 0]
    B --> C{For each tx}
    C --> D["check_gas_limit(gas_used_for_limit_check,\n tx.gas_limit(), block.gas_limit)"]
    D -- Exceeds limit --> E[Return Err: Gas allowance exceeded]
    D -- OK --> F[execute_tx → report]
    F --> G["cumulative_gas_used += report.gas_spent  (POST-REFUND)\ngas_used_for_limit_check += report.gas_spent  (POST-REFUND)\nblock_gas_used += report.gas_used  (PRE-REFUND)"]
    G --> H[Emit receipt with cumulative_gas_used]
    H --> C
    C -- Done --> I["BlockExecutionResult { block_gas_used }  ← PRE-REFUND\n(used by validate_gas_used for header check)"]
Loading

Last reviewed commit: fe44f38

@edg-l edg-l moved this to In Review in ethrex_l1 Mar 4, 2026
Comment thread crates/vm/backends/levm/mod.rs Outdated
Comment thread crates/vm/backends/levm/mod.rs Outdated
Comment thread crates/vm/backends/levm/mod.rs Outdated
edg-l added 2 commits March 5, 2026 08:15
Use cumulative_gas_used directly for check_gas_limit since both
accumulate the same value (report.gas_spent from zero).
@edg-l edg-l requested a review from iovoid March 5, 2026 07:31
// - gas_used (PRE-REFUND for Amsterdam+) for block accounting
// - gas_spent (POST-REFUND) for receipt cumulative_gas_used and limit check
// - gas_used (PRE-REFUND for Amsterdam+) for block accounting / header validation
cumulative_gas_used += report.gas_spent;

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.

I think it might be a good idea to rename cumulative_gas_used to cumulative_gas_spent to make it even clearer what it tracks

@ElFantasma ElFantasma 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.

Clean fix — using post-refund cumulative_gas_used for check_gas_limit is correct per EIP-7778, and the post-mortem in the PR description is excellent. The change is symmetric across both execute_block and execute_block_pipeline, and pre-Amsterdam behavior is preserved since both values are identical.

Comment thread crates/vm/backends/levm/mod.rs Outdated
for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() {
check_gas_limit(block_gas_used, tx.gas_limit(), block.header.gas_limit)?;
check_gas_limit(
cumulative_gas_used,

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.

nit: check_gas_limit's first parameter is still named block_gas_used and the error message says "Block gas used overflow", but it now receives the post-refund cumulative value. Consider renaming the parameter to cumulative_gas (or similar) and adjusting the error message so future debugging sessions aren't misleading.

@ElFantasma ElFantasma 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.

Just one nit

@edg-l edg-l enabled auto-merge March 5, 2026 17:25
@edg-l edg-l added this pull request to the merge queue Mar 5, 2026
Merged via the queue into main with commit 9d36838 Mar 5, 2026
51 checks passed
@edg-l edg-l deleted the fix/check-gas-limit-post-refund branch March 5, 2026 19:03
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Mar 5, 2026
lakshya-sky pushed a commit to lakshya-sky/ethrex that referenced this pull request Mar 10, 2026
…ution (lambdaclass#6309)

## Summary
- Use post-refund gas (`report.gas_spent`) for the `check_gas_limit`
accumulator in both sequential execution paths (`execute_block` and
`execute_block_pipeline`)
- Keep pre-refund gas (`report.gas_used`) for `block_gas_used` in
`BlockExecutionResult` (used for header validation via
`validate_gas_used`)

Pre-Amsterdam, both values are identical so there is no behavior change.

## Post-mortem

**Observed error on bal-devnet-2:**
```
Error: Invalid Block: Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Caused by:
    Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Location:
    /ethrex/cmd/ethrex/initializers.rs:629:9
```

**Root cause:** When a node executes Amsterdam+ blocks sequentially
(e.g. full sync, engine API catch-up, or any path without a BAL),
`check_gas_limit` accumulates gas to verify each transaction fits within
the block gas limit. EIP-7778 introduced separate gas tracking:
`gas_used` (pre-refund, higher) for block-level accounting and
`gas_spent` (post-refund, lower) for what the user actually pays. The
`check_gas_limit` accumulator was using pre-refund gas, which inflates
the running total.

**Why it failed:** The accumulated pre-refund gas (79.3M) + next tx's
gas limit (1M) = 80.3M, exceeding the block gas limit (80M). With
post-refund gas the accumulated value is lower (accounting for refunds
from SSTORE clears, etc.), keeping the total within the 80M limit. The
block is valid — it's at 99.2% capacity and the refund difference is
just enough to push the pre-refund accumulator over.

**Fix:** Use post-refund gas (`report.gas_spent`) for the
`check_gas_limit` accumulator, matching the pre-Amsterdam behavior where
both values were identical. Pre-refund gas (`report.gas_used`) is still
used for `block_gas_used` in the `BlockExecutionResult`, which is
validated against `header.gas_used`.

**Why EF tests didn't catch this:** The existing EIP-7778 EF test
(`test_multi_transaction_gas_accounting`) has `gasUsed == gasLimit`
(fully packed block), but each transaction's pre-refund gas is bounded
by its own `gas_limit`. The check is `accumulated_gas +
next_tx.gas_limit <= block_gas_limit` — since a tx's actual gas can
never exceed its own limit, the accumulated pre-refund gas from prior
txs never pushes the check over. The bug only manifests with real-world
blocks where large refunds (e.g. SSTORE clears) create a significant gap
between pre-refund and post-refund gas AND the block is packed tightly
enough that the inflated accumulator + next tx's gas limit overflows the
block limit.

<img width="2816" height="1420" alt="image"
src="https://github.com/user-attachments/assets/0908a3d4-a6bd-44de-8667-ef32e2338458"
/>


## Test plan
- [x] `cargo build` passes
- [x] `cargo test` passes
- [x] EIP-7778 EF tests pass (`test_multi_transaction_gas_accounting`)
- [ ] Manual devnet test on bal-devnet-2
@ElFantasma ElFantasma mentioned this pull request Mar 12, 2026
11 tasks
iovoid pushed a commit that referenced this pull request Mar 16, 2026
…ution (#6309)

## Summary
- Use post-refund gas (`report.gas_spent`) for the `check_gas_limit`
accumulator in both sequential execution paths (`execute_block` and
`execute_block_pipeline`)
- Keep pre-refund gas (`report.gas_used`) for `block_gas_used` in
`BlockExecutionResult` (used for header validation via
`validate_gas_used`)

Pre-Amsterdam, both values are identical so there is no behavior change.

## Post-mortem

**Observed error on bal-devnet-2:**
```
Error: Invalid Block: Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Caused by:
    Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Location:
    /ethrex/cmd/ethrex/initializers.rs:629:9
```

**Root cause:** When a node executes Amsterdam+ blocks sequentially
(e.g. full sync, engine API catch-up, or any path without a BAL),
`check_gas_limit` accumulates gas to verify each transaction fits within
the block gas limit. EIP-7778 introduced separate gas tracking:
`gas_used` (pre-refund, higher) for block-level accounting and
`gas_spent` (post-refund, lower) for what the user actually pays. The
`check_gas_limit` accumulator was using pre-refund gas, which inflates
the running total.

**Why it failed:** The accumulated pre-refund gas (79.3M) + next tx's
gas limit (1M) = 80.3M, exceeding the block gas limit (80M). With
post-refund gas the accumulated value is lower (accounting for refunds
from SSTORE clears, etc.), keeping the total within the 80M limit. The
block is valid — it's at 99.2% capacity and the refund difference is
just enough to push the pre-refund accumulator over.

**Fix:** Use post-refund gas (`report.gas_spent`) for the
`check_gas_limit` accumulator, matching the pre-Amsterdam behavior where
both values were identical. Pre-refund gas (`report.gas_used`) is still
used for `block_gas_used` in the `BlockExecutionResult`, which is
validated against `header.gas_used`.

**Why EF tests didn't catch this:** The existing EIP-7778 EF test
(`test_multi_transaction_gas_accounting`) has `gasUsed == gasLimit`
(fully packed block), but each transaction's pre-refund gas is bounded
by its own `gas_limit`. The check is `accumulated_gas +
next_tx.gas_limit <= block_gas_limit` — since a tx's actual gas can
never exceed its own limit, the accumulated pre-refund gas from prior
txs never pushes the check over. The bug only manifests with real-world
blocks where large refunds (e.g. SSTORE clears) create a significant gap
between pre-refund and post-refund gas AND the block is packed tightly
enough that the inflated accumulator + next tx's gas limit overflows the
block limit.

<img width="2816" height="1420" alt="image"
src="https://github.com/user-attachments/assets/0908a3d4-a6bd-44de-8667-ef32e2338458"
/>


## Test plan
- [x] `cargo build` passes
- [x] `cargo test` passes
- [x] EIP-7778 EF tests pass (`test_multi_transaction_gas_accounting`)
- [ ] Manual devnet test on bal-devnet-2
Muzry pushed a commit to Muzry/ethrex that referenced this pull request Mar 17, 2026
…ution (lambdaclass#6309)

## Summary
- Use post-refund gas (`report.gas_spent`) for the `check_gas_limit`
accumulator in both sequential execution paths (`execute_block` and
`execute_block_pipeline`)
- Keep pre-refund gas (`report.gas_used`) for `block_gas_used` in
`BlockExecutionResult` (used for header validation via
`validate_gas_used`)

Pre-Amsterdam, both values are identical so there is no behavior change.

## Post-mortem

**Observed error on bal-devnet-2:**
```
Error: Invalid Block: Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Caused by:
    Invalid transaction: Gas allowance exceeded: Block gas used overflow: used 79340683 + tx limit 1000000 > block limit 80000000

Location:
    /ethrex/cmd/ethrex/initializers.rs:629:9
```

**Root cause:** When a node executes Amsterdam+ blocks sequentially
(e.g. full sync, engine API catch-up, or any path without a BAL),
`check_gas_limit` accumulates gas to verify each transaction fits within
the block gas limit. EIP-7778 introduced separate gas tracking:
`gas_used` (pre-refund, higher) for block-level accounting and
`gas_spent` (post-refund, lower) for what the user actually pays. The
`check_gas_limit` accumulator was using pre-refund gas, which inflates
the running total.

**Why it failed:** The accumulated pre-refund gas (79.3M) + next tx's
gas limit (1M) = 80.3M, exceeding the block gas limit (80M). With
post-refund gas the accumulated value is lower (accounting for refunds
from SSTORE clears, etc.), keeping the total within the 80M limit. The
block is valid — it's at 99.2% capacity and the refund difference is
just enough to push the pre-refund accumulator over.

**Fix:** Use post-refund gas (`report.gas_spent`) for the
`check_gas_limit` accumulator, matching the pre-Amsterdam behavior where
both values were identical. Pre-refund gas (`report.gas_used`) is still
used for `block_gas_used` in the `BlockExecutionResult`, which is
validated against `header.gas_used`.

**Why EF tests didn't catch this:** The existing EIP-7778 EF test
(`test_multi_transaction_gas_accounting`) has `gasUsed == gasLimit`
(fully packed block), but each transaction's pre-refund gas is bounded
by its own `gas_limit`. The check is `accumulated_gas +
next_tx.gas_limit <= block_gas_limit` — since a tx's actual gas can
never exceed its own limit, the accumulated pre-refund gas from prior
txs never pushes the check over. The bug only manifests with real-world
blocks where large refunds (e.g. SSTORE clears) create a significant gap
between pre-refund and post-refund gas AND the block is packed tightly
enough that the inflated accumulator + next tx's gas limit overflows the
block limit.

<img width="2816" height="1420" alt="image"
src="https://github.com/user-attachments/assets/0908a3d4-a6bd-44de-8667-ef32e2338458"
/>

## Test plan
- [x] `cargo build` passes
- [x] `cargo test` passes
- [x] EIP-7778 EF tests pass (`test_multi_transaction_gas_accounting`)
- [ ] Manual devnet test on bal-devnet-2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

levm Lambda EVM implementation

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants