Open
Conversation
* feat(gas): implement EIP-8037 issue #2 — 0→x→0 storage reservoir refill When a storage slot is restored to its original zero value within the same transaction, the state gas originally charged for the 0→x transition is now returned directly to the reservoir rather than routed through the capped (spent/5) refund counter. This preserves the EIP-8037 invariant that state gas consumption correlates with actual state creation. Changes: - `GasTracker.state_gas_spent` becomes `i64`. A child frame's count can legitimately go negative when it clears a slot that the parent had set; the net is reconciled on frame return. - New `GasTracker::refill_reservoir(amount)` and `Gas::refill_reservoir` wrappers: add to reservoir, subtract from `state_gas_spent`. - New `GasParams::sstore_state_gas_refill` returns `32 * CPSB` only on 0→x→0 restoration. - SSTORE instruction calls `refill_reservoir` when EIP-8037 is enabled and the restoration condition matches. - EIP-8037 table: `sstore_set_refund` drops from `32 * CPSB + 2800` to `2800`; the state portion now flows through the reservoir refill path rather than the refund counter. - `handle_reservoir_remaining_gas` (frame.rs) uses saturating i64 math on both success and revert/halt paths so the parent's matching 0→x charge is correctly netted out on success, and the combined `state_gas_spent + reservoir` is clamped to 0 on failure. - Top-frame reconciliation (handler.rs) and `build_result_gas` (post_execution.rs) clamp to 0 before the public `u64` surface. - Precompile + inspector plumbing updated for the i64 switch. Test updated: `test_eip8037_sstore_set_then_clear_refund` now asserts `state_gas_spent == 0` and that total gas spent matches the baseline (was asserting +200k and a strict `>` inequality). Golden JSON regenerated. * fix(ee-tests): adapt custom opcode test to new insert_instruction signature `insert_instruction` gained a `gas: u16` parameter in the instruction-table refactor (#3561), and `Instruction::new` now takes only the fn. Update the DOUBLE opcode registration in `revm_tests.rs` so the ee-tests crate compiles. * test(eip8037): add same-frame and cross-frame 0→x→0 restoration tests Covers the two EIP-8037 issue #2 code paths: - `test_eip8037_sstore_refill_same_frame`: 0→1→0 within one frame ends with `state_gas_spent == 0`; compared against a set-only variant that retains the full `STATE_GAS_SSTORE_SET` charge. - `test_eip8037_sstore_refill_cross_frame`: parent SSTORE(0,1), then DELEGATECALLs a child that does SSTORE(0,0). The refill fires in the child frame (driving child `state_gas_spent` negative); on frame return the parent's +200k and child's -200k net out, leaving only the CREATE + code-deposit state gas on the books.
* feat(gas): EIP-7976 — increase calldata floor cost to 16/64 gas per byte Introduces a new `tx_floor_token_zero_byte_weight` GasId so the floor tokens formula becomes `zero × floor_zero_weight + nonzero × tx_token_non_zero_byte_multiplier`. Under EIP-7623 (Prague) the weight stays at 1 — reproducing the existing `tokens_in_calldata`-based floor. Under EIP-7976 (Amsterdam) the weight is raised to `tx_token_non_zero_byte_multiplier`, which yields `floor_tokens_in_calldata = (zero + nonzero) × 4` and, together with the per-token bump from 10 to 16, a uniform 64 gas/byte floor. * docs: correct EIP-7976 floor cost comment (64/64, not 16/64) * perf(gas): skip zero-byte scan when floor weights match When (Amsterdam / EIP-7976), every calldata byte contributes the same number of floor tokens. In that case we can use `input.len() * weight` directly and skip the filter+count pass over the calldata. * refactor(gas): rename tx_floor_token_zero_byte_weight to _multiplier Matches the naming of `tx_token_non_zero_byte_multiplier`. No behavior change. * refactor(gas): reuse get_tokens_in_calldata in tx_floor_cost The EIP-7623 branch (zero-byte multiplier = 1) produces exactly `zero + nonzero * non_zero_multiplier`, which is what get_tokens_in_calldata already computes. Delegate to it instead of re-implementing the scan inline. The EIP-7976 uniform path keeps its `input.len() * multiplier` shortcut. * refactor(gas): extract tx_floor_cost_with_tokens helper
* feat(gas): EIP-7976 — increase calldata floor cost to 16/64 gas per byte Introduces a new `tx_floor_token_zero_byte_weight` GasId so the floor tokens formula becomes `zero × floor_zero_weight + nonzero × tx_token_non_zero_byte_multiplier`. Under EIP-7623 (Prague) the weight stays at 1 — reproducing the existing `tokens_in_calldata`-based floor. Under EIP-7976 (Amsterdam) the weight is raised to `tx_token_non_zero_byte_multiplier`, which yields `floor_tokens_in_calldata = (zero + nonzero) × 4` and, together with the per-token bump from 10 to 16, a uniform 64 gas/byte floor. * docs: correct EIP-7976 floor cost comment (64/64, not 16/64) * perf(gas): skip zero-byte scan when floor weights match When (Amsterdam / EIP-7976), every calldata byte contributes the same number of floor tokens. In that case we can use `input.len() * weight` directly and skip the filter+count pass over the calldata. * refactor(gas): rename tx_floor_token_zero_byte_weight to _multiplier Matches the naming of `tx_token_non_zero_byte_multiplier`. No behavior change. * refactor(gas): reuse get_tokens_in_calldata in tx_floor_cost The EIP-7623 branch (zero-byte multiplier = 1) produces exactly `zero + nonzero * non_zero_multiplier`, which is what get_tokens_in_calldata already computes. Delegate to it instead of re-implementing the scan inline. The EIP-7976 uniform path keeps its `input.len() * multiplier` shortcut. * refactor(gas): extract tx_floor_cost_with_tokens helper * feat(gas): implement EIP-7981 access list cost increase Folds the per-byte data charge into the per-item access-list cost and extends the EIP-7623/7976 floor to cover access-list bytes, both gated on AMSTERDAM. - tx_access_list_address_cost: 2400 -> 3680 (+20 * 64) - tx_access_list_storage_key_cost: 1900 -> 3948 (+32 * 64) - New GasId::tx_access_list_floor_byte_multiplier (= 4 at AMSTERDAM), surfaced via tx_floor_tokens_in_access_list; initial_tx_gas now adds the access-list contribution on top of the calldata floor.
* feat(gas): EIP-8037 dynamic cost_per_state_byte derived from block gas limit Thread CPSB through state-gas accounting so state-gas charges scale with the current block's gas limit per EIP-8037 instead of using a hard-coded 1174. State-gas table entries now store *byte counts*; helpers multiply by CPSB at charge time via new `Cfg::cpsb` / `Host::cpsb` methods and a `CfgEnv::cpsb_override` for tests and replay. Also refund the parent's upfront CREATE state gas to its reservoir when a child create reverts or halts, and split the EIP-7702 refund into separate regular and state-bytes components (new `tx_eip7702_auth_refund_state_bytes` GasId). * refactor(frame): move CREATE state-gas refund into return_create Drop the `state_gas_charged` field from `CreateOutcome` and handle the upfront CREATE state-gas refund inside `return_create` instead of `return_result`. The value is threaded through `CreateFrame`: `return_create` refills it into the reservoir on entry and re-records it on a successful commit, undoing the refund. Revert `test_eip8037_reverted_create_child` expectations (and the associated json testdata) back to the prior semantics, where the parent's upfront CREATE state gas is not refunded on child revert. * refactor(frame): derive CREATE state gas inside return_create `return_create` now takes `&mut impl ContextTr` and derives both the CPSB and the upfront `create_state_gas` charge from `cfg` + `block`, removing the need to thread `state_gas_charged` (and `cpsb`) as parameters. Drops the now-unused `state_gas_charged` fields from `CreateInputs` and `CreateFrame`, and the corresponding tracking in the CREATE opcode. * perf(context): cache EIP-8037 cpsb on LocalContext Adds a cpsb field to LocalContext exposed via LocalContextTr, populated at the start of every execution entry point (Handler::run, run_system_call, and their inspector variants) so the hot-path Host::cpsb is a single field read instead of recomputing cfg.cpsb(block.gas_limit()) on each call. * refactor(frame): use cached cpsb from local context in return_create Reads cpsb from LocalContext (set at tx entry) instead of recomputing from cfg+block, and simplifies the EIP-8037 gating now that the state-gas charge is always derivable from the cached value. * fix(frame): unwind 0→x→0 reservoir refund on sub-frame revert Per EIP-8037, when a sub-frame reverts, any 0→x→0 reservoir refund it performed must be rolled back. The previous formula `parent.reservoir = child.state_gas_spent + child.reservoir` left the parent's reservoir inflated when the child drained and refilled via a 0→x→0 restoration pattern. Cap the child's reservoir contribution at the parent's pre-call value (parent's reservoir isn't modified during the call, so the current value is the pre-call value) and clamp state_gas_spent to non-negative to preserve the parent's prior charges. * fix(post-execution): clamp reservoir when floor gas exceeds limit budget (#3607) * fix(post-execution): clamp reservoir when floor gas exceeds limit budget When EIP-7623's data floor clamps `gas_used` upward and `floor_gas + reservoir > gas.limit` (e.g. an EIP-7702 reservoir refund pushed the reservoir above `limit - floor_gas`), the plain `set_spent(floor_gas + reservoir)` saturates `remaining` to 0 but leaves the reservoir intact, so `reimbursable = remaining + reservoir + refund` overshoots by `floor_gas + reservoir - limit` gas and the caller is over-refunded by that amount. In that branch clamp the reservoir to `limit - floor_gas` and zero `remaining` instead. * feat(eip8037): refund state gas for CREATE+SELFDESTRUCT and restructure CREATE upfront charge Adds `JournalTr::eip8037_selfdestruct_state_gas_refund` (and the `JournalInner` implementation) which sums the state gas charged during the tx for every account that was both created and self-destructed (per EIP-6780) — account creation, code deposit, and 0→non-zero storage slot sets — and skips the CREATE-tx target whose creation gas is already in `initial_state_gas`. The handler invokes it at the start of post-execution and refills the reservoir before refund/reimbursement so the returned gas bypasses the 1/5 refund cap. Also moves the CREATE opcode's upfront `create_state_gas` refund out of `return_create` and into the CREATE opcode return path in the parent frame: the parent charged it and, on child failure (revert/halt/ early-fail with `address == None`), the parent now refunds it directly and undoes its `state_gas_spent`. The child frame is no longer allowed to borrow that upfront charge to cover code deposit. * refactor(journal): iterate selfdestructed_addresses set in EIP-8037 refund Replace full-state scan with a direct walk over the dedicated `selfdestructed_addresses` set, hoist the per-tx `sstore_set` constant out of the inner loop, and drop the redundant `original_value.is_zero()` check (locally-created accounts always start with empty storage, so every non-zero present slot was charged sstore_set). * fix(post-execution): zero reservoir when EIP-7623 floor wins Match execution-specs: when the floor is enforced, unused state gas is absorbed into the floor cost, not reimbursed separately. Without this, `reimburse_caller` (which sums `remaining + reservoir + refunded`) over-refunds the caller and `reward_beneficiary` under-pays the beneficiary by exactly `gas.reservoir()`. Also drop the prior commented-out "set_spent(floor + reservoir)" workaround — its premise that the reservoir must be added to spent conflicted with the spec's single-counter model. * fix(eip8037): correct CREATE state-gas refund propagation and unwind (#3614) * fix(post-execution): clamp reservoir when floor gas exceeds limit budget When EIP-7623's data floor clamps `gas_used` upward and `floor_gas + reservoir > gas.limit` (e.g. an EIP-7702 reservoir refund pushed the reservoir above `limit - floor_gas`), the plain `set_spent(floor_gas + reservoir)` saturates `remaining` to 0 but leaves the reservoir intact, so `reimbursable = remaining + reservoir + refund` overshoots by `floor_gas + reservoir - limit` gas and the caller is over-refunded by that amount. In that branch clamp the reservoir to `limit - floor_gas` and zero `remaining` instead. * feat(eip8037): refund state gas for CREATE+SELFDESTRUCT and restructure CREATE upfront charge Adds `JournalTr::eip8037_selfdestruct_state_gas_refund` (and the `JournalInner` implementation) which sums the state gas charged during the tx for every account that was both created and self-destructed (per EIP-6780) — account creation, code deposit, and 0→non-zero storage slot sets — and skips the CREATE-tx target whose creation gas is already in `initial_state_gas`. The handler invokes it at the start of post-execution and refills the reservoir before refund/reimbursement so the returned gas bypasses the 1/5 refund cap. Also moves the CREATE opcode's upfront `create_state_gas` refund out of `return_create` and into the CREATE opcode return path in the parent frame: the parent charged it and, on child failure (revert/halt/ early-fail with `address == None`), the parent now refunds it directly and undoes its `state_gas_spent`. The child frame is no longer allowed to borrow that upfront charge to cover code deposit. * refactor(journal): iterate selfdestructed_addresses set in EIP-8037 refund Replace full-state scan with a direct walk over the dedicated `selfdestructed_addresses` set, hoist the per-tx `sstore_set` constant out of the inner loop, and drop the redundant `original_value.is_zero()` check (locally-created accounts always start with empty storage, so every non-zero present slot was charged sstore_set). * fix(post-execution): zero reservoir when EIP-7623 floor wins Match execution-specs: when the floor is enforced, unused state gas is absorbed into the floor cost, not reimbursed separately. Without this, `reimburse_caller` (which sums `remaining + reservoir + refunded`) over-refunds the caller and `reward_beneficiary` under-pays the beneficiary by exactly `gas.reservoir()`. Also drop the prior commented-out "set_spent(floor + reservoir)" workaround — its premise that the reservoir must be added to spent conflicted with the spec's single-counter model. * fix(eip8037): correct CREATE/CALL state-gas refund propagation Three fixes to EIP-8037 reservoir accounting that together resolve 12 failing state tests under fixtures_bal_v570/state_tests/for_amsterdam/: 1. CREATE nonce overflow: the early-fail path returns `InstructionResult::Return` (ok) with `address == None`, but the upfront `create_state_gas` refund was gated on `!is_ok()`. Gate on `address.is_none() || !is_ok()` so the refund applies. 2. 0→x→0 refill unwind on parent revert/halt: add a per-frame `refill_amount` counter on `GasTracker` that accumulates `refill_reservoir` calls (and now the CREATE upfront-state-gas refund on child failure). On revert/halt, `handle_reservoir_remaining_gas` subtracts this from the propagated reservoir so refill-driven inflation does not leak up; on success the total is propagated so an ancestor revert can still unwind it. The CREATE-failed refund now uses `refill_reservoir` to participate in this tracking. 3. EIP-8037 selfdestruct refund: include the CREATE-tx contract in the iteration (the execution-specs reference iterates all created+ destroyed accounts) and cap the aggregate refund at `gas.state_gas_spent()` to match the per-address `min(refund, state_gas_used)` cap from the spec. * wip(eip8037): refactor reservoir handling and selfdestruct refund - Refactor handle_reservoir_remaining_gas to take is_success: bool directly - Drop is_created_locally / len != 0 guards in journal selfdestruct refund - Add is_ok_without_selfdestruct helper on InstructionResult - Stash WIP commented logic in handler for top-level CREATE failure refund * fix(merge): drop duplicated CREATE state-gas refund block The merge of origin/dev4-cpsb (00ca181) into functional kept both the upstream `set_reservoir`/`set_state_gas_spent` block and the local `refill_reservoir` block, causing every failed child CREATE to refund `create_state_gas` to the parent's reservoir twice. Keep only the `refill_reservoir` version: it handles the nonce-overflow path (address == None with is_ok()) and registers the refund in `refill_amount` so the parent's own revert/halt unwinds it correctly.
Conflicts:
- crates/context/interface/src/cfg/gas.rs
- crates/interpreter/src/gas.rs
- crates/inspector/src/gas.rs
Kept i64 type for state_gas_spent (EIP-8037 issue #2 needs negative
values for 0→x→0 storage refill).
- crates/handler/src/frame.rs
Kept the is_success: bool signature on handle_reservoir_remaining_gas.
- crates/ee-tests/src/revm_tests.rs
Took main's new instruction API (popn_top! + Result return) from
refactor #3558.
Also uncomment the devnet statetest run; the devnet btest line is left as-is.
On child revert, both the parent's upfront CREATE state gas and the child's SSTORE state gas are refunded to the reservoir, leaving state_gas_spent at 0.
CPSB is fixed at 1174 for Glamsterdam, so the block-gas-limit-derived formula is no longer needed. Deprecate cost_per_state_byte, expose CPSB_GLAMSTERDAM, and update callers and doc comments accordingly.
Merging this PR will degrade performance by 4.3%
Performance Changes
Comparing Footnotes
|
…#3633) Per the spec, tx_state_gas = intrinsic_state_gas + execution_state_gas. The EIP-7702 reservoir refund is added back to the state gas reservoir at tx start, so it must not also be subtracted from the gross state gas reported in the result.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.