Implement EIP-8037: "State Creation Gas Cost Increase" - #1672
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1672 +/- ##
==========================================
- Coverage 98.02% 97.90% -0.12%
==========================================
Files 179 181 +2
Lines 16317 16544 +227
Branches 3760 3792 +32
==========================================
+ Hits 15994 16198 +204
- Misses 243 253 +10
- Partials 80 93 +13
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
78114ae to
6dc7d64
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The mocked host mishandles a valid zero state-gas result, and important meter, system-call, and block-accounting coverage is missing.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Implements EIP-8037’s Amsterdam two-dimensional execution/state-gas model across EVMC, VM execution, transaction processing, and block accounting.
Changes:
- Adds state-gas reservoirs, spilling, refilling, and frame rollback.
- Charges state creation for accounts, storage, and deployed code.
- Adds dimensional validation, receipts, block accounting, and tests.
File summaries
| File | Description |
|---|---|
test/utils/test_state.hpp |
Extends transition helper API. |
test/utils/test_state.cpp |
Forwards the state-gas budget. |
test/utils/statetest_runner.cpp |
Supplies the statetest state-gas budget. |
test/utils/error_matching.cpp |
Adds gas-overflow exception mapping. |
test/utils/block_transition.cpp |
Implements dimensional block accounting; needs nonzero state-gas aggregation coverage. |
test/unittests/state_tx_test.cpp |
Updates validation calls. |
test/unittests/state_transition.hpp |
Adds expected state-gas fields. |
test/unittests/state_transition.cpp |
Verifies receipt state gas. |
test/unittests/state_transition_eip8037_test.cpp |
Adds EIP-8037 transition tests. |
test/unittests/state_transition_create_test.cpp |
Updates deployment gas limits. |
test/unittests/CMakeLists.txt |
Registers new tests. |
test/state/transaction.hpp |
Adds gas dimensions to transaction properties and receipts. |
test/state/system_contracts.cpp |
Adds the system-call state reservoir; boundary coverage is missing. |
test/state/state.hpp |
Extends the transaction validation API. |
test/state/state.cpp |
Implements validation, gas splitting, and settlement. |
test/state/host.cpp |
Implements account and code state charging. |
test/state/account.hpp |
Adds an account-liveness helper. |
lib/evmone/state_gas.hpp |
Defines reservoir accounting; focused boundary tests are needed. |
lib/evmone/instructions.hpp |
Adds child merging and SELFDESTRUCT charging. |
lib/evmone/instructions_storage.cpp |
Implements SSTORE state gas. |
lib/evmone/instructions_calls.cpp |
Implements CALL/CREATE state gas. |
lib/evmone/execution_state.hpp |
Tracks and returns frame state gas. |
lib/evmone/constants.hpp |
Defines EIP-8037 costs. |
evmc/include/evmc/mocked_host.hpp |
Propagates mocked state gas, but incorrectly overloads zero as default behavior. |
evmc/include/evmc/evmc.hpp |
Exposes result fields in C++. |
evmc/include/evmc/evmc.h |
Extends ABI structures and bumps the ABI. |
Review details
Suppressed comments (3)
lib/evmone/state_gas.hpp:31
- The new consensus-critical meter has no direct unit coverage: the added transition tests do not exercise atomic charge failure, a mixed reservoir/spill charge, LIFO refill, or
repay_spill()after a child refund. Please add focused boundary tests for these operations so pool-conservation regressions are caught independently of transaction plumbing.
[[nodiscard]] bool charge(int64_t& gas_left, int64_t cost) noexcept
test/state/system_contracts.cpp:80
- The referenced
system_contract_reaches_gas_limittest is not present in the repository, leaving the new consensus-visible 16-SSTORE reservoir unverified. Add an Amsterdam system-call regression that exercises this exact reservoir boundary (and the behavior immediately beyond it), rather than documenting a test that does not exist.
/// Cap on the number of SSTOREs a system contract may fund out of its state-gas budget. The value
/// is observable: `system_contract_reaches_gas_limit` sizes a contract to exactly
/// `30M + SYSTEM_MAX_SSTORES_PER_CALL × STORAGE_SET_STATE_GAS` (EIP-8037).
constexpr int64_t SYSTEM_MAX_SSTORES_PER_CALL = 16;
test/utils/block_transition.cpp:123
- No block test exercises this new two-dimensional aggregation with nonzero state gas; the existing EIP-7778 tooling test uses only a storage clear, whose state component is zero. Add a multi-transaction Amsterdam case with differing regular/state components so it distinguishes
max(sum_regular, sum_state)from summing per-transaction maxima and verifies both remaining-dimension checks.
// The block's 2D gas formula (EIP-7778).
const auto block_gas_used = std::max(sum_regular_gas, sum_state_gas);
- Files reviewed: 26/26 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // A zero state_gas_left means "the callee consumed the caller's whole reservoir". | ||
| // The mock runs no code, so echo the reservoir it was handed unless a test set one. | ||
| if (result.state_gas_left == 0) | ||
| result.state_gas_left = msg.state_gas; |
bfb12d4 to
ed0ba05
Compare
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. 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.
9ba0a9e to
f7befb5
Compare
The deposit splits into a regular and a state component. When the reservoir covers the whole state component, dropping the regular check lets charge() take its reservoir-only branch and report success on a negative gas_left, deploying code that was never paid for. Reaching that needs a gas limit above MAX_TX_GAS_LIMIT, which no existing deposit test uses, so the check was unverified: the mutation passed the unit tests and the execution-spec-tests.
max(block_execution_gas_used, block_state_gas_used) is EIP-8037's rule. The EIP-7778 max is max(tx_gas_used, calldata_floor_gas_cost), per transaction and one-dimensional; EIP-8037 preserves it inside the execution dimension, which is the max already cited correctly in transition().
Restore the collision return and the pre-Amsterdam deployment cost to their original shapes: neither is touched by this change. Setting the state gas on the Frontier path was the only reason to unroll the ternary, and it is a no-op there — both pools are zero for every pre-Amsterdam frame and the result is zero-initialized.
b7a835e to
6c438bd
Compare
Keep the rule and the reason it is not obvious; drop the restatements of what the code below does and the EELS function names, which date faster than the code. The two host.cpp blocks lead with the rule and leave the TODO as a separate sentence.
Two call sites, both in host.cpp; it does not need to be in the Account header.
EIP-8037 calls it execution gas and never says "regular", but execution_gas_limit already names something else here.
The clamp cannot trigger: a refill needs a matching allocation, and the top frame has no ancestor to have made one. Asserting says so, and catches the case the clamp would have hidden.
Prefix them by the level they measure, as block_gas_left already is, and put the state dimension next to it in the parameter lists rather than after the blob gas.
The value is the spec's SYSTEM_MAX_SSTORES_PER_CALL times the slot cost; the prose around it restated the EIP.
The cap was pinned, and the EIP-8038 CREATE reprice moved the window out from under it. Take the CREATE price from the cost table and the rest from the deposit's own constants, so the frame lands mid-window on either schedule. Replace the pinned gas_used with a second case one regular component richer, which deploys. The pair fails if the window ever moves, where a single case would keep passing on the initcode running out of gas instead.
3441455 to
23ed6c9
Compare
Give each test a short intro saying what it pins, and drop the commentary that restated the assertions below it. Say execution gas, as the EIP and the execution-specs do, and name the constants in upper case. Factor the code deposit's shared setup into named constants and one helper, so the two cases differ only in the gas cap. Let the gas expressions carry their own breakdown instead of repeating the operands in a comment. Assert the state gas in the three tests that left it unchecked, including the deposit pair, whose subject is which half of the deposit gets charged.
8e7df36 to
5ab7922
Compare
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.
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.