feat(l1): bal-devnet-7 v7.2.0#6671
Conversation
…et-7 v7.2.0) EIP-8037 inline state_gas refunds (SSTORE 0->x->0 restoration, CREATE collision/revert, EIP-7702 auth refills) now credit the local frame's reservoir immediately instead of being clamped to local state_gas_used and deferred via state_gas_refund_pending. Ports execution-specs#2863 (fuzz-driven correction; EELS/nimbus/nethermind clamped, reth was correct). state_gas_used becomes signed (i64): can go negative when an inline refund matches a charge that lives in an ancestor frame. The signed sum carries refunds up through incorporate_child_on_success automatically. Drops the clamp-and-spill scaffolding (state_gas_refund_pending, state_gas_refund_absorbed, state_gas_spill_outstanding, state_gas_credit_against_drain, intrinsic_state_gas_charged, plus 4 frame snapshots).
|
Lines of code reportTotal lines added: Detailed view |
Benchmark Results ComparisonNo significant difference was registered for any benchmark run. Detailed ResultsBenchmark Results: BubbleSort
Benchmark Results: ERC20Approval
Benchmark Results: ERC20Mint
Benchmark Results: ERC20Transfer
Benchmark Results: Factorial
Benchmark Results: FactorialRecursive
Benchmark Results: Fibonacci
Benchmark Results: FibonacciRecursive
Benchmark Results: ManyHashes
Benchmark Results: MstoreBench
Benchmark Results: Push
Benchmark Results: SstoreBench_no_opt
|
…7 v7.2.0) EELS keeps tx_env.intrinsic_state_gas and tx_output.state_gas_used in separate fields; the error handler zeroes only the latter and leaves intrinsic to be billed in block_state_gas_used. ethrex lumps both into vm.state_gas_used, so the previous v7.2.0 port refunded the whole sum to the reservoir and underbilled the block by intrinsic_state_gas. Re-introduce intrinsic_state_gas as a dedicated VM field, captured in add_intrinsic_gas. On top-level error, split state_gas_used and refund only the execution portion to the reservoir; keep intrinsic so block accounting matches EELS `tx_state_gas = intrinsic + state_gas_used`. Fixes 67 ef-test failures across stSStoreTest, eip7702_set_code_tx, eip8037_state_creation, eip7778_block_gas_accounting, and ported_static stStackTests on tests-bal@v7.2.0 fixtures. Full suite now 8726 passed, 0 failed.
…devnet-7 v7.2.0)
Per the engine-API spec, calling engine_newPayloadV4 with an Amsterdam
payload (or one carrying the BAL field) and calling engine_newPayloadV5
with a pre-Amsterdam payload (or one missing the BAL field) MUST return
JSON-RPC error -32602, not PayloadStatus.INVALID. ethrex previously fell
through to block validation which surfaced INVALID; clients couldn't
distinguish "wrong API version" from "bad block".
Add structural checks at the top of both handlers:
- V4: reject if block_access_list is present or timestamp is Amsterdam.
- V5: reject if block_access_list is absent or timestamp is pre-Amsterdam.
Both map to RpcErr::WrongParam (-32602). Fixes 2 hive
eels/consume-engine fork-transition failures in
test_fork_transition::test_invalid_{pre,post}_fork_block_*_bal_hash_field.
🤖 Kimi Code ReviewThe changes look correct. This PR updates the Amsterdam fork (EIP-8037) implementation to align with execution-specs v7.2.0 (EELS PR #2863), simplifying the state gas accounting from a complex "clamp-and-spill" mechanism to signed arithmetic. Correctness & Safety:
Minor observations:
Suggestion (non-blocking): Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewHere is my review of PR #6671: PR #6671 —
|
Greptile SummaryThis PR ports ethereum/execution-specs#2863 into ethrex's LEVM, replacing the clamp-and-spill state-gas scaffolding with a simpler inline-credit model:
Confidence Score: 4/5The core accounting model is mechanically sound and fully tested (8726/8726 fixtures pass); two spots use silent saturation where the rest of the PR propagates errors explicitly. The inline-credit simplification is well-reasoned: signed The
|
| Filename | Overview |
|---|---|
| crates/vm/levm/src/vm.rs | Core state-gas model rewrite: state_gas_used promoted to i64, clamp-and-spill scaffolding removed; error-path arithmetic mixes saturating_* with checked_* |
| crates/vm/levm/src/hooks/default_hook.rs | Collision and refund-sender paths updated to read net state_gas_used (i64) with .max(0) clamp; consistent with new VM fields |
| crates/vm/levm/src/opcode_handlers/system.rs | Revert handling simplified to single state_gas_used_at_entry snapshot; checked_sub/add used correctly |
| crates/vm/levm/src/call_frame.rs | Five snapshot fields collapsed into single state_gas_used_at_entry: i64; straightforward cleanup |
| crates/vm/levm/src/utils.rs | intrinsic_state_gas now stores raw state_gas (not cumulative), enabling correct split on failure |
| crates/networking/rpc/engine/payload.rs | V4/V5 handlers return -32602 for structural mismatches; correct per engine-API spec |
Sequence Diagram
sequenceDiagram
participant TX as Transaction
participant VM as VM (finalize_execution)
participant Hook as DefaultHook
participant RS as refund_sender
TX->>VM: top-level failure (REVERT/HALT/OOG)
VM->>VM: "if !collision: reservoir += (state_gas_used - intrinsic).max(0)"
VM->>VM: "state_gas_used = intrinsic_state_gas"
VM->>VM: "if CREATE-tx: reservoir += new_account_refund, state_refund += new_account_refund"
VM->>Hook: finalize_execution(ctx_result)
alt collision
Hook->>Hook: "state_gas = (state_gas_used - state_refund).max(0)"
Hook->>Hook: "regular_gas = gas_limit - reservoir"
Hook-->>TX: return early
else non-collision
Hook->>RS: refund_sender(vm, ctx_result)
RS->>RS: "state_gas = (state_gas_used - state_refund).max(0)"
RS->>RS: "regular_gas = raw_consumed - intrinsic_state - reservoir_initial - spill"
RS-->>TX: gas accounting complete
end
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
crates/vm/levm/src/vm.rs:898-901
The execution-portion refund calculation in the error path uses `saturating_sub` and `saturating_add` while the rest of this PR consistently propagates errors via `checked_*`. If `state_gas_used` were ever less than `i64::MIN + intrinsic_signed`, saturation would silently produce `i64::MIN` as `execution_state_gas_used` and then clamp the reservoir to zero — obscuring the accounting error rather than surfacing it.
```suggestion
let execution_state_gas_used = self
.state_gas_used
.checked_sub(intrinsic_signed)
.ok_or(InternalError::Underflow)?;
let reservoir_signed = i64::try_from(self.state_gas_reservoir)
.map_err(|_| InternalError::Overflow)?
.checked_add(execution_state_gas_used)
.ok_or(InternalError::Overflow)?
```
### Issue 2 of 2
crates/vm/levm/src/vm.rs:918-920
These two `saturating_add` calls on `u64` silently cap on overflow, inconsistent with the `checked_add` + `ok_or(InternalError::Overflow)` pattern used everywhere else in this PR (including in `charge_state_gas` and `credit_state_gas_refund` just above).
```suggestion
self.state_gas_reservoir = self
.state_gas_reservoir
.checked_add(new_account_refund)
.ok_or(InternalError::Overflow)?;
self.state_refund = self
.state_refund
.checked_add(new_account_refund)
.ok_or(InternalError::Overflow)?;
```
Reviews (1): Last reviewed commit: "chore(l1): drop devnet/PR refs from EIP-..." | Re-trigger Greptile
| let execution_state_gas_used = self.state_gas_used.saturating_sub(intrinsic_signed); | ||
| let reservoir_signed = i64::try_from(self.state_gas_reservoir) | ||
| .map_err(|_| InternalError::Overflow)? | ||
| .saturating_add(execution_state_gas_used); |
There was a problem hiding this comment.
The execution-portion refund calculation in the error path uses
saturating_sub and saturating_add while the rest of this PR consistently propagates errors via checked_*. If state_gas_used were ever less than i64::MIN + intrinsic_signed, saturation would silently produce i64::MIN as execution_state_gas_used and then clamp the reservoir to zero — obscuring the accounting error rather than surfacing it.
| let execution_state_gas_used = self.state_gas_used.saturating_sub(intrinsic_signed); | |
| let reservoir_signed = i64::try_from(self.state_gas_reservoir) | |
| .map_err(|_| InternalError::Overflow)? | |
| .saturating_add(execution_state_gas_used); | |
| let execution_state_gas_used = self | |
| .state_gas_used | |
| .checked_sub(intrinsic_signed) | |
| .ok_or(InternalError::Underflow)?; | |
| let reservoir_signed = i64::try_from(self.state_gas_reservoir) | |
| .map_err(|_| InternalError::Overflow)? | |
| .checked_add(execution_state_gas_used) | |
| .ok_or(InternalError::Overflow)? |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/levm/src/vm.rs
Line: 898-901
Comment:
The execution-portion refund calculation in the error path uses `saturating_sub` and `saturating_add` while the rest of this PR consistently propagates errors via `checked_*`. If `state_gas_used` were ever less than `i64::MIN + intrinsic_signed`, saturation would silently produce `i64::MIN` as `execution_state_gas_used` and then clamp the reservoir to zero — obscuring the accounting error rather than surfacing it.
```suggestion
let execution_state_gas_used = self
.state_gas_used
.checked_sub(intrinsic_signed)
.ok_or(InternalError::Underflow)?;
let reservoir_signed = i64::try_from(self.state_gas_reservoir)
.map_err(|_| InternalError::Overflow)?
.checked_add(execution_state_gas_used)
.ok_or(InternalError::Overflow)?
```
How can I resolve this? If you propose a fix, please make it concise.| self.state_gas_reservoir = | ||
| self.state_gas_reservoir.saturating_add(new_account_refund); | ||
| self.state_gas_refund_absorbed = self | ||
| .state_gas_refund_absorbed | ||
| .saturating_add(new_account_refund); | ||
| self.state_refund = self.state_refund.saturating_add(new_account_refund); |
There was a problem hiding this comment.
These two
saturating_add calls on u64 silently cap on overflow, inconsistent with the checked_add + ok_or(InternalError::Overflow) pattern used everywhere else in this PR (including in charge_state_gas and credit_state_gas_refund just above).
| self.state_gas_reservoir = | |
| self.state_gas_reservoir.saturating_add(new_account_refund); | |
| self.state_gas_refund_absorbed = self | |
| .state_gas_refund_absorbed | |
| .saturating_add(new_account_refund); | |
| self.state_refund = self.state_refund.saturating_add(new_account_refund); | |
| self.state_gas_reservoir = self | |
| .state_gas_reservoir | |
| .checked_add(new_account_refund) | |
| .ok_or(InternalError::Overflow)?; | |
| self.state_refund = self | |
| .state_refund | |
| .checked_add(new_account_refund) | |
| .ok_or(InternalError::Overflow)?; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/vm/levm/src/vm.rs
Line: 918-920
Comment:
These two `saturating_add` calls on `u64` silently cap on overflow, inconsistent with the `checked_add` + `ok_or(InternalError::Overflow)` pattern used everywhere else in this PR (including in `charge_state_gas` and `credit_state_gas_refund` just above).
```suggestion
self.state_gas_reservoir = self
.state_gas_reservoir
.checked_add(new_account_refund)
.ok_or(InternalError::Overflow)?;
self.state_refund = self
.state_refund
.checked_add(new_account_refund)
.ok_or(InternalError::Overflow)?;
```
How can I resolve this? If you propose a fix, please make it concise.
🤖 Codex Code ReviewFindings
The LEVM signed state-gas refactor looked internally coherent from a static pass; my concrete concerns are on the RPC/versioning edges above. I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Address bot review on PR #6671: - Extract the duplicated EIP-8037 revert-path math from handle_return_call and handle_return_create into a single VM helper. - Switch checked_sub error label in the helper from InternalError::Underflow to InternalError::Overflow (i64::checked_sub only fails on range overflow). - Drop the dead u64::try_from(...max(0)) map_err arm — non-negativity is proven by the .max(0) so `as u64` is sound. - Replace remaining saturating_add on state_refund / state_gas_reservoir in the CREATE-tx error branch with checked_add propagating Overflow, matching the pattern in default_hook.rs. - Restore the explanatory comment on the post-CREATE-charge snapshot ordering in CREATE/CREATE2 entry sites.
Block hash mismatch indicates the sender's hash was computed over header
fields the receiver doesn't know about (or vice versa) — a structural
payload schema error, not a block-validity failure. Per engine-API spec
this surfaces as JSON-RPC -32602, not PayloadStatus.INVALID.
Fixes the 2 remaining hive eels/consume-engine fork-transition failures
(test_invalid_{pre,post}_fork_block_*_bal_hash_field), where the test
injects/removes block_access_list_hash via rlp_modifier on the header.
The injected field changes the payload's block_hash; ethrex reconstructs
the header from V4/V5 schema and detects the mismatch.
This reverts commit d3e14f7.
…Payload On block_hash mismatch, V4 also tries reconstructing the header with block_access_list_hash injected (Some(H256::zero())) and V5 tries reconstructing without it (None). If the alternate matches the payload's block_hash, the sender used the wrong API version for that block's fork — return JSON-RPC -32602 (Invalid params) per engine-API spec. Real value-mismatch tests (BAL hash with wrong value, withdrawals_root, requests_hash, etc.) don't match the alternate and fall through to the usual PayloadStatus.INVALID path. Targets the 2 hive eels/consume-engine fork-transition failures: - test_invalid_pre_fork_block_with_bal_hash_field (V4 + zero-injected block_access_list_hash at FORK_TIMESTAMP-1) - test_invalid_post_fork_block_without_bal_hash_field (V5 + removed block_access_list_hash at FORK_TIMESTAMP).
ElFantasma
left a comment
There was a problem hiding this comment.
The i64 transition is everywhere — try_from(u64)/try_from(i64) conversions are added at every boundary with InternalError::Overflow mapping. All bounded by tx gas_limit (well below i64::MAX), so the conversions are belt-and-suspenders in practice. Worth a one-line // state_gas_used is i64 throughout; tx gas_limit caps the magnitude well below i64::MAX comment near the field declaration so future readers don't wonder why the conversions exist.
Gate `block_access_list_hash` presence on Amsterdam activation across all fork header validators. Pre-Amsterdam blocks with the field injected via RLP (e.g. EEST consume-rlp fork-transition fixtures) were silently accepted; now they are rejected before execution. The Amsterdam side is also tightened: missing field is rejected as BlockAccessListHashNotPresent instead of the looser BlockAccessListHashMismatch.
ElFantasma
left a comment
There was a problem hiding this comment.
Follow-up on the previous round. The structural header-validator addition in 4884189d05 is good (verified the routing through validate_block_pre_execution and the new InvalidBlockHeaderError variants), and the V4-Amsterdam-timestamp UnsupportedFork fix in abb964f311 is correct. But the V5 side of the same spec rule didn't get the symmetric update — one inline below.
…ngine-API spec Symmetric with abb964f. amsterdam.md: client MUST return -38005 if payload timestamp doesn't fall within Amsterdam time frame, in either direction. Geth uses unsupportedForkErr for the same case. Also cite the EELS fork-transition fixtures next to the V4/V5 alt-hash heuristics so readers know which test pins them to InvalidParams.
|
Good catch, applied in deced65.
Verified via hive + consume engine on v7.2.0 fixtures: 21/21 amsterdam fork-transition tests, 6/6 cancun excess_blob_gas fork-transition tests. |
Aligns ethrex with bal-devnet-7 v7.2.0. Tracking: lambdaclass#6583 (item 10). ## Changes - Port ethereum/execution-specs#2863: inline `state_gas` refunds (SSTORE `0→x→0`, CREATE collision/revert, EIP-7702 auth refills) credit the local frame's reservoir immediately. `state_gas_used` becomes `i64`; the clamp-and-spill scaffolding (`state_gas_refund_pending`, `state_gas_refund_absorbed`, `state_gas_spill_outstanding`, `state_gas_credit_against_drain`, intrinsic-tracking field + frame snapshots) is removed. - Bump fixtures and `eels_commit` to `tests-bal@v7.2.0` / `a3e5201a5`. - Preserve intrinsic state gas on top-level error: split out a dedicated `vm.intrinsic_state_gas` so the error path refunds only the execution portion. Without this, EIP-7702 set-code reverts and CREATE-tx reverts underbilled `block_state_gas_used` by `AUTH_TOTAL × CPSB` / `NEW_ACCOUNT × CPSB`. - `default_hook.rs` `state_refund` i64 cast: propagate `InternalError::Overflow` instead of silently saturating (match `vm.rs`). - `engine_newPayloadV{4,5}`: return JSON-RPC `-32602` on fork/field mismatch (V4 with BAL field or Amsterdam timestamp; V5 missing BAL field or pre-Amsterdam timestamp). Was returning `PayloadStatus.INVALID`. ## Test plan - `cargo check`, `clippy`, `fmt`: clean across `ethrex-levm`, `ethrex-rpc`, `ethrex-vm`, `ethrex-blockchain`, `ethrex-l2`. - `make -C tooling/ef_tests/blockchain test-levm` against `tests-bal@v7.2.0`: 8726 passed, 0 failed. - Hive `eels/consume-engine` Amsterdam: pending re-run; engine-API fix targets the 2 remaining `test_fork_transition` failures.
Aligns ethrex with bal-devnet-7 v7.2.0.
Tracking: #6583 (item 10).
Changes
state_gasrefunds (SSTORE0→x→0, CREATE collision/revert, EIP-7702 auth refills) credit the local frame's reservoir immediately.state_gas_usedbecomesi64; the clamp-and-spill scaffolding (state_gas_refund_pending,state_gas_refund_absorbed,state_gas_spill_outstanding,state_gas_credit_against_drain, intrinsic-tracking field + frame snapshots) is removed.eels_committotests-bal@v7.2.0/a3e5201a5.vm.intrinsic_state_gasso the error path refunds only the execution portion. Without this, EIP-7702 set-code reverts and CREATE-tx reverts underbilledblock_state_gas_usedbyAUTH_TOTAL × CPSB/NEW_ACCOUNT × CPSB.default_hook.rsstate_refundi64 cast: propagateInternalError::Overflowinstead of silently saturating (matchvm.rs).engine_newPayloadV{4,5}: return JSON-RPC-32602on fork/field mismatch (V4 with BAL field or Amsterdam timestamp; V5 missing BAL field or pre-Amsterdam timestamp). Was returningPayloadStatus.INVALID.Test plan
cargo check,clippy,fmt: clean acrossethrex-levm,ethrex-rpc,ethrex-vm,ethrex-blockchain,ethrex-l2.make -C tooling/ef_tests/blockchain test-levmagainsttests-bal@v7.2.0: 8726 passed, 0 failed.eels/consume-engineAmsterdam: pending re-run; engine-API fix targets the 2 remainingtest_fork_transitionfailures.