diff --git a/circle.yml b/circle.yml index 316111ea5..962008a51 100644 --- a/circle.yml +++ b/circle.yml @@ -373,14 +373,13 @@ jobs: working_directory: ~/spec-tests/fixtures/state_tests command: > ~/build/bin/evmone-statetest ~/spec-tests/fixtures/state_tests - --gtest_filter='-*eip7702*' - run: name: "Execution spec tests (develop, blockchain_tests)" # Tests for in-development EVM revision currently passing. working_directory: ~/spec-tests/fixtures/blockchain_tests command: > ~/build/bin/evmone-blockchaintest ~/spec-tests/fixtures/blockchain_tests - --gtest_filter='-*block_hashes.block_hashes_history:*eip7702*:*eip7623*' + --gtest_filter='-*block_hashes.block_hashes_history' - collect_coverage_gcc - upload_coverage: flags: execution_spec_tests diff --git a/lib/evmone/CMakeLists.txt b/lib/evmone/CMakeLists.txt index 777c0800f..c4fa51909 100644 --- a/lib/evmone/CMakeLists.txt +++ b/lib/evmone/CMakeLists.txt @@ -17,6 +17,8 @@ add_library(evmone baseline_instruction_table.cpp baseline_instruction_table.hpp constants.hpp + delegation.cpp + delegation.hpp eof.cpp eof.hpp instructions.hpp diff --git a/lib/evmone/delegation.cpp b/lib/evmone/delegation.cpp new file mode 100644 index 000000000..cb39733c9 --- /dev/null +++ b/lib/evmone/delegation.cpp @@ -0,0 +1,29 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2025 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#include "delegation.hpp" +#include + +namespace evmone +{ +std::optional get_delegate_address( + const evmc::HostInterface& host, const evmc::address& addr) noexcept +{ + // Load the code prefix up to the delegation designation size. + // The HostInterface::copy_code() copies up to the addr's code size + // and returns the number of bytes copied. + uint8_t designation_buffer[std::size(DELEGATION_MAGIC) + sizeof(evmc::address)]; + const auto size = host.copy_code(addr, 0, designation_buffer, std::size(designation_buffer)); + const bytes_view designation{designation_buffer, size}; + + if (!is_code_delegated(designation)) + return {}; + + // Copy the delegate address from the designation buffer. + evmc::address delegate_address; + // Assume the designation with the valid magic has also valid length. + assert(designation.size() == std::size(designation_buffer)); + std::ranges::copy(designation.substr(std::size(DELEGATION_MAGIC)), delegate_address.bytes); + return delegate_address; +} +} // namespace evmone diff --git a/lib/evmone/delegation.hpp b/lib/evmone/delegation.hpp new file mode 100644 index 000000000..9c3799e66 --- /dev/null +++ b/lib/evmone/delegation.hpp @@ -0,0 +1,28 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2025 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +namespace evmone +{ +using evmc::bytes_view; + +/// Prefix of code for delegated accounts +/// defined by [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) +constexpr uint8_t DELEGATION_MAGIC_BYTES[] = {0xef, 0x01, 0x00}; +constexpr bytes_view DELEGATION_MAGIC{DELEGATION_MAGIC_BYTES, std::size(DELEGATION_MAGIC_BYTES)}; + +/// Check if code contains EIP-7702 delegation designator +constexpr bool is_code_delegated(bytes_view code) noexcept +{ + return code.starts_with(DELEGATION_MAGIC); +} + +/// Get EIP-7702 delegate address from the code of addr, if it is delegated. +EVMC_EXPORT std::optional get_delegate_address( + const evmc::HostInterface& host, const evmc::address& addr) noexcept; +} // namespace evmone diff --git a/lib/evmone/instructions_calls.cpp b/lib/evmone/instructions_calls.cpp index a97000deb..67736066a 100644 --- a/lib/evmone/instructions_calls.cpp +++ b/lib/evmone/instructions_calls.cpp @@ -2,6 +2,7 @@ // Copyright 2019 The evmone Authors. // SPDX-License-Identifier: Apache-2.0 +#include "delegation.hpp" #include "eof.hpp" #include "instructions.hpp" @@ -16,6 +17,34 @@ constexpr auto EXTCALL_ABORT = 2; namespace evmone::instr::core { +namespace +{ +/// Get target address of a code executing instruction. +/// +/// Returns EIP-7702 delegate address if addr is delegated, or addr itself otherwise. +/// Applies gas charge for accessing delegate account and may fail with out of gas. +inline std::variant get_target_address( + const evmc::address& addr, int64_t& gas_left, ExecutionState& state) noexcept +{ + if (state.rev < EVMC_PRAGUE) + return addr; + + const auto delegate_addr = get_delegate_address(state.host, addr); + if (!delegate_addr) + return addr; + + const auto delegate_account_access_cost = + (state.host.access_account(*delegate_addr) == EVMC_ACCESS_COLD ? + instr::cold_account_access_cost : + instr::warm_storage_read_cost); + + if ((gas_left -= delegate_account_access_cost) < 0) + return Result{EVMC_OUT_OF_GAS, gas_left}; + + return *delegate_addr; +} +} // namespace + /// Converts an opcode to matching EVMC call kind. /// NOLINTNEXTLINE(misc-use-internal-linkage) fixed in clang-tidy 20. consteval evmc_call_kind to_call_kind(Opcode op) noexcept @@ -67,6 +96,12 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce return {EVMC_OUT_OF_GAS, gas_left}; } + const auto target_addr_or_result = get_target_address(dst, gas_left, state); + if (const auto* result = std::get_if(&target_addr_or_result)) + return *result; + + const auto& code_addr = std::get(target_addr_or_result); + if (!check_memory(gas_left, state.memory, input_offset_u256, input_size_u256)) return {EVMC_OUT_OF_GAS, gas_left}; @@ -80,9 +115,13 @@ Result call_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noexce evmc_message msg{.kind = to_call_kind(Op)}; msg.flags = (Op == OP_STATICCALL) ? uint32_t{EVMC_STATIC} : state.msg->flags; + if (dst != code_addr) + msg.flags |= EVMC_DELEGATED; + else + msg.flags &= ~std::underlying_type_t{EVMC_DELEGATED}; msg.depth = state.msg->depth + 1; msg.recipient = (Op == OP_CALL || Op == OP_STATICCALL) ? dst : state.msg->recipient; - msg.code_address = dst; + msg.code_address = code_addr; msg.sender = (Op == OP_DELEGATECALL) ? state.msg->sender : state.msg->recipient; msg.value = (Op == OP_DELEGATECALL) ? state.msg->value : intx::be::store(value); @@ -178,6 +217,12 @@ Result extcall_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noe return {EVMC_OUT_OF_GAS, gas_left}; } + const auto target_addr_or_result = get_target_address(dst, gas_left, state); + if (const auto* result = std::get_if(&target_addr_or_result)) + return *result; + + const auto& code_addr = std::get(target_addr_or_result); + if (!check_memory(gas_left, state.memory, input_offset_u256, input_size_u256)) return {EVMC_OUT_OF_GAS, gas_left}; @@ -186,9 +231,13 @@ Result extcall_impl(StackTop stack, int64_t gas_left, ExecutionState& state) noe evmc_message msg{.kind = to_call_kind(Op)}; msg.flags = (Op == OP_EXTSTATICCALL) ? uint32_t{EVMC_STATIC} : state.msg->flags; + if (dst != code_addr) + msg.flags |= EVMC_DELEGATED; + else + msg.flags &= ~std::underlying_type_t{EVMC_DELEGATED}; msg.depth = state.msg->depth + 1; msg.recipient = (Op != OP_EXTDELEGATECALL) ? dst : state.msg->recipient; - msg.code_address = dst; + msg.code_address = code_addr; msg.sender = (Op == OP_EXTDELEGATECALL) ? state.msg->sender : state.msg->recipient; msg.value = (Op == OP_EXTDELEGATECALL) ? state.msg->value : intx::be::store(value); diff --git a/test/state/errors.hpp b/test/state/errors.hpp index 5d9f95d6b..ac2f57500 100644 --- a/test/state/errors.hpp +++ b/test/state/errors.hpp @@ -27,6 +27,8 @@ enum ErrorCode : int EMPTY_BLOB_HASHES_LIST, INVALID_BLOB_HASH_VERSION, BLOB_GAS_LIMIT_EXCEEDED, + CREATE_SET_CODE_TX, + EMPTY_AUTHORIZATION_LIST, UNKNOWN_ERROR, }; @@ -73,6 +75,10 @@ inline const std::error_category& evmone_category() noexcept return "invalid blob hash version"; case BLOB_GAS_LIMIT_EXCEEDED: return "blob gas limit exceeded"; + case CREATE_SET_CODE_TX: + return "set code transaction must not be a create transaction"; + case EMPTY_AUTHORIZATION_LIST: + return "empty authorization list"; case UNKNOWN_ERROR: return "Unknown error"; default: diff --git a/test/state/host.cpp b/test/state/host.cpp index 4f51d1982..99ed70b43 100644 --- a/test/state/host.cpp +++ b/test/state/host.cpp @@ -440,7 +440,7 @@ evmc::Result Host::execute_message(const evmc_message& msg) noexcept } } - if (is_precompile(m_rev, msg.code_address)) + if ((msg.flags & EVMC_DELEGATED) == 0 && is_precompile(m_rev, msg.code_address)) return call_precompile(m_rev, msg); // TODO: get_code() performs the account lookup. Add a way to get an account with code? diff --git a/test/state/precompiles_stubs.cpp b/test/state/precompiles_stubs.cpp index 74f3c54fe..37626fe44 100644 --- a/test/state/precompiles_stubs.cpp +++ b/test/state/precompiles_stubs.cpp @@ -321,6 +321,7 @@ ExecutionResult expmod_stub( "0000000000000000000000000000000000000000000000000000000000000000"_hex}, {0xd09419104ce1c64b6a06bcf063e98c2c91ad9e1beaf98b21c9d4734b4a3c9956_bytes32, "0000000000000000000000000000000000000000000000000000000000000000"_hex}, + {0xd397b3b043d87fcd6fad1291ff0bfd16401c274896d8c63a923727f077b8e0b5_bytes32, ""_hex}, {0xd6c0c03ec1f713b63be3d39b4fa8ef082b3407adc29baf74669fd2a574c638ac_bytes32, "01"_hex}, {0xd837f9dcf93155fe558c02c7a660edc0cd238a8b8f95ee6b68e4a5c6a41fc70a_bytes32, "0000000000000000000000000000000000000000000000000000000000000001"_hex}, diff --git a/test/state/state.cpp b/test/state/state.cpp index 8d19f798d..22cc492eb 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -7,13 +7,24 @@ #include "host.hpp" #include "state_view.hpp" #include +#include #include +#include #include +using namespace intx; + namespace evmone::state { namespace { +/// Secp256k1's N/2 is the upper bound of the signature's s value. +constexpr auto SECP256K1N_OVER_2 = evmmax::secp256k1::Order / 2; +/// EIP-7702: The cost of authorization that sets delegation to an account that didn't exist before. +constexpr auto AUTHORIZATION_EMPTY_ACCOUNT_COST = 25000; +/// EIP-7702: The cost of authorization that sets delegation to an account that already exists. +constexpr auto AUTHORIZATION_BASE_COST = 12500; + constexpr int64_t num_words(size_t size_in_bytes) noexcept { return static_cast((size_in_bytes + 31) / 32); @@ -63,11 +74,14 @@ TransactionCost compute_tx_intrinsic_cost(evmc_revision rev, const Transaction& const auto access_list_cost = compute_access_list_cost(tx.access_list); + const auto auth_list_cost = + static_cast(tx.authorization_list.size()) * AUTHORIZATION_EMPTY_ACCOUNT_COST; + const auto initcode_cost = (is_create && rev >= EVMC_SHANGHAI) ? INITCODE_WORD_COST * num_words(tx.data.size()) : 0; const auto intrinsic_cost = - TX_BASE_COST + create_cost + data_cost + access_list_cost + initcode_cost; + TX_BASE_COST + create_cost + data_cost + access_list_cost + auth_list_cost + initcode_cost; // EIP-7623: Compute the minimum cost for the transaction by. If disabled, just use 0. const auto min_cost = @@ -324,7 +338,7 @@ 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 { - switch (tx.type) + switch (tx.type) // Validate "special" transaction types. { case Transaction::Type::blob: if (rev < EVMC_CANCUN) @@ -344,16 +358,21 @@ std::variant validate_transaction( return make_error_code(BLOB_GAS_LIMIT_EXCEEDED); break; + case Transaction::Type::set_code: + if (rev < EVMC_PRAGUE) + return make_error_code(TX_TYPE_NOT_SUPPORTED); + if (!tx.to.has_value()) + return make_error_code(CREATE_SET_CODE_TX); + if (tx.authorization_list.empty()) + return make_error_code(EMPTY_AUTHORIZATION_LIST); + break; + default:; } - switch (tx.type) + switch (tx.type) // Validate the "regular" transaction type hierarchy. { case Transaction::Type::set_code: - if (rev < EVMC_PRAGUE) - return make_error_code(TX_TYPE_NOT_SUPPORTED); - [[fallthrough]]; - case Transaction::Type::blob: case Transaction::Type::eip1559: if (rev < EVMC_LONDON) @@ -384,7 +403,8 @@ std::variant validate_transaction( const auto sender_acc = state_view.get_account(tx.sender).value_or( StateView::Account{.code_hash = Account::EMPTY_CODE_HASH}); - if (sender_acc.code_hash != Account::EMPTY_CODE_HASH) + if (sender_acc.code_hash != Account::EMPTY_CODE_HASH && + !is_code_delegated(state_view.get_account_code(tx.sender))) return make_error_code(SENDER_NOT_EOA); // Origin must not be a contract (EIP-3607). if (sender_acc.nonce == Account::NonceMax) // Nonce value limit (EIP-2681). @@ -460,6 +480,89 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc assert(sender_acc.nonce < Account::NonceMax); // Required for valid tx. ++sender_acc.nonce; // Bump sender nonce. + int64_t delegation_refund = 0; + + for (const auto& auth : tx.authorization_list) + { + // 1. Verify the chain id is either 0 or the chain’s current ID. + // FIXME: Transaction chain id is not the chain id. Can it be 0? + if (auth.chain_id != 0 && auth.chain_id != tx.chain_id) + continue; + + // 2. Verify the nonce is less than 2**64 - 1. + if (auth.nonce == Account::NonceMax) + continue; + + // 3. Verify if the signer has been successfully recovered from the signature. + // authority = ecrecover(...) + if (!auth.signer.has_value()) + continue; + + // s value must be less than or equal to secp256k1n/2, as specified in EIP-2. + if (auth.s > SECP256K1N_OVER_2) + continue; + + // Get or create the authority account. + // It is still empty at this point until nonce bump followed by successful authorization. + auto& authority = state.get_or_insert(*auth.signer, {.erase_if_empty = true}); + + // 4. Add authority to accessed_addresses (as defined in EIP-2929.) + authority.access_status = EVMC_ACCESS_WARM; + + // 5. Verify the code of authority is either empty or already delegated. + if (authority.code_hash != Account::EMPTY_CODE_HASH && + !is_code_delegated(state.get_code(*auth.signer))) + continue; + + // 6. Verify the nonce of authority is equal to nonce. + // In case authority does not exist in the trie, verify that nonce is equal to 0. + if (auth.nonce != authority.nonce) + continue; + + // 7. Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter + // if authority exists in the trie. + // We are interested in _empty_ accounts (EIP-161) because _empty_ implies they don't exist + // in the state (EIP-7523). Successful authorisation validation makes an account non-empty. + // We apply the refund only if the account has been non-empty before. + // TODO: The additional condition (erase_if_empty) is to handle existent-but-empty which + // are against EIP-7523 and may happen only in testing environments. + if (!authority.is_empty() || !authority.erase_if_empty) + { + // FIXME: Maybe it's better to count number of refunds and compute the total amount + // in the end. This way we can avoid overflow. + static constexpr auto EXISTING_AUTHORITY_REFUND = + AUTHORIZATION_EMPTY_ACCOUNT_COST - AUTHORIZATION_BASE_COST; + delegation_refund += EXISTING_AUTHORITY_REFUND; + } + + // As a special case, if address is 0 do not write the designation. + // Clear the account’s code and reset the account’s code hash to the empty hash. + if (is_zero(auth.addr)) + { + if (authority.code_hash != Account::EMPTY_CODE_HASH) + { + authority.code_changed = true; + authority.code.clear(); + authority.code_hash = Account::EMPTY_CODE_HASH; + } + } + // 8. Set the code of authority to be 0xef0100 || address. This is a delegation designation. + else + { + auto new_code = bytes(DELEGATION_MAGIC) + bytes(auth.addr); + if (authority.code != new_code) + { + // We are doing this only if the code is different to make the state diff precise. + authority.code_changed = true; + authority.code = std::move(new_code); + authority.code_hash = keccak256(authority.code); + } + } + + // 9. Increase the nonce of authority by one. + ++authority.nonce; + } + const auto base_fee = (rev >= EVMC_LONDON) ? block.base_fee : 0; assert(tx.max_gas_price >= base_fee); // Required for valid tx. assert(tx.max_gas_price >= tx.max_priority_gas_price); // Required for valid tx. @@ -500,13 +603,24 @@ TransactionReceipt transition(const StateView& state_view, const BlockInfo& bloc if (rev >= EVMC_SHANGHAI) host.access_account(block.coinbase); - const auto result = host.call(build_message(tx, tx_props.execution_gas_limit, rev)); + auto message = build_message(tx, tx_props.execution_gas_limit, rev); + if (tx.to.has_value()) + { + if (const auto delegate = get_delegate_address(host, *tx.to)) + { + message.code_address = *delegate; + message.flags |= EVMC_DELEGATED; + host.access_account(message.code_address); + } + } + + const auto result = host.call(message); auto gas_used = tx.gas_limit - result.gas_left; const auto max_refund_quotient = rev >= EVMC_LONDON ? 5 : 2; const auto refund_limit = gas_used / max_refund_quotient; - const auto refund = std::min(result.gas_refund, refund_limit); + const auto refund = std::min(delegation_refund + result.gas_refund, refund_limit); gas_used -= refund; assert(gas_used > 0); diff --git a/test/state/transaction.cpp b/test/state/transaction.cpp index e992e6f5c..35ed4c781 100644 --- a/test/state/transaction.cpp +++ b/test/state/transaction.cpp @@ -16,7 +16,7 @@ namespace evmone::state [[nodiscard]] bytes rlp_encode(const Transaction& tx) { - assert(tx.type <= Transaction::Type::blob); + assert(tx.type <= Transaction::Type::set_code); // TODO: Refactor this function. For all type of transactions most of the code is similar. if (tx.type == Transaction::Type::legacy) @@ -46,7 +46,7 @@ namespace evmone::state tx.to.has_value() ? tx.to.value() : bytes_view(), tx.value, tx.data, tx.access_list, tx.v, tx.r, tx.s); } - else // Transaction::Type::blob + else if (tx.type == Transaction::Type::blob) { // tx_type + // rlp [chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, @@ -57,6 +57,17 @@ namespace evmone::state tx.to.has_value() ? tx.to.value() : bytes_view(), tx.value, tx.data, tx.access_list, tx.max_blob_gas_price, tx.blob_hashes, tx.v, tx.r, tx.s); } + else // Transaction::Type::set_code + { + // tx_type + + // rlp [chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, + // data, access_list, authorization_list, sig_parity, r, s]; + return bytes{0x04} + // Transaction type (set_code type == 4) + rlp::encode_tuple(tx.chain_id, tx.nonce, tx.max_priority_gas_price, tx.max_gas_price, + static_cast(tx.gas_limit), + tx.to.has_value() ? tx.to.value() : bytes_view(), tx.value, tx.data, + tx.access_list, tx.authorization_list, tx.v, tx.r, tx.s); + } } [[nodiscard]] bytes rlp_encode(const TransactionReceipt& receipt) @@ -80,4 +91,10 @@ namespace evmone::state bytes_view(receipt.logs_bloom_filter), receipt.logs); } } + +[[nodiscard]] bytes rlp_encode(const Authorization& authorization) +{ + return rlp::encode_tuple(authorization.chain_id, authorization.addr, authorization.nonce, + authorization.v, authorization.r, authorization.s); +} } // namespace evmone::state diff --git a/test/state/transaction.hpp b/test/state/transaction.hpp index 0932b7037..80b6c2370 100644 --- a/test/state/transaction.hpp +++ b/test/state/transaction.hpp @@ -14,6 +14,20 @@ namespace evmone::state { using AccessList = std::vector>>; +struct Authorization +{ + intx::uint256 chain_id; + address addr; + uint64_t nonce = 0; + /// Signer is empty if it cannot be ecrecovered from r, s, v. + std::optional
signer; + intx::uint256 r; + intx::uint256 s; + intx::uint256 v; +}; + +using AuthorizationList = std::vector; + struct Transaction { /// The type of the transaction. @@ -65,6 +79,7 @@ struct Transaction intx::uint256 r; intx::uint256 s; uint8_t v = 0; + AuthorizationList authorization_list; }; /// Transaction properties computed during the validation needed for the execution. @@ -119,4 +134,7 @@ struct TransactionReceipt /// Defines how to RLP-encode a Log. [[nodiscard]] bytes rlp_encode(const Log& log); + +/// Defines how to RLP-encode an Authorization (EIP-7702). +[[nodiscard]] bytes rlp_encode(const Authorization& authorization); } // namespace evmone::state diff --git a/test/statetest/statetest_loader.cpp b/test/statetest/statetest_loader.cpp index f5d003f7f..df88c7ab9 100644 --- a/test/statetest/statetest_loader.cpp +++ b/test/statetest/statetest_loader.cpp @@ -121,6 +121,26 @@ state::AccessList from_json(const json::json& j) return o; } +template <> +state::AuthorizationList from_json(const json::json& j) +{ + state::AuthorizationList o; + for (const auto& a : j) + { + state::Authorization authorization{}; + authorization.chain_id = from_json(a.at("chainId")); + authorization.addr = from_json
(a.at("address")); + authorization.nonce = from_json(a.at("nonce")); + if (a.contains("signer")) + authorization.signer = from_json
(a["signer"]); + authorization.r = from_json(a.at("r")); + authorization.s = from_json(a.at("s")); + authorization.v = from_json(a.at("v")); + o.emplace_back(authorization); + } + return o; +} + // Based on calculateEIP1559BaseFee from ethereum/retesteth static uint64_t calculate_current_base_fee_eip1559( uint64_t parent_gas_used, uint64_t parent_gas_limit, uint64_t parent_base_fee) @@ -343,6 +363,7 @@ static void from_json_tx_common(const json::json& j, state::Transaction& o) else if (const auto au_it = j.find("authorizationList"); au_it != j.end()) { o.type = state::Transaction::Type::set_code; + o.authorization_list = from_json(*au_it); } } diff --git a/test/statetest/statetest_runner.cpp b/test/statetest/statetest_runner.cpp index 9ea557f36..b4f3abb7e 100644 --- a/test/statetest/statetest_runner.cpp +++ b/test/statetest/statetest_runner.cpp @@ -23,13 +23,6 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_su // if (case_index != 3) // continue; - if (test.multi_tx.type == state::Transaction::Type::set_code) - { - // FIXME: Remove this once EIP-7702 is implemented. - std::cout << "WARNING: test case with set_code transaction (type 4) skipped\n"; - continue; - } - const auto& expected = cases[case_index]; const auto tx = test.multi_tx.get(expected.indexes); auto state = test.pre_state; diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index 03014aaa4..d86803d71 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -68,6 +68,7 @@ target_sources( state_transition_call_test.cpp state_transition_create_test.cpp state_transition_eip663_test.cpp + state_transition_eip7702_test.cpp state_transition_eof_calls_test.cpp state_transition_eof_create_test.cpp state_transition_extcode_test.cpp diff --git a/test/unittests/evm_eof_calls_test.cpp b/test/unittests/evm_eof_calls_test.cpp index d3f40ae11..50cf3ae89 100644 --- a/test/unittests/evm_eof_calls_test.cpp +++ b/test/unittests/evm_eof_calls_test.cpp @@ -203,11 +203,12 @@ TEST_P(evm, extcall_new_account_creation_cost) EXPECT_EQ(call_msg.value.bytes[31], 0); EXPECT_EQ(call_msg.input_size, 0); EXPECT_GAS_USED(EVMC_SUCCESS, gas_before_call + call_msg.gas + 3 + 3 + 3 + 3 + 3); - ASSERT_EQ(host.recorded_account_accesses.size(), 4); + ASSERT_EQ(host.recorded_account_accesses.size(), 5); EXPECT_EQ(host.recorded_account_accesses[0], 0x00_address); // EIP-2929 tweak EXPECT_EQ(host.recorded_account_accesses[1], msg.recipient); // EIP-2929 tweak - EXPECT_EQ(host.recorded_account_accesses[2], call_dst); // ? - EXPECT_EQ(host.recorded_account_accesses[3], call_dst); // Call. + EXPECT_EQ(host.recorded_account_accesses[2], call_dst); // EIP-2929 warm up call_dst + EXPECT_EQ(host.recorded_account_accesses[3], call_dst); // EIP-7702 delegation check + EXPECT_EQ(host.recorded_account_accesses[4], call_dst); // Call. host.recorded_account_accesses.clear(); host.recorded_calls.clear(); } @@ -227,13 +228,14 @@ TEST_P(evm, extcall_new_account_creation_cost) EXPECT_EQ(call_msg.value.bytes[31], 1); EXPECT_EQ(call_msg.input_size, 0); EXPECT_GAS_USED(EVMC_SUCCESS, gas_before_call + call_msg.gas + 3 + 3 + 3 + 3 + 3); - ASSERT_EQ(host.recorded_account_accesses.size(), 6); + ASSERT_EQ(host.recorded_account_accesses.size(), 7); EXPECT_EQ(host.recorded_account_accesses[0], 0x00_address); // EIP-2929 tweak EXPECT_EQ(host.recorded_account_accesses[1], msg.recipient); // EIP-2929 tweak - EXPECT_EQ(host.recorded_account_accesses[2], call_dst); // ? - EXPECT_EQ(host.recorded_account_accesses[3], call_dst); // Account exist?. - EXPECT_EQ(host.recorded_account_accesses[4], msg.recipient); // Balance. - EXPECT_EQ(host.recorded_account_accesses[5], call_dst); // Call. + EXPECT_EQ(host.recorded_account_accesses[2], call_dst); // EIP-2929 warm up call_dst + EXPECT_EQ(host.recorded_account_accesses[3], call_dst); // EIP-7702 delegation check + EXPECT_EQ(host.recorded_account_accesses[4], call_dst); // Account exist?. + EXPECT_EQ(host.recorded_account_accesses[5], msg.recipient); // Balance. + EXPECT_EQ(host.recorded_account_accesses[6], call_dst); // Call. host.recorded_account_accesses.clear(); host.recorded_calls.clear(); } diff --git a/test/unittests/state_transition.cpp b/test/unittests/state_transition.cpp index f1991feae..3d2c42859 100644 --- a/test/unittests/state_transition.cpp +++ b/test/unittests/state_transition.cpp @@ -232,6 +232,25 @@ void state_transition::export_state_test( } } + if (!tx.authorization_list.empty()) + { + auto& ja = jtx["authorizationList"]; + for (const auto& [chain_id, addr, nonce, signer, r, s, y_parity] : tx.authorization_list) + { + json::json je; + je["chainId"] = hex0x(chain_id); + je["address"] = hex0x(addr); + je["nonce"] = hex0x(nonce); + je["v"] = hex0x(y_parity); + je["r"] = hex0x(r); + je["s"] = hex0x(s); + if (signer.has_value()) + je["signer"] = hex0x(*signer); + ja.emplace_back(std::move(je)); + } + } + + auto& jpost = jt["post"][to_test_fork_name(rev)][0]; jpost["indexes"] = {{"data", 0}, {"gas", 0}, {"value", 0}}; jpost["hash"] = hex0x(mpt_hash(post)); diff --git a/test/unittests/state_transition_eip7702_test.cpp b/test/unittests/state_transition_eip7702_test.cpp new file mode 100644 index 000000000..f74a1f4cf --- /dev/null +++ b/test/unittests/state_transition_eip7702_test.cpp @@ -0,0 +1,305 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2024 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "../utils/bytecode.hpp" +#include "state_transition.hpp" + +using namespace evmc::literals; +using namespace evmone::test; + +TEST_F(state_transition, eip7702_set_code_transaction) +{ + rev = EVMC_PRAGUE; + + constexpr auto authority = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[delegate] = {.code = bytecode{OP_STOP}}; + tx.to = To; + tx.type = Transaction::Type::set_code; + tx.authorization_list = {{.addr = delegate, .nonce = 0, .signer = authority}}; + pre[To] = {.code = ret(0)}; + + expect.post[To].exists = true; + expect.post[delegate].exists = true; + expect.post[authority].nonce = 1; + expect.post[authority].code = bytes{0xef, 0x01, 0x00} + hex(delegate); +} + +TEST_F(state_transition, eip7702_set_code_transaction_authority_is_sender) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + pre[delegate] = {.code = bytecode{OP_STOP}}; + tx.to = To; + tx.type = Transaction::Type::set_code; + // Sender nonce is 1 in prestate, it is bumped once for tx and then another time for delegation + tx.authorization_list = {{.addr = delegate, .nonce = 2, .signer = Sender}}; + pre[To] = {.code = ret(0)}; + + expect.post[Sender].nonce = 3; + expect.post[Sender].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + expect.post[To].exists = true; + expect.post[delegate].exists = true; +} + +TEST_F(state_transition, eip7702_set_code_transaction_authority_is_to) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + pre[delegate] = {.code = bytecode{OP_STOP}}; + tx.to = To; + tx.type = Transaction::Type::set_code; + tx.authorization_list = {{.addr = delegate, .nonce = 0, .signer = To}}; + + expect.post[delegate].exists = true; + expect.post[To].nonce = pre[To].nonce + 1; + expect.post[To].code = bytes{0xef, 0x01, 0x00} + hex(delegate); +} + +TEST_F(state_transition, eip7702_extcodesize) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + pre[delegate] = {.code = 1024 * OP_JUMPDEST}; + tx.to = To; + pre[To] = {.code = sstore(1, push(callee) + OP_EXTCODESIZE)}; + + expect.post[callee].exists = true; + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x17_bytes32; +} + +TEST_F(state_transition, eip7702_extcodehash_delegation_to_empty) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + tx.to = To; + pre[To] = {.code = sstore(0, push(callee) + OP_EXTCODEHASH) + sstore(1, 1)}; + + expect.post[callee].exists = true; + expect.post[delegate].exists = false; + expect.post[To].storage[0x00_bytes32] = keccak256(bytes{0xef, 0x01, 0x00} + hex(delegate)); + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; +} + +TEST_F(state_transition, eip7702_extcodecopy) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + tx.to = To; + pre[To] = {.code = push(10) + push0() + push0() + push(callee) + OP_EXTCODECOPY + + sstore(0, mload(0)) + sstore(1, 1)}; + + expect.post[callee].exists = true; + expect.post[delegate].exists = false; + expect.post[To].storage[0x00_bytes32] = + 0xef01000000000000000000000000000000000000000000000000000000000000_bytes32; + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; +} + +TEST_F(state_transition, eip7702_call) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + pre[delegate] = {.code = sstore(0, 0x11)}; + tx.to = To; + pre[To] = {.code = sstore(1, call(callee).gas(50'000))}; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; + expect.post[callee].storage[0x00_bytes32] = 0x11_bytes32; +} + +TEST_F(state_transition, eip7702_call_with_value) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + pre[delegate] = {.code = sstore(0, 0x11)}; + tx.to = To; + pre[To] = {.balance = 10, .code = sstore(1, call(callee).gas(50'000).value(10))}; + + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; + expect.post[To].balance = 0; + expect.post[callee].storage[0x00_bytes32] = 0x11_bytes32; + expect.post[callee].balance = 10; + expect.post[delegate].balance = 0; +} + +TEST_F(state_transition, eip7702_call_warms_up_delegate) +{ + rev = EVMC_PRAGUE; + + constexpr auto callee = 0xca11ee_address; + constexpr auto delegate = 0xde1e_address; + pre[callee] = {.nonce = 1, .code = bytes{0xef, 0x01, 0x00} + hex(delegate)}; + pre[delegate] = {.code = bytecode{OP_STOP}}; + tx.to = To; + pre[To] = {.code = sstore(1, call(callee).gas(50'000)) + OP_GAS + call(delegate).gas(50'000) + + OP_GAS + OP_SWAP1 + push(2) + OP_SSTORE + OP_SWAP1 + OP_SUB + push(3) + + OP_SSTORE}; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; + expect.post[To].storage[0x02_bytes32] = 0x01_bytes32; + // 100 gas for warm call + 7 * 3 for argument pushes + 2 for GAS = 123 = 0x7b + expect.post[To].storage[0x03_bytes32] = 0x7b_bytes32; + expect.post[callee].exists = true; +} + +TEST_F(state_transition, eip7702_transaction_from_delegated_account) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + pre[Sender].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + pre[delegate] = {.code = 1024 * OP_JUMPDEST}; + + tx.to = To; + pre[To] = {.code = sstore(1, OP_CALLER)}; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = to_bytes32(Sender); +} + +TEST_F(state_transition, eip7702_transaction_to_delegated_account) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + pre[To].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + + pre[delegate] = {.code = sstore(1, 1)}; + tx.to = To; + pre[To] = {.code = sstore(1, OP_CALLER)}; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = to_bytes32(Sender); +} + +TEST_F(state_transition, eip7702_transaction_to_delegation_to_precompile) +{ + rev = EVMC_PRAGUE; + + constexpr auto ecadd_precompile = 0x06_address; // reverts on invalid input + pre[To].code = bytes{0xef, 0x01, 0x00} + hex(ecadd_precompile); + + tx.to = To; + tx.data = "01"_hex; + + expect.status = EVMC_SUCCESS; + expect.post[To].exists = true; +} + +TEST_F(state_transition, eip7702_transaction_to_delegation_to_empty) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + pre[To].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + + tx.to = To; + + expect.status = EVMC_SUCCESS; + expect.post[To].exists = true; + expect.post[delegate].exists = false; +} + +TEST_F(state_transition, eip7702_delegated_mode_propagation_call) +{ + rev = EVMC_PRAGUE; + + constexpr auto delegate = 0xde1e_address; + constexpr auto identity_precompile = 0x04_address; + pre[delegate] = { + .code = call(identity_precompile).input(0, 10).gas(OP_GAS) + sstore(1, returndatasize())}; + pre[To].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + + tx.to = To; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x0a_bytes32; +} + +TEST_F(state_transition, eip7702_delegated_mode_propagation_extcall) +{ + rev = EVMC_OSAKA; + + constexpr auto delegate = 0xde1e_address; + constexpr auto identity_precompile = 0x04_address; + pre[delegate] = { + .code = eof_bytecode( + extcall(identity_precompile).input(0, 10) + sstore(1, returndatasize()) + OP_STOP, 4)}; + pre[To].code = bytes{0xef, 0x01, 0x00} + hex(delegate); + + tx.to = To; + + expect.post[delegate].exists = true; + expect.post[To].storage[0x01_bytes32] = 0x0a_bytes32; +} + +TEST_F(state_transition, eip7702_selfdestruct) +{ + rev = EVMC_PRAGUE; + constexpr auto callee = 0xca11ee_address; + constexpr bytes32 salt{0xff}; + + const auto deploy_code = bytecode{selfdestruct(0x00_address)}; + const auto initcode = + mstore(0, push(deploy_code)) + ret(32 - deploy_code.size(), deploy_code.size()); + const auto deployed_address = compute_create2_address(To, salt, initcode); + + pre[To].code = mstore(0, push(initcode)) + + sstore(0, create2().input(32 - initcode.size(), initcode.size()).salt(salt)) + + sstore(1, call(callee).gas(OP_GAS)); + pre[callee].code = bytes{0xef, 0x01, 0x00} + hex(deployed_address); + + tx.to = To; + + expect.post[deployed_address].code = deploy_code; + expect.post[To].storage[0x00_bytes32] = to_bytes32(deployed_address); + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; + expect.post[callee].code = bytes{0xef, 0x01, 0x00} + hex(deployed_address); +} + +TEST_F(state_transition, eip7702_set_code_transaction_with_selfdestruct) +{ + rev = EVMC_PRAGUE; + constexpr auto callee = 0xca11ee_address; + constexpr bytes32 salt{0xff}; + + const auto deploy_code = bytecode{selfdestruct(0x00_address)}; + const auto initcode = + mstore(0, push(deploy_code)) + ret(32 - deploy_code.size(), deploy_code.size()); + const auto deployed_address = compute_create2_address(To, salt, initcode); + + pre[To].code = mstore(0, push(initcode)) + + sstore(0, create2().input(32 - initcode.size(), initcode.size()).salt(salt)) + + sstore(1, call(callee).gas(OP_GAS)); + + tx.to = To; + tx.type = Transaction::Type::set_code; + tx.authorization_list = {{.addr = deployed_address, .nonce = 0, .signer = callee}}; + + expect.post[deployed_address].code = deploy_code; + expect.post[To].storage[0x00_bytes32] = to_bytes32(deployed_address); + expect.post[To].storage[0x01_bytes32] = 0x01_bytes32; + expect.post[callee].code = bytes{0xef, 0x01, 0x00} + hex(deployed_address); +}