From 368a85bfc1a23e12c0dc2bf92f972ef9ff764e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 24 Aug 2026 10:13:41 +0200 Subject: [PATCH] Implement EIP-8037: "State Creation Gas Cost Increase" Add state gas: a second gas dimension metering state growth, priced at COST_PER_STATE_BYTE (1530) per byte and funded by a per-frame reservoir separate from the execution gas. - evmc: `evmc_message::state_gas` hands a frame its reservoir and `evmc_result::state_gas` returns what is left and what spilled. - StateGas: charges draw from the reservoir, then spill into `gas_left`; refills return them in LIFO order. A frame's net use is `initial - left + spilled`. Revert and halt roll the frame back to its entry reservoir. - Charge sites: a new account by CREATE/CREATE2, by value-CALL and by SELFDESTRUCT to a new beneficiary (120 bytes); an SSTORE 0->non-zero slot allocation (64 bytes), whose execution cost drops to its 2900 component; and the deployed code per byte, replacing the 200/byte deposit with a 6-per-word hash cost. Failed creations refill. - Transaction: the gas limit above TX_MAX_GAS_LIMIT becomes the reservoir, the rest covers the intrinsic and the execution gas. Amsterdam drops the Osaka per-tx cap and checks each dimension against the block instead. - Block: the receipt carries its total and its state part; the block spends both allowances and reports the bottleneck one (EIP-7778). - System calls get a 16-SSTORE reservoir of their own so the state dimension cannot run them out of gas. CREATE and the create transaction keep the flat 32000 execution cost, and the intrinsic keeps the pre-Amsterdam formula. --- evmc/include/evmc/evmc.h | 26 +- evmc/include/evmc/evmc.hpp | 26 +- evmc/include/evmc/helpers.h | 3 +- lib/evmone/constants.hpp | 9 + lib/evmone/execution_state.hpp | 25 +- lib/evmone/instructions.hpp | 13 +- lib/evmone/instructions_calls.cpp | 58 ++- lib/evmone/instructions_storage.cpp | 15 +- lib/evmone/state_gas.hpp | 47 +++ test/state/host.cpp | 66 ++- test/state/precompiles.cpp | 3 +- test/state/state.cpp | 56 ++- test/state/state.hpp | 5 +- test/state/system_contracts.cpp | 2 + test/state/transaction.hpp | 7 + test/unittests/CMakeLists.txt | 1 + test/unittests/evmone_test.cpp | 29 ++ test/unittests/state_transition.cpp | 7 +- test/unittests/state_transition.hpp | 3 + .../state_transition_create_test.cpp | 4 +- ...tate_transition_eip8037_state_gas_test.cpp | 389 ++++++++++++++++++ test/unittests/state_tx_test.cpp | 33 +- test/utils/block_transition.cpp | 15 +- test/utils/error_matching.cpp | 1 + test/utils/statetest_runner.cpp | 1 + test/utils/test_state.cpp | 6 +- test/utils/test_state.hpp | 2 +- 27 files changed, 778 insertions(+), 74 deletions(-) create mode 100644 lib/evmone/state_gas.hpp create mode 100644 test/unittests/state_transition_eip8037_state_gas_test.cpp diff --git a/evmc/include/evmc/evmc.h b/evmc/include/evmc/evmc.h index 2807838ab1..29626ab1d6 100644 --- a/evmc/include/evmc/evmc.h +++ b/evmc/include/evmc/evmc.h @@ -45,7 +45,7 @@ enum * * @see @ref versioning */ - EVMC_ABI_VERSION = 18 + EVMC_ABI_VERSION = 19 }; @@ -120,6 +120,11 @@ struct evmc_message */ int64_t gas; + /** + * The amount of state gas available (EIP-8037). + */ + int64_t state_gas; + /** * The recipient of the message. * @@ -392,6 +397,16 @@ struct evmc_result; */ typedef void (*evmc_release_result_fn)(const struct evmc_result* result); +/** The state-gas counters of an execution (EIP-8037). */ +struct evmc_state_gas +{ + /** The amount of state gas left. */ + int64_t left = 0; + + /** The portion of the consumed state gas taken from evmc_result::gas_left. */ + int64_t spilled = 0; +}; + /** The EVM code execution result. */ struct evmc_result { @@ -414,6 +429,15 @@ struct evmc_result */ int64_t gas_refund; + /** + * The state-gas counters after execution (EIP-8037). + * + * If evmc_result::status_code is other than ::EVMC_SUCCESS, evmc_state_gas::left MUST equal + * ::evmc_message::state_gas and evmc_state_gas::spilled MUST be 0. The VM returns the spill + * to evmc_result::gas_left for ::EVMC_REVERT; any other failure consumes it with gas_left. + */ + struct evmc_state_gas state_gas; + /** * The reference to output data. * diff --git a/evmc/include/evmc/evmc.hpp b/evmc/include/evmc/evmc.hpp index 6eeb912e3a..08da50a7ee 100644 --- a/evmc/include/evmc/evmc.hpp +++ b/evmc/include/evmc/evmc.hpp @@ -321,6 +321,9 @@ inline const char* to_string(evmc_revision rev) noexcept /// Alias for evmc_make_result(). constexpr auto make_result = evmc_make_result; +/// @copydoc evmc_state_gas +using StateGas = evmc_state_gas; + /// @copydoc evmc_result /// /// This is a RAII wrapper for evmc_result and objects of this type @@ -332,6 +335,7 @@ class Result : private evmc_result using evmc_result::gas_refund; using evmc_result::output_data; using evmc_result::output_size; + using evmc_result::state_gas; using evmc_result::status_code; /// Creates the result from the provided arguments. @@ -344,23 +348,39 @@ class Result : private evmc_result /// @param _gas_refund The amount of refunded gas. /// @param _output_data The pointer to the output. /// @param _output_size The output size. + /// @param _state_gas The state-gas fields. explicit Result(evmc_status_code _status_code, int64_t _gas_left, int64_t _gas_refund, const uint8_t* _output_data, - size_t _output_size) noexcept + size_t _output_size, + StateGas _state_gas = {}) noexcept : evmc_result{make_result(_status_code, _gas_left, _gas_refund, _output_data, _output_size)} - {} + { + state_gas = _state_gas; + } /// Creates the result without output. /// /// @param _status_code The status code. /// @param _gas_left The amount of gas left. /// @param _gas_refund The amount of refunded gas. + /// @param _state_gas The state-gas fields. explicit Result(evmc_status_code _status_code = EVMC_INTERNAL_ERROR, int64_t _gas_left = 0, - int64_t _gas_refund = 0) noexcept + int64_t _gas_refund = 0, + StateGas _state_gas = {}) noexcept : evmc_result{make_result(_status_code, _gas_left, _gas_refund, nullptr, 0)} + { + state_gas = _state_gas; + } + + /// Creates the result without output and without gas left. + /// + /// @param _status_code The status code. + /// @param _state_gas The state-gas fields. + explicit Result(evmc_status_code _status_code, StateGas _state_gas) noexcept + : Result{_status_code, 0, 0, _state_gas} {} /// Converting constructor from raw evmc_result. diff --git a/evmc/include/evmc/helpers.h b/evmc/include/evmc/helpers.h index ce9d2e08f8..fe46846f4f 100644 --- a/evmc/include/evmc/helpers.h +++ b/evmc/include/evmc/helpers.h @@ -120,8 +120,7 @@ static inline struct evmc_result evmc_make_result(enum evmc_status_code status_c const uint8_t* output_data, size_t output_size) { - struct evmc_result result; - memset(&result, 0, sizeof(result)); + struct evmc_result result = {}; if (output_size != 0) { diff --git a/lib/evmone/constants.hpp b/lib/evmone/constants.hpp index 6bbb9f9a6f..5b1feb540e 100644 --- a/lib/evmone/constants.hpp +++ b/lib/evmone/constants.hpp @@ -27,4 +27,13 @@ constexpr auto MAX_NONCE = 0xffff'ffff'ffff'ffff; /// The gas given back to a value-transferring CALL, the Yellow Paper's G_callstipend. constexpr auto CALL_STIPEND = 2300; + +/// The fixed cost per state byte (EIP-8037). +constexpr auto COST_PER_STATE_BYTE = 1530; + +/// State-gas cost of creating a new account (EIP-8037). +constexpr auto NEW_ACCOUNT_STATE_GAS = 120 * COST_PER_STATE_BYTE; + +/// State-gas cost of allocating a storage slot (EIP-8037). +constexpr auto STORAGE_SET_STATE_GAS = 64 * COST_PER_STATE_BYTE; } // namespace evmone diff --git a/lib/evmone/execution_state.hpp b/lib/evmone/execution_state.hpp index 9511612a7c..d9c35eb188 100644 --- a/lib/evmone/execution_state.hpp +++ b/lib/evmone/execution_state.hpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include "state_gas.hpp" #include #include #include @@ -154,6 +155,9 @@ class ExecutionState const advanced::AdvancedCodeAnalysis* advanced; } analysis{}; + /// The frame's state-gas counters (EIP-8037). + StateGas state_gas; + /// Stack space allocation. /// /// This is the last field to make other fields' offsets of reasonable values. @@ -164,7 +168,11 @@ class ExecutionState ExecutionState(const evmc_message& message, evmc_revision revision, const evmc_host_interface& host_interface, evmc_host_context* host_ctx, bytes_view _code) noexcept - : msg{&message}, host{host_interface, host_ctx}, rev{revision}, original_code{_code} + : msg{&message}, + host{host_interface, host_ctx}, + rev{revision}, + original_code{_code}, + state_gas{{.left = message.state_gas}} {} /// Resets the contents of the ExecutionState so that it could be reused. @@ -173,6 +181,7 @@ class ExecutionState bytes_view _code) noexcept { gas_refund = 0; + state_gas = {{.left = message.state_gas}}; memory.clear(); msg = &message; host = {host_interface, host_ctx}; @@ -202,13 +211,23 @@ class ExecutionState /// success, and the output is the memory range recorded in the state. inline evmc_result make_execution_result(ExecutionState& state, int64_t gas_left) noexcept { + if (state.rev >= EVMC_AMSTERDAM && state.status != EVMC_SUCCESS) + { + // Unsuccessful frame doesn't commit any state changes, roll-back all state-gas costs. + gas_left += state.state_gas.spilled; + state.state_gas.left = state.msg->state_gas; + state.state_gas.spilled = 0; + } + // An exceptional halt consumes all gas; only a success or revert keeps gas_left. if (state.status != EVMC_SUCCESS && state.status != EVMC_REVERT) gas_left = 0; const auto gas_refund = (state.status == EVMC_SUCCESS) ? state.gas_refund : 0; assert(state.output_size != 0 || state.output_offset == 0); - return evmc::make_result(state.status, gas_left, gas_refund, - state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size); + return evmc::Result{state.status, gas_left, gas_refund, + state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size, + state.state_gas} + .release_raw(); } } // namespace evmone diff --git a/lib/evmone/instructions.hpp b/lib/evmone/instructions.hpp index 76c6a63216..2c38e5a9dc 100644 --- a/lib/evmone/instructions.hpp +++ b/lib/evmone/instructions.hpp @@ -4,6 +4,7 @@ #pragma once #include "baseline.hpp" +#include "constants.hpp" #include "execution_state.hpp" #include "instructions_traits.hpp" #include "instructions_xmacro.hpp" @@ -1081,8 +1082,16 @@ inline TermResult selfdestruct(StackTop stack, int64_t gas_left, ExecutionState& // sending value to a non-existing account. if (!state.host.account_exists(beneficiary)) { - if ((gas_left -= 25000) < 0) - return {EVMC_OUT_OF_GAS, gas_left}; + if (state.rev >= EVMC_AMSTERDAM) + { + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + } + else + { + if ((gas_left -= 25000) < 0) + return {EVMC_OUT_OF_GAS, gas_left}; + } } } } diff --git a/lib/evmone/instructions_calls.cpp b/lib/evmone/instructions_calls.cpp index 4ec0e33b27..4fa31129cc 100644 --- a/lib/evmone/instructions_calls.cpp +++ b/lib/evmone/instructions_calls.cpp @@ -39,6 +39,30 @@ inline std::variant get_target_address( return *delegate_addr; } + +/// Absorbs a child's state-gas back to the parent (EIP-8037). +inline void absorb_child_state_gas( + int64_t& gas_left, ExecutionState& state, const evmc::Result& result) noexcept +{ + assert(result.state_gas.left >= 0); + assert(result.state_gas.spilled >= 0); + + // At most one of the two pools is ever non-empty. + assert(state.state_gas.left == 0 || state.state_gas.spilled == 0); + assert(result.state_gas.left == 0 || result.state_gas.spilled == 0); + + // In a non-successful result, all is returned back. + assert(result.status_code == EVMC_SUCCESS || + (result.state_gas.left == state.state_gas.left && result.state_gas.spilled == 0)); + + // Accumulate the spilled state-gas. + state.state_gas.spilled += result.state_gas.spilled; + + // Rebalance the state-gas refills: the caller must move callee's refills to gas_left up to the + // caller's spilled counter. Do this by refilling all returned state-gas to zeroed `left`. + state.state_gas.left = 0; + state.state_gas.refill(gas_left, result.state_gas.left); +} } // namespace /// Converts an opcode to matching EVMC call kind. @@ -119,11 +143,18 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce const auto& code_addr = std::get(target_addr_or_result); + bool new_account_charged = false; // NOLINT(*-const-correctness) if constexpr (Op == OP_CALL) { if ((has_value || state.rev < EVMC_SPURIOUS_DRAGON) && !state.host.account_exists(dst)) { - if ((gas_left -= ACCOUNT_CREATION_COST) < 0) + if (state.rev >= EVMC_AMSTERDAM) + { + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + new_account_charged = true; + } + else if ((gas_left -= ACCOUNT_CREATION_COST) < 0) return {EVMC_OUT_OF_GAS, gas_left}; } } @@ -135,6 +166,7 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce else msg.flags &= ~std::underlying_type_t{EVMC_DELEGATED}; msg.depth = state.msg->depth + 1; + msg.state_gas = state.state_gas.left; msg.recipient = (Op == OP_CALL || Op == OP_STATICCALL) ? dst : state.msg->recipient; msg.code_address = code_addr; msg.sender = (Op == OP_DELEGATECALL) ? state.msg->sender : state.msg->recipient; @@ -171,7 +203,11 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce msg.gas += CALL_STIPEND; gas_left += CALL_STIPEND; if (intx::be::load(state.host.get_balance(state.msg->recipient)) < value) + { + if (new_account_charged) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); return {EVMC_SUCCESS, gas_left}; // "Light" failure. + } } } @@ -188,6 +224,14 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce const auto gas_used = msg.gas - result.gas_left; gas_left -= gas_used; state.gas_refund += result.gas_refund; + absorb_child_state_gas(gas_left, state, result); + + if constexpr (Op == OP_CALL) + { + if (new_account_charged && result.status_code != EVMC_SUCCESS) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); + } + return {EVMC_SUCCESS, gas_left}; } @@ -257,10 +301,19 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex if (state.rev >= EVMC_BERLIN) state.host.access_account(msg.recipient); + bool new_account_charged = false; + if (state.rev >= EVMC_AMSTERDAM && !state.host.account_exists(msg.recipient)) + { + if (!state.state_gas.charge(gas_left, NEW_ACCOUNT_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + new_account_charged = true; + } + msg.gas = gas_left; if (state.rev >= EVMC_TANGERINE_WHISTLE) msg.gas -= msg.gas / 64; + msg.state_gas = state.state_gas.left; msg.input_data = init_code.data(); msg.input_size = init_code.size(); msg.sender = sender; @@ -270,6 +323,9 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex const auto result = state.host.call(msg); gas_left -= msg.gas - result.gas_left; state.gas_refund += result.gas_refund; + absorb_child_state_gas(gas_left, state, result); + if (new_account_charged && result.status_code != EVMC_SUCCESS) + state.state_gas.refill(gas_left, NEW_ACCOUNT_STATE_GAS); state.return_data.assign(result.output_data, result.output_size); if (result.status_code == EVMC_SUCCESS) diff --git a/lib/evmone/instructions_storage.cpp b/lib/evmone/instructions_storage.cpp index 58d91dea73..93375ca6a7 100644 --- a/lib/evmone/instructions_storage.cpp +++ b/lib/evmone/instructions_storage.cpp @@ -42,7 +42,8 @@ constexpr auto storage_cost_spec = []() noexcept { tbl[EVMC_PRAGUE] = tbl[EVMC_LONDON]; tbl[EVMC_OSAKA] = tbl[EVMC_LONDON]; tbl[EVMC_AMSTERDAM] = tbl[EVMC_LONDON]; - tbl[EVMC_EXPERIMENTAL] = tbl[EVMC_LONDON]; + tbl[EVMC_AMSTERDAM].set = tbl[EVMC_AMSTERDAM].reset; // Only execution cost (EIP-8037). + tbl[EVMC_EXPERIMENTAL] = tbl[EVMC_AMSTERDAM]; return tbl; }(); @@ -138,6 +139,18 @@ Result sstore(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept const auto gas_cost = gas_cost_warm + gas_cost_cold; if ((gas_left -= gas_cost) < 0) return {EVMC_OUT_OF_GAS, gas_left}; + + if (state.rev >= EVMC_AMSTERDAM) + { + // The refill part can be done here because gas_left check always succeeds in this case. + static_assert(cold_sload_cost + warm_storage_read_cost <= CALL_STIPEND); + if (status == EVMC_STORAGE_ADDED_DELETED) + state.state_gas.refill(gas_left, STORAGE_SET_STATE_GAS); + else if (status == EVMC_STORAGE_ADDED && + !state.state_gas.charge(gas_left, STORAGE_SET_STATE_GAS)) + return {EVMC_OUT_OF_GAS, gas_left}; + } + state.gas_refund += gas_refund; return {EVMC_SUCCESS, gas_left}; } diff --git a/lib/evmone/state_gas.hpp b/lib/evmone/state_gas.hpp new file mode 100644 index 0000000000..2c3a0d961a --- /dev/null +++ b/lib/evmone/state_gas.hpp @@ -0,0 +1,47 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include +#include +#include + +namespace evmone +{ +/// A frame's state-gas counters with the charging rules on top (EIP-8037). +struct StateGas : evmc::StateGas +{ + /// Charges `cost`, first from `left`, then from `gas_left` (recorded in `spilled`). + [[nodiscard]] bool charge(int64_t& gas_left, int64_t cost) noexcept + { + assert(cost >= 0); // 0 charge happens in code deployment. + if (left >= cost) + { + left -= cost; + return true; + } + const auto spill = cost - left; + if (gas_left < spill) + return false; + gas_left -= spill; + spilled += spill; + left = 0; + return true; + } + + /// Refund state-gas. + /// + /// Give the `cost` to `gas_left` (up to `spilled`) and `left` (whatever remains). + void refill(int64_t& gas_left, int64_t cost) noexcept + { + assert(cost >= 0); // 0 refill happens in absorb. + const auto to_gas_left = std::min(cost, spilled); + gas_left += to_gas_left; + spilled -= to_gas_left; + left += cost - to_gas_left; + } +}; +} // namespace evmone diff --git a/test/state/host.cpp b/test/state/host.cpp index a959a852f1..b86639e43f 100644 --- a/test/state/host.cpp +++ b/test/state/host.cpp @@ -6,6 +6,7 @@ #include "precompiles.hpp" #include "system_contracts.hpp" #include +#include namespace evmone::state { @@ -190,8 +191,9 @@ evmc::Result Host::create(const evmc_message& msg) noexcept } else { + // TODO: Add EVMC errors for creation failures. if (is_create_collision(*new_acc)) - return evmc::Result{EVMC_FAILURE}; // TODO: Add EVMC errors for creation failures. + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; m_state.journal_create(msg.recipient); } @@ -229,20 +231,33 @@ evmc::Result Host::create(const evmc_message& msg) noexcept const size_t max_code_size = m_rev >= EVMC_AMSTERDAM ? MAX_CODE_SIZE_AMSTERDAM : MAX_CODE_SIZE; if (m_rev >= EVMC_SPURIOUS_DRAGON && code.size() > max_code_size) - return evmc::Result{EVMC_FAILURE}; + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; // Reject new contract code starting with the 0xEF byte (EIP-3541). if (m_rev >= EVMC_LONDON && code.starts_with(0xEF)) - return evmc::Result{EVMC_CONTRACT_VALIDATION_FAILURE}; + return evmc::Result{EVMC_CONTRACT_VALIDATION_FAILURE, {.left = msg.state_gas}}; - // Code deployment cost. - const auto cost = std::ssize(code) * 200; - gas_left -= cost; - if (gas_left < 0) + StateGas state_gas{result.state_gas}; // The initcode's state-gas for code deposit. + if (m_rev >= EVMC_AMSTERDAM) { - return (m_rev == EVMC_FRONTIER) ? - evmc::Result{EVMC_SUCCESS, result.gas_left, result.gas_refund} : - evmc::Result{EVMC_FAILURE}; + // The code deposit splits into an execution-gas and a state-gas component (EIP-8037). + const auto execution_cost = 6 * ((std::ssize(code) + 31) / 32); + const auto state_cost = std::ssize(code) * COST_PER_STATE_BYTE; + gas_left -= execution_cost; + if (gas_left < 0 || !state_gas.charge(gas_left, state_cost)) + return evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; + } + else + { + // Code deployment cost. + const auto cost = std::ssize(code) * 200; + gas_left -= cost; + if (gas_left < 0) + { + return (m_rev == EVMC_FRONTIER) ? + evmc::Result{EVMC_SUCCESS, result.gas_left, result.gas_refund} : + evmc::Result{EVMC_FAILURE, {.left = msg.state_gas}}; + } } if (!code.empty()) @@ -252,7 +267,7 @@ evmc::Result Host::create(const evmc_message& msg) noexcept new_acc->code_changed = true; } - return evmc::Result{result.status_code, gas_left, result.gas_refund}; + return evmc::Result{result.status_code, gas_left, result.gas_refund, state_gas}; } evmc::Result Host::execute_message(const evmc_message& msg) noexcept @@ -300,8 +315,8 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept // TODO: get_code() performs the account lookup. Add a way to get an account with code? const auto code = m_state.get_code(msg.code_address); - if (code.empty()) - return evmc::Result{EVMC_SUCCESS, msg.gas}; // Skip trivial execution. + if (code.empty()) // Skip trivial execution. + return evmc::Result{EVMC_SUCCESS, msg.gas, 0, {.left = msg.state_gas}}; return m_vm.execute(*this, m_rev, msg, code.data(), code.size()); } @@ -317,13 +332,34 @@ evmc::Result Host::call(const evmc_message& msg) noexcept ++sender_acc.nonce; } + auto exec_msg = msg; + StateGas state_gas{{.left = msg.state_gas}}; // State-gas for top-level account creation cost. + if (msg.depth == 0 && m_rev >= EVMC_AMSTERDAM && + (msg.kind == EVMC_CREATE || !evmc::is_zero(msg.value)) && !account_exists(msg.recipient)) + { + if (!state_gas.charge(exec_msg.gas, NEW_ACCOUNT_STATE_GAS)) + return evmc::Result{EVMC_OUT_OF_GAS, {.left = msg.state_gas}}; + exec_msg.state_gas = state_gas.left; + } + const auto logs_checkpoint = m_logs.size(); const auto state_checkpoint = m_state.checkpoint(); - auto result = execute_message(msg); + auto result = execute_message(exec_msg); - if (result.status_code != EVMC_SUCCESS) + if (result.status_code == EVMC_SUCCESS) { + result.state_gas.spilled += state_gas.spilled; // Commit the top-level new account cost. + } + else + { + // Rollback state-gas costs. + assert(result.state_gas.left == exec_msg.state_gas); + assert(result.state_gas.spilled == 0); + if (result.status_code == EVMC_REVERT) + result.gas_left += state_gas.spilled; + result.state_gas.left = msg.state_gas; + // The 0x03 (RIPEMD-160) touch quirk: a touch on this address is // never reverted. It only matters when the account is empty, so gate it by rev range. static constexpr auto ADDR_03 = 0x03_address; diff --git a/test/state/precompiles.cpp b/test/state/precompiles.cpp index d24708d42e..ea83c76c39 100644 --- a/test/state/precompiles.cpp +++ b/test/state/precompiles.cpp @@ -864,7 +864,7 @@ evmc::Result call_precompile(evmc_revision rev, const evmc_message& msg) noexcep const auto [gas_cost, max_output_size] = analyze(input, rev); const auto gas_left = msg.gas - gas_cost; if (gas_left < 0) - return evmc::Result{EVMC_OUT_OF_GAS}; + return evmc::Result{EVMC_OUT_OF_GAS, {.left = msg.state_gas}}; // Allocate buffer for the precompile's output and pass its ownership to evmc::Result. // TODO: This can be done more elegantly by providing constructor evmc::Result(std::unique_ptr). @@ -874,6 +874,7 @@ evmc::Result call_precompile(evmc_revision rev, const evmc_message& msg) noexcep return evmc::Result{{ .status_code = status_code, .gas_left = status_code == EVMC_SUCCESS ? gas_left : 0, + .state_gas = {.left = msg.state_gas}, .output_data = output_data, .output_size = output_size, .release = [](const evmc_result* res) noexcept { delete[] res->output_data; }, diff --git a/test/state/state.cpp b/test/state/state.cpp index f112d76470..6ee9278a72 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -194,7 +194,7 @@ int64_t process_authorization_list( return delegation_refund; } -evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) noexcept +evmc_message build_message(const Transaction& tx, const TransactionProperties& tx_props) noexcept { const auto recipient = tx.to.has_value() ? *tx.to : compute_create_address(tx.sender, tx.nonce); @@ -202,7 +202,8 @@ evmc_message build_message(const Transaction& tx, int64_t execution_gas_limit) n .kind = tx.to.has_value() ? EVMC_CALL : EVMC_CREATE, .flags = 0, .depth = 0, - .gas = execution_gas_limit, + .gas = tx_props.execution_gas_limit, + .state_gas = tx_props.state_gas_limit, .recipient = recipient, .sender = tx.sender, .input_data = tx.data.data(), @@ -432,11 +433,11 @@ void State::rollback(size_t checkpoint) } } -/// Validates transaction and computes its execution gas limit (the amount of gas provided to EVM). -/// @return Execution gas limit or transaction validation error. +/// Validates transaction and computes the gas limits it provides to the EVM. +/// @return The transaction's computed gas properties or a validation error. std::variant validate_transaction( const StateView& state_view, const BlockInfo& block, const Transaction& tx, evmc_revision rev, - int64_t block_gas_left, int64_t blob_gas_left) noexcept + int64_t block_gas_left, int64_t block_state_gas_left, int64_t blob_gas_left) noexcept { if (tx.chain_id_protected() && tx.chain_id != block.chain_id) return make_error_code(INVALID_CHAIN_ID); @@ -497,11 +498,22 @@ std::variant validate_transaction( assert(tx.max_priority_gas_price <= tx.max_gas_price); - if (rev >= EVMC_OSAKA && tx.gas_limit > MAX_TX_GAS_LIMIT) + if (rev == EVMC_OSAKA && tx.gas_limit > MAX_TX_GAS_LIMIT) return make_error_code(GAS_LIMIT_EXCEEDS_MAXIMUM); - if (tx.gas_limit > block_gas_left) - return make_error_code(GAS_ALLOWANCE_EXCEEDED); + if (rev < EVMC_AMSTERDAM) + { + if (tx.gas_limit > block_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + } + else + { + // Check gas limits in both dimensions. + if (std::min(tx.gas_limit, int64_t{MAX_TX_GAS_LIMIT}) > block_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + if (tx.gas_limit > block_state_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + } if (tx.max_gas_price < block.base_fee) return make_error_code(INSUFFICIENT_MAX_FEE_PER_GAS); @@ -542,11 +554,17 @@ std::variant validate_transaction( return make_error_code(INSUFFICIENT_ACCOUNT_FUNDS); const auto [intrinsic_cost, min_cost] = compute_tx_intrinsic_cost(rev, tx); - if (tx.gas_limit < std::max(intrinsic_cost, min_cost)) + + // The transaction state-gas limit is all above the cap constant (EIP-8037). + const auto state_gas_limit = + rev >= EVMC_AMSTERDAM ? std::max(tx.gas_limit - MAX_TX_GAS_LIMIT, int64_t{0}) : 0; + + // Transaction gas limit with state-gas limit excluded must cover intrinsic and min cost. + if (tx.gas_limit - state_gas_limit < std::max(intrinsic_cost, min_cost)) return make_error_code(INTRINSIC_GAS_TOO_LOW); - const auto execution_gas_limit = tx.gas_limit - intrinsic_cost; - return TransactionProperties{execution_gas_limit, min_cost}; + const auto execution_gas_limit = tx.gas_limit - intrinsic_cost - state_gas_limit; + return TransactionProperties{execution_gas_limit, state_gas_limit, min_cost}; } StateDiff finalize(const StateView& state_view, evmc_revision rev, const address& coinbase, @@ -614,7 +632,7 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc Host host{rev, vm, state, block, block_hashes, tx}; - auto message = build_message(tx, tx_props.execution_gas_limit); + auto message = build_message(tx, tx_props); sender_acc.access_status = EVMC_ACCESS_WARM; // Sender is always warm. host.access_account(message.recipient); // Recipient (incl. create address) is always warm. @@ -644,7 +662,7 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc const auto result = host.call(message); - const auto gas_used_b4_refund = tx.gas_limit - result.gas_left; + const auto gas_used_b4_refund = tx.gas_limit - result.gas_left - result.state_gas.left; const auto refund_limit = rev >= EVMC_LONDON ? gas_used_b4_refund / 5 : gas_used_b4_refund / 2; const auto refund = std::min(delegation_refund + result.gas_refund, refund_limit); @@ -654,9 +672,16 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc // The gas used by the transaction must be at least the min_gas_cost (EIP-7623). gas_used = std::max(gas_used, tx_props.min_gas_cost); - // For block gas accounting, exclude refunds and enforce the min gas cost (EIP-7778). + const auto state_gas_used = + tx_props.state_gas_limit - result.state_gas.left + result.state_gas.spilled; + assert(state_gas_used >= 0); + + // For block gas accounting, exclude refunds and enforce the min gas cost, raised by the + // state gas so state-gas spending cannot discount it (EIP-7778, EIP-8037). const auto block_gas_used = - (rev >= EVMC_AMSTERDAM) ? std::max(gas_used_b4_refund, tx_props.min_gas_cost) : gas_used; + (rev >= EVMC_AMSTERDAM) ? + std::max(gas_used_b4_refund, tx_props.min_gas_cost + state_gas_used) : + gas_used; sender_acc.balance += tx_max_cost - gas_used * effective_gas_price; state.touch(block.coinbase).balance += gas_used * priority_gas_price; @@ -667,6 +692,7 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc .status = result.status_code, .gas_used = gas_used, .block_gas_used = block_gas_used, + .state_gas_used = state_gas_used, .logs = host.take_logs(), .state_diff = state.build_diff(rev), }; diff --git a/test/state/state.hpp b/test/state/state.hpp index 7240704fcc..46f8b46ab6 100644 --- a/test/state/state.hpp +++ b/test/state/state.hpp @@ -143,8 +143,9 @@ TransactionReceipt transition(const StateView& state, const BlockInfo& block, /// Validate a transaction. /// -/// @return Computed execution gas limit or validation error. +/// @param block_state_gas_left Remaining block state-gas (EIP-8037). +/// @return The transaction's computed gas properties or a validation error. [[nodiscard]] std::variant validate_transaction( const StateView& state_view, const BlockInfo& block, const Transaction& tx, evmc_revision rev, - int64_t block_gas_left, int64_t blob_gas_left) noexcept; + int64_t block_gas_left, int64_t block_state_gas_left, int64_t blob_gas_left) noexcept; } // namespace evmone::state diff --git a/test/state/system_contracts.cpp b/test/state/system_contracts.cpp index 7a5cd431ae..819ee50450 100644 --- a/test/state/system_contracts.cpp +++ b/test/state/system_contracts.cpp @@ -6,6 +6,7 @@ #include "errors.hpp" #include "host.hpp" #include "state_view.hpp" +#include namespace evmone::state { @@ -80,6 +81,7 @@ evmc::Result execute_system_call(State& state, const BlockInfo& block, const evmc_message msg{ .kind = EVMC_CALL, .gas = 30'000'000, + .state_gas = 16 * STORAGE_SET_STATE_GAS, // Additional state-gas (EIP-8037). .recipient = addr, .sender = SYSTEM_ADDRESS, .input_data = input.data(), diff --git a/test/state/transaction.hpp b/test/state/transaction.hpp index 35c78f7fe4..157497afd0 100644 --- a/test/state/transaction.hpp +++ b/test/state/transaction.hpp @@ -104,6 +104,9 @@ struct TransactionProperties /// The amount of gas provided to the EVM for the transaction execution. int64_t execution_gas_limit = 0; + /// The amount of state-gas spendable by EVM on state increase (since EIP-8037). + int64_t state_gas_limit = 0; + /// The minimal amount of gas the transaction must use. int64_t min_gas_cost = 0; }; @@ -135,6 +138,10 @@ struct TransactionReceipt /// Amount of gas counted against the block gas limit by this transaction (EIP-7778). int64_t block_gas_used = 0; + /// The amount of state-gas used by this transaction (since EIP-8037). + /// It is the state-gas part of #block_gas_used; the rest is the execution-gas part. + int64_t state_gas_used = 0; + /// Amount of gas used by this and previous transactions in the block. int64_t cumulative_gas_used = 0; std::vector logs; diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index 3bfc1410a4..a1c9fa5e50 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -71,6 +71,7 @@ target_sources( state_transition_create_test.cpp state_transition_eip7702_test.cpp state_transition_eip7778_block_gas_test.cpp + state_transition_eip8037_state_gas_test.cpp state_transition_extcode_test.cpp state_transition_selfdestruct_test.cpp state_transition_snippets_test.cpp diff --git a/test/unittests/evmone_test.cpp b/test/unittests/evmone_test.cpp index ad2d35e58a..fcdc46eeeb 100644 --- a/test/unittests/evmone_test.cpp +++ b/test/unittests/evmone_test.cpp @@ -15,6 +15,35 @@ TEST(evmone, info) EXPECT_TRUE(vm.is_abi_compatible()); } +TEST(evmc, result_with_state_gas) +{ + const auto result = evmc::Result{EVMC_SUCCESS, 1, 2, {.left = 3, .spilled = 4}}; + EXPECT_EQ(result.status_code, EVMC_SUCCESS); + EXPECT_EQ(result.gas_left, 1); + EXPECT_EQ(result.gas_refund, 2); + EXPECT_EQ(result.state_gas.left, 3); + EXPECT_EQ(result.state_gas.spilled, 4); + EXPECT_EQ(result.output_data, nullptr); + EXPECT_EQ(result.output_size, 0); + + const auto default_result = evmc::Result{}; + EXPECT_EQ(default_result.state_gas.left, 0); + EXPECT_EQ(default_result.state_gas.spilled, 0); + + const uint8_t output[] = {0x01}; + const auto output_result = + evmc::Result{EVMC_REVERT, 1, 0, output, std::size(output), {.left = 5, .spilled = 6}}; + EXPECT_EQ(output_result.state_gas.left, 5); + EXPECT_EQ(output_result.state_gas.spilled, 6); + + const auto failure_result = evmc::Result{EVMC_OUT_OF_GAS, {.left = 7}}; + EXPECT_EQ(failure_result.status_code, EVMC_OUT_OF_GAS); + EXPECT_EQ(failure_result.gas_left, 0); + EXPECT_EQ(failure_result.gas_refund, 0); + EXPECT_EQ(failure_result.state_gas.left, 7); + EXPECT_EQ(failure_result.state_gas.spilled, 0); +} + TEST(evmone, set_option_invalid) { auto vm = evmc_create_evmone(); diff --git a/test/unittests/state_transition.cpp b/test/unittests/state_transition.cpp index 136b200e1a..51caa3419a 100644 --- a/test/unittests/state_transition.cpp +++ b/test/unittests/state_transition.cpp @@ -61,7 +61,8 @@ void state_transition::TearDown() // After EVMC_PRAGUE, get_blob_params will not work like that without a blob schedule. // TODO: add a blob schedule to use with state_transition tests, should they be added. const auto res = test::transition(state, block, block_hashes, tx, rev, selected_vm, - block.gas_limit, static_cast(state::max_blob_gas_per_block(get_blob_params(rev)))); + block.gas_limit, block.gas_limit, + static_cast(state::max_blob_gas_per_block(get_blob_params(rev)))); test::finalize(state, rev, block.coinbase, block_reward, block.ommers, block.withdrawals); const auto& post = state; @@ -101,6 +102,10 @@ void state_transition::TearDown() << "log " << i << " topics"; } } + if (expect.state_gas.has_value()) + { + EXPECT_EQ(receipt.state_gas_used, *expect.state_gas); + } const auto& diff = receipt.state_diff; for (const auto& [addr, expected_acc] : expect.post) diff --git a/test/unittests/state_transition.hpp b/test/unittests/state_transition.hpp index 99e7b7f51d..8193114e50 100644 --- a/test/unittests/state_transition.hpp +++ b/test/unittests/state_transition.hpp @@ -74,6 +74,9 @@ class state_transition : public ExportableFixture /// exactly: count, address, data, topics, and order. std::optional> logs; + /// The expected state-gas component of the receipt (EIP-8037). + std::optional state_gas; + /// The expected post-execution state. std::unordered_map post; diff --git a/test/unittests/state_transition_create_test.cpp b/test/unittests/state_transition_create_test.cpp index e004e5ad9e..f587f18049 100644 --- a/test/unittests/state_transition_create_test.cpp +++ b/test/unittests/state_transition_create_test.cpp @@ -431,7 +431,7 @@ TEST_F(state_transition, eip7954_create_tx_at_max_code_size) // A create transaction deploying code of exactly the new limit succeeds. rev = EVMC_AMSTERDAM; static constexpr auto code_size = 0x10000; // MAX_CODE_SIZE_AMSTERDAM. - tx.gas_limit = 16'000'000; // Covers the ~13.1M code-deposit gas (200/byte). + tx.gas_limit = 110'000'000; // Covers the ~100M code-deposit state gas (EIP-8037). block.gas_limit = tx.gas_limit; pre[Sender].balance = tx.gas_limit * tx.max_gas_price; tx.data = ret(0, code_size); // Init code returns `code_size` zero bytes as the deployed code. @@ -445,7 +445,7 @@ TEST_F(state_transition, eip7954_create_tx_above_max_code_size) // Code one byte above the new 0x10000 limit is still rejected on Amsterdam (EIP-7954). rev = EVMC_AMSTERDAM; static constexpr auto code_size = 0x10000 + 1; - tx.gas_limit = 16'000'000; // Enough to deposit the code, so only the limit can reject it. + tx.gas_limit = 110'000'000; // Enough to deposit the code, so only the limit can reject it. block.gas_limit = tx.gas_limit; pre[Sender].balance = tx.gas_limit * tx.max_gas_price; tx.data = ret(0, code_size); // Init code returns code one byte over the limit. diff --git a/test/unittests/state_transition_eip8037_state_gas_test.cpp b/test/unittests/state_transition_eip8037_state_gas_test.cpp new file mode 100644 index 0000000000..6c525dfdfe --- /dev/null +++ b/test/unittests/state_transition_eip8037_state_gas_test.cpp @@ -0,0 +1,389 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "state_transition.hpp" +#include +#include +#include + +using namespace evmc::literals; +using namespace evmone::test; + +TEST_F(state_transition, eip8037_create_tx_collision_excess_reservoir_refunded) +{ + // A create transaction colliding with an existing account (EIP-7610) returns its state-gas + // reservoir instead of forfeiting it, so the sender is billed at most MAX_TX_GAS_LIMIT. + rev = EVMC_AMSTERDAM; + + constexpr int64_t TX_GAS_LIMIT = 18'000'000; + static_assert(TX_GAS_LIMIT > MAX_TX_GAS_LIMIT); // The excess forms the reservoir. + + block.gas_limit = TX_GAS_LIMIT * 2; + tx.gas_limit = TX_GAS_LIMIT; // tx.to stays nullopt: a create transaction. + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + pre[create_address] = {.nonce = 1, .code = bytecode{OP_STOP}}; + + // The colliding account is alive, so the preparation charge does not apply. + expect.status = EVMC_FAILURE; + expect.gas_used = MAX_TX_GAS_LIMIT; + expect.block_gas_used = expect.gas_used; // No EVM gas refund. + expect.state_gas = 0; + expect.post[create_address] = {.nonce = 1, .code = bytecode{OP_STOP}}; +} + +TEST_F(state_transition, eip8037_create_tx_revert_refunds_spilled_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.data = revert(0, 0); + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + expect.status = EVMC_REVERT; + // Intrinsic create and initcode costs plus two PUSH1 instructions. NEW_ACCOUNT is refunded. + expect.gas_used = 21'000 + 32'000 + 56 + 2 + 2 * instr::gas_costs[EVMC_AMSTERDAM][OP_PUSH1]; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = 0; + expect.post[create_address].exists = false; +} + +TEST_F(state_transition, eip8037_create_tx_halt_returns_excess_reservoir) +{ + rev = EVMC_AMSTERDAM; + + constexpr int64_t TX_GAS_LIMIT = 18'000'000; + static_assert(TX_GAS_LIMIT > MAX_TX_GAS_LIMIT); + + block.gas_limit = TX_GAS_LIMIT * 2; + tx.gas_limit = TX_GAS_LIMIT; + tx.data = bytecode{OP_INVALID}; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + expect.status = EVMC_INVALID_INSTRUCTION; + // The halt consumes the execution-gas dimension, but the unused reservoir is returned. + expect.gas_used = MAX_TX_GAS_LIMIT; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = 0; + expect.post[create_address].exists = false; +} + +TEST_F(state_transition, eip8037_create_tx_charges_new_account_and_code_deposit) +{ + rev = EVMC_AMSTERDAM; + tx.data = ret(0, 1); // Deploy a single zero byte. + + expect.state_gas = NEW_ACCOUNT_STATE_GAS + COST_PER_STATE_BYTE; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].code = bytes{0x00}; +} + +TEST_F(state_transition, eip8037_create_tx_with_value_pays_new_account_once) +{ + rev = EVMC_AMSTERDAM; + tx.value = 1; + + expect.gas_used = 53'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; + expect.post[compute_create_address(Sender, pre[Sender].nonce)] = {.nonce = 1, .balance = 1}; +} + +TEST_F(state_transition, eip8037_create_tx_uses_reservoir_then_execution_gas) +{ + rev = EVMC_AMSTERDAM; + tx.gas_limit = MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + expect.gas_used = 53'000 + NEW_ACCOUNT_STATE_GAS; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = NEW_ACCOUNT_STATE_GAS; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].nonce = 1; +} + +TEST_F(state_transition, eip8037_create_tx_to_prefunded_account_has_no_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + + const auto create_address = compute_create_address(Sender, pre[Sender].nonce); + pre[create_address].balance = 1; + + expect.gas_used = 53'000; + expect.state_gas = 0; + expect.post[create_address] = {.nonce = 1, .balance = 1}; +} + +TEST_F(state_transition, eip8037_create_tx_out_of_gas_on_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.gas_limit = 60'000; // Intrinsic gas leaves less than NEW_ACCOUNT_STATE_GAS. + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[compute_create_address(Sender, pre[Sender].nonce)].exists = false; +} + +TEST_F(state_transition, eip8037_nested_create_revert_refills_new_account_charge) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; + pre[To] = {.code = mstore(0, push(revert(0, 0))) + create().input(27, 5) + OP_STOP}; + + expect.state_gas = 0; + expect.post[To].nonce = 1; +} + +namespace +{ +constexpr int64_t CALL_VALUE_COST = 9000; // Not exported by the interpreter. + +/// The intrinsic plus the CALL's execution gas: its seven arguments, the warm call, the +/// cold-account surcharge and the value transfer, less the stipend a light failure never spends. +/// The NEW_ACCOUNT state charge is refilled, so it does not appear here. +constexpr int64_t CALL_LIGHTFAIL_EXECUTION_GAS = + 21'000 + 7 * instr::gas_costs[EVMC_AMSTERDAM][OP_PUSH1] + + instr::gas_costs[EVMC_AMSTERDAM][OP_CALL] + instr::additional_cold_account_access_cost + + CALL_VALUE_COST - CALL_STIPEND; +} // namespace + +TEST_F(state_transition, eip8037_call_value_lightfail_new_account_charge_refilled) +{ + // A value-CALL charges NEW_ACCOUNT for an absent target before the sender-balance check. + // The light failure creates no account, so the charge is refilled and the net state-gas is + // zero — matching the baseline below, which differs only in the target existing. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto TARGET = 0xbeef_address; // Absent from `pre`. + + pre[To] = {.code = call(TARGET).value(1).gas(0xffff) + OP_STOP}; // To cannot pay the value. + + expect.status = EVMC_SUCCESS; // To STOPs after the light failure. + expect.post[To].exists = true; + expect.post[TARGET].exists = false; + expect.gas_used = CALL_LIGHTFAIL_EXECUTION_GAS; + expect.state_gas = 0; +} + +TEST_F(state_transition, eip8037_call_value_lightfail_existing_account_baseline) +{ + // The baseline for the case above: an existing target is never charged, so both the execution + // gas and the state-gas must come out identical. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto TARGET = 0xbeef_address; + + pre[To] = {.code = call(TARGET).value(1).gas(0xffff) + OP_STOP}; + pre[TARGET] = {.nonce = 1, .code = bytecode{OP_STOP}}; + + expect.status = EVMC_SUCCESS; + expect.post[To].exists = true; + expect.post[TARGET] = {.nonce = 1}; + expect.gas_used = CALL_LIGHTFAIL_EXECUTION_GAS; + expect.state_gas = 0; +} + +TEST_F(state_transition, eip8037_value_to_zero_balance_precompile_pays_new_account) +{ + // Funding a zero-balance precompile materializes a state account, so it pays NEW_ACCOUNT + // (EIP-161). The reservoir is empty below the cap, so the charge spills into execution gas + // and the precompile runs on what is left. + rev = EVMC_AMSTERDAM; + tx.to = 0x04_address; // Identity, absent from `pre`. + tx.value = 1; + + constexpr int64_t IDENTITY_BASE_COST = 15; + + expect.status = EVMC_SUCCESS; + expect.post[*tx.to].balance = 1; + expect.gas_used = 21'000 + IDENTITY_BASE_COST + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_pays_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS; // Exact successful boundary. + + expect.post[To].balance = 1; + expect.gas_used = tx.gas_limit; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_existing_empty_account_pays_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; + tx.value = 1; + pre[To] = {}; + + expect.post[To].balance = 1; + expect.gas_used = 21'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_uses_reservoir_then_execution_gas) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + expect.post[To].balance = 1; + expect.gas_used = 21'000 + NEW_ACCOUNT_STATE_GAS; + expect.state_gas = NEW_ACCOUNT_STATE_GAS; +} + +TEST_F(state_transition, eip8037_value_to_new_account_out_of_gas) +{ + rev = EVMC_AMSTERDAM; + tx.to = To; // Absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS - 1; + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[To].exists = false; +} + +TEST_F(state_transition, eip8037_value_to_new_precompile_failure_refunds_new_account) +{ + rev = EVMC_AMSTERDAM; + tx.to = 0x04_address; // Identity, absent from pre. + tx.value = 1; + tx.gas_limit = 21'000 + NEW_ACCOUNT_STATE_GAS + 14; // Identity requires 15 gas. + + expect.status = EVMC_OUT_OF_GAS; + expect.gas_used = tx.gas_limit; + expect.state_gas = 0; + expect.post[*tx.to].exists = false; +} + +TEST_F(state_transition, eip8037_sstore_slot_allocated_and_cleared_in_one_tx) +{ + // Allocating a slot and clearing it in the same transaction (0 -> 1 -> 0) refills the + // allocation charge, leaving the net state-gas at zero. + rev = EVMC_AMSTERDAM; + tx.to = To; + pre[To] = {.code = sstore(1, 1) + sstore(1, 0)}; + + // Intrinsic, four PUSHes, the cold allocation, the warm clear, less the clear's refund. + expect.gas_used = 21'000 + 12 + 5000 + 100 - 2800; + expect.block_gas_used = *expect.gas_used + 2800; + expect.state_gas = 0; + expect.post[To].exists = true; +} + +TEST_F(state_transition, eip8037_sstore_slot_cleared_in_a_child_frame) +{ + // A slot allocated in one frame and cleared in a deeper one refills more state-gas than the + // child was given, so the child returns a bigger reservoir than it received. The credit must + // reach the `gas_left` that funded the spilled allocation charge. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto CLEARER = 0xdead_address; + pre[CLEARER] = {.code = sstore(1, 0)}; + pre[To] = {.code = sstore(1, 1) + delegatecall(CLEARER).gas(0xffff) + OP_STOP}; + + // Intrinsic, ten PUSHes, the cold allocation, the cold DELEGATECALL, the warm clear, + // less the clear's refund. + expect.gas_used = 21'000 + 30 + 5000 + 2600 + 100 - 2800; + expect.block_gas_used = *expect.gas_used + 2800; + expect.state_gas = 0; + expect.post[To].exists = true; + expect.post[CLEARER].exists = true; +} + +TEST_F(state_transition, eip8037_reverted_child_keeps_the_slot_allocation_charged) +{ + // A child clearing a slot its caller allocated refills more state-gas than it was given, + // leaving its reservoir above its own budget. Reverting must restore that budget rather than + // credit the refill, so the allocation stays charged. + rev = EVMC_AMSTERDAM; + tx.to = To; + constexpr auto CLEARER = 0xdead_address; + pre[CLEARER] = {.code = sstore(1, 0) + revert(0, 0)}; + pre[To] = {.code = sstore(1, 1) + delegatecall(CLEARER).gas(0xffff) + OP_STOP}; + + // Intrinsic, twelve PUSHes, the cold allocation and its state charge, the cold DELEGATECALL, + // the reverted warm clear. The clear's refund dies with the frame. + expect.gas_used = 21'000 + 36 + 5000 + STORAGE_SET_STATE_GAS + 2600 + 100; + expect.block_gas_used = expect.gas_used; // No refund. + expect.state_gas = STORAGE_SET_STATE_GAS; + expect.post[To].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; // The child's clear is rolled back. + expect.post[CLEARER].exists = true; +} + +namespace +{ +/// The code deposit of a maximum-size contract, split into its two components (EIP-8037). +constexpr auto DEPOSIT_CODE_WORDS = MAX_CODE_SIZE_AMSTERDAM / 32; +constexpr auto DEPOSIT_EXECUTION = 6 * DEPOSIT_CODE_WORDS; +constexpr int64_t DEPOSIT_STATE = int64_t{MAX_CODE_SIZE_AMSTERDAM} * COST_PER_STATE_BYTE; + +/// Gas limit whose excess over the cap covers the deposit's state component outright. +constexpr auto DEPOSIT_TX_GAS = MAX_TX_GAS_LIMIT + DEPOSIT_STATE + 1'000'000; + +/// Cap leaving the initcode frame mid-window: enough for CREATE and the memory the returned code +/// needs, plus half the execution component. The CREATE price is the only term a reprice has +/// moved, so it comes from the cost table rather than being pinned. +/// DEPOSIT_MEMORY mirrors the expansion formula in check_memory(); a change there shifts the +/// window rather than failing here. +constexpr auto DEPOSIT_MEMORY = + 3 * DEPOSIT_CODE_WORDS + DEPOSIT_CODE_WORDS * DEPOSIT_CODE_WORDS / 512; +constexpr auto DEPOSIT_GAS_CAP = + instr::gas_costs[EVMC_AMSTERDAM][OP_CREATE] + DEPOSIT_MEMORY + DEPOSIT_EXECUTION / 2; + +constexpr auto DEPOSIT_CREATOR = 0xbbbb_address; + +/// Code deploying MAX_CODE_SIZE_AMSTERDAM zero bytes through a nested CREATE. +bytecode deposit_creator_code() +{ + const auto initcode = ret(0, MAX_CODE_SIZE_AMSTERDAM); + return mstore(0, push(initcode)) + create().input(32 - initcode.size(), initcode.size()); +} +} // namespace + +TEST_F(state_transition, eip8037_code_deposit_out_of_execution_gas_with_a_full_reservoir) +{ + // The code deposit splits into an execution and a state component. A reservoir covering the + // state component must not let the deposit through when the execution component is + // unaffordable. + rev = EVMC_AMSTERDAM; + tx.gas_limit = DEPOSIT_TX_GAS; + block.gas_limit = tx.gas_limit; + tx.to = To; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + 1; + pre[DEPOSIT_CREATOR] = {.code = deposit_creator_code()}; + pre[To] = {.code = call(DEPOSIT_CREATOR).gas(DEPOSIT_GAS_CAP) + OP_STOP}; + + expect.state_gas = 0; // The refused deposit charges none, and the CREATE's is refilled. + expect.post[To].exists = true; + expect.post[DEPOSIT_CREATOR].nonce = pre[DEPOSIT_CREATOR].nonce + 1; // Bumped by the CREATE. + expect.post[compute_create_address(DEPOSIT_CREATOR, pre[DEPOSIT_CREATOR].nonce)].exists = false; +} + +TEST_F(state_transition, eip8037_code_deposit_execution_gas_boundary) +{ + // The same deposit one execution component richer succeeds, pinning the case above to the + // execution gas rather than to anything else the CREATE pays for. + rev = EVMC_AMSTERDAM; + tx.gas_limit = DEPOSIT_TX_GAS; + block.gas_limit = tx.gas_limit; + tx.to = To; + pre[Sender].balance = uint256{tx.gas_limit} * tx.max_gas_price + 1; + pre[DEPOSIT_CREATOR] = {.code = deposit_creator_code()}; + pre[To] = {.code = call(DEPOSIT_CREATOR).gas(DEPOSIT_GAS_CAP + DEPOSIT_EXECUTION) + OP_STOP}; + + expect.state_gas = DEPOSIT_STATE + NEW_ACCOUNT_STATE_GAS; // Deposit plus the new account. + expect.post[To].exists = true; + expect.post[DEPOSIT_CREATOR].nonce = pre[DEPOSIT_CREATOR].nonce + 1; + expect.post[compute_create_address(DEPOSIT_CREATOR, pre[DEPOSIT_CREATOR].nonce)].code = + bytes(MAX_CODE_SIZE_AMSTERDAM, 0x00); +} diff --git a/test/unittests/state_tx_test.cpp b/test/unittests/state_tx_test.cpp index 1db2b90934..72e0c1630a 100644 --- a/test/unittests/state_tx_test.cpp +++ b/test/unittests/state_tx_test.cpp @@ -26,17 +26,17 @@ TEST(state_tx, validate_nonce) const TestState state{{tx.sender, {.nonce = 1, .balance = 1'000'000}}}; ASSERT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0))); + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0))); tx.nonce = 0; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0)) .message(), "TransactionException.NONCE_MISMATCH_TOO_LOW"); tx.nonce = 2; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0)) .message(), "TransactionException.NONCE_MISMATCH_TOO_HIGH"); } @@ -54,19 +54,19 @@ TEST(state_tx, validate_sender) const TestState state{{tx.sender, {}}}; ASSERT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0))); + validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0, 0))); block.base_fee = 1; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0, 0)) .message(), "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS"); tx.max_gas_price = block.base_fee; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) + validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0, 0)) .message(), "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS"); } @@ -92,17 +92,17 @@ TEST(state_tx, validate_blob_tx) const auto blob_gas_limit = static_cast(max_blob_gas_per_block(get_blob_params(EVMC_CANCUN))); EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_SHANGHAI, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_SHANGHAI, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::TYPE_NOT_SUPPORTED)); EXPECT_EQ(std::get(validate_transaction(state, block, tx, EVMC_CANCUN, - block.gas_limit, blob_gas_limit)) + block.gas_limit, 0, blob_gas_limit)) .message(), make_error_code(ErrorCode::CREATE_BLOB_TX).message()); tx.to = 0x01_address; EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::EMPTY_BLOB_HASHES_LIST)); for (uint8_t i = 0; i < 6; ++i) @@ -114,7 +114,7 @@ TEST(state_tx, validate_blob_tx) const auto expect_error = [&](int64_t g) { return std::get( - validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, g)); + validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, 0, g)); }; EXPECT_EQ(expect_error(blob_gas_limit), @@ -130,7 +130,7 @@ TEST(state_tx, validate_blob_tx) expect_error(blob_gas_limit - 1), make_error_code(ErrorCode::BLOB_GAS_LIMIT_EXCEEDED)); EXPECT_EQ(std::get(validate_transaction(state, block, tx, EVMC_CANCUN, - block.gas_limit, blob_gas_limit)) + block.gas_limit, 0, blob_gas_limit)) .execution_gas_limit, 39000); @@ -157,7 +157,8 @@ TEST(state_tx, validate_eof_create_transaction) for (int r = EVMC_CANCUN; r <= EVMC_MAX_REVISION; ++r) { const auto rev = static_cast(r); - const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0); + const auto res = + validate_transaction(state, block, tx, rev, block.gas_limit, block.gas_limit, 0); EXPECT_FALSE(holds_alternative(res)); } } @@ -179,7 +180,7 @@ TEST(state_tx, validate_tx_data_cost) const TestState state{{tx.sender, {.balance = 1'000'000}}}; const auto get_props = [&](evmc_revision rev) { - const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0); + const auto res = validate_transaction(state, block, tx, rev, block.gas_limit, 0, 0); EXPECT_TRUE(holds_alternative(res)); if (holds_alternative(res)) return get(res); @@ -233,14 +234,14 @@ TEST(state_tx, max_blob_count) // Should be valid EXPECT_FALSE(holds_alternative( - validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit))); + validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit))); // Add one more blob to exceed the limit tx.blob_hashes.emplace_back( 0x01000000000000000000000000000000000000000000000000000000000000FF_bytes32); EXPECT_EQ(std::get(validate_transaction( - state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit)), + state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit)), make_error_code(ErrorCode::BLOB_GAS_LIMIT_EXCEEDED)); } @@ -251,6 +252,6 @@ TEST(state_tx, max_gas_limit_exceeded) const TestState state; EXPECT_EQ(std::get( - validate_transaction(state, block, tx, EVMC_OSAKA, block.gas_limit, 0)), + validate_transaction(state, block, tx, EVMC_OSAKA, block.gas_limit, 0, 0)), make_error_code(ErrorCode::GAS_LIMIT_EXCEEDS_MAXIMUM)); } diff --git a/test/utils/block_transition.cpp b/test/utils/block_transition.cpp index 470876ba96..36e321597f 100644 --- a/test/utils/block_transition.cpp +++ b/test/utils/block_transition.cpp @@ -49,6 +49,7 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: std::vector receipts; int64_t block_gas_left = block.gas_limit; + int64_t block_state_gas_left = block.gas_limit; int64_t cumulative_gas_used = 0; auto blob_gas_left = blob_gas_limit; @@ -61,8 +62,8 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: if (trace_enabled) trace_guard.emplace(std::clog, opts.open_trace(i, computed_tx_hash).rdbuf()); - auto res = transition( - block_state, block, block_hashes, tx, rev, vm, block_gas_left, blob_gas_left); + auto res = transition(block_state, block, block_hashes, tx, rev, vm, block_gas_left, + block_state_gas_left, blob_gas_left); if (holds_alternative(res)) { @@ -77,8 +78,10 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: if (rev < EVMC_BYZANTIUM) receipt.post_state = state::mpt_hash(block_state); - // Block gas accounting, with EIP-7778 applied in transition(). - block_gas_left -= receipt.block_gas_used; + // Block gas accounting, with EIP-7778 applied in transition(). The execution + // dimension is the block gas less the state one (EIP-8037). + block_gas_left -= receipt.block_gas_used - receipt.state_gas_used; + block_state_gas_left -= receipt.state_gas_used; blob_gas_left -= static_cast(tx.blob_gas_used()); receipts.emplace_back(std::move(receipt)); } @@ -108,7 +111,9 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: finalize(block_state, rev, block.coinbase, opts.block_reward, block.ommers, block.withdrawals); - const auto block_gas_used = block.gas_limit - block_gas_left; + // Both counters start at block.gas_limit, so this is max(execution used, state used): + // the block's gas used is its bottleneck dimension (EIP-8037). + const auto block_gas_used = block.gas_limit - std::min(block_gas_left, block_state_gas_left); const auto bloom = compute_bloom_filter(receipts); return {std::move(receipts), std::move(rejected_txs), std::move(requests), requests_error, diff --git a/test/utils/error_matching.cpp b/test/utils/error_matching.cpp index b239ad0fa3..f710690000 100644 --- a/test/utils/error_matching.cpp +++ b/test/utils/error_matching.cpp @@ -43,6 +43,7 @@ constexpr AlternativeExceptions ALTERNATIVE_TX_EXCEPTIONS[]{ // decode_transaction() reports one code for every malformed encoding, so this accepts more // than the v rule; narrowing it needs the decoder to report the v domain separately. {state::INVALID_ENCODING, "TransactionException.INVALID_SIGNATURE_VRS"}, + }; /// The same, for the rules evmone checks on the block rather than the transaction. diff --git a/test/utils/statetest_runner.cpp b/test/utils/statetest_runner.cpp index ce1a0a5d36..019f059faf 100644 --- a/test/utils/statetest_runner.cpp +++ b/test/utils/statetest_runner.cpp @@ -61,6 +61,7 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, const StateTe const auto res = error ? error : transition(state, block, test.block_hashes, *tx, rev, vm, block.gas_limit, + block.gas_limit, static_cast(state::max_blob_gas_per_block(blob_params))); if (holds_alternative(res)) diff --git a/test/utils/test_state.cpp b/test/utils/test_state.cpp index 0d3ac7a4ab..23dafea8c7 100644 --- a/test/utils/test_state.cpp +++ b/test/utils/test_state.cpp @@ -73,10 +73,10 @@ bytes32 TestBlockHashes::get_block_hash(int64_t block_number) const noexcept [[nodiscard]] std::variant transition(TestState& state, const state::BlockInfo& block, const state::BlockHashes& block_hashes, const state::Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, - int64_t blob_gas_left) + int64_t block_state_gas_left, int64_t blob_gas_left) { - const auto tx_props_or_error = - state::validate_transaction(state, block, tx, rev, block_gas_left, blob_gas_left); + const auto tx_props_or_error = state::validate_transaction( + state, block, tx, rev, block_gas_left, block_state_gas_left, blob_gas_left); if (const auto err = get_if(&tx_props_or_error)) return *err; diff --git a/test/utils/test_state.hpp b/test/utils/test_state.hpp index 54330c756c..0dba2f20ea 100644 --- a/test/utils/test_state.hpp +++ b/test/utils/test_state.hpp @@ -72,7 +72,7 @@ class TestBlockHashes : public state::BlockHashes, public std::unordered_map transition(TestState& state, const state::BlockInfo& block, const state::BlockHashes& block_hashes, const state::Transaction& tx, evmc_revision rev, evmc::VM& vm, int64_t block_gas_left, - int64_t blob_gas_left); + int64_t block_state_gas_left, int64_t blob_gas_left); /// Wrapping of state::finalize() which operates on TestState. void finalize(TestState& state, evmc_revision rev, const address& coinbase,