Implement EIP-8037: "State Creation Gas Cost Increase" - #1672
Open
chfast wants to merge 9 commits into
Open
Conversation
Contributor
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1672 +/- ##
==========================================
- Coverage 97.72% 97.48% -0.25%
==========================================
Files 171 173 +2
Lines 15666 15888 +222
Branches 3625 3665 +40
==========================================
+ Hits 15310 15488 +178
- Misses 269 295 +26
- Partials 87 105 +18
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Introduce the Amsterdam two-dimensional gas model: state-creation costs move out of regular gas into a separate state-gas dimension, priced at COST_PER_STATE_BYTE (1530) per byte of new state. - evmc: add `state_gas` to the message and `state_gas_left`/`state_gas_spilled` to the result, threading a per-frame state-gas reservoir through the VM. - StateGas (state_gas.hpp): a (reservoir-left, spilled) pair. Charges draw from the reservoir first and spill into regular gas_left; refunds refill in LIFO order; a frame's net use derives as `initial - left + spilled`. Frames roll their state gas back on revert/halt (make_execution_result). - Charges at state-creation sites: new account by CREATE/CREATE2 (at the deployment-address access), by value-CALL — including the depth-0 value-transfer charge the EIP-2780 decomposition later builds on — and by SELFDESTRUCT to a new beneficiary (NEW_ACCOUNT = 120 bytes); SSTORE 0->non-zero slot allocation (64 bytes, with the 0->Y->0 LIFO refill; the regular set cost drops to its 2900 component); code deposit per byte. Failed creations refund the charge. Opcode CREATE and the create transaction keep the legacy 32000 execution cost here: EIP-8037 defers its execution component to EIP-8038's CREATE_ACCESS, and EIP-8038 states that the flat GAS_CREATE is what CREATE_ACCESS replaces, so the reprice lands with it. - Transaction processing: execution gas splits into a regular budget (capped by TX_MAX_GAS_LIMIT - intrinsic) and the state-gas reservoir. Amsterdam lifts the Osaka per-tx gas cap; validation instead caps the regular intrinsic and applies the per-dimension block-inclusion rules against the new block state-gas budget. - Block accounting: per-tx receipts carry regular/state components; block gas_used = max(sum_regular, sum_state) (EIP-7778 2D formula). - System calls get a separate 16-SSTORE state-gas reservoir so the state dimension cannot OOG them, and a failed block-start system call now rolls back its partial state changes instead of asserting. The intrinsic cost otherwise keeps the pre-Amsterdam formula; the EIP-2780 resource decomposition lands separately. The EIP-7702 per-authorization state charges (AUTH_BASE and the authority's NEW_ACCOUNT) are not part of this commit: they are only expressible through the top-frame charging model that the EIP-2780 intrinsic decomposition introduces, so they land with it. The intrinsic keeps the pre-Amsterdam formula here. Includes the state-gas unit tests and the GAS_ALLOWANCE_EXCEEDED / BlockException.GAS_USED_OVERFLOW acceptance, which exists because the per-dimension inclusion checks keep an over-block-gas transaction a transaction-level rule.
EIP-8037 appends `state_gas` to evmc_message and `state_gas_left` / `state_gas_spilled` to evmc_result. Both structs change layout, and evmc_result is returned by value, so a host built against ABI 18 allocates the smaller struct while a VM built against this header writes the larger one. evmc_is_abi_compatible() compares only this number, so without the bump such a pair loads and reads past the end of the peer's structs straight into gas accounting.
Host::execute_message debits the depth-0 NEW_ACCOUNT charge from a local StateGas and from msg.gas, but left msg.state_gas at the entry reservoir. The precompile and empty-code exits commit the charged pools; the exit into the interpreter did not, so the reservoir portion of the charge stayed spendable and the caller's net-used derivation missed it. Not reachable today: a charged recipient is not alive, so it has no code and the empty-code exit returns first. The charge does not depend on that, and the deferred EIP-7702 authorization charges land in the same block.
state_gas_left defaults to 0, which the protocol reads as "the callee consumed the caller's whole reservoir", so every evmc_result producer has to set it. Two did not: MockedHost::call, which backs the unit-test fixture and the baseline-vs-advanced fuzzer, and the Frontier code-deposit exit, which sits beside a path that already sets it. Both are correct today only because their callers are pre-Amsterdam and hand out an empty reservoir. A delta-encoded field would make the zero default right by construction; echoing the reservoir is the smaller fix.
Inserting StateGas between gas_refund and memory shifted every later member by 16 bytes, pushing `status` from offset 120 to 136 and `host` from 40 to 56. On x86-64 that moves `status` past the disp8 window, so each of the ~195 accesses in a dispatch loop grows 3 bytes, and the host pointers land on a second cache line. Moving the field next to the stack space, which the class already documents as last "to make other fields' offsets of reasonable values", recovers 1728 bytes of baseline_execution .text.
create_impl spelled the EIP-161 aliveness rule out of three host callbacks where account_exists computes the same predicate: under the Amsterdam gate its pre-Spurious-Dragon arm is unreachable, leaving `acc != nullptr && !is_empty()`. That made four spellings of one rule, and this was the only one also assuming get_code_size agrees with code_hash. Three indirect host calls become one; each is a state lookup that, for the usual non-existent deployment address, misses the modified set and queries the uncached view.
sstore re-tested the revision and the storage status twice to decide the EIP-8037 slot-allocation charge and its refill, on values it had already used to index sstore_costs. Put the amount in that table as a third column, signed: positive charges, negative refills, zero before Amsterdam. The body reduces to two sign tests on a value already in a register, and the rule lives next to the other SSTORE costs rather than beside them. int32_t because 64 * COST_PER_STATE_BYTE is 97'920.
A value transfer to a zero-balance precompile at depth 0 creates a state account and pays NEW_ACCOUNT_STATE_GAS. With a below-cap gas limit the reservoir is empty, so the charge spills entirely into regular gas and the precompile must execute on what is left. No fixture in either EEST release covers this, so the interaction between the depth-0 charge and the precompile dispatch was unguarded.
The interpreter exit carried the depth-0 charge's spill out of every frame regardless of outcome, which commits the charge on a frame failure. EELS refills it there: it is charged after commit_state_gas, so restore_state_gas returns it. The path is unreachable — a charge implies a non-alive recipient, which has empty code and returns at the exit above — so assert that rather than encode a guess. The EIP-7702 authorization charges EIP-2780 will add next to this one must survive a failure, the opposite of this charge, so carrying both out uniformly would be wrong for one of them.
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.
Implement EIP-8037: "State Creation Gas Cost Increase"
Introduce the Amsterdam two-dimensional gas model: state-creation costs move
out of regular gas into a separate state-gas dimension, priced at
COST_PER_STATE_BYTE (1530) per byte of new state.
state_gasto the message andstate_gas_left/state_gas_spilledto the result, threading a per-frame state-gas reservoir through the VM.
from the reservoir first and spill into regular gas_left; refunds refill in
LIFO order; a frame's net use derives as
initial - left + spilled. Framesroll their state gas back on revert/halt (make_execution_result).
deployment-address access), by value-CALL — including the depth-0
value-transfer charge the EIP-2780 decomposition later builds on — and by
SELFDESTRUCT to a new beneficiary (NEW_ACCOUNT = 120 bytes); SSTORE
0->non-zero slot allocation (64 bytes, with the 0->Y->0 LIFO refill; the
regular set cost drops to its 2900 component); code deposit per byte.
Failed creations refund the charge. Opcode CREATE and the create
transaction keep the legacy 32000 execution cost here: EIP-8037 defers its
execution component to EIP-8038's CREATE_ACCESS, and EIP-8038 states that
the flat GAS_CREATE is what CREATE_ACCESS replaces, so the reprice lands
with it.
by TX_MAX_GAS_LIMIT - intrinsic) and the state-gas reservoir. Amsterdam
lifts the Osaka per-tx gas cap; validation instead caps the regular
intrinsic and applies the per-dimension block-inclusion rules against the
new block state-gas budget.
block gas_used = max(sum_regular, sum_state) (EIP-7778 2D formula).
dimension cannot OOG them, and a failed block-start system call now rolls
back its partial state changes instead of asserting.
The intrinsic cost otherwise keeps the pre-Amsterdam formula; the EIP-2780
resource decomposition lands separately.
The EIP-7702 per-authorization state charges (AUTH_BASE and the authority's
NEW_ACCOUNT) are not part of this commit: they are only expressible through
the top-frame charging model that the EIP-2780 intrinsic decomposition
introduces, so they land with it. The intrinsic keeps the pre-Amsterdam
formula here.
Includes the state-gas unit tests and the GAS_ALLOWANCE_EXCEEDED /
BlockException.GAS_USED_OVERFLOW acceptance, which exists because the
per-dimension inclusion checks keep an over-block-gas transaction a
transaction-level rule.
The follow-up commits are separable and each build and pass
cteston their own: