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,