Skip to content

feat(bal-devnet-4): Changes needed for devnet#3624

Open
rakita wants to merge 11 commits intomainfrom
bal-devnet-4
Open

feat(bal-devnet-4): Changes needed for devnet#3624
rakita wants to merge 11 commits intomainfrom
bal-devnet-4

Conversation

@rakita
Copy link
Copy Markdown
Member

@rakita rakita commented Apr 28, 2026

No description provided.

rakita and others added 10 commits April 21, 2026 14:30
* 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.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 28, 2026

Merging this PR will degrade performance by 4.3%

❌ 6 regressed benchmarks
✅ 170 untouched benchmarks
⏩ 1 skipped benchmark1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation JUMPDEST_50 16 µs 16.6 µs -3.67%
Simulation SLOAD_50 24.3 µs 25.4 µs -4.3%
Simulation SSTORE_50 30.2 µs 31.2 µs -3.19%
Simulation subcall_1000_transfer_1wei 1.2 ms 1.3 ms -3.19%
Simulation subcall_1000_same_account 1.1 ms 1.2 ms -3.69%
Simulation subcall_1000_nested 2.2 ms 2.2 ms -3.37%

Comparing bal-devnet-4 (58a95fc) with main (bd89862)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@rakita rakita changed the title feat(bal-devnet-4): Changes needed for branch feat(bal-devnet-4): Changes needed for devnet Apr 28, 2026
…#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.
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