diff --git a/include/evm_runtime/config_wrapper.hpp b/include/evm_runtime/config_wrapper.hpp new file mode 100644 index 00000000..c109bafb --- /dev/null +++ b/include/evm_runtime/config_wrapper.hpp @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include +#include +#include +namespace evm_runtime { + +struct fee_parameters; +struct config_wrapper { + + config_wrapper(eosio::name self); + ~config_wrapper(); + + void flush(); + bool exists(); + + eosio::unsigned_int get_version()const; + void set_version(const eosio::unsigned_int version); + + uint64_t get_chainid()const; + void set_chainid(uint64_t chainid); + + const eosio::time_point_sec& get_genesis_time()const; + void set_genesis_time(eosio::time_point_sec genesis_time); + + const eosio::asset& get_ingress_bridge_fee()const; + void set_ingress_bridge_fee(const eosio::asset& ingress_bridge_fee); + + uint64_t get_gas_price()const; + void set_gas_price(uint64_t gas_price); + + uint32_t get_miner_cut()const; + void set_miner_cut(uint32_t miner_cut); + + uint32_t get_status()const; + void set_status(uint32_t status); + + uint64_t get_evm_version()const; + uint64_t get_evm_version_and_maybe_promote(); + void set_evm_version(uint64_t new_version); + + void set_fee_parameters(const fee_parameters& fee_params, + bool allow_any_to_be_unspecified); + +private: + bool is_dirty()const; + void set_dirty(); + void clear_dirty(); + + eosio::time_point get_current_time()const; + + bool _dirty = false; + bool _exists = false; + config _cached_config; + + eosio::name _self; + eosio::singleton<"config"_n, config> _config; + mutable std::optional current_time_point; +}; + +} //namespace evm_runtime \ No newline at end of file diff --git a/include/evm_runtime/evm_contract.hpp b/include/evm_runtime/evm_contract.hpp index 672ef687..6ef51f72 100644 --- a/include/evm_runtime/evm_contract.hpp +++ b/include/evm_runtime/evm_contract.hpp @@ -2,11 +2,14 @@ #include #include #include +#include #include - +#include +#include #include #include + #ifdef WITH_TEST_ACTIONS #include #endif @@ -19,20 +22,7 @@ class [[eosio::contract]] evm_contract : public contract { public: using contract::contract; - - struct fee_parameters - { - std::optional gas_price; ///< Minimum gas price (in 10^-18 EOS, aka wei) that is enforced on all - ///< transactions. Required during initialization. - - std::optional miner_cut; ///< Percentage cut (maximum allowed value of 100,000 which equals 100%) of the - ///< gas fee collected for a transaction that is sent to the indicated miner of - ///< that transaction. Required during initialization. - - std::optional ingress_bridge_fee; ///< Fee (in EOS) deducted from ingress transfers of EOS across bridge. - ///< Symbol must be in EOS and quantity must be non-negative. If not - ///< provided during initialization, the default fee of 0 will be used. - }; + evm_contract(eosio::name receiver, eosio::name code, const datastream& ds); /** * @brief Initialize EVM contract @@ -70,7 +60,7 @@ class [[eosio::contract]] evm_contract : public contract [[eosio::action]] void exec(const exec_input& input, const std::optional& callback); - [[eosio::action]] void pushtx(eosio::name miner, const bytes& rlptx); + [[eosio::action]] void pushtx(eosio::name miner, bytes rlptx); [[eosio::action]] void open(eosio::name owner); @@ -93,6 +83,13 @@ class [[eosio::contract]] evm_contract : public contract [[eosio::action]] void assertnonce(eosio::name account, uint64_t next_nonce); + [[eosio::action]] void setversion(uint64_t version); + + // Events + [[eosio::action]] void evmtx(eosio::ignore event){ + eosio::check(get_sender() == get_self(), "forbidden to call"); + }; + #ifdef WITH_ADMIN_ACTIONS [[eosio::action]] void rmgcstore(uint64_t id); [[eosio::action]] void setkvstore(uint64_t account_id, const bytes& key, const std::optional& value); @@ -116,40 +113,19 @@ class [[eosio::contract]] evm_contract : public contract [[eosio::action]] void testbaldust(const name test); #endif - struct [[eosio::table]] [[eosio::contract("evm_contract")]] config - { - unsigned_int version; // placeholder for future variant index - uint64_t chainid = 0; - time_point_sec genesis_time; - asset ingress_bridge_fee = asset(0, token_symbol); - uint64_t gas_price = 0; - uint32_t miner_cut = 0; - uint32_t status = 0; // <- bit mask values from status_flags - - EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)); - }; - private: + void open_internal_balance(eosio::name owner); + std::shared_ptr _config; + enum class status_flags : uint32_t { frozen = 0x1 }; - eosio::singleton<"config"_n, config> _config{get_self(), get_self().value}; - - void assert_inited() - { - check(_config.exists(), "contract not initialized"); - check(_config.get().version == 0u, "unsupported configuration singleton"); - } - - void assert_unfrozen() - { - assert_inited(); - check((_config.get().status & static_cast(status_flags::frozen)) == 0, "contract is frozen"); - } + void assert_inited(); + void assert_unfrozen(); - silkworm::Receipt execute_tx(eosio::name miner, silkworm::Block& block, silkworm::Transaction& tx, silkworm::ExecutionProcessor& ep, bool enforce_chain_id); + silkworm::Receipt execute_tx(const runtime_config& rc, eosio::name miner, silkworm::Block& block, const transaction& tx, silkworm::ExecutionProcessor& ep); void process_filtered_messages(const std::vector& filtered_messages); uint64_t get_and_increment_nonce(const name owner); @@ -159,23 +135,13 @@ class [[eosio::contract]] evm_contract : public contract void handle_account_transfer(const eosio::asset& quantity, const std::string& memo); void handle_evm_transfer(eosio::asset quantity, const std::string& memo); - void call_(intx::uint256 s, const bytes& to, intx::uint256 value, const bytes& data, uint64_t gas_limit, uint64_t nonce); + void call_(const runtime_config& rc, intx::uint256 s, const bytes& to, intx::uint256 value, const bytes& data, uint64_t gas_limit, uint64_t nonce); - // to allow sending through a Bytes (basic_string) w/o copying over to a std::vector - void pushtx_bytes(eosio::name miner, const std::basic_string& rlptx); - using pushtx_action = eosio::action_wrapper<"pushtx"_n, &evm_contract::pushtx_bytes>; -}; + using pushtx_action = eosio::action_wrapper<"pushtx"_n, &evm_contract::pushtx>; + void process_tx(const runtime_config& rc, eosio::name miner, const transaction& tx); + void dispatch_tx(const runtime_config& rc, const transaction& tx); +}; } // namespace evm_runtime -namespace std { -template -DataStream& operator<<(DataStream& ds, const std::basic_string& bs) -{ - ds << (unsigned_int)bs.size(); - if (bs.size()) - ds.write((const char*)bs.data(), bs.size()); - return ds; -} -} // namespace std diff --git a/include/evm_runtime/runtime_config.hpp b/include/evm_runtime/runtime_config.hpp new file mode 100644 index 00000000..404fec59 --- /dev/null +++ b/include/evm_runtime/runtime_config.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace evm_runtime { + +struct runtime_config { + bool allow_special_signature = false; + bool abort_on_failure = false; + bool enforce_chain_id = true; + bool allow_non_self_miner = true; +}; + +} //namespace evm_runtime \ No newline at end of file diff --git a/include/evm_runtime/tables.hpp b/include/evm_runtime/tables.hpp index 019e8fcb..1be9088c 100644 --- a/include/evm_runtime/tables.hpp +++ b/include/evm_runtime/tables.hpp @@ -7,6 +7,8 @@ #include #include +#include + #include namespace evm_runtime { @@ -229,4 +231,60 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] config2 EOSLIB_SERIALIZE(config2, (next_account_id)); }; +struct evm_version_type { + struct pending { + uint64_t version; + time_point time; + + bool is_active(time_point_sec genesis_time, time_point current_time)const { + eosevm::block_mapping bm(genesis_time.sec_since_epoch()); + auto current_block_num = bm.timestamp_to_evm_block_num(current_time.time_since_epoch().count()); + auto pending_block_num = bm.timestamp_to_evm_block_num(time.time_since_epoch().count()); + return current_block_num > pending_block_num; + } + }; + + uint64_t get_version(time_point_sec genesis_time, time_point current_time)const { + uint64_t current_version = cached_version; + if(pending_version.has_value() && pending_version->is_active(genesis_time, current_time)) { + current_version = pending_version->version; + } + return current_version; + } + + std::pair get_version_and_maybe_promote(time_point_sec genesis_time, time_point current_time) { + uint64_t current_version = cached_version; + bool promoted = false; + if(pending_version.has_value() && pending_version->is_active(genesis_time, current_time)) { + current_version = pending_version->version; + promote_pending(); + promoted = true; + } + return std::make_pair(current_version, promoted); + } + + void promote_pending() { + eosio::check(pending_version.has_value(), "no pending version"); + cached_version = pending_version.value().version; + pending_version.reset(); + } + + std::optional pending_version; + uint64_t cached_version=0; +}; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] config +{ + unsigned_int version; // placeholder for future variant index + uint64_t chainid = 0; + time_point_sec genesis_time; + asset ingress_bridge_fee = asset(0, token_symbol); + uint64_t gas_price = 0; + uint32_t miner_cut = 0; + uint32_t status = 0; // <- bit mask values from status_flags + binary_extension evm_version; + + EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)(evm_version)); +}; + } //namespace evm_runtime diff --git a/include/evm_runtime/test/config.hpp b/include/evm_runtime/test/config.hpp index 635533c9..bf8af1c0 100644 --- a/include/evm_runtime/test/config.hpp +++ b/include/evm_runtime/test/config.hpp @@ -7,14 +7,14 @@ namespace test { inline constexpr ChainConfig kTestNetwork{ .chain_id = 1, .protocol_rule_set = protocol::RuleSetType::kNoProof, - .homestead_block = 0, - .dao_block = 0, - .tangerine_whistle_block = 0, - .spurious_dragon_block = 0, - .byzantium_block = 0, - .constantinople_block = 0, - .petersburg_block = 0, - .istanbul_block = 0, + ._homestead_block = 0, + ._dao_block = 0, + ._tangerine_whistle_block = 0, + ._spurious_dragon_block = 0, + ._byzantium_block = 0, + ._constantinople_block = 0, + ._petersburg_block = 0, + ._istanbul_block = 0, }; } //namespace test diff --git a/include/evm_runtime/test/engine.hpp b/include/evm_runtime/test/engine.hpp index 1d938319..866ccf59 100644 --- a/include/evm_runtime/test/engine.hpp +++ b/include/evm_runtime/test/engine.hpp @@ -19,7 +19,7 @@ class engine : public silkworm::protocol::IRuleSet { void finalize(IntraBlockState& state, const Block& block) override { intx::uint256 block_reward; - const evmc_revision revision{config.revision(block.header.number, block.header.timestamp)}; + const evmc_revision revision{config.revision(block.header)}; if (revision >= EVMC_CONSTANTINOPLE) { block_reward = silkworm::protocol::kBlockRewardConstantinople; } else if (revision >= EVMC_BYZANTIUM) { diff --git a/include/evm_runtime/transaction.hpp b/include/evm_runtime/transaction.hpp new file mode 100644 index 00000000..6e21932b --- /dev/null +++ b/include/evm_runtime/transaction.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace evm_runtime { + +using silkworm::Bytes; +using silkworm::ByteView; + +struct transaction { + + transaction() = delete; + explicit transaction(bytes rlptx) : rlptx_(std::move(rlptx)) {} + explicit transaction(silkworm::Transaction tx) : tx_(std::move(tx)) {} + + const bytes& get_rlptx()const { + if(!rlptx_) { + eosio::check(tx_.has_value(), "no tx"); + Bytes rlp; + silkworm::rlp::encode(rlp, tx_.value()); + rlptx_.emplace(bytes{rlp.begin(), rlp.end()}); + } + return rlptx_.value(); + } + + const silkworm::Transaction& get_tx()const { + if(!tx_) { + eosio::check(rlptx_.has_value(), "no rlptx"); + ByteView bv{(const uint8_t*)rlptx_->data(), rlptx_->size()}; + silkworm::Transaction tmp; + eosio::check(silkworm::rlp::decode(bv, tmp) && bv.empty(), "unable to decode transaction"); + tx_.emplace(tmp); + } + return tx_.value(); + } + + void recover_sender()const { + eosio::check(tx_.has_value(), "no tx"); + auto& tx = tx_.value(); + tx.from.reset(); + tx.recover_sender(); + } + +private: + mutable std::optional rlptx_; + mutable std::optional tx_; +}; + +} //namespace evm_runtime \ No newline at end of file diff --git a/include/evm_runtime/types.hpp b/include/evm_runtime/types.hpp index 50a6293c..cf52e68b 100644 --- a/include/evm_runtime/types.hpp +++ b/include/evm_runtime/types.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,7 @@ namespace evm_runtime { using intx::operator""_u256; + static constexpr uint32_t hundred_percent = 100'000; constexpr unsigned evm_precision = 18; constexpr eosio::name token_account(eosio::name(TOKEN_ACCOUNT_NAME)); @@ -75,6 +77,29 @@ namespace evm_runtime { using bridge_message = std::variant; + struct evmtx_v0 { + uint64_t eos_evm_version; + bytes rlptx; + + EOSLIB_SERIALIZE(evmtx_v0, (eos_evm_version)(rlptx)); + }; + + using evmtx_type = std::variant; + + struct fee_parameters + { + std::optional gas_price; ///< Minimum gas price (in 10^-18 EOS, aka wei) that is enforced on all + ///< transactions. Required during initialization. + + std::optional miner_cut; ///< Percentage cut (maximum allowed value of 100,000 which equals 100%) of the + ///< gas fee collected for a transaction that is sent to the indicated miner of + ///< that transaction. Required during initialization. + + std::optional ingress_bridge_fee; ///< Fee (in EOS) deducted from ingress transfers of EOS across bridge. + ///< Symbol must be in EOS and quantity must be non-negative. If not + ///< provided during initialization, the default fee of 0 will be used. + }; + } //namespace evm_runtime namespace eosio { @@ -87,4 +112,15 @@ inline datastream& operator>>(datastream& ds, evm_runtime::uint2 return ds; } -} //namespace eosio \ No newline at end of file +} //namespace eosio + +namespace std { +template +DataStream& operator<<(DataStream& ds, const std::basic_string& bs) +{ + ds << (eosio::unsigned_int)bs.size(); + if (bs.size()) + ds.write((const char*)bs.data(), bs.size()); + return ds; +} +} // namespace std \ No newline at end of file diff --git a/silkworm b/silkworm index 8a3b69ea..eb305ece 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit 8a3b69ea81ff567dadbf98db4f86652782acd6b4 +Subproject commit eb305eced3dcbec3abc04d199224146aef21cad1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bfee316e..8facc47e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,7 @@ list(APPEND SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/state.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/actions.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/config_wrapper.cpp ) if (WITH_TEST_ACTIONS) add_compile_definitions(WITH_TEST_ACTIONS) diff --git a/src/actions.cpp b/src/actions.cpp index b06c0e17..91872256 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -8,8 +8,7 @@ #include #include #include - -#include +#include #include // included here so NDEBUG is defined to disable assert macro @@ -32,66 +31,50 @@ namespace silkworm { return {}; } } - namespace evm_runtime { -static constexpr uint32_t hundred_percent = 100'000; static constexpr char err_msg_invalid_addr[] = "invalid address"; using namespace silkworm; -void set_fee_parameters(evm_contract::config& current_config, - const evm_contract::fee_parameters& fee_params, - bool allow_any_to_be_unspecified) -{ - if (fee_params.gas_price.has_value()) { - current_config.gas_price = *fee_params.gas_price; - } else { - check(allow_any_to_be_unspecified, "All required fee parameters not specified: missing gas_price"); - } - - if (fee_params.miner_cut.has_value()) { - check(*fee_params.miner_cut <= hundred_percent, "miner_cut cannot exceed 100,000 (100%)"); - - current_config.miner_cut = *fee_params.miner_cut; - } else { - check(allow_any_to_be_unspecified, "All required fee parameters not specified: missing miner_cut"); - } +evm_contract::evm_contract(eosio::name receiver, eosio::name code, const datastream& ds) : + contract(receiver, code, ds), _config(std::make_shared(get_self())) {} - if (fee_params.ingress_bridge_fee.has_value()) { - check(fee_params.ingress_bridge_fee->symbol == token_symbol, "unexpected bridge symbol"); - check(fee_params.ingress_bridge_fee->amount >= 0, "ingress bridge fee cannot be negative"); +void evm_contract::assert_inited() +{ + check(_config->exists(), "contract not initialized"); + check(_config->get_version() == 0u, "unsupported configuration singleton"); +} - current_config.ingress_bridge_fee = *fee_params.ingress_bridge_fee; - } +void evm_contract::assert_unfrozen() +{ + assert_inited(); + check((_config->get_status() & static_cast(status_flags::frozen)) == 0, "contract is frozen"); } void evm_contract::init(const uint64_t chainid, const fee_parameters& fee_params) { eosio::require_auth(get_self()); - check(!_config.exists(), "contract already initialized"); + check(!_config->exists(), "contract already initialized"); check(!!lookup_known_chain(chainid), "unknown chainid"); // Convert current time to EVM compatible block timestamp used as genesis time by rounding down to nearest second. time_point_sec genesis_time = eosio::current_time_point(); - config new_config = { - .version = 0, - .chainid = chainid, - .genesis_time = genesis_time, - }; - - // Other fee parameters in new_config are still left at their (undesired) default values. - // Correct those values now using the fee_params passed in as an argument to the init function. + _config->set_version(0); + _config->set_chainid(chainid); + _config->set_genesis_time(genesis_time); - set_fee_parameters(new_config, fee_params, false); // enforce that all fee parameters are specified + // Other fee parameters in new_config are still left at their (undesired) + // default values. Correct those values now using the fee_params passed in as + // an argument to the init function. - _config.set(new_config, get_self()); + _config->set_fee_parameters(fee_params, false); // enforce that all fee parameters are specified inevm_singleton(get_self(), get_self().value).get_or_create(get_self()); - open(get_self()); + open_internal_balance(get_self()); } void evm_contract::setfeeparams(const fee_parameters& fee_params) @@ -99,9 +82,9 @@ void evm_contract::setfeeparams(const fee_parameters& fee_params) assert_inited(); require_auth(get_self()); - config current_config = _config.get(); - set_fee_parameters(current_config, fee_params, true); // do not enforce that all fee parameters are specified - _config.set(current_config, get_self()); + _config->set_fee_parameters( + fee_params, + true); // do not enforce that all fee parameters are specified } void evm_contract::addegress(const std::vector& accounts) { @@ -132,13 +115,13 @@ void evm_contract::freeze(bool value) { eosio::require_auth(get_self()); assert_inited(); - config config2 = _config.get(); + auto status = _config->get_status(); if (value) { - config2.status |= static_cast(status_flags::frozen); + status |= static_cast(status_flags::frozen); } else { - config2.status &= ~static_cast(status_flags::frozen); + status &= ~static_cast(status_flags::frozen); } - _config.set(config2, get_self()); + _config->set_status(status); } void check_result( ValidationResult r, const Transaction& txn, const char* desc ) { @@ -191,10 +174,8 @@ void check_result( ValidationResult r, const Transaction& txn, const char* desc eosio::check( false, std::move(err_msg)); } -Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& tx, silkworm::ExecutionProcessor& ep, bool enforce_chain_id) { - //when being called as an inline action, clutch in allowance for reserved addresses & signatures by setting from_self=true - const bool from_self = get_sender() == get_self(); - +Receipt evm_contract::execute_tx(const runtime_config& rc, eosio::name miner, Block& block, const transaction& txn, silkworm::ExecutionProcessor& ep) { + const auto& tx = txn.get_tx(); balances balance_table(get_self(), get_self().value); if (miner == get_self()) { @@ -218,8 +199,7 @@ Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& bool is_special_signature = silkworm::is_special_signature(tx.r, tx.s); - tx.from.reset(); - tx.recover_sender(); + txn.recover_sender(); eosio::check(tx.from.has_value(), "unable to recover sender"); LOGTIME("EVM RECOVER SENDER"); @@ -228,7 +208,7 @@ Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& // 2 For special signature, we will reject calls not from self // and process the special balance if the tx is from reserved address. if (is_special_signature) { - check(from_self, "bridge signature used outside of bridge transaction"); + check(rc.allow_special_signature, "bridge signature used outside of bridge transaction"); if (is_reserved_address(*tx.from)) { const name ingress_account(*extract_reserved_address(*tx.from)); @@ -247,9 +227,9 @@ Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& } } - // A tx from self with regular signature can potentially from external source. + // A tx from self with regular signature can potentially from external source. // Therefore, only tx with special signature can waive the chain_id check. - if(enforce_chain_id && !is_special_signature) { + if (rc.enforce_chain_id) { check(tx.chain_id.has_value(), "tx without chain-id"); } @@ -268,12 +248,12 @@ Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& uint64_t tx_gas_used = receipt.cumulative_gas_used; // Only transaction in the "block" so cumulative_gas_used is the tx gas_used. intx::uint512 gas_fee = intx::uint256(tx_gas_used) * tx.max_fee_per_gas; check(gas_fee < std::numeric_limits::max(), "too much gas"); - gas_fee *= _config.get().miner_cut; + gas_fee *= _config->get_miner_cut(); gas_fee /= hundred_percent; gas_fee_miner_portion.emplace(static_cast(gas_fee)); } - if(from_self) + if (rc.abort_on_failure) eosio::check(receipt.success, "tx executed inline by contract must succeed"); if(!ep.state().reserved_objects().empty()) { @@ -341,15 +321,14 @@ void evm_contract::exec(const exec_input& input, const std::optional> found_chain_config = lookup_known_chain(current_config.chainid); + std::optional> found_chain_config = lookup_known_chain(_config->get_chainid()); check( found_chain_config.has_value(), "failed to find expected chain config" ); - eosevm::block_mapping bm(current_config.genesis_time.sec_since_epoch()); + eosevm::block_mapping bm(_config->get_genesis_time().sec_since_epoch()); Block block; eosevm::prepare_block_header(block.header, bm, get_self().value, - bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count())); + bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count()), _config->get_evm_version()); evm_runtime::state state{get_self(), get_self(), true}; IntraBlockState ibstate{state}; @@ -388,7 +367,6 @@ void evm_contract::process_filtered_messages(const std::vector(msg.value()); - eosio::print("FIL MESSAGE: ", uint64_t(msg_v0.force_atomic), "\n"); const auto& receiver = msg_v0.get_account_as_name(); eosio::check(eosio::is_account(receiver), "receiver is not account"); @@ -435,36 +413,31 @@ void evm_contract::process_filtered_messages(const std::vector> found_chain_config = lookup_known_chain(current_config.chainid); + auto current_version = _config->get_evm_version_and_maybe_promote(); + + std::optional> found_chain_config = lookup_known_chain(_config->get_chainid()); check( found_chain_config.has_value(), "failed to find expected chain config" ); - eosevm::block_mapping bm(current_config.genesis_time.sec_since_epoch()); + eosevm::block_mapping bm(_config->get_genesis_time().sec_since_epoch()); Block block; - eosevm::prepare_block_header(block.header, bm, get_self().value, - bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count())); + eosevm::prepare_block_header(block.header, bm, get_self().value, + bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count()), current_version); silkworm::protocol::TrustRuleSet engine{*found_chain_config->second}; evm_runtime::state state{get_self(), get_self(), false, false}; silkworm::ExecutionProcessor ep{block, engine, state, *found_chain_config->second}; - Transaction tx; - ByteView bv{(const uint8_t*)rlptx.data(), rlptx.size()}; - eosio::check(rlp::decode(bv,tx) && bv.empty(), "unable to decode transaction"); - LOGTIME("EVM TX DECODE"); - check(tx.max_priority_fee_per_gas == tx.max_fee_per_gas, "max_priority_fee_per_gas must be equal to max_fee_per_gas"); - check(tx.max_fee_per_gas >= current_config.gas_price, "gas price is too low"); + check(tx.max_fee_per_gas >= _config->get_gas_price(), "gas price is too low"); // Filter EVM messages (with data) that are sent to the reserved address // corresponding to the EOS account holding the contract (self) @@ -473,19 +446,57 @@ void evm_contract::pushtx( eosio::name miner, const bytes& rlptx ) { return message.recipient == me && message.input_size > 0; }); - auto receipt = execute_tx(miner, block, tx, ep, true); + auto receipt = execute_tx(rc, miner, block, txn, ep); process_filtered_messages(ep.state().filtered_messages()); engine.finalize(ep.state(), ep.evm().block()); ep.state().write_to_db(ep.evm().block().header.number); + if (current_version >= 1) { + auto event = evmtx_type{evmtx_v0{current_version, txn.get_rlptx()}}; + action(std::vector{}, get_self(), "evmtx"_n, event) + .send(); + } LOGTIME("EVM END"); } +void evm_contract::pushtx(eosio::name miner, bytes rlptx) { + LOGTIME("EVM START0"); + assert_unfrozen(); + + // Use default runtime configuration parameters. + runtime_config rc; + + // Check if the transaction is initiated by the contract itself. + // When the contract calls this as an inline action, it implies a special + // context where certain rules are relaxed or altered. + // 1. Allowance for special signatures: This permits the use of special + // signatures that are otherwise restricted. + // 2. Enforce immediate EOS transaction failure on error: In case of failure + // in the EVM transaction execution, the EOS transaction is set to abort + // immediately. + // 3. Disable chainId enforcement: This is applicable because the RLP + // transaction sent by the contract is a legacy pre-EIP155 transaction that is + // not replay-protected (lacks a chain ID). + // 4. Restrict miner to self: Ensures that if the transaction originates from + // the contract then the miner must also be the contract itself. + if (get_sender() == get_self()) { + rc.allow_special_signature = true; + rc.abort_on_failure = true; + rc.enforce_chain_id = false; + rc.allow_non_self_miner = false; + } + + process_tx(rc, miner, transaction{std::move(rlptx)}); +} + void evm_contract::open(eosio::name owner) { assert_unfrozen(); require_auth(owner); + open_internal_balance(owner); +} +void evm_contract::open_internal_balance(eosio::name owner) { balances balance_table(get_self(), get_self().value); if(balance_table.find(owner.value) == balance_table.end()) balance_table.emplace(owner, [&](balance& a) { @@ -531,7 +542,6 @@ uint64_t evm_contract::get_and_increment_nonce(const name owner) { checksum256 evm_contract::get_code_hash(name account) const { char buff[64]; - eosio::check(internal_use_do_not_use::get_code_hash(account.value, 0, buff, sizeof(buff)) <= sizeof(buff), "get_code_hash() too big"); using start_of_code_hash_return = std::tuple; const auto& [v, s, code_hash] = unpack(buff, sizeof(buff)); @@ -557,10 +567,8 @@ void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& b.balance.balance += quantity; }); - const auto& current_config = _config.get(); - //subtract off the ingress bridge fee from the quantity that will be bridged - quantity -= current_config.ingress_bridge_fee; + quantity -= _config->get_ingress_bridge_fee(); eosio::check(quantity.amount > 0, "must bridge more than ingress bridge fee"); const std::optional address_bytes = from_hex(memo); @@ -572,18 +580,21 @@ void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& Transaction txn; txn.type = TransactionType::kLegacy; txn.nonce = get_and_increment_nonce(get_self()); - txn.max_priority_fee_per_gas = current_config.gas_price; - txn.max_fee_per_gas = current_config.gas_price; + txn.max_priority_fee_per_gas = _config->get_gas_price(); + txn.max_fee_per_gas = _config->get_gas_price(); txn.gas_limit = 21000; txn.to = to_evmc_address(*address_bytes); txn.value = value; txn.r = 0u; // r == 0 is pseudo signature that resolves to reserved address range txn.s = get_self().value; - Bytes rlp; - rlp::encode(rlp, txn); - pushtx_action pushtx_act(get_self(), {{get_self(), "active"_n}}); - pushtx_act.send(get_self(), rlp); + runtime_config rc; + rc.allow_special_signature = true; + rc.abort_on_failure = true; + rc.enforce_chain_id = false; + rc.allow_non_self_miner = false; + + dispatch_tx(rc, transaction{std::move(txn)}); } void evm_contract::transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo) { @@ -627,14 +638,13 @@ bool evm_contract::gc(uint32_t max) { return state.gc(max); } -void evm_contract::call_(intx::uint256 s, const bytes& to, intx::uint256 value, const bytes& data, uint64_t gas_limit, uint64_t nonce) { - const auto& current_config = _config.get(); +void evm_contract::call_(const runtime_config& rc, intx::uint256 s, const bytes& to, intx::uint256 value, const bytes& data, uint64_t gas_limit, uint64_t nonce) { Transaction txn; txn.type = TransactionType::kLegacy; txn.nonce = nonce; - txn.max_priority_fee_per_gas = current_config.gas_price; - txn.max_fee_per_gas = current_config.gas_price; + txn.max_priority_fee_per_gas = _config->get_gas_price(); + txn.max_fee_per_gas = _config->get_gas_price(); txn.gas_limit = gas_limit; txn.value = value; txn.data = Bytes{(const uint8_t*)data.data(), data.size()}; @@ -647,10 +657,17 @@ void evm_contract::call_(intx::uint256 s, const bytes& to, intx::uint256 value, txn.to = to_evmc_address(bv_to); } - Bytes rlp; - rlp::encode(rlp, txn); - pushtx_action pushtx_act(get_self(), {{get_self(), "active"_n}}); - pushtx_act.send(get_self(), rlp); + dispatch_tx(rc, transaction{std::move(txn)}); +} + +void evm_contract::dispatch_tx(const runtime_config& rc, const transaction& tx) { + if (_config->get_evm_version_and_maybe_promote() >= 1) { + process_tx(rc, get_self(), tx); + } else { + eosio::check(rc.allow_special_signature && rc.abort_on_failure && !rc.enforce_chain_id && !rc.allow_non_self_miner, "invalid runtime config"); + pushtx_action pushtx_act(get_self(), {{get_self(), "active"_n}}); + pushtx_act.send(get_self(), tx.get_rlptx()); + } } void evm_contract::call(eosio::name from, const bytes& to, const bytes& value, const bytes& data, uint64_t gas_limit) { @@ -661,7 +678,14 @@ void evm_contract::call(eosio::name from, const bytes& to, const bytes& value, c eosio::check(value.size() == sizeof(intx::uint256), "invalid value"); intx::uint256 v = intx::be::unsafe::load((const uint8_t *)value.data()); - call_(from.value, to, v, data, gas_limit, get_and_increment_nonce(from)); + runtime_config rc { + .allow_special_signature = true, + .abort_on_failure = true, + .enforce_chain_id = false, + .allow_non_self_miner = false + }; + + call_(rc, from.value, to, v, data, gas_limit, get_and_increment_nonce(from)); } void evm_contract::admincall(const bytes& from, const bytes& to, const bytes& value, const bytes& data, uint64_t gas_limit) { @@ -693,8 +717,15 @@ void evm_contract::admincall(const bytes& from, const bytes& to, const bytes& va check(!!account, err_msg_invalid_addr); nonce = account->nonce; } - - call_(s, to, v, data, gas_limit, nonce); + + runtime_config rc { + .allow_special_signature = true, + .abort_on_failure = true, + .enforce_chain_id = false, + .allow_non_self_miner = false + }; + + call_(rc, s, to, v, data, gas_limit, nonce); } void evm_contract::bridgereg(eosio::name receiver, eosio::name handler, const eosio::asset& min_fee) { @@ -747,4 +778,9 @@ void evm_contract::assertnonce(eosio::name account, uint64_t next_nonce) { } } +void evm_contract::setversion(uint64_t version) { + require_auth(get_self()); + _config->set_evm_version(version); +} + } //evm_runtime diff --git a/src/config_wrapper.cpp b/src/config_wrapper.cpp new file mode 100644 index 00000000..c6656176 --- /dev/null +++ b/src/config_wrapper.cpp @@ -0,0 +1,166 @@ +#pragma once +#include +#include + +namespace evm_runtime { + +config_wrapper::config_wrapper(eosio::name self) : _self(self), _config(self, self.value) { + _exists = _config.exists(); + if(_exists) { + _cached_config = _config.get(); + } +} + +config_wrapper::~config_wrapper() { + flush(); +} + +void config_wrapper::flush() { + if(!is_dirty()) { + return; + } + _config.set(_cached_config, _self); + clear_dirty(); + _exists = true; +} + +bool config_wrapper::exists() { + return _exists; +} + +eosio::unsigned_int config_wrapper::get_version()const { + return _cached_config.version; +} + +void config_wrapper::set_version(const eosio::unsigned_int version) { + _cached_config.version = version; + set_dirty(); +} + +uint64_t config_wrapper::get_chainid()const { + return _cached_config.chainid; +} + +void config_wrapper::set_chainid(uint64_t chainid) { + _cached_config.chainid = chainid; + set_dirty(); +} + +const eosio::time_point_sec& config_wrapper::get_genesis_time()const { + return _cached_config.genesis_time; +} + +void config_wrapper::set_genesis_time(eosio::time_point_sec genesis_time) { + _cached_config.genesis_time = genesis_time; + set_dirty(); +} + +const eosio::asset& config_wrapper::get_ingress_bridge_fee()const { + return _cached_config.ingress_bridge_fee; +} + +void config_wrapper::set_ingress_bridge_fee(const eosio::asset& ingress_bridge_fee) { + _cached_config.ingress_bridge_fee = ingress_bridge_fee; + set_dirty(); +} + +uint64_t config_wrapper::get_gas_price()const { + return _cached_config.gas_price; +} + +void config_wrapper::set_gas_price(uint64_t gas_price) { + _cached_config.gas_price = gas_price; + set_dirty(); +} + +uint32_t config_wrapper::get_miner_cut()const { + return _cached_config.miner_cut; +} + +void config_wrapper::set_miner_cut(uint32_t miner_cut) { + _cached_config.miner_cut = miner_cut; + set_dirty(); +} + +uint32_t config_wrapper::get_status()const { + return _cached_config.status; +} + +void config_wrapper::set_status(uint32_t status) { + _cached_config.status = status; + set_dirty(); +} + +uint64_t config_wrapper::get_evm_version()const { + uint64_t current_version = 0; + if(_cached_config.evm_version.has_value()) { + current_version = _cached_config.evm_version->get_version(_cached_config.genesis_time, get_current_time()); + } + return current_version; +} + +uint64_t config_wrapper::get_evm_version_and_maybe_promote() { + uint64_t current_version = 0; + bool promoted = false; + if(_cached_config.evm_version.has_value()) { + std::tie(current_version, promoted) = _cached_config.evm_version->get_version_and_maybe_promote(_cached_config.genesis_time, get_current_time()); + } + if(promoted) set_dirty(); + return current_version; +} + +void config_wrapper::set_evm_version(uint64_t new_version) { + eosio::check(new_version <= eosevm::max_eos_evm_version, "Unsupported version"); + auto current_version = get_evm_version_and_maybe_promote(); + eosio::check(new_version > current_version, "new version must be greater than the active one"); + _cached_config.evm_version.emplace(evm_version_type{evm_version_type::pending{new_version, get_current_time()}, current_version}); + set_dirty(); +} + +void config_wrapper::set_fee_parameters(const fee_parameters& fee_params, + bool allow_any_to_be_unspecified) +{ + if (fee_params.gas_price.has_value()) { + _cached_config.gas_price = *fee_params.gas_price; + } else { + eosio::check(allow_any_to_be_unspecified, "All required fee parameters not specified: missing gas_price"); + } + + if (fee_params.miner_cut.has_value()) { + eosio::check(*fee_params.miner_cut <= hundred_percent, "miner_cut cannot exceed 100,000 (100%)"); + + _cached_config.miner_cut = *fee_params.miner_cut; + } else { + eosio::check(allow_any_to_be_unspecified, "All required fee parameters not specified: missing miner_cut"); + } + + if (fee_params.ingress_bridge_fee.has_value()) { + eosio::check(fee_params.ingress_bridge_fee->symbol == token_symbol, "unexpected bridge symbol"); + eosio::check(fee_params.ingress_bridge_fee->amount >= 0, "ingress bridge fee cannot be negative"); + + _cached_config.ingress_bridge_fee = *fee_params.ingress_bridge_fee; + } + + set_dirty(); +} + +bool config_wrapper::is_dirty()const { + return _dirty; +} + +void config_wrapper::set_dirty() { + _dirty=true; +} + +void config_wrapper::clear_dirty() { + _dirty=false; +} + +time_point config_wrapper::get_current_time()const { + if(!current_time_point.has_value()) { + current_time_point = eosio::current_time_point(); + } + return current_time_point.value(); +} + +} //namespace evm_runtime \ No newline at end of file diff --git a/src/test_actions.cpp b/src/test_actions.cpp index 6e802f35..8f36d9c1 100644 --- a/src/test_actions.cpp +++ b/src/test_actions.cpp @@ -4,7 +4,8 @@ #include #include #include - +#include +#include namespace evm_runtime { using namespace silkworm; @@ -25,7 +26,13 @@ using namespace silkworm; ByteView bv{(const uint8_t*)orlptx->data(), orlptx->size()}; eosio::check(rlp::decode(bv,tx) && bv.empty(), "unable to decode transaction"); - execute_tx(eosio::name{}, block, tx, ep, false); + runtime_config rc { + .allow_special_signature = false, + .abort_on_failure = false, + .enforce_chain_id = false, + .allow_non_self_miner = true + }; + execute_tx(rc, eosio::name{}, block, transaction{std::move(tx)}, ep); } engine.finalize(ep.state(), ep.evm().block()); ep.state().write_to_db(ep.evm().block().header.number); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 707b21f9..ddde4253 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,6 +22,7 @@ include_directories( ) add_eosio_test_executable( unit_test + ${CMAKE_SOURCE_DIR}/version_tests.cpp ${CMAKE_SOURCE_DIR}/account_id_tests.cpp ${CMAKE_SOURCE_DIR}/basic_evm_tester.cpp ${CMAKE_SOURCE_DIR}/evm_runtime_tests.cpp diff --git a/tests/basic_evm_tester.cpp b/tests/basic_evm_tester.cpp index 7b68a471..858b5115 100644 --- a/tests/basic_evm_tester.cpp +++ b/tests/basic_evm_tester.cpp @@ -69,6 +69,25 @@ namespace fc { namespace raw { tmp.flags=0; if(ds.remaining()) { fc::raw::unpack(ds, tmp.flags); } } FC_RETHROW_EXCEPTIONS(warn, "error unpacking partial_account_table_row") } + + template<> + inline void unpack( datastream& ds, evm_test::config_table_row& tmp) + { try { + fc::raw::unpack(ds, tmp.version); + fc::raw::unpack(ds, tmp.chainid); + fc::raw::unpack(ds, tmp.genesis_time); + fc::raw::unpack(ds, tmp.ingress_bridge_fee); + fc::raw::unpack(ds, tmp.gas_price); + fc::raw::unpack(ds, tmp.miner_cut); + fc::raw::unpack(ds, tmp.status); + + tmp.evm_version = {}; + if(ds.remaining()) { + evm_test::evm_version_type version; + fc::raw::unpack(ds, version); + tmp.evm_version.emplace(version); + } + } FC_RETHROW_EXCEPTIONS(warn, "error unpacking partial_account_table_row") } }} @@ -327,7 +346,7 @@ transaction_trace_ptr basic_evm_tester::exec(const exec_input& input, const std: return basic_evm_tester::push_action(evm_account_name, "exec"_n, evm_account_name, bytes{binary_data.begin(), binary_data.end()}); } -void basic_evm_tester::call(name from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor) +transaction_trace_ptr basic_evm_tester::call(name from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor) { bytes to_bytes; to_bytes.resize(to.size()); @@ -341,10 +360,10 @@ void basic_evm_tester::call(name from, const evmc::bytes& to, const evmc::bytes& value_bytes.resize(value.size()); memcpy(value_bytes.data(), value.data(), value.size()); - push_action(evm_account_name, "call"_n, actor, mvo()("from", from)("to", to_bytes)("value", value_bytes)("data", data_bytes)("gas_limit", gas_limit)); + return push_action(evm_account_name, "call"_n, actor, mvo()("from", from)("to", to_bytes)("value", value_bytes)("data", data_bytes)("gas_limit", gas_limit)); } -void basic_evm_tester::admincall(const evmc::bytes& from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor) +transaction_trace_ptr basic_evm_tester::admincall(const evmc::bytes& from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor) { bytes to_bytes; to_bytes.resize(to.size()); @@ -362,7 +381,7 @@ void basic_evm_tester::admincall(const evmc::bytes& from, const evmc::bytes& to, value_bytes.resize(value.size()); memcpy(value_bytes.data(), value.data(), value.size()); - push_action(evm_account_name, "admincall"_n, actor, mvo()("from", from_bytes)("to", to_bytes)("value", value_bytes)("data", data_bytes)("gas_limit", gas_limit)); + return push_action(evm_account_name, "admincall"_n, actor, mvo()("from", from_bytes)("to", to_bytes)("value", value_bytes)("data", data_bytes)("gas_limit", gas_limit)); } transaction_trace_ptr basic_evm_tester::bridgereg(name receiver, name handler, asset min_fee, vector extra_signers) { @@ -395,6 +414,11 @@ transaction_trace_ptr basic_evm_tester::pushtx(const silkworm::Transaction& trx, return push_action(evm_account_name, "pushtx"_n, miner, mvo()("miner", miner)("rlptx", rlp_bytes)); } +transaction_trace_ptr basic_evm_tester::setversion(uint64_t version, name actor) { + return basic_evm_tester::push_action(evm_account_name, "setversion"_n, actor, + mvo()("version", version)); +} + transaction_trace_ptr basic_evm_tester::rmgcstore(uint64_t id, name actor) { return basic_evm_tester::push_action(evm_account_name, "rmgcstore"_n, actor, mvo()("id", id)); diff --git a/tests/basic_evm_tester.hpp b/tests/basic_evm_tester.hpp index 27c8efe9..85afe70f 100644 --- a/tests/basic_evm_tester.hpp +++ b/tests/basic_evm_tester.hpp @@ -48,6 +48,23 @@ void to_variant(const evmc::address& o, fc::variant& v); namespace evm_test { +struct evmtx_v0 { + uint64_t eos_evm_version; + bytes rlptx; +}; + +using evmtx_type = std::variant; + +struct evm_version_type { + struct pending { + uint64_t version; + fc::time_point time; + }; + + std::optional pending_version; + uint64_t cached_version=0; +}; + struct config_table_row { unsigned_int version; @@ -57,6 +74,7 @@ struct config_table_row uint64_t gas_price; uint32_t miner_cut; uint32_t status; + std::optional evm_version; }; struct config2_table_row @@ -159,8 +177,9 @@ using bridge_message = std::variant; } // namespace evm_test -FC_REFLECT(evm_test::config_table_row, - (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)) +FC_REFLECT(evm_test::config_table_row, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)(evm_version)) +FC_REFLECT(evm_test::evm_version_type, (pending_version)(cached_version)) +FC_REFLECT(evm_test::evm_version_type::pending, (version)(time)) FC_REFLECT(evm_test::config2_table_row,(next_account_id)) FC_REFLECT(evm_test::balance_and_dust, (balance)(dust)); FC_REFLECT(evm_test::account_object, (id)(address)(nonce)(balance)) @@ -175,6 +194,7 @@ FC_REFLECT(evm_test::message_receiver, (account)(handler)(min_fee)(flags)); FC_REFLECT(evm_test::bridge_message_v0, (receiver)(sender)(timestamp)(value)(data)); FC_REFLECT(evm_test::gcstore, (id)(storage_id)); FC_REFLECT(evm_test::account_code, (id)(ref_count)(code)(code_hash)); +FC_REFLECT(evm_test::evmtx_v0, (eos_evm_version)(rlptx)); namespace evm_test { class evm_eoa @@ -379,8 +399,9 @@ class basic_evm_tester : public evm_validating_tester transaction_trace_ptr exec(const exec_input& input, const std::optional& callback); transaction_trace_ptr assertnonce(name account, uint64_t next_nonce); transaction_trace_ptr pushtx(const silkworm::Transaction& trx, name miner = evm_account_name); - void call(name from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); - void admincall(const evmc::bytes& from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); + transaction_trace_ptr setversion(uint64_t version, name actor); + transaction_trace_ptr call(name from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); + transaction_trace_ptr admincall(const evmc::bytes& from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); evmc::address deploy_contract(evm_eoa& eoa, evmc::bytes bytecode); void addegress(const std::vector& accounts); diff --git a/tests/silkworm/core/silkworm/common/util.hpp b/tests/silkworm/core/silkworm/common/util.hpp index 67de428c..b3e7cb6a 100644 --- a/tests/silkworm/core/silkworm/common/util.hpp +++ b/tests/silkworm/core/silkworm/common/util.hpp @@ -106,6 +106,22 @@ inline ethash::hash256 keccak256(ByteView view) { return ethash::keccak256(view. // Splits a string by delimiter and returns a vector of tokens std::vector split(std::string_view source, std::string_view delimiter); +inline std::optional extract_reserved_address(const evmc::address& addr) { + constexpr uint8_t reserved_address_prefix[] = {0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb}; + + if(!std::equal(std::begin(reserved_address_prefix), std::end(reserved_address_prefix), static_cast(addr).begin())) + return std::nullopt; + uint64_t reserved; + memcpy(&reserved, static_cast(addr).data()+sizeof(reserved_address_prefix), sizeof(reserved)); + return be64toh(reserved); +} + +inline bool is_reserved_address(const evmc::address& addr) { + return extract_reserved_address(addr) != std::nullopt; +} + inline evmc::address make_reserved_address(uint64_t account) { return evmc_address({0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, @@ -120,6 +136,28 @@ inline evmc::address make_reserved_address(uint64_t account) { static_cast(account >> 0)}); } +inline evmc::address decode_special_signature(const intx::uint256& s) { + // Assumen already tested by is_special_signature() + if (s <= std::numeric_limits::max()) { + return make_reserved_address(static_cast(s)); + } + else { + evmc::address from = evmc::address{}; + intx::be::trunc(from.bytes, s); + return from; + } +} + +inline bool is_special_signature(const intx::uint256& r, const intx::uint256& s) { + // s contains a regular evm_address if padded with '1's + // otherwise it should be an eos name + return r == 0 && + (s <= std::numeric_limits::max() || + s >> kAddressLength * 8 == (~intx::uint256(0)) >> kAddressLength * 8); +} + + + } // namespace silkworm -#endif // SILKWORM_COMMON_UTIL_HPP_ \ No newline at end of file +#endif // SILKWORM_COMMON_UTIL_HPP_ diff --git a/tests/silkworm/core/silkworm/types/transaction.cpp b/tests/silkworm/core/silkworm/types/transaction.cpp index eff2497b..9e3fd74d 100644 --- a/tests/silkworm/core/silkworm/types/transaction.cpp +++ b/tests/silkworm/core/silkworm/types/transaction.cpp @@ -380,6 +380,12 @@ void Transaction::recover_sender() { if (from.has_value()) { return; } + + if(is_special_signature(r, s)) { + from = decode_special_signature(s); + return; + } + Bytes rlp{}; rlp::encode(rlp, *this, /*for_signing=*/true, /*wrap_eip2718_into_array=*/false); ethash::hash256 hash{keccak256(rlp)}; diff --git a/tests/version_tests.cpp b/tests/version_tests.cpp new file mode 100644 index 00000000..499f1477 --- /dev/null +++ b/tests/version_tests.cpp @@ -0,0 +1,423 @@ +#include "basic_evm_tester.hpp" +#include +#include +#include + +using namespace evm_test; +using eosio::testing::eosio_assert_message_is; + +struct version_tester : basic_evm_tester { + + static constexpr char* increment_ = "0xd09de08a"; // sha3(increment())[:4] + static constexpr char* retrieve_ = "0x0a79309b"; // sha3(retrieve(address))[:4] + + version_tester() { + create_accounts({"alice"_n}); + transfer_token(faucet_account_name, "alice"_n, make_asset(10000'0000)); + init(); + } + + /* + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.8.2 <0.9.0; + contract Increment { + mapping (address => uint256) values; + function increment() public { + values[msg.sender]++; + } + function retrieve(address a) public view returns (uint256){ + return values[a]; + } + } + */ + + const std::string contract_bytecode = + "608060405234801561001057600080fd5b50610284806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80630a79309b1461003b578063d09de08a1461006b575b600080fd5b61005560048036038101906100509190610176565b610075565b60405161006291906101bc565b60405180910390f35b6100736100bd565b005b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600081548092919061010c90610206565b9190505550565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061014382610118565b9050919050565b61015381610138565b811461015e57600080fd5b50565b6000813590506101708161014a565b92915050565b60006020828403121561018c5761018b610113565b5b600061019a84828501610161565b91505092915050565b6000819050919050565b6101b6816101a3565b82525050565b60006020820190506101d160008301846101ad565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610211826101a3565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610243576102426101d7565b5b60018201905091905056fea26469706673582212205ef4ef3ff462d43a7ce545531c2f77184355d61dcc93de7deb2d57696ceb073164736f6c63430008110033"; + + auto deploy_contract(evm_eoa& eoa, evmc::bytes bytecode) + { + uint64_t nonce = eoa.next_nonce; + + const auto gas_price = get_config().gas_price; + + silkworm::Transaction tx{ + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = gas_price, + .max_fee_per_gas = gas_price, + .gas_limit = 10'000'000, + .data = std::move(bytecode), + }; + + eoa.sign(tx); + auto trace = pushtx(tx); + + return std::make_tuple(trace, silkworm::create_address(eoa.address, nonce)); + } + + auto deploy_test_contract(evm_eoa& eoa) { + return deploy_contract(eoa, evmc::from_hex(contract_bytecode).value()); + } + + intx::uint256 retrieve(const evmc::address& contract_addr, const evmc::address& account) { + exec_input input; + input.context = {}; + input.to = bytes{std::begin(contract_addr.bytes), std::end(contract_addr.bytes)}; + + silkworm::Bytes data; + data += evmc::from_hex(retrieve_).value(); + data += silkworm::to_bytes32(account); + input.data = bytes{data.begin(), data.end()}; + + auto res = exec(input, {}); + auto out = fc::raw::unpack(res->action_traces[0].return_value); + BOOST_REQUIRE(out.status == 0); + BOOST_REQUIRE(out.data.size() == 32); + + auto count = intx::be::unsafe::load(reinterpret_cast(out.data.data())); + return count; + } + +}; + +BOOST_AUTO_TEST_SUITE(verion_evm_tests) +BOOST_FIXTURE_TEST_CASE(set_version, version_tester) try { + + auto config = get_config(); + eosevm::block_mapping bm(config.genesis_time.sec_since_epoch()); + + BOOST_REQUIRE_EXCEPTION(setversion(0, "alice"_n), + missing_auth_exception, eosio::testing::fc_exception_message_starts_with("missing authority")); + + BOOST_REQUIRE_EXCEPTION(setversion(0, evm_account_name), + eosio_assert_message_exception, + eosio_assert_message_is("new version must be greater than the active one")); + + BOOST_REQUIRE_EXCEPTION(setversion(2, evm_account_name), + eosio_assert_message_exception, + eosio_assert_message_is("Unsupported version")); + + setversion(1, evm_account_name); + config = get_config(); + + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 0); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), true); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().time == control->pending_block_time()); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().version == 1); + + // Fund evm1 address with 100 EOS (this will NOT trigger an update of the evm version) + evm_eoa evm1; + const int64_t to_bridge = 1000000; + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + config = get_config(); + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 0); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), true); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().time == control->pending_block_time()); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().version == 1); + + // Produce blocks until the next EVM block (pending version activation) + const auto& pending_version = config.evm_version.value().pending_version.value(); + auto b0 = bm.timestamp_to_evm_block_num(pending_version.time.time_since_epoch().count()); + while(bm.timestamp_to_evm_block_num(control->pending_block_time().time_since_epoch().count()) <= b0) { + produce_block(); + } + + // Fund evm1 address with 100 more EOS (this will trigger an update of the evm version) + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + config = get_config(); + + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 1); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), false); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(traces_in_different_eosevm_version, version_tester) try { + + auto config = get_config(); + + evm_eoa evm1; + const int64_t to_bridge = 1000000; + + // Open alice internal balance + open("alice"_n); + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), "alice"); + auto alice_addr = make_reserved_address("alice"_n.to_uint64_t()); + + // Fund evm1 address + // Test traces of `handle_evm_transfer` (EVM VERSION=0) + auto trace = transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + BOOST_REQUIRE(trace->action_traces.size() == 4); + BOOST_REQUIRE(trace->action_traces[0].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[2].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[2].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[3].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[3].act.name == "pushtx"_n); + + // Test traces of `pushtx` (EVM VERSION=0) + auto [trace2, contract_address] = deploy_test_contract(evm1); + BOOST_REQUIRE(trace2->action_traces.size() == 1); + BOOST_REQUIRE(trace2->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace2->action_traces[0].act.name == "pushtx"_n); + + auto to = evmc::bytes{std::begin(contract_address.bytes), std::end(contract_address.bytes)}; + auto data = evmc::from_hex(increment_); + + // Test traces of `call` (EVM VERSION=0) + trace = call("alice"_n, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, "alice"_n); + BOOST_REQUIRE(trace->action_traces.size() == 2); + BOOST_REQUIRE(trace->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "call"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "pushtx"_n); + + // Test traces of `admincall` (EVM VERSION=0) + auto from = evmc::bytes{std::begin(alice_addr.bytes), std::end(alice_addr.bytes)}; + trace = admincall(from, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, evm_account_name); + BOOST_REQUIRE(trace->action_traces.size() == 2); + BOOST_REQUIRE(trace->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "admincall"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "pushtx"_n); + + ///////////////////////////////////// + /// change EOS EVM VERSION => 1 /// + ///////////////////////////////////// + + setversion(1, evm_account_name); + produce_blocks(2); + + auto get_tx_from_trace = [&](const bytes& v) { + auto evmtx_v = fc::raw::unpack(v.data(), v.size()); + + BOOST_REQUIRE(std::holds_alternative(evmtx_v)); + + const auto& evmtx = std::get(evmtx_v); + BOOST_REQUIRE(evmtx.eos_evm_version == 1); + + silkworm::Transaction tx; + silkworm::ByteView bv{(const uint8_t*)evmtx.rlptx.data(), evmtx.rlptx.size()}; + silkworm::rlp::decode(bv, tx); + BOOST_REQUIRE(bv.empty()); + + return tx; + }; + + // Test traces of `handle_evm_transfer` (EVM VERSION=1) + trace = transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + BOOST_REQUIRE(trace->action_traces.size() == 4); + BOOST_REQUIRE(trace->action_traces[0].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[2].act.account == token_account_name); + BOOST_REQUIRE(trace->action_traces[2].act.name == "transfer"_n); + BOOST_REQUIRE(trace->action_traces[3].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[3].act.name == "evmtx"_n); + + auto tx = get_tx_from_trace(trace->action_traces[3].act.data); + BOOST_REQUIRE(tx.value == intx::uint256(balance_and_dust{make_asset(to_bridge), 0})); + BOOST_REQUIRE(tx.to == evm1.address); + + // Test traces of `pushtx` (EVM VERSION=1) + silkworm::Transaction txin { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = config.gas_price, + .max_fee_per_gas = config.gas_price, + .gas_limit = 10'000'000, + .to = contract_address, + .data = *data + }; + + evm1.sign(txin); + trace = pushtx(txin); + + BOOST_REQUIRE(trace->action_traces.size() == 2); + BOOST_REQUIRE(trace->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "pushtx"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "evmtx"_n); + + auto txout = get_tx_from_trace(trace->action_traces[1].act.data); + BOOST_REQUIRE(txout == txin); + BOOST_REQUIRE(retrieve(contract_address, evm1.address) == intx::uint256(1)); + + // Test traces of `call` (EVM VERSION=1) + trace = call("alice"_n, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, "alice"_n); + BOOST_REQUIRE(trace->action_traces.size() == 2); + BOOST_REQUIRE(trace->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "call"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "evmtx"_n); + + tx = get_tx_from_trace(trace->action_traces[1].act.data); + tx.recover_sender(); + + BOOST_REQUIRE(tx.value == intx::uint256(0)); + BOOST_REQUIRE(tx.to == contract_address); + BOOST_REQUIRE(tx.data == *data); + BOOST_REQUIRE(*tx.from == alice_addr); + + // Test traces of `admincall` (EVM VERSION=1) + trace = admincall(from, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, evm_account_name); + BOOST_REQUIRE(trace->action_traces.size() == 2); + BOOST_REQUIRE(trace->action_traces[0].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[0].act.name == "admincall"_n); + BOOST_REQUIRE(trace->action_traces[1].act.account == evm_account_name); + BOOST_REQUIRE(trace->action_traces[1].act.name == "evmtx"_n); + + tx = get_tx_from_trace(trace->action_traces[1].act.data); + tx.recover_sender(); + + BOOST_REQUIRE(tx.value == intx::uint256(0)); + BOOST_REQUIRE(tx.to == contract_address); + BOOST_REQUIRE(tx.data == *data); + BOOST_REQUIRE(*tx.from == alice_addr); + + // Check 4 times call to `increment` from alice_addr + BOOST_REQUIRE(retrieve(contract_address, alice_addr) == intx::uint256(4)); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(exec_does_not_update_version, version_tester) try { + auto config = get_config(); + + evm_eoa evm1; + const int64_t to_bridge = 1000000; + + // Fund evm1 address + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + // Deploy contract + auto [_, contract_address] = deploy_test_contract(evm1); + + // Call `increment` from evm1 address + silkworm::Transaction txin { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = config.gas_price, + .max_fee_per_gas = config.gas_price, + .gas_limit = 10'000'000, + .to = contract_address, + .data = *evmc::from_hex(increment_) + }; + + evm1.sign(txin); + pushtx(txin); + + setversion(1, evm_account_name); + auto activation_time = control->pending_block_time(); + produce_blocks(10); + + // Use `exec` action + BOOST_REQUIRE(retrieve(contract_address, evm1.address) == intx::uint256(1)); + + // Validate config still at 0 + config = get_config(); + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 0); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), true); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().time == activation_time); + BOOST_REQUIRE(config.evm_version.value().pending_version.value().version == 1); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(call_promote_pending, version_tester) try { + auto config = get_config(); + + evm_eoa evm1; + const int64_t to_bridge = 1000000; + + // Open alice internal balance + open("alice"_n); + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), "alice"); + auto alice_addr = make_reserved_address("alice"_n.to_uint64_t()); + + // Fund evm1 address + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + // Deploy contract + auto [_, contract_address] = deploy_test_contract(evm1); + + // Call `increment` from evm1 address + silkworm::Transaction txin { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = config.gas_price, + .max_fee_per_gas = config.gas_price, + .gas_limit = 10'000'000, + .to = contract_address, + .data = *evmc::from_hex(increment_) + }; + + evm1.sign(txin); + pushtx(txin); + + setversion(1, evm_account_name); + produce_blocks(2); + + // Call increment as allice using `call` action + auto to = evmc::bytes{std::begin(contract_address.bytes), std::end(contract_address.bytes)}; + auto data = evmc::from_hex(increment_); + call("alice"_n, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, "alice"_n); + + // Validate config is now 1 + config = get_config(); + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 1); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), false); + +} FC_LOG_AND_RETHROW() + + +BOOST_FIXTURE_TEST_CASE(admincall_promote_pending, version_tester) try { + auto config = get_config(); + + evm_eoa evm1; + const int64_t to_bridge = 1000000; + + // Open alice internal balance + open("alice"_n); + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), "alice"); + auto alice_addr = make_reserved_address("alice"_n.to_uint64_t()); + + // Fund evm1 address + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + // Deploy contract + auto [_, contract_address] = deploy_test_contract(evm1); + + // Call `increment` from evm1 address + silkworm::Transaction txin { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = config.gas_price, + .max_fee_per_gas = config.gas_price, + .gas_limit = 10'000'000, + .to = contract_address, + .data = *evmc::from_hex(increment_) + }; + + evm1.sign(txin); + pushtx(txin); + + setversion(1, evm_account_name); + produce_blocks(2); + + // Call increment as allice using `call` action + auto to = evmc::bytes{std::begin(contract_address.bytes), std::end(contract_address.bytes)}; + auto data = evmc::from_hex(increment_); + auto from = evmc::bytes{std::begin(alice_addr.bytes), std::end(alice_addr.bytes)}; + admincall(from, to, silkworm::Bytes(evmc::bytes32{}), *data, 1000000, evm_account_name); + + // Validate config is now 1 + config = get_config(); + BOOST_CHECK_EQUAL(config.evm_version.has_value(), true); + BOOST_CHECK_EQUAL(config.evm_version.value().cached_version, 1); + BOOST_CHECK_EQUAL(config.evm_version.value().pending_version.has_value(), false); + +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END()