diff --git a/evmc/include/evmc/evmc.h b/evmc/include/evmc/evmc.h index 2807838ab1..ab2c209062 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. * @@ -414,6 +419,23 @@ struct evmc_result */ int64_t gas_refund; + /** + * The amount of state gas left after execution (EIP-8037). + * + * If evmc_result::status_code is a positive failure code, this MUST equal + * ::evmc_message::state_gas supplied to the execution. + */ + int64_t state_gas_left; + + /** + * The portion of consumed state gas taken from gas_left (EIP-8037). + * + * If evmc_result::status_code is a positive failure code, this MUST be 0. + * State gas spilled during a failed execution is not committed: for ::EVMC_REVERT it MUST + * be returned to evmc_result::gas_left; other failures consume it with the rest of the gas. + */ + int64_t state_gas_spilled; + /** * The reference to output data. * diff --git a/evmc/include/evmc/evmc.hpp b/evmc/include/evmc/evmc.hpp index 6eeb912e3a..4fe5d4069f 100644 --- a/evmc/include/evmc/evmc.hpp +++ b/evmc/include/evmc/evmc.hpp @@ -328,10 +328,19 @@ constexpr auto make_result = evmc_make_result; class Result : private evmc_result { public: + /// State-gas fields of an execution result. + struct StateGas + { + int64_t left = 0; + int64_t spilled = 0; + }; + using evmc_result::gas_left; using evmc_result::gas_refund; using evmc_result::output_data; using evmc_result::output_size; + using evmc_result::state_gas_left; + using evmc_result::state_gas_spilled; using evmc_result::status_code; /// Creates the result from the provided arguments. @@ -344,6 +353,8 @@ 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. + /// + /// The state-gas fields are initialized to 0. explicit Result(evmc_status_code _status_code, int64_t _gas_left, int64_t _gas_refund, @@ -363,6 +374,22 @@ class Result : private evmc_result : evmc_result{make_result(_status_code, _gas_left, _gas_refund, nullptr, 0)} {} + /// 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, + int64_t _gas_left, + int64_t _gas_refund, + StateGas _state_gas) noexcept + : Result{_status_code, _gas_left, _gas_refund} + { + state_gas_left = _state_gas.left; + state_gas_spilled = _state_gas.spilled; + } + /// Converting constructor from raw evmc_result. /// /// This object takes ownership of the resources of @p res. 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..f586be75b9 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,11 @@ class ExecutionState const advanced::AdvancedCodeAnalysis* advanced; } analysis{}; + /// The frame's state-gas counters (EIP-8037). + /// + /// Kept in the cold tail so `status` and `host` are accessed with shorted instructions. + StateGas state_gas; + /// Stack space allocation. /// /// This is the last field to make other fields' offsets of reasonable values. @@ -164,7 +170,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 +183,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 +213,25 @@ 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, + // TODO: Simplify result creation. + auto result = evmc::make_result(state.status, gas_left, gas_refund, state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size); + result.state_gas_left = state.state_gas.left; + result.state_gas_spilled = state.state_gas.spilled; + return result; } } // 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..1bf1257af2 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,12 +143,21 @@ 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 +168,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 +205,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 +226,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 +303,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 +325,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..794411a1b3 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; }(); @@ -134,10 +135,18 @@ Result sstore(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept 0; const auto status = state.host.set_storage(state.msg->recipient, key, value); + if (state.rev >= EVMC_AMSTERDAM && status == EVMC_STORAGE_ADDED_DELETED) + state.state_gas.refill(gas_left, STORAGE_SET_STATE_GAS); + const auto [gas_cost_warm, gas_refund] = sstore_costs[state.rev][status]; 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 && 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..fd0d5c6ec4 --- /dev/null +++ b/lib/evmone/state_gas.hpp @@ -0,0 +1,51 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2026 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +namespace evmone +{ +/// A frame's state-gas as a (left, spilled) pair, independent from the execution gas (EIP-8037). +struct StateGas +{ + /// Remaining state-gas reservoir. + int64_t left = 0; + + /// Consumed state-gas taken from `gas_left` (happens when `left` is empty). + int64_t spilled = 0; + + /// 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..bc8f98edca 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 { @@ -181,6 +182,11 @@ evmc::Result Host::create(const evmc_message& msg) noexcept assert(msg.kind == EVMC_CREATE || msg.kind == EVMC_CREATE2); assert(msg.recipient != address{}); // Must be computed already. + // A failed create commits no state gas, so it returns the caller's baseline (EIP-8037). + const auto fail = [&msg](evmc_status_code status) noexcept { + return evmc::Result{status, 0, 0, {.left = msg.state_gas}}; + }; + // TODO: find()+insert() probes m_modified twice for a new recipient. auto* new_acc = m_state.find(msg.recipient); if (new_acc == nullptr) @@ -191,7 +197,7 @@ evmc::Result Host::create(const evmc_message& msg) noexcept else { if (is_create_collision(*new_acc)) - return evmc::Result{EVMC_FAILURE}; // TODO: Add EVMC errors for creation failures. + return fail(EVMC_FAILURE); // TODO: Add EVMC errors for creation failures. m_state.journal_create(msg.recipient); } @@ -217,6 +223,7 @@ evmc::Result Host::create(const evmc_message& msg) noexcept auto create_msg = msg; create_msg.input_data = nullptr; create_msg.input_size = 0; + const bytes_view initcode{msg.input_data, msg.input_size}; auto result = m_vm.execute(*this, m_rev, create_msg, initcode.data(), initcode.size()); if (result.status_code != EVMC_SUCCESS) @@ -229,20 +236,37 @@ 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 fail(EVMC_FAILURE); // 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 fail(EVMC_CONTRACT_VALIDATION_FAILURE); - // Code deployment cost. - const auto cost = std::ssize(code) * 200; - gas_left -= cost; - if (gas_left < 0) + // The initcode frame's state-gas pools, carried into the code-deposit charge. + StateGas state_gas{ + .left = result.state_gas_left, + .spilled = result.state_gas_spilled, + }; + 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 fail(EVMC_FAILURE); + } + 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} : + fail(EVMC_FAILURE); + } } if (!code.empty()) @@ -252,7 +276,8 @@ 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, + {.left = state_gas.left, .spilled = state_gas.spilled}}; } evmc::Result Host::execute_message(const evmc_message& msg) noexcept @@ -301,7 +326,10 @@ 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. + { + // 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()); } @@ -324,6 +352,11 @@ evmc::Result Host::call(const evmc_message& msg) noexcept if (result.status_code != EVMC_SUCCESS) { + // A failed frame commits none of its state-gas charges: it returns the caller's baseline + // and carries no spill (EIP-8037). + assert(result.state_gas_left == msg.state_gas); + assert(result.state_gas_spilled == 0); + // 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..5727c3f6bb 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, 0, 0, {.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 851a8c0a8c..e67d64d249 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -9,6 +9,7 @@ #include "state_view.hpp" #include #include +#include #include #include @@ -194,7 +195,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 +203,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 +434,12 @@ 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: the execution gas +/// and, since EIP-8037, the state-gas reservoir. +/// @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 +500,24 @@ 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); + // The tx must fit in the block's remaining gas. Checked before the nonce and balance, as + // before, so a transaction invalid in several ways can report a different one than EELS. + if (rev < EVMC_AMSTERDAM) + { + if (tx.gas_limit > block_gas_left) + return make_error_code(GAS_ALLOWANCE_EXCEEDED); + } + else + { + // Check limits in both dimensions, any failure invalidates the transaction. + 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 +558,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 +636,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. @@ -642,9 +664,45 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc } } - const auto result = host.call(message); + const auto state_gas_limit = message.state_gas; + StateGas state_gas{.left = state_gas_limit}; + auto preparation_state_cost = int64_t{0}; + // A top-level create or value transfer materializing a new state leaf pays NEW_ACCOUNT here, + // after authorizations and before execution. There is no calling opcode to charge it instead. + if (rev >= EVMC_AMSTERDAM && (!tx.to.has_value() || tx.value != 0)) + { + const auto* const recipient = state.find(message.recipient); + if (recipient == nullptr || recipient->is_empty()) + preparation_state_cost = NEW_ACCOUNT_STATE_GAS; + } + const auto charge_succeeded = state_gas.charge(message.gas, preparation_state_cost); + message.state_gas = state_gas.left; + + // A failed runtime preparation charge is an included out-of-gas transaction (EIP-2780). + // TODO(EIP-2780): Roll back authorization changes when this charge fails. + auto result = charge_succeeded ? host.call(message) : + evmc::Result{EVMC_OUT_OF_GAS, 0, 0, {.left = state_gas_limit}}; + + // Settle the preparation charge like any frame charge: committed on success, and on failure + // returned whole, its spill going back to gas_left on a revert and consumed by a halt. + // A failed charge leaves both counters untouched, making this a no-op for it. + if (result.status_code == EVMC_SUCCESS) + { + result.state_gas_spilled += state_gas.spilled; + } + else + { + assert(result.state_gas_left == message.state_gas); + assert(result.state_gas_spilled == 0); + if (result.status_code == EVMC_REVERT) + result.gas_left += state_gas.spilled; + result.state_gas_left = state_gas_limit; + } + + const auto state_gas_used = state_gas_limit - result.state_gas_left + result.state_gas_spilled; + assert(state_gas_used >= 0); - 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); @@ -655,7 +713,8 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc gas_used = std::max(gas_used, tx_props.min_gas_cost); // For block gas accounting, compute the gas refund capped by the min gas cost (EIP-7778). - const auto block_gas_used = std::max(gas_used_b4_refund, tx_props.min_gas_cost); + const auto block_gas_used = + std::max(gas_used_b4_refund, tx_props.min_gas_cost + state_gas_used); const auto gas_refund = block_gas_used - gas_used; sender_acc.balance += tx_max_cost - gas_used * effective_gas_price; @@ -667,6 +726,7 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc .status = result.status_code, .gas_used = gas_used, .gas_refund = gas_refund, + .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 70879aec1c..6a4cf5203d 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; }; @@ -136,8 +139,12 @@ struct TransactionReceipt /// Effectively, the difference between "block" and "user" gas. int64_t gas_refund = 0; + /// The amount of state-gas used by this transaction (since EIP-8037). + 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; BloomFilter logs_bloom_filter; StateDiff state_diff; 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..faa28e72dd 100644 --- a/test/unittests/evmone_test.cpp +++ b/test/unittests/evmone_test.cpp @@ -15,6 +15,27 @@ 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)}; + EXPECT_EQ(output_result.state_gas_left, 0); + EXPECT_EQ(output_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 43700503a0..1ca2d974b0 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 71e65ad919..57b163df7c 100644 --- a/test/unittests/state_transition.hpp +++ b/test/unittests/state_transition.hpp @@ -75,6 +75,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..3dbac873b6 --- /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 > state::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 = intx::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 = state::MAX_TX_GAS_LIMIT; + expect.gas_refund = 0; + 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.gas_refund = 0; + 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 > state::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 = intx::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 = state::MAX_TX_GAS_LIMIT; + expect.gas_refund = 0; + 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 = state::MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = intx::uint256{tx.gas_limit} * tx.max_gas_price + tx.value + 1; + + expect.gas_used = 53'000 + NEW_ACCOUNT_STATE_GAS; + expect.gas_refund = 0; + 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 = state::MAX_TX_GAS_LIMIT + NEW_ACCOUNT_STATE_GAS / 2; + block.gas_limit = tx.gas_limit; + pre[Sender].balance = intx::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.gas_refund = 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.gas_refund = 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.gas_refund = 0; + 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 = state::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 = intx::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 = intx::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..eec6aa4e98 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), @@ -129,10 +129,10 @@ TEST(state_tx, validate_blob_tx) EXPECT_EQ( 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)) - .execution_gas_limit, - 39000); + const auto res = + validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, 0, blob_gas_limit); + const auto& tx_props = std::get(res); + EXPECT_EQ(tx_props.execution_gas_limit, 39000); tx.blob_hashes[0] = 0x0200000000000000000000000000000000000000000000000000000000000001_bytes32; EXPECT_EQ(expect_error(blob_gas_limit), make_error_code(ErrorCode::INVALID_BLOB_HASH_VERSION)); @@ -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 c8a36a733b..0a0e0227d3 100644 --- a/test/utils/block_transition.cpp +++ b/test/utils/block_transition.cpp @@ -49,8 +49,8 @@ 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; - int64_t block_gas_used = 0; auto blob_gas_left = blob_gas_limit; for (size_t i = 0; i < txs.size(); ++i) @@ -62,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)) { @@ -78,11 +78,12 @@ 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, refunds excluded (EIP-7778). + // The execution dimension is the block gas less the state one; transition() floors + // gas_refund above the state component to keep it at the EIP-7623 calldata floor. const auto block_tx_gas = (rev >= EVMC_AMSTERDAM) ? receipt.gas_used + receipt.gas_refund : receipt.gas_used; - block_gas_used += block_tx_gas; - block_gas_left -= block_tx_gas; + block_gas_left -= block_tx_gas - 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)); } @@ -114,6 +115,9 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state:: const auto bloom = compute_bloom_filter(receipts); + // 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); return {std::move(receipts), std::move(rejected_txs), std::move(requests), requests_error, block_gas_used, bloom, blob_gas_left, std::move(block_state)}; } diff --git a/test/utils/error_matching.cpp b/test/utils/error_matching.cpp index b239ad0fa3..2d1e526205 100644 --- a/test/utils/error_matching.cpp +++ b/test/utils/error_matching.cpp @@ -43,6 +43,10 @@ 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"}, + + // A transaction whose gas limit exceeds the block's remaining gas is a transaction rule to + // evmone and a block rule to the specs, which count it into the header's gas used. + {state::GAS_ALLOWANCE_EXCEEDED, "BlockException.GAS_USED_OVERFLOW"}, }; /// 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 19e64e178e..de0c228468 100644 --- a/test/utils/statetest_runner.cpp +++ b/test/utils/statetest_runner.cpp @@ -61,6 +61,7 @@ void run_state_test( 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,