Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion evmc/include/evmc/evmc.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ enum
*
* @see @ref versioning
*/
EVMC_ABI_VERSION = 18
EVMC_ABI_VERSION = 19
};


Expand Down Expand Up @@ -189,6 +189,13 @@ struct evmc_message
* The length of the code to be executed.
*/
size_t code_size;

/**
* The amount of state gas available (EIP-8037).
*
* It draws from a reservoir allocated at transaction level.
*/
int64_t state_gas;
};

/** The transaction and block data for execution. */
Expand Down Expand Up @@ -455,6 +462,17 @@ struct evmc_result
* function to the result itself allows VM composition.
*/
evmc_release_result_fn release;

/**
* The amount of state gas left after execution (EIP-8037).
*/
// FIXME: Move after gas_refund.
int64_t state_gas_left;

/**
* The portion of consumed state gas taken from gas_left (EIP-8037).
*/
int64_t state_gas_spilled;
};


Expand Down
2 changes: 2 additions & 0 deletions evmc/include/evmc/evmc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ 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_left;
using evmc_result::state_gas_spilled;
using evmc_result::status_code;

/// Creates the result from the provided arguments.
Expand Down
17 changes: 17 additions & 0 deletions lib/evmone/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,21 @@ 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 bytes charged for creating a new account (EIP-8037).
constexpr auto STATE_BYTES_PER_NEW_ACCOUNT = 120;

/// State bytes charged when a storage slot is newly allocated (EIP-8037).
constexpr auto STATE_BYTES_PER_STORAGE_SET = 64;

/// State-gas cost of creating a new account (EIP-8037).
constexpr auto NEW_ACCOUNT_STATE_GAS = STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE;

/// State-gas cost of allocating a storage slot (EIP-8037).
constexpr auto STORAGE_SET_STATE_GAS = STATE_BYTES_PER_STORAGE_SET * COST_PER_STATE_BYTE;

// State-gas charging and refills live on the StateGas type (state_gas.hpp).
} // namespace evmone
33 changes: 31 additions & 2 deletions lib/evmone/execution_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include "state_gas.hpp"
#include <evmc/evmc.hpp>
#include <intx/intx.hpp>
#include <cassert>
Expand Down Expand Up @@ -154,6 +155,12 @@ class ExecutionState
const advanced::AdvancedCodeAnalysis* advanced;
} analysis{};

/// The frame's state-gas reservoir + spill; used is derived (EIP-8037).
///
/// Kept in the cold tail: earlier placement pushes `status` and `host` out of the x86-64
/// disp8 window, costing 3 bytes on every `status` access in the dispatch loop.
StateGas state_gas;

/// Stack space allocation.
///
/// This is the last field to make other fields' offsets of reasonable values.
Expand All @@ -164,7 +171,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.
Expand All @@ -173,6 +184,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};
Expand Down Expand Up @@ -202,13 +214,30 @@ 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
{
// A rolled-back frame created no state, so its net state gas used is zero: the reservoir is
// restored to the frame's budget and the spilled portion returns to `gas_left`, kept on a
// revert and consumed by the halt's gas_left = 0 below (EIP-8037).
if (state.rev >= EVMC_AMSTERDAM && state.status != EVMC_SUCCESS)
{
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,
auto result = evmc::make_result(state.status, gas_left, gas_refund,
state.output_size != 0 ? &state.memory[state.output_offset] : nullptr, state.output_size);

// Return the leftover reservoir and spill; the caller derives the net used as
// `initial - state_gas_left + state_gas_spilled` (EIP-8037).
assert(state.state_gas.left >= 0);
result.state_gas_left = state.state_gas.left;
result.state_gas_spilled = state.state_gas.spilled;
return result;
}
} // namespace evmone
14 changes: 12 additions & 2 deletions lib/evmone/instructions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#pragma once

#include "baseline.hpp"
#include "constants.hpp"
#include "execution_state.hpp"
#include "instructions_traits.hpp"
#include "instructions_xmacro.hpp"
Expand Down Expand Up @@ -1081,8 +1082,17 @@ 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)
{
// The new account leaf is paid in state gas (EIP-8037).
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};
}
}
}
}
Expand Down
97 changes: 92 additions & 5 deletions lib/evmone/instructions_calls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ inline std::variant<evmc::address, Result> 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.
Expand Down Expand Up @@ -119,11 +143,29 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce

const auto& code_addr = std::get<evmc::address>(target_addr_or_result);

// State gas for creating the called account, i.e. a value-CALL to a nonexistent one. Tracked
// at function scope so every non-success exit below can refill it: a light failure or a child
// revert/halt undoes the account creation (EIP-8037).
int64_t new_account_state_gas = 0;
const auto refund_new_account_state_gas = [&]() noexcept {
if (new_account_state_gas != 0)
state.state_gas.refill(gas_left, new_account_state_gas);
};

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)
{
// The state charge comes after every regular cost of this instruction is
// committed (reservoir model), so a regular OOG cannot leave committed
// state growth behind.
new_account_state_gas = NEW_ACCOUNT_STATE_GAS;
if (!state.state_gas.charge(gas_left, new_account_state_gas))
return {EVMC_OUT_OF_GAS, gas_left};
}
else if ((gas_left -= ACCOUNT_CREATION_COST) < 0)
return {EVMC_OUT_OF_GAS, gas_left};
}
}
Expand Down Expand Up @@ -171,12 +213,22 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce
msg.gas += CALL_STIPEND;
gas_left += CALL_STIPEND;
if (intx::be::load<uint256>(state.host.get_balance(state.msg->recipient)) < value)
{
refund_new_account_state_gas(); // No transfer, so no account created.
return {EVMC_SUCCESS, gas_left}; // "Light" failure.
}
}
}

if (state.msg->depth >= 1024)
{
refund_new_account_state_gas(); // Child never runs, so no account created.
return {EVMC_SUCCESS, gas_left}; // "Light" failure.
}

// The reservoir passes to the child in full; the 63/64 rule applies to gas_left only
// (EIP-8037).
msg.state_gas = state.state_gas.left;

const auto result = state.host.call(msg);
state.return_data.assign(result.output_data, result.output_size);
Expand All @@ -188,6 +240,11 @@ 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;
// Thread the child's state gas back. A failed child rolls the created account back, so its
// NEW_ACCOUNT charge is refilled (EIP-8037).
absorb_child_state_gas(gas_left, state, result);
if (result.status_code != EVMC_SUCCESS)
refund_new_account_state_gas();
return {EVMC_SUCCESS, gas_left};
}

Expand Down Expand Up @@ -249,13 +306,33 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex
const auto init_code =
bytes_view{init_code_size > 0 ? &state.memory[init_code_offset] : nullptr, init_code_size};

evmc_message msg{.kind = to_call_kind(Op)};
msg.recipient = (Op == OP_CREATE) ? compute_create_address(sender, sender_nonce) :
compute_create2_address(sender, salt, init_code);
// Compute the address of the account to be created. The Host bumps the sender's
// nonce on create-frame entry, so CREATE uses the pre-bump value read above.
const auto create_addr = (Op == OP_CREATE) ? compute_create_address(sender, sender_nonce) :
compute_create2_address(sender, salt, init_code);

// Access to the new address is warmed and never reverted (EIP-2929).
if (state.rev >= EVMC_BERLIN)
state.host.access_account(msg.recipient);
state.host.access_account(create_addr);

// Charge NEW_ACCOUNT for a deployment onto a not-alive address (EIP-161), after warming and
// before the 63/64 split so a reservoir spill correctly lowers the gas forwarded to the
// child. Refilled below when no account is created (EIP-8037).
int64_t create_state_gas_charged = 0;
if (state.rev >= EVMC_AMSTERDAM)
{
// EIP-161 aliveness. account_exists() is the same predicate: its pre-Spurious-Dragon
// arm is unreachable under the Amsterdam gate, leaving `acc != nullptr && !is_empty()`.
if (!state.host.account_exists(create_addr))
{
create_state_gas_charged = NEW_ACCOUNT_STATE_GAS;
if (!state.state_gas.charge(gas_left, create_state_gas_charged))
return {EVMC_OUT_OF_GAS, gas_left};
}
}

evmc_message msg{.kind = to_call_kind(Op)};
msg.recipient = create_addr;

msg.gas = gas_left;
if (state.rev >= EVMC_TANGERINE_WHISTLE)
Expand All @@ -267,9 +344,19 @@ Result create_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noex
msg.depth = state.msg->depth + 1;
msg.value = intx::be::store<evmc::uint256be>(endowment);

// The reservoir passes to the child in full; the 63/64 rule applies to gas_left only
// (EIP-8037).
msg.state_gas = state.state_gas.left;

const auto result = state.host.call(msg);
gas_left -= msg.gas - result.gas_left;
state.gas_refund += result.gas_refund;
// Thread the child's state gas back. A non-success result — a rolled-back initcode or an
// address collision — creates no account, so its NEW_ACCOUNT charge is refilled; a create
// onto an already-alive account was never charged (EIP-8037).
absorb_child_state_gas(gas_left, state, result);
if (create_state_gas_charged != 0 && result.status_code != EVMC_SUCCESS)
state.state_gas.refill(gas_left, create_state_gas_charged);

state.return_data.assign(result.output_data, result.output_size);
if (result.status_code == EVMC_SUCCESS)
Expand Down
28 changes: 26 additions & 2 deletions lib/evmone/instructions_storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 2900; // EIP-8037: regular component only (was 20000).
tbl[EVMC_EXPERIMENTAL] = tbl[EVMC_AMSTERDAM];
return tbl;
}();

Expand All @@ -51,6 +52,9 @@ struct StorageStoreCost
{
int16_t gas_cost;
int16_t gas_refund;
/// State gas for the slot allocation: positive to charge, negative to refill, zero before
/// Amsterdam. Wider than int16_t because 64 * COST_PER_STATE_BYTE is 97'920 (EIP-8037).
int32_t state_gas = 0;
};

// The lookup table of SSTORE costs by the storage update status.
Expand Down Expand Up @@ -89,6 +93,14 @@ constexpr auto sstore_costs = []() noexcept {
e[EVMC_STORAGE_MODIFIED_RESTORED] = {
c.warm_access, static_cast<int16_t>(c.reset - c.warm_access)};
}

// Allocating a slot (0 -> non-zero) costs state gas; undoing it in the same
// transaction (0 -> Y -> 0) refills it (EIP-8037).
if (rev >= EVMC_AMSTERDAM)
{
e[EVMC_STORAGE_ADDED].state_gas = STORAGE_SET_STATE_GAS;
e[EVMC_STORAGE_ADDED_DELETED].state_gas = -STORAGE_SET_STATE_GAS;
}
}

return tbl;
Expand Down Expand Up @@ -134,10 +146,22 @@ Result sstore(StackTop stack, int64_t gas_left, ExecutionState& state) noexcept
0;
const auto status = state.host.set_storage(state.msg->recipient, key, value);

const auto [gas_cost_warm, gas_refund] = sstore_costs[state.rev][status];
const auto [gas_cost_warm, gas_refund, state_gas] = sstore_costs[state.rev][status];
const auto gas_cost = gas_cost_warm + gas_cost_cold;

// A refill (0 -> Y -> 0) is applied BEFORE the regular charge, as in EELS, so gas returned
// to gas_left from a prior spill can fund that charge (EIP-8037).
// FIXME: .refill(c) looks like .charge(-c). Can we combine these?
if (state_gas < 0)
state.state_gas.refill(gas_left, -state_gas);

// Charge regular gas FIRST, then state gas: this order prevents a state-gas spill from
// counting committed state growth behind a subsequent regular OOG (EIP-8037).
if ((gas_left -= gas_cost) < 0)
return {EVMC_OUT_OF_GAS, gas_left};

if (state_gas > 0 && !state.state_gas.charge(gas_left, state_gas))
return {EVMC_OUT_OF_GAS, gas_left};
state.gas_refund += gas_refund;
return {EVMC_SUCCESS, gas_left};
}
Expand Down
Loading