diff --git a/CMakeLists.txt b/CMakeLists.txt index a98cd5cb..7429d86b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,7 +42,7 @@ HunterGate( ) project(trustevm_node) -set(PROJECT_VERSION 0.2.1) +set(PROJECT_VERSION 0.3.0) string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" _ ${PROJECT_VERSION}) set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1}) diff --git a/cmd/block_conversion_plugin.cpp b/cmd/block_conversion_plugin.cpp index 056f89b8..e2871ff1 100644 --- a/cmd/block_conversion_plugin.cpp +++ b/cmd/block_conversion_plugin.cpp @@ -52,20 +52,27 @@ class block_conversion_plugin_impl : std::enable_shared_from_thisnonce.data()); - // TODO: Consider redefining genesis nonce to be in units of seconds rather than milliseconds? Or just hardcode to 1 second? + bm.emplace(genesis_header->timestamp, 1); // Hardcoded to 1 second block interval - if (block_interval_ms == 0 || block_interval_ms % 1000 != 0) { - SILK_CRIT << "Genesis nonce is invalid. Must be a positive multiple of 1000 representing the block interval in milliseconds. " - "Instead got: " << block_interval_ms; + SILK_INFO << "Block interval (in seconds): " << bm->block_interval; + SILK_INFO << "Genesis timestamp (in seconds since Unix epoch): " << bm->genesis_timestamp; + + // The nonce in the genesis header encodes the name of the Antelope account on which the EVM contract has been deployed. + // This name is necessary to determine which reserved address to use as the beneficiary of the blocks. + evm_contract_name = silkworm::endian::load_big_u64(genesis_header->nonce.data()); + + SILK_INFO << "Genesis nonce (as hex): " << silkworm::to_hex(evm_contract_name, true); + SILK_INFO << "Genesis nonce (as Antelope name): " << eosio::name{evm_contract_name}.to_string(); + + if (evm_contract_name == 0 || (evm_contract_name == 1000)) { + // TODO: Remove the (evm_contract_name == 1000) condition once we feel comfortable other tests and scripts have been + // updated to reflect this new meaning of the nonce (used to be block interval in milliseconds). + + SILK_CRIT << "Genesis nonce does not represent a valid Antelope account name. " + "It must be the name of the account on which the EVM contract is deployed"; sys::error("Invalid genesis nonce"); return; } - - bm.emplace(genesis_header->timestamp, block_interval_ms/1e3); - - SILK_INFO << "Block interval: " << bm->block_interval; - SILK_INFO << "Genesis timestamp: " << bm->genesis_timestamp; } evmc::bytes32 compute_transaction_root(const silkworm::BlockBody& body) { @@ -84,11 +91,8 @@ class block_conversion_plugin_impl : std::enable_shared_from_this bm; + uint64_t evm_contract_name = 0; }; block_conversion_plugin::block_conversion_plugin() : my(new block_conversion_plugin_impl()) {} diff --git a/cmd/block_conversion_plugin.hpp b/cmd/block_conversion_plugin.hpp index bbee8c98..e6cadf78 100644 --- a/cmd/block_conversion_plugin.hpp +++ b/cmd/block_conversion_plugin.hpp @@ -7,11 +7,11 @@ #include struct pushtx { - eosio::name ram_payer; + eosio::name miner; std::vector rlpx; }; -EOSIO_REFLECT(pushtx, ram_payer, rlpx) +EOSIO_REFLECT(pushtx, miner, rlpx) class block_conversion_plugin : public appbase::plugin { public: APPBASE_PLUGIN_REQUIRES((sys_plugin)(ship_receiver_plugin)(engine_plugin)); diff --git a/cmd/contract_common/evm_common/block_mapping.hpp b/cmd/contract_common/evm_common/block_mapping.hpp index b865e0b0..3687101f 100644 --- a/cmd/contract_common/evm_common/block_mapping.hpp +++ b/cmd/contract_common/evm_common/block_mapping.hpp @@ -1,52 +1,74 @@ -#pragma once - #include +#include +#include namespace evm_common { -struct block_mapping { - +struct block_mapping +{ /** * @brief Construct object that maps from Antelope timestamp to EVM block number and timestamp - * + * * @param genesis_timestamp_sec - the EVM genesis timestamp in seconds * @param block_interval_sec - time interval between consecutive EVM blocks in seconds (must be positive) - * - * @note The timestamp of the Antelope block containing the init action rounded down to the nearest second must equal genesis_timestamp_sec. + * + * @note The timestamp of the Antelope block containing the init action rounded down to the nearest second must equal + * genesis_timestamp_sec. */ - block_mapping(uint64_t genesis_timestamp_sec, uint32_t block_interval_sec = 1) - : block_interval(block_interval_sec == 0 ? 1 : block_interval_sec), - genesis_timestamp(genesis_timestamp_sec) + block_mapping(uint64_t genesis_timestamp_sec, uint32_t block_interval_sec = 1) : + block_interval(block_interval_sec == 0 ? 1 : block_interval_sec), genesis_timestamp(genesis_timestamp_sec) {} - const uint64_t block_interval; // seconds - const uint64_t genesis_timestamp; // seconds + const uint64_t block_interval; // seconds + const uint64_t genesis_timestamp; // seconds /** * @brief Map Antelope timestamp to EVM block num - * + * * @param timestamp - Antelope timestamp in microseconds * @return mapped EVM block number (returns 0 for all timestamps prior to the genesis timestamp) */ - inline uint32_t timestamp_to_evm_block_num(uint64_t timestamp_us) const { + inline uint32_t timestamp_to_evm_block_num(uint64_t timestamp_us) const + { uint64_t timestamp = timestamp_us / 1e6; // map Antelope block timestamp to EVM block timestamp - if( timestamp < genesis_timestamp ) { + if (timestamp < genesis_timestamp) { // There should not be any transactions prior to the init action. - // But any entity with an associated timestamp prior to the genesis timestamp can be considered as part of the genesis block. + // But any entity with an associated timestamp prior to the genesis timestamp can be considered as part of the + // genesis block. return 0; } - return 1 + (timestamp - genesis_timestamp) / block_interval; + return 1 + (timestamp - genesis_timestamp) / block_interval; } /** * @brief Map EVM block num to EVM block timestamp - * + * * @param block_num - EVM block number * @return EVM block timestamp associated with the given EVM block number */ - inline uint64_t evm_block_num_to_evm_timestamp(uint32_t block_num) const { + inline uint64_t evm_block_num_to_evm_timestamp(uint32_t block_num) const + { return genesis_timestamp + block_num * block_interval; } }; -} // namespace evm_common \ No newline at end of file +/** + * @brief Prepare block header + * + * Modifies header by setting the common fields shared between the contract code and the EVM node. + * It sets the beneficiary, difficulty, number, gas_limit, and timestamp fields only. + */ +inline void prepare_block_header(silkworm::BlockHeader& header, + const block_mapping& bm, + uint64_t evm_contract_name, + uint32_t evm_block_num) +{ + header.beneficiary = silkworm::make_reserved_address(evm_contract_name); + header.difficulty = 1; + header.number = evm_block_num; + header.gas_limit = 0x7ffffffffff; + header.timestamp = bm.evm_block_num_to_evm_timestamp(header.number); +} + + +} // namespace evm_common \ No newline at end of file diff --git a/contract/include/evm_runtime/eosio.token.hpp b/contract/include/evm_runtime/eosio.token.hpp index 295946dd..37010497 100644 --- a/contract/include/evm_runtime/eosio.token.hpp +++ b/contract/include/evm_runtime/eosio.token.hpp @@ -29,6 +29,13 @@ namespace eosio { const asset& quantity, const string& memo ); + //exists to send along a std::basic_string as a memo without copying over to string first + [[eosio::action]] + void transferb( const name& from, + const name& to, + const asset& quantity, + const std::basic_string& memo ); + [[eosio::action]] void open( const name& owner, const symbol& symbol, const name& ram_payer ); @@ -39,6 +46,7 @@ namespace eosio { using issue_action = eosio::action_wrapper<"issue"_n, &token::issue>; using retire_action = eosio::action_wrapper<"retire"_n, &token::retire>; using transfer_action = eosio::action_wrapper<"transfer"_n, &token::transfer>; + using transfer_bytes_memo_action = eosio::action_wrapper<"transfer"_n, &token::transferb>; using open_action = eosio::action_wrapper<"open"_n, &token::open>; using close_action = eosio::action_wrapper<"close"_n, &token::close>; }; diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index e9a09edb..c13e1c84 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -14,82 +14,144 @@ using namespace eosio; namespace evm_runtime { -CONTRACT evm_contract : public contract { - public: - using contract::contract; +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. + }; + + /** + * @brief Initialize EVM contract + * + * @param chainid Chain ID of the EVM. Choose 15555 for a production network. + * For test networks, choose either 15556 for a public test network or 25555 for a local test + * network. + * @param fee_params See documentation of fee_parameters struct. + */ + [[eosio::action]] void init(const uint64_t chainid, const fee_parameters& fee_params); + + /** + * @brief Change fee parameter values + * + * @param fee_params If a member of fee params is empty, the existing value of that parameter in state is not + * changed. + */ + [[eosio::action]] void setfeeparams(const fee_parameters& fee_params); + + [[eosio::action]] void addegress(const std::vector& accounts); + + [[eosio::action]] void removeegress(const std::vector& accounts); + + /** + * @brief Freeze (or unfreeze) ability for user interaction with EVM contract. + * + * If the contract is in a frozen state, users are not able to deposit or withdraw tokens to/from the contract + * whether via opening a balance and transferring into it, using the withdraw action, or using the EVM bridge. + * Additionally, if the contract is in a frozen state, the pushtx action is rejected. + * + * @param value If true, puts the contract into a frozen state. If false, puts the contract into an unfrozen + * state. + */ + [[eosio::action]] void freeze(bool value); + + [[eosio::action]] void pushtx(eosio::name miner, const bytes& rlptx); + + [[eosio::action]] void open(eosio::name owner); + + [[eosio::action]] void close(eosio::name owner); + + [[eosio::on_notify(TOKEN_ACCOUNT_NAME "::transfer")]] void + transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo); + + [[eosio::action]] void withdraw(eosio::name owner, eosio::asset quantity); + + /// @return true if all garbage has been collected + [[eosio::action]] bool gc(uint32_t max); - [[eosio::action]] - void init(const uint64_t chainid); +#ifdef WITH_TEST_ACTIONS + [[eosio::action]] void testtx(const std::optional& orlptx, const evm_runtime::test::block_info& bi); + [[eosio::action]] void + updatecode(const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code); + [[eosio::action]] void updateaccnt(const bytes& address, const bytes& initial, const bytes& current); + [[eosio::action]] void updatestore( + const bytes& address, uint64_t incarnation, const bytes& location, const bytes& initial, const bytes& current); + [[eosio::action]] void dumpstorage(const bytes& addy); + [[eosio::action]] void clearall(); + [[eosio::action]] void dumpall(); + [[eosio::action]] void setbal(const bytes& addy, const bytes& bal); + [[eosio::action]] void testbaldust(const name test); +#endif - [[eosio::action]] - void freeze(bool value); + 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 - [[eosio::action]] - void pushtx(eosio::name ram_payer, const bytes& rlptx); + EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)); + }; - [[eosio::action]] - void open(eosio::name owner, eosio::name ram_payer); +private: + enum class status_flags : uint32_t + { + frozen = 0x1 + }; - [[eosio::action]] - void close(eosio::name owner); + eosio::singleton<"config"_n, config> _config{get_self(), get_self().value}; - [[eosio::on_notify("eosio.token::transfer")]] - void transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo); + void assert_inited() + { + check(_config.exists(), "contract not initialized"); + check(_config.get().version == 0u, "unsupported configuration singleton"); + } - [[eosio::action]] - void withdraw(eosio::name owner, eosio::asset quantity); - - /// @return true if all garbage has been collected - [[eosio::action]] - bool gc(uint32_t max); + void assert_unfrozen() + { + assert_inited(); + check((_config.get().status & static_cast(status_flags::frozen)) == 0, "contract is frozen"); + } -#ifdef WITH_TEST_ACTIONS - ACTION testtx( const std::optional& orlptx, const evm_runtime::test::block_info& bi ); - ACTION updatecode( const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code); - ACTION updateaccnt(const bytes& address, const bytes& initial, const bytes& current); - ACTION updatestore(const bytes& address, uint64_t incarnation, const bytes& location, const bytes& initial, const bytes& current); - ACTION dumpstorage(const bytes& addy); - ACTION clearall(); - ACTION dumpall(); - ACTION setbal(const bytes& addy, const bytes& bal); -#endif - private: - struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { - name owner; - asset balance; - uint64_t dust = 0; - - uint64_t primary_key() const { return owner.value; } - }; - - typedef eosio::multi_index<"accounts"_n, account> accounts; - - enum class status_flags : uint32_t { - frozen = 0x1 - }; - - struct [[eosio::table]] [[eosio::contract("evm_contract")]] config { - eosio::unsigned_int version; //placeholder for future variant index - uint64_t chainid = 0; - time_point_sec genesis_time; - uint32_t status = 0; // <- bit mask values from status_flags - }; - EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(status)); - - 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"); - } - - silkworm::Receipt execute_tx( silkworm::Block& block, const bytes& rlptx, silkworm::ExecutionProcessor& ep ); + silkworm::Receipt execute_tx(eosio::name miner, silkworm::Block& block, silkworm::Transaction& tx, silkworm::ExecutionProcessor& ep); + + uint64_t get_and_increment_nonce(const name owner); + + checksum256 get_code_hash(name account) const; + + void handle_account_transfer(const eosio::asset& quantity, const std::string& memo); + void handle_evm_transfer(eosio::asset quantity, const std::string& memo); + + // 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>; }; -} //evm_runtime +} // 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/contract/include/evm_runtime/intrinsics.hpp b/contract/include/evm_runtime/intrinsics.hpp index 85592c98..bd1c4790 100644 --- a/contract/include/evm_runtime/intrinsics.hpp +++ b/contract/include/evm_runtime/intrinsics.hpp @@ -2,6 +2,9 @@ namespace eosio { namespace internal_use_do_not_use { extern "C" { + __attribute__((eosio_wasm_import)) + uint32_t get_code_hash(uint64_t account, uint32_t struct_version, char* data, uint32_t size); + #ifdef WITH_LOGTIME __attribute__((eosio_wasm_import)) void logtime(const char*); diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 0ef274d6..cdf90f52 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -2,19 +2,19 @@ #include #include +#include +#include #include - +#include namespace evm_runtime { using namespace eosio; - struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { uint64_t id; bytes eth_address; uint64_t nonce; bytes balance; - bytes code; - bytes code_hash; + std::optional code_id; uint64_t primary_key()const { return id; } @@ -22,29 +22,41 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { return make_key(eth_address); } - checksum256 by_code_hash()const { - return make_key(code_hash); - } - uint256be get_balance()const { uint256be res; std::copy(balance.begin(), balance.end(), res.bytes); return res; } + EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code_id)); +}; + +typedef multi_index< "account"_n, account, + indexed_by<"by.address"_n, const_mem_fun> +> account_table; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] account_code { + uint64_t id; + uint32_t ref_count; + bytes code; + bytes code_hash; + + uint64_t primary_key()const { return id; } + + checksum256 by_code_hash()const { + return make_key(code_hash); + } + bytes32 get_code_hash()const { - bytes32 res; - std::copy(code_hash.begin(), code_hash.end(), res.bytes); - return res; + return to_bytes32(code_hash); } - EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code)(code_hash)); + EOSLIB_SERIALIZE(account_code, (id)(ref_count)(code)(code_hash)); }; -typedef multi_index< "account"_n, account, - indexed_by<"by.address"_n, const_mem_fun>, - indexed_by<"by.codehash"_n, const_mem_fun> -> account_table; +typedef multi_index< "accountcode"_n, account_code, + indexed_by<"by.codehash"_n, const_mem_fun> +> account_code_table; struct [[eosio::table]] [[eosio::contract("evm_contract")]] storage { uint64_t id; @@ -75,4 +87,99 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] gcstore { typedef multi_index< "gcstore"_n, gcstore> gc_store_table; +struct balance_with_dust { + asset balance = asset(0, token_symbol); + uint64_t dust = 0; + + bool operator==(const balance_with_dust& o) const { + return balance == o.balance && dust == o.dust; + } + bool operator!=(const balance_with_dust& o) const { + return !(*this == o); + } + + balance_with_dust& operator+=(const intx::uint256& amount) { + const intx::div_result div_result = udivrem(amount, minimum_natively_representable); + + //asset::max_amount is conservative at 2^62-1, this means two max_amounts of (2^62-1)+(2^62-1) cannot + // overflow an int64_t which can represent up to 2^63-1. In other words, asset::max_amount+asset::max_amount + // are guaranteed greater than asset::max_amount without need to worry about int64_t overflow. Even more, + // asset::max_amount+asset::max_amount+1 is guaranteed greater than asset::max_amount without need to worry + // about int64_t overflow. The latter property ensures that if the existing value is max_amount and max_amount + // is added with a dust roll over, an int64_t rollover still does not occur on the balance. + //This means that we just need to check that whatever we're adding is no more than 2^62-1 (max_amount), and that + // the current value is no more than 2^62-1 (max_amount), and adding them together will not overflow. + check(div_result.quot <= asset::max_amount, "accumulation overflow"); + check(balance.amount <= asset::max_amount, "accumulation overflow"); + + const int64_t base_amount = div_result.quot[0]; + balance.amount += base_amount; + dust += div_result.rem[0]; + + if(dust >= min_asset) { + balance.amount++; + dust -= min_asset; + } + + check(balance.amount <= asset::max_amount, "accumulation overflow"); + + return *this; + } + + balance_with_dust& operator-=(const intx::uint256& amount) { + const intx::div_result div_result = udivrem(amount, minimum_natively_representable); + + check(div_result.quot <= balance.amount, "decrementing more than available"); + + balance.amount -= div_result.quot[0]; + dust -= div_result.rem[0]; + + if(dust & (UINT64_C(1) << 63)) { + balance.amount--; + dust += min_asset; + check(balance.amount >= 0, "decrementing more than available"); + } + + return *this; + } + + static constexpr uint64_t min_asset = minimum_natively_representable[0]; + + EOSLIB_SERIALIZE(balance_with_dust, (balance)(dust)); +}; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] balance { + name owner; + balance_with_dust balance; + + uint64_t primary_key() const { return owner.value; } + + EOSLIB_SERIALIZE(struct balance, (owner)(balance)); +}; + +typedef eosio::multi_index<"balances"_n, balance> balances; + +typedef eosio::singleton<"inevm"_n, balance_with_dust> inevm_singleton; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] nextnonce { + name owner; + uint64_t next_nonce = 0; + + uint64_t primary_key() const { return owner.value; } + + EOSLIB_SERIALIZE(nextnonce, (owner)(next_nonce)); +}; + +typedef eosio::multi_index<"nextnonces"_n, nextnonce> nextnonces; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] allowed_egress_account { + name account; + + uint64_t primary_key() const { return account.value; } + + EOSLIB_SERIALIZE(allowed_egress_account, (account)); +}; + +typedef eosio::multi_index<"egresslist"_n, allowed_egress_account> egresslist; + } //namespace evm_runtime \ No newline at end of file diff --git a/contract/include/evm_runtime/types.hpp b/contract/include/evm_runtime/types.hpp index 9a521cce..903f1e18 100644 --- a/contract/include/evm_runtime/types.hpp +++ b/contract/include/evm_runtime/types.hpp @@ -1,11 +1,22 @@ #pragma once #include +#include +#include #include #include #include +#define TOKEN_ACCOUNT_NAME "eosio.token" + namespace evm_runtime { + using intx::operator""_u256; + + constexpr unsigned evm_precision = 18; + constexpr eosio::name token_account(eosio::name(TOKEN_ACCOUNT_NAME)); + constexpr eosio::symbol token_symbol("EOS", 4u); + constexpr intx::uint256 minimum_natively_representable = intx::exp(10_u256, intx::uint256(evm_precision - token_symbol.precision())); + static_assert(evm_precision - token_symbol.precision() <= 14, "dust math may overflow a uint64_t"); typedef intx::uint<256> uint256; typedef intx::uint<512> uint512; @@ -26,7 +37,6 @@ namespace evm_runtime { evmc::address to_address(const bytes& addr); evmc::bytes32 to_bytes32(const bytes& data); uint256 to_uint256(const bytes& value); - } //namespace evm_runtime namespace eosio { diff --git a/contract/src/CMakeLists.txt b/contract/src/CMakeLists.txt index 2675e535..8de9b468 100644 --- a/contract/src/CMakeLists.txt +++ b/contract/src/CMakeLists.txt @@ -20,7 +20,7 @@ if (WITH_LOGTIME) endif() add_compile_definitions(ANTELOPE) -add_compile_definitions(PROJECT_VERSION="0.2.1") +add_compile_definitions(PROJECT_VERSION="0.3.0") # ethash list(APPEND SOURCES @@ -82,8 +82,10 @@ target_include_directories( evm_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../silkworm/third_party/evmone/evmc/include ) +target_compile_options(evm_runtime PUBLIC --no-missing-ricardian-clause) + if (WITH_LARGE_STACK) target_link_options(evm_runtime PUBLIC --stack-size=50000000) else() - target_link_options(evm_runtime PUBLIC --stack-size=20480) + target_link_options(evm_runtime PUBLIC --stack-size=19984) endif() diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 67378e14..618a175a 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -32,24 +32,98 @@ namespace silkworm { } } -static constexpr eosio::name token_account("eosio.token"_n); -static constexpr eosio::symbol token_symbol("EOS", 4u); - namespace evm_runtime { +static constexpr uint32_t hundred_percent = 100'000; + using namespace silkworm; -void evm_contract::init(const uint64_t chainid) { - eosio::require_auth(get_self()); +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"); + } + + 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"); + + current_config.ingress_bridge_fee = *fee_params.ingress_bridge_fee; + } +} + +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(!!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, + }; - check( !_config.exists(), "contract already initialized" ); - check( !!lookup_known_chain(chainid), "unknown chainid" ); + // 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, - .chainid = chainid, - .genesis_time = eosio::current_time_point() // implicitly converts from Antelope timestamp to EVM compatible timestamp - }, get_self()); + set_fee_parameters(new_config, fee_params, false); // enforce that all fee parameters are specified + + _config.set(new_config, get_self()); + + inevm_singleton(get_self(), get_self().value).get_or_create(get_self()); + + open(get_self()); +} + +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()); +} + +void evm_contract::addegress(const std::vector& accounts) { + assert_inited(); + require_auth(get_self()); + + egresslist egresslist_table(get_self(), get_self().value); + + for(const name& account : accounts) + if(egresslist_table.find(account.value) == egresslist_table.end()) + egresslist_table.emplace(get_self(), [&](allowed_egress_account& a) { + a.account = account; + }); +} + +void evm_contract::removeegress(const std::vector& accounts) { + assert_inited(); + require_auth(get_self()); + + egresslist egresslist_table(get_self(), get_self().value); + + for(const name& account : accounts) + if(auto it = egresslist_table.find(account.value); it != egresslist_table.end()) + egresslist_table.erase(it); } void evm_contract::freeze(bool value) { @@ -85,18 +159,56 @@ void check_result( ValidationResult r, const Transaction& txn, const char* desc eosio::check( false, desc ); } -Receipt evm_contract::execute_tx( Block& block, const bytes& rlptx, silkworm::ExecutionProcessor& ep ) { +Receipt evm_contract::execute_tx( eosio::name miner, Block& block, Transaction& tx, silkworm::ExecutionProcessor& ep ) { + //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(); - Transaction tx; - ByteView bv{(const uint8_t*)rlptx.data(), rlptx.size()}; - eosio::check(rlp::decode(bv,tx) == DecodingResult::kOk && bv.empty(), "unable to decode transaction"); - LOGTIME("EVM TX DECODE"); + balances balance_table(get_self(), get_self().value); + + if (miner == get_self()) { + // If the miner is the contract itself, then there is no need to send the miner its cut. + miner = {}; + } + + if (miner) { + // Ensure the miner has a balance open early. + balance_table.get(miner.value, "no balance open for miner"); + } + + bool deducted_miner_cut = false; + + std::optional inevm; + auto populate_bridge_accessors = [&]() { + if(inevm) + return; + inevm.emplace(get_self(), get_self().value); + }; tx.from.reset(); tx.recover_sender(); eosio::check(tx.from.has_value(), "unable to recover sender"); LOGTIME("EVM RECOVER SENDER"); + if(from_self) { + check(is_reserved_address(*tx.from), "actions from self without a reserved from address are unexpected"); + const name ingress_account(*extract_reserved_address(*tx.from)); + + const intx::uint512 max_gas_cost = intx::uint256(tx.gas_limit) * tx.max_fee_per_gas; + check(max_gas_cost + tx.value < std::numeric_limits::max(), "too much gas"); + const intx::uint256 value_with_max_gas = tx.value + (intx::uint256)max_gas_cost; + + populate_bridge_accessors(); + balance_table.modify(balance_table.get(ingress_account.value), eosio::same_payer, [&](balance& b){ + b.balance -= value_with_max_gas; + }); + inevm->set(inevm->get() += value_with_max_gas, eosio::same_payer); + + ep.state().set_balance(*tx.from, value_with_max_gas); + ep.state().set_nonce(*tx.from, tx.nonce); + } + else if(is_reserved_address(*tx.from)) + check(from_self, "bridge signature used outside of bridge transaction"); + ValidationResult r = consensus::pre_validate_transaction(tx, ep.evm().block().header.number, ep.evm().config(), ep.evm().block().header.base_fee_per_gas); check_result( r, tx, "pre_validate_transaction error" ); @@ -106,47 +218,132 @@ Receipt evm_contract::execute_tx( Block& block, const bytes& rlptx, silkworm::Ex Receipt receipt; ep.execute_transaction(tx, receipt); + // Calculate the miner portion of the actual gas fee (if necessary): + std::optional gas_fee_miner_portion; + if (miner) { + 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 /= hundred_percent; + gas_fee_miner_portion.emplace(static_cast(gas_fee)); + } + + if(from_self) + eosio::check(receipt.success, "ingress bridge actions must succeed"); + + if(!ep.state().reserved_objects().empty()) { + bool non_open_account_sent = false; + intx::uint256 total_egress; + populate_bridge_accessors(); + + for(const auto& reserved_object : ep.state().reserved_objects()) { + const evmc::address& address = reserved_object.first; + const name egress_account(*extract_reserved_address(address)); + const Account& reserved_account = *reserved_object.second.current; + + check(reserved_account.code_hash == kEmptyHash, "contracts cannot be created in the reserved address space"); + check(egress_account.value != 0, "reserved 0 address cannot be used"); + + if(reserved_account.balance == 0_u256) + continue; + total_egress += reserved_account.balance; + + if(auto it = balance_table.find(egress_account.value); it != balance_table.end()) { + balance_table.modify(balance_table.get(egress_account.value), eosio::same_payer, [&](balance& b){ + b.balance += reserved_account.balance; + if (gas_fee_miner_portion.has_value() && egress_account == get_self()) { + check(!deducted_miner_cut, "unexpected error: contract account appears twice in reserved objects"); + b.balance -= *gas_fee_miner_portion; + deducted_miner_cut = true; + } + }); + } + else { + check(!non_open_account_sent, "only one non-open account for egress bridging allowed in single transaction"); + check(is_account(egress_account), "can only egress bridge to existing accounts"); + if(get_code_hash(egress_account) != checksum256()) + egresslist(get_self(), get_self().value).get(egress_account.value, "non-open accounts containing contract code must be on allow list for egress bridging"); + + check(reserved_account.balance % minimum_natively_representable == 0_u256, "egress bridging to non-open accounts must not contain dust"); + + const bool was_to = tx.to && *tx.to == address; + const Bytes exit_memo = {'E', 'V', 'M', ' ', 'e', 'x', 'i', 't'}; //yikes + + token::transfer_bytes_memo_action transfer_act(token_account, {{get_self(), "active"_n}}); + transfer_act.send(get_self(), egress_account, asset((uint64_t)(reserved_account.balance / minimum_natively_representable), token_symbol), was_to ? tx.data : exit_memo); + + non_open_account_sent = true; + } + } + + if(total_egress != 0_u256) + inevm->set(inevm->get() -= total_egress, eosio::same_payer); + } + + // Send miner portion of the gas fee, if any, to the balance of the miner: + if (gas_fee_miner_portion.has_value() && *gas_fee_miner_portion != 0) { + check(deducted_miner_cut, "unexpected error: contract account did not receive any funds through its reserved address"); + balance_table.modify(balance_table.get(miner.value), eosio::same_payer, [&](balance& b){ + b.balance += *gas_fee_miner_portion; + }); + } + LOGTIME("EVM EXECUTE"); return receipt; } -void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { +void evm_contract::pushtx( eosio::name miner, const bytes& rlptx ) { LOGTIME("EVM START"); assert_unfrozen(); - std::optional> found_chain_config = lookup_known_chain(_config.get().chainid); + + eosio::check((get_sender() != get_self()) || (miner == get_self()), + "unexpected error: EVM contract generated inline pushtx without setting itself as the miner"); + + const auto& current_config = _config.get(); + std::optional> found_chain_config = lookup_known_chain(current_config.chainid); check( found_chain_config.has_value(), "failed to find expected chain config" ); - eosio::require_auth(ram_payer); - evm_common::block_mapping bm(_config.get().genesis_time.sec_since_epoch()); + evm_common::block_mapping bm(current_config.genesis_time.sec_since_epoch()); Block block; - block.header.difficulty = 1; - block.header.gas_limit = 0x7ffffffffff; - block.header.number = bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count()); - block.header.timestamp = bm.evm_block_num_to_evm_timestamp(block.header.number); + evm_common::prepare_block_header(block.header, bm, get_self().value, + bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count())); silkworm::consensus::TrustEngine engine{*found_chain_config->second}; - evm_runtime::state state{get_self(), ram_payer}; + evm_runtime::state state{get_self(), get_self()}; silkworm::ExecutionProcessor ep{block, engine, state, *found_chain_config->second}; - auto receipt = execute_tx(block, rlptx, ep); + Transaction tx; + ByteView bv{(const uint8_t*)rlptx.data(), rlptx.size()}; + eosio::check(rlp::decode(bv,tx) == DecodingResult::kOk && 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"); + + auto receipt = execute_tx(miner, block, tx, ep); engine.finalize(ep.state(), ep.evm().block(), ep.evm().revision()); ep.state().write_to_db(ep.evm().block().header.number); } -void evm_contract::open(eosio::name owner, eosio::name ram_payer) { +void evm_contract::open(eosio::name owner) { assert_unfrozen(); - require_auth(ram_payer); - check(is_account(owner), "owner account does not exist"); + require_auth(owner); + + balances balance_table(get_self(), get_self().value); + if(balance_table.find(owner.value) == balance_table.end()) + balance_table.emplace(owner, [&](balance& a) { + a.owner = owner; + }); - accounts account_table(get_self(), get_self().value); - if(account_table.find(owner.value) == account_table.end()) - account_table.emplace(ram_payer, [&](account& a) { + nextnonces nextnonce_table(get_self(), get_self().value); + if(nextnonce_table.find(owner.value) == nextnonce_table.end()) + nextnonce_table.emplace(owner, [&](nextnonce& a) { a.owner = owner; - a.balance = asset(0, token_symbol); }); } @@ -154,41 +351,115 @@ void evm_contract::close(eosio::name owner) { assert_unfrozen(); require_auth(owner); - accounts account_table(get_self(), get_self().value); - const account& owner_account = account_table.get(owner.value, "account is not open"); + eosio::check(owner != get_self(), "Cannot close self"); + + balances balance_table(get_self(), get_self().value); + const balance& owner_account = balance_table.get(owner.value, "account is not open"); + + eosio::check(owner_account.balance == balance_with_dust(), "cannot close because balance is not zero"); + balance_table.erase(owner_account); - eosio::check(owner_account.balance.amount == 0 && owner_account.dust == 0, "cannot close because balance is not zero"); - account_table.erase(owner_account); + nextnonces nextnonce_table(get_self(), get_self().value); + const nextnonce& next_nonce_for_owner = nextnonce_table.get(owner.value); + //if the account has performed an EOS->EVM transfer the nonce needs to be maintained in case the account is re-opened in the future + if(next_nonce_for_owner.next_nonce == 0) + nextnonce_table.erase(next_nonce_for_owner); } -void evm_contract::transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo) { - assert_unfrozen(); +uint64_t evm_contract::get_and_increment_nonce(const name owner) { + nextnonces nextnonce_table(get_self(), get_self().value); - if(to != get_self() || from == get_self()) - return; + const nextnonce& nonce = nextnonce_table.get(owner.value); + uint64_t ret = nonce.next_nonce; + nextnonce_table.modify(nonce, eosio::same_payer, [](nextnonce& n){ + ++n.next_nonce; + }); + return ret; +} + +checksum256 evm_contract::get_code_hash(name account) const { + char buff[64]; - eosio::check(!memo.empty(), "memo must be already opened account name to credit deposit to"); + 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)); + return code_hash; +} + +void evm_contract::handle_account_transfer(const eosio::asset& quantity, const std::string& memo) { eosio::name receiver(memo); - accounts account_table(get_self(), get_self().value); - const account& receiver_account = account_table.get(receiver.value, "receiving account has not been opened"); + balances balance_table(get_self(), get_self().value); + const balance& receiver_account = balance_table.get(receiver.value, "receiving account has not been opened"); - account_table.modify(receiver_account, eosio::same_payer, [&](account& a) { - a.balance += quantity; + balance_table.modify(receiver_account, eosio::same_payer, [&](balance& a) { + a.balance.balance += quantity; }); } +void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& memo) { + //move all incoming quantity in to the contract's balance. the evm bridge trx will "pull" from this balance + balances balance_table(get_self(), get_self().value); + balance_table.modify(balance_table.get(get_self().value), eosio::same_payer, [&](balance& b){ + 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; + eosio::check(quantity.amount > 0, "must bridge more than ingress bridge fee"); + + const std::optional address_bytes = from_hex(memo); + eosio::check(!!address_bytes, "unable to parse destination address"); + + intx::uint256 value((uint64_t)quantity.amount); + value *= minimum_natively_representable; + + const Transaction txn { + .type = Transaction::Type::kLegacy, + .nonce = get_and_increment_nonce(get_self()), + .max_priority_fee_per_gas = current_config.gas_price, + .max_fee_per_gas = current_config.gas_price, + .gas_limit = 21000, + .to = to_evmc_address(*address_bytes), + .value = value, + .r = 0u, // r == 0 is pseudo signature that resolves to reserved address range + .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); +} + +void evm_contract::transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo) { + assert_unfrozen(); + eosio::check(quantity.symbol == token_symbol, "received unexpected token"); + + if(to != get_self() || from == get_self()) + return; + + if(memo.size() == 42 && memo[0] == '0' && memo[1] == 'x') + handle_evm_transfer(quantity, memo); + else if(!memo.empty() && memo.size() <= 13) + handle_account_transfer(quantity, memo); + else + eosio::check(false, "memo must be either 0x EVM address or already opened account name to credit deposit to"); +} + void evm_contract::withdraw(eosio::name owner, eosio::asset quantity) { assert_unfrozen(); require_auth(owner); - accounts account_table(get_self(), get_self().value); - const account& owner_account = account_table.get(owner.value, "account is not open"); + balances balance_table(get_self(), get_self().value); + const balance& owner_account = balance_table.get(owner.value, "account is not open"); - check(owner_account.balance.amount >= quantity.amount, "overdrawn balance"); - account_table.modify(owner_account, eosio::same_payer, [&](account& a) { - a.balance -= quantity; + check(owner_account.balance.balance.amount >= quantity.amount, "overdrawn balance"); + balance_table.modify(owner_account, eosio::same_payer, [&](balance& a) { + a.balance.balance -= quantity; }); token::transfer_action transfer_act(token_account, {{get_self(), "active"_n}}); @@ -203,7 +474,7 @@ bool evm_contract::gc(uint32_t max) { } #ifdef WITH_TEST_ACTIONS -ACTION evm_contract::testtx( const std::optional& orlptx, const evm_runtime::test::block_info& bi ) { +[[eosio::action]] void evm_contract::testtx( const std::optional& orlptx, const evm_runtime::test::block_info& bi ) { assert_unfrozen(); eosio::require_auth(get_self()); @@ -216,13 +487,17 @@ ACTION evm_contract::testtx( const std::optional& orlptx, const evm_runti silkworm::ExecutionProcessor ep{block, engine, state, evm_runtime::test::kTestNetwork}; if(orlptx) { - execute_tx(block, *orlptx, ep); + Transaction tx; + ByteView bv{(const uint8_t*)orlptx->data(), orlptx->size()}; + eosio::check(rlp::decode(bv,tx) == DecodingResult::kOk && bv.empty(), "unable to decode transaction"); + + execute_tx(eosio::name{}, block, tx, ep); } engine.finalize(ep.state(), ep.evm().block(), ep.evm().revision()); ep.state().write_to_db(ep.evm().block().header.number); } -ACTION evm_contract::dumpstorage(const bytes& addy) { +[[eosio::action]] void evm_contract::dumpstorage(const bytes& addy) { assert_inited(); eosio::require_auth(get_self()); @@ -256,7 +531,7 @@ ACTION evm_contract::dumpstorage(const bytes& addy) { eosio::print(" = ", cnt, "\n"); } -ACTION evm_contract::dumpall() { +[[eosio::action]] void evm_contract::dumpall() { assert_inited(); eosio::require_auth(get_self()); @@ -306,7 +581,7 @@ ACTION evm_contract::dumpall() { } -ACTION evm_contract::clearall() { +[[eosio::action]] void evm_contract::clearall() { assert_unfrozen(); eosio::require_auth(get_self()); @@ -333,6 +608,13 @@ ACTION evm_contract::clearall() { eosio::print("db size:", uint64_t(db_size), "\n"); itr = accounts.erase(itr); } + + account_code_table codes(_self, _self.value); + auto itrc = codes.begin(); + while(itrc != codes.end()) { + itrc = codes.erase(itrc); + } + gc(std::numeric_limits::max()); auto account_size = std::distance(accounts.cbegin(), accounts.cend()); @@ -341,7 +623,7 @@ ACTION evm_contract::clearall() { eosio::print("CLEAR end\n"); } -ACTION evm_contract::updatecode( const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code) { +[[eosio::action]] void evm_contract::updatecode( const bytes& address, uint64_t incarnation, const bytes& code_hash, const bytes& code) { assert_unfrozen(); eosio::require_auth(get_self()); @@ -351,7 +633,7 @@ ACTION evm_contract::updatecode( const bytes& address, uint64_t incarnation, con state.update_account_code(to_address(address), incarnation, to_bytes32(code_hash), bvcode); } -ACTION evm_contract::updatestore(const bytes& address, uint64_t incarnation, const bytes& location, const bytes& initial, const bytes& current) { +[[eosio::action]] void evm_contract::updatestore(const bytes& address, uint64_t incarnation, const bytes& location, const bytes& initial, const bytes& current) { assert_unfrozen(); eosio::require_auth(get_self()); @@ -368,7 +650,7 @@ ACTION evm_contract::updatestore(const bytes& address, uint64_t incarnation, con state.update_storage(to_address(address), incarnation, to_bytes32(location), to_bytes32(initial), to_bytes32(current)); } -ACTION evm_contract::updateaccnt(const bytes& address, const bytes& initial, const bytes& current) { +[[eosio::action]] void evm_contract::updateaccnt(const bytes& address, const bytes& initial, const bytes& current) { assert_unfrozen(); eosio::require_auth(get_self()); @@ -392,7 +674,7 @@ ACTION evm_contract::updateaccnt(const bytes& address, const bytes& initial, con state.update_account(to_address(address), oinitial, ocurrent); } -ACTION evm_contract::setbal(const bytes& addy, const bytes& bal) { +[[eosio::action]] void evm_contract::setbal(const bytes& addy, const bytes& bal) { assert_unfrozen(); eosio::require_auth(get_self()); @@ -404,7 +686,7 @@ ACTION evm_contract::setbal(const bytes& addy, const bytes& bal) { if(itr == inx.end()) { accounts.emplace(get_self(), [&](auto& row){ row.id = accounts.available_primary_key();; - row.code_hash = to_bytes(kEmptyHash); + row.code_id = std::nullopt; row.eth_address = addy; row.balance = bal; }); @@ -414,6 +696,148 @@ ACTION evm_contract::setbal(const bytes& addy, const bytes& bal) { }); } } + +[[eosio::action]] void evm_contract::testbaldust(const name test) { + if(test == "basic"_n) { + balance_with_dust b; + // ↱minimum EOS + // .123456789abcdefghi EEOS + b += 200000000_u256; //adds to dust only + b += 3000_u256; //adds to dust only + b += 100000000000000_u256; //adds strictly to balance + b += 200000000007000_u256; //adds to both balance and dust + b += 60000000000000_u256; //adds to dust only + b += 55000000000000_u256; //adds to dust only but dust overflows +1 to balance + + //expect: 415000200010000; .0004 EOS, 15000200010000 dust + check(b.balance.amount == 4, ""); + check(b.dust == 15000200010000, ""); + + // ↱minimum EOS + // .123456789abcdefghi EEOS + b -= 45_u256; //substracts from dust only + b -= 100000000000000_u256; //subtracts strictly from balance + b -= 120000000000000_u256; //subtracts from both dust and balance, causes underflow on dust thus -1 balance + + //expect: 195000200009955; .0001 EOS, 95000200009955 dust + check(b.balance.amount == 1, ""); + check(b.dust == 95000200009955, ""); + } + else if(test == "underflow1"_n) { + balance_with_dust b; + // ↱minimum EOS + // .123456789abcdefghi EEOS + b -= 45_u256; + //should fail with underflow on dust causing an underflow of balance + } + else if(test == "underflow2"_n) { + balance_with_dust b; + // ↱minimum EOS + // .123456789abcdefghi EEOS + b -= 100000000000000_u256; + //should fail with underflow on balance + } + else if(test == "underflow3"_n) { + balance_with_dust b; + // ↱minimum EOS + // .123456789abcdefghi EEOS + b += 200000000000000_u256; + b -= 300000000000000_u256; + //should fail with underflow on balance + } + else if(test == "underflow4"_n) { + balance_with_dust b; + // ↱minimum EOS + // .123456789abcdefghi EEOS + b += 50000_u256; + b -= 500000000_u256; + //should fail with underflow on dust causing an underflow of balance + } + else if(test == "underflow5"_n) { + balance_with_dust b; + // do a decrement that would overflow an int64_t but not uint64_t (for balance) + // ↱int64t max ↱minimum EOS + // 9223372036854775807 (2^63)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 50000_u256; + b -= 100000000000000000000000000000000_u256; + //should fail with underflow + } + else if(test == "overflow1"_n) { + balance_with_dust b; + // increment a value that would overflow a int64_t, but not uint64_t + // ↱int64t max ↱minimum EOS + // 9223372036854775807 (2^63)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 1000000000000000000000000000000000_u256; + //should fail with overflow + } + else if(test == "overflow2"_n) { + balance_with_dust b; + // increment a value that would overflow a max_asset, but not an int64_t + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 500000000000000000000000000000000_u256; + //should fail with overflow + } + else if(test == "overflow3"_n || test == "overflow4"_n || test == "overflow5"_n || test == "overflowa"_n || test == "overflowb"_n || test == "overflowc"_n) { + balance_with_dust b; + // start with a value that should be the absolute max allowed + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 461168601842738790399999999999999_u256; + if(test == "overflow4"_n) { + //add 1 to balance, should fail since it rolls balance over max_asset + // ↱minimum EOS + // .123456789abcdefghi EEOS + b += 100000000000000_u256; + //should fail with overflow + } + if(test == "overflow5"_n) { + //add 1 to dust, causing a rollover making balance > max_asset + // ↱minimum EOS + // .123456789abcdefghi EEOS + b += 1_u256; + //should fail with overflow + } + if(test == "overflowa"_n) { + //add something huge + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 999461168601842738790399999999999999_u256; + //should fail with overflow + } + if(test == "overflowb"_n) { + // add max allowed to balance again; this should be a 2^62-1 + 2^62-1 + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 461168601842738790300000000000000_u256; + //should fail with overflow + } + if(test == "overflowc"_n) { + // add max allowed to balance again; but also with max dust; should be a 2^62-1 + 2^62-1 + 1 on asset balance + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 461168601842738790399999999999999_u256; + //should fail with overflow + } + } + if(test == "overflowd"_n) { + balance_with_dust b; + //add something massive + // ↱max_asset max ↱minimum EOS + // 4611686018427387903 (2^62)-1 + // 543210987654321̣123456789abcdefghi EEOS + b += 99999999461168601842738790399999999999999_u256; + //should fail with overflow + } +} + #endif //WITH_TEST_ACTIONS } //evm_runtime diff --git a/contract/src/state.cpp b/contract/src/state.cpp index c05c93df..25799d8b 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -17,9 +17,23 @@ std::optional state::read_account(const evmc::address& address) const n return {}; } - auto code_hash = itr->get_code_hash(); addr2id[address] = itr->id; - addr2code[code_hash] = itr->code; + + evmc::bytes32 code_hash; + if (itr->code_id) { + account_code_table codes(_self, _self.value); + auto citr = codes.find(itr->code_id.value()); + if (citr != codes.end()) { + code_hash = to_bytes32(citr->code_hash); + addr2code[code_hash] = citr->code; + } else { + // Should not reach here! + // Return empty hash for robustness. + code_hash = silkworm::kEmptyHash; + } + } else { + code_hash = silkworm::kEmptyHash; + } return Account{itr->nonce, intx::be::load(itr->get_balance()), code_hash, 0}; } @@ -31,11 +45,10 @@ ByteView state::read_code(const evmc::bytes32& code_hash) const noexcept { return ByteView{(const uint8_t*)code.data(), code.size()}; } - account_table accounts(_self, _self.value); - auto inx = accounts.get_index<"by.codehash"_n>(); + account_code_table codes(_self, _self.value); + auto inx = codes.get_index<"by.codehash"_n>(); auto itr = inx.find(make_key(code_hash)); - ++stats.account.read; - + if (itr == inx.end() || itr->code.size() == 0) { return ByteView{}; } @@ -84,7 +97,7 @@ void state::update_account(const evmc::address& address, std::optional const bool equal{current == initial}; if(equal) return; - + account_table accounts(_self, _self.value); auto inx = accounts.get_index<"by.address"_n>(); auto itr = inx.find(make_key(address)); @@ -95,13 +108,14 @@ void state::update_account(const evmc::address& address, std::optional row.eth_address = to_bytes(address); row.nonce = current->nonce; row.balance = to_bytes(current->balance); - row.code_hash = to_bytes(current->code_hash); + // Codes are not supposed to changed in this call. + row.code_id = std::nullopt; }; auto update = [&](auto& row) { row.nonce = current->nonce; row.balance = to_bytes(current->balance); - row.code_hash = to_bytes(current->code_hash); + // Codes are not supposed to changed in this call. }; auto remove_account = [&](auto& itr) { @@ -112,6 +126,18 @@ void state::update_account(const evmc::address& address, std::optional row.id = gc.available_primary_key(); row.storage_id = itr->id; }); + // Remove code if necessary + if (itr->code_id) { + account_code_table codes(_self, _self.value); + const auto& itrc = codes.get(itr->code_id.value(), "code not found"); + if(itrc.ref_count-1) { + codes.modify(itrc, eosio::same_payer, [&](auto& row){ + row.ref_count--; + }); + } else { + codes.erase(itrc); + } + } accounts.erase(*itr); }; @@ -151,18 +177,38 @@ bool state::gc(uint32_t max) { i = gc.erase(i); --max; } + return gc.begin() == gc.end(); } void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { + account_code_table codes(_self, _self.value); + auto inxc = codes.get_index<"by.codehash"_n>(); + auto itrc = inxc.find(make_key(code_hash)); + uint64_t code_id; + if(itrc == inxc.end()) { + code_id = codes.available_primary_key(); + codes.emplace(_ram_payer, [&](auto& row){ + row.id = code_id; + row.code_hash = to_bytes(code_hash); + row.code = bytes{code.begin(), code.end()}; + row.ref_count = 1; + }); + } else { + // code should be immutable + codes.modify(*itrc, eosio::same_payer, [&](auto& row){ + row.ref_count++; + }); + code_id = itrc->id; + } + account_table accounts(_self, _self.value); auto inx = accounts.get_index<"by.address"_n>(); auto itr = inx.find(make_key(address)); ++stats.account.read; if( itr != inx.end() ) { accounts.modify(*itr, eosio::same_payer, [&](auto& row){ - row.code = bytes{code.begin(), code.end()}; - row.code_hash = to_bytes(code_hash); + row.code_id = code_id; }); ++stats.account.update; } else { @@ -170,8 +216,7 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev row.id = accounts.available_primary_key();; row.eth_address = to_bytes(address); row.nonce = 0; - row.code = bytes{code.begin(), code.end()}; - row.code_hash = to_bytes(code_hash); + row.code_id = code_id; }); ++stats.account.create; } @@ -202,7 +247,7 @@ void state::update_storage(const evmc::address& address, uint64_t incarnation, c row.id = table_id; row.eth_address = to_bytes(address); row.nonce = 0; - row.code_hash = to_bytes(kEmptyHash); + row.code_id = std::nullopt; }); ++stats.account.read; } else { diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index c4a901e7..59ec8224 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -25,10 +25,13 @@ include_directories( set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations") add_eosio_test_executable( unit_test + ${CMAKE_SOURCE_DIR}/basic_evm_tester.cpp ${CMAKE_SOURCE_DIR}/evm_runtime_tests.cpp ${CMAKE_SOURCE_DIR}/init_tests.cpp ${CMAKE_SOURCE_DIR}/native_token_tests.cpp ${CMAKE_SOURCE_DIR}/mapping_tests.cpp + ${CMAKE_SOURCE_DIR}/gas_fee_tests.cpp + ${CMAKE_SOURCE_DIR}/blockhash_tests.cpp ${CMAKE_SOURCE_DIR}/main.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/encode.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/decode.cpp @@ -38,6 +41,7 @@ add_eosio_test_executable( unit_test ${CMAKE_SOURCE_DIR}/silkworm/node/silkworm/common/stopwatch.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/common/util.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/common/endian.cpp + ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/execution/address.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/crypto/ecdsa.cpp ${CMAKE_SOURCE_DIR}/../external/ethash/lib/keccak/keccak.c ${CMAKE_SOURCE_DIR}/../external/ethash/lib/ethash/ethash.cpp diff --git a/contract/tests/basic_evm_tester.cpp b/contract/tests/basic_evm_tester.cpp new file mode 100644 index 00000000..afdd092b --- /dev/null +++ b/contract/tests/basic_evm_tester.cpp @@ -0,0 +1,442 @@ +#include "basic_evm_tester.hpp" + +namespace fc { + +void to_variant(const intx::uint256& o, fc::variant& v) +{ + std::string output = intx::to_string(o, 10); + v = std::move(output); +} + +void to_variant(const evmc::address& o, fc::variant& v) +{ + std::string output = "0x"; + output += fc::to_hex((char*)o.bytes, sizeof(o.bytes)); + v = std::move(output); +} + +} // namespace fc + +namespace evm_test { + +bool balance_and_dust::operator==(const balance_and_dust& o) const { return balance == o.balance && dust == o.dust; } +bool balance_and_dust::operator!=(const balance_and_dust& o) const { return !(*this == o); } + +balance_and_dust::operator intx::uint256() const +{ + intx::uint256 result = balance.get_amount(); + result *= intx::exp(10_u256, intx::uint256{18 - balance.decimals()}); + result += dust; + return result; +} + +struct vault_balance_row +{ + name owner; + asset balance; + uint64_t dust = 0; +}; + +struct partial_account_table_row +{ + uint64_t id; + bytes eth_address; + uint64_t nonce; + bytes balance; +}; + +struct storage_table_row +{ + uint64_t id; + bytes key; + bytes value; +}; + +} // namespace evm_test + +FC_REFLECT(evm_test::vault_balance_row, (owner)(balance)(dust)) +FC_REFLECT(evm_test::partial_account_table_row, (id)(eth_address)(nonce)(balance)) +FC_REFLECT(evm_test::storage_table_row, (id)(key)(value)) + +namespace evm_test { + +evm_eoa::evm_eoa(std::basic_string optional_private_key) +{ + if (optional_private_key.size() == 0) { + // No private key specified. So randomly generate one. + fc::rand_bytes((char*)private_key.data(), private_key.size()); + } else { + if (optional_private_key.size() != 32) { + throw std::runtime_error("private key provided to evm_eoa must be exactly 32 bytes"); + } + std::memcpy(private_key.data(), optional_private_key.data(), private_key.size()); + } + + public_key.resize(65); + + secp256k1_pubkey pubkey; + BOOST_REQUIRE(secp256k1_ec_pubkey_create(ctx, &pubkey, private_key.data())); + + size_t serialized_result_sz = public_key.size(); + secp256k1_ec_pubkey_serialize(ctx, public_key.data(), &serialized_result_sz, &pubkey, SECP256K1_EC_UNCOMPRESSED); + + std::optional addr = silkworm::ecdsa::public_key_to_address(public_key); + BOOST_REQUIRE(!!addr); + address = *addr; +} + +std::string evm_eoa::address_0x() const { return fc::variant(address).as_string(); } + +key256_t evm_eoa::address_key256() const +{ + uint8_t buffer[32] = {0}; + memcpy(buffer, address.bytes, sizeof(address.bytes)); + return fixed_bytes<32>(buffer).get_array(); +} + +void evm_eoa::sign(silkworm::Transaction& trx) +{ + silkworm::Bytes rlp; + trx.chain_id = basic_evm_tester::evm_chain_id; + trx.nonce = next_nonce++; + silkworm::rlp::encode(rlp, trx, true, false); + ethash::hash256 hash{silkworm::keccak256(rlp)}; + + secp256k1_ecdsa_recoverable_signature sig; + BOOST_REQUIRE(secp256k1_ecdsa_sign_recoverable(ctx, &sig, hash.bytes, private_key.data(), NULL, NULL)); + uint8_t r_and_s[64]; + int recid; + secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, r_and_s, &recid, &sig); + + trx.r = intx::be::unsafe::load(r_and_s); + trx.s = intx::be::unsafe::load(r_and_s + 32); + trx.odd_y_parity = recid; +} + +evm_eoa::~evm_eoa() { secp256k1_context_destroy(ctx); } + + +evmc::address basic_evm_tester::make_reserved_address(uint64_t account) +{ + // clang-format off + return evmc_address({0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + static_cast(account >> 56), + static_cast(account >> 48), + static_cast(account >> 40), + static_cast(account >> 32), + static_cast(account >> 24), + static_cast(account >> 16), + static_cast(account >> 8), + static_cast(account >> 0)}); + // clang-format on +} + +basic_evm_tester::basic_evm_tester(std::string native_symbol_str) : + native_symbol(symbol::from_string(native_symbol_str)) +{ + create_accounts({token_account_name, faucet_account_name, evm_account_name}); + produce_block(); + + set_code(token_account_name, testing::contracts::eosio_token_wasm()); + set_abi(token_account_name, testing::contracts::eosio_token_abi().data()); + + push_action(token_account_name, + "create"_n, + token_account_name, + mvo()("issuer", "eosio.token"_n)("maximum_supply", asset(10'000'000'000'0000, native_symbol))); + push_action(token_account_name, + "issue"_n, + token_account_name, + mvo()("to", faucet_account_name)("quantity", asset(1'000'000'000'0000, native_symbol))("memo", "")); + + produce_block(); + + set_code(evm_account_name, testing::contracts::evm_runtime_wasm()); + set_abi(evm_account_name, testing::contracts::evm_runtime_abi().data()); + produce_block(); +} + +asset basic_evm_tester::make_asset(int64_t amount) const { return asset(amount, native_symbol); } + +transaction_trace_ptr basic_evm_tester::transfer_token(name from, name to, asset quantity, std::string memo) +{ + return push_action( + token_account_name, "transfer"_n, from, mvo()("from", from)("to", to)("quantity", quantity)("memo", memo)); +} + +void basic_evm_tester::init(const uint64_t chainid, + const uint64_t gas_price, + const uint32_t miner_cut, + const std::optional ingress_bridge_fee, + const bool also_prepare_self_balance) +{ + mvo fee_params; + fee_params("gas_price", gas_price)("miner_cut", miner_cut); + + if (ingress_bridge_fee.has_value()) { + fee_params("ingress_bridge_fee", *ingress_bridge_fee); + } else { + fee_params("ingress_bridge_fee", fc::variant()); + } + + push_action(evm_account_name, "init"_n, evm_account_name, mvo()("chainid", chainid)("fee_params", fee_params)); + + if (also_prepare_self_balance) { + prepare_self_balance(); + } +} + +void basic_evm_tester::prepare_self_balance(uint64_t fund_amount) +{ + // Ensure internal balance for evm_account_name has at least 1 EOS to cover max bridge gas fee with even high gas + // price. + transfer_token(faucet_account_name, evm_account_name, make_asset(1'0000), evm_account_name.to_string()); +} + +config_table_row basic_evm_tester::get_config() const +{ + static constexpr eosio::chain::name config_singleton_name = "config"_n; + const vector d = + get_row_by_account(evm_account_name, evm_account_name, config_singleton_name, config_singleton_name); + return fc::raw::unpack(d); +} + +void basic_evm_tester::setfeeparams(const fee_parameters& fee_params) +{ + mvo fee_params_vo; + + if (fee_params.gas_price.has_value()) { + fee_params_vo("gas_price", *fee_params.gas_price); + } else { + fee_params_vo("gas_price", fc::variant()); + } + + if (fee_params.miner_cut.has_value()) { + fee_params_vo("miner_cut", *fee_params.miner_cut); + } else { + fee_params_vo("miner_cut", fc::variant()); + } + + if (fee_params.ingress_bridge_fee.has_value()) { + fee_params_vo("ingress_bridge_fee", *fee_params.ingress_bridge_fee); + } else { + fee_params_vo("ingress_bridge_fee", fc::variant()); + } + + push_action(evm_account_name, "setfeeparams"_n, evm_account_name, mvo()("fee_params", fee_params_vo)); +} + +silkworm::Transaction +basic_evm_tester::generate_tx(const evmc::address& to, const intx::uint256& value, uint64_t gas_limit) const +{ + const auto gas_price = get_config().gas_price; + + return silkworm::Transaction{ + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = gas_price, + .max_fee_per_gas = gas_price, + .gas_limit = gas_limit, + .to = to, + .value = value, + }; +} + +void basic_evm_tester::pushtx(const silkworm::Transaction& trx, name miner) +{ + silkworm::Bytes rlp; + silkworm::rlp::encode(rlp, trx); + + bytes rlp_bytes; + rlp_bytes.resize(rlp.size()); + memcpy(rlp_bytes.data(), rlp.data(), rlp.size()); + + push_action(evm_account_name, "pushtx"_n, miner, mvo()("miner", miner)("rlptx", rlp_bytes)); +} + +evmc::address basic_evm_tester::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); + pushtx(tx); + + return silkworm::create_address(eoa.address, nonce); +} + +void basic_evm_tester::addegress(const std::vector& accounts) +{ + push_action(evm_account_name, "addegress"_n, evm_account_name, mvo()("accounts", accounts)); +} + +void basic_evm_tester::removeegress(const std::vector& accounts) +{ + push_action(evm_account_name, "removeegress"_n, evm_account_name, mvo()("accounts", accounts)); +} + +void basic_evm_tester::open(name owner) { push_action(evm_account_name, "open"_n, owner, mvo()("owner", owner)); } + +void basic_evm_tester::close(name owner) { push_action(evm_account_name, "close"_n, owner, mvo()("owner", owner)); } + +void basic_evm_tester::withdraw(name owner, asset quantity) +{ + push_action(evm_account_name, "withdraw"_n, owner, mvo()("owner", owner)("quantity", quantity)); +} + +balance_and_dust basic_evm_tester::vault_balance(name owner) const +{ + const vector d = get_row_by_account(evm_account_name, evm_account_name, "balances"_n, owner); + FC_ASSERT(d.size(), "EVM not open"); + auto [_, amount, dust] = fc::raw::unpack(d); + return {.balance = amount, .dust = dust}; +} + +std::optional basic_evm_tester::evm_balance(const evmc::address& address) const +{ + const auto& a = find_account_by_address(address); + + if (!a) { + return std::nullopt; + } + + return a->balance; +} + +std::optional basic_evm_tester::evm_balance(const evm_eoa& account) const +{ + return evm_balance(account.address); +} + +std::optional convert_to_account_object(const partial_account_table_row& row) +{ + evmc::address address(0); + + if (row.eth_address.size() != sizeof(address.bytes)) { + return std::nullopt; + } + + if (row.balance.size() != 32) { + return std::nullopt; + } + + std::memcpy(address.bytes, row.eth_address.data(), sizeof(address.bytes)); + + return account_object{ + .id = row.id, + .address = std::move(address), + .nonce = row.nonce, + .balance = intx::be::unsafe::load(reinterpret_cast(row.balance.data())), + }; +} + +bool basic_evm_tester::scan_accounts(std::function visitor) const +{ + static constexpr eosio::chain::name account_table_name = "account"_n; + + bool successful = true; + + scan_table( + account_table_name, evm_account_name, [this, &visitor, &successful](partial_account_table_row&& row) { + if (auto obj = convert_to_account_object(row)) { + return visitor(std::move(*obj)); + } + successful = false; + return true; + }); + + return successful; +} + +std::optional basic_evm_tester::scan_for_account_by_address(const evmc::address& address) const +{ + std::optional result; + + std::basic_string_view address_view{address}; + + scan_accounts([&](account_object&& account) -> bool { + if (account.address == address) { + result.emplace(account); + return true; + } + return false; + }); + + return result; +} + +std::optional basic_evm_tester::find_account_by_address(const evmc::address& address) const +{ + static constexpr eosio::chain::name account_table_name = "account"_n; + + std::optional result; + + const auto& db = control->db(); + + const auto* t_id = db.find( + boost::make_tuple(evm_account_name, evm_account_name, account_table_name)); + + if (!t_id) { + return std::nullopt; + } + + uint8_t address_buffer[32] = {0}; + std::memcpy(address_buffer, address.bytes, sizeof(address.bytes)); + + const auto* secondary_row = db.find( + boost::make_tuple(t_id->id, fixed_bytes<32>(address_buffer).get_array())); + + if (!secondary_row) { + return std::nullopt; + } + + const auto* primary_row = db.find( + boost::make_tuple(t_id->id, secondary_row->primary_key)); + + if (!primary_row) { + return std::nullopt; + } + + { + partial_account_table_row row; + fc::datastream ds(primary_row->value.data(), primary_row->value.size()); + fc::raw::unpack(ds, row); + + result = convert_to_account_object(row); + } + + return result; +} + +bool basic_evm_tester::scan_account_storage(uint64_t account_id, std::function visitor) const +{ + static constexpr eosio::chain::name storage_table_name = "storage"_n; + + bool successful = true; + + scan_table( + storage_table_name, name{account_id}, [&visitor, &successful](storage_table_row&& row) { + if (row.key.size() != 32 || row.value.size() != 32) { + successful = false; + return true; + } + return visitor(storage_slot{ + .id = row.id, + .key = intx::be::unsafe::load(reinterpret_cast(row.key.data())), + .value = intx::be::unsafe::load(reinterpret_cast(row.value.data()))}); + }); + + return successful; +} + +} // namespace evm_test \ No newline at end of file diff --git a/contract/tests/basic_evm_tester.hpp b/contract/tests/basic_evm_tester.hpp index 6b4538b2..a4db6066 100644 --- a/contract/tests/basic_evm_tester.hpp +++ b/contract/tests/basic_evm_tester.hpp @@ -2,8 +2,21 @@ #include #include +#include #include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include #include @@ -11,16 +24,251 @@ using namespace eosio; using namespace eosio::chain; using mvo = fc::mutable_variant_object; -class basic_evm_tester : public testing::validating_tester { +using intx::operator""_u256; + +namespace intx { + +inline std::ostream& operator<<(std::ostream& ds, const intx::uint256& num) +{ + ds << intx::to_string(num, 10); + return ds; +} + +} // namespace intx + +namespace fc { + +void to_variant(const intx::uint256& o, fc::variant& v); +void to_variant(const evmc::address& o, fc::variant& v); + +} // namespace fc + +namespace evm_test { + +struct config_table_row +{ + unsigned_int version; + uint64_t chainid; + time_point_sec genesis_time; + asset ingress_bridge_fee; + uint64_t gas_price; + uint32_t miner_cut; + uint32_t status; +}; + +struct balance_and_dust +{ + asset balance; + uint64_t dust = 0; + + explicit operator intx::uint256() const; + + bool operator==(const balance_and_dust&) const; + bool operator!=(const balance_and_dust&) const; +}; + +struct account_object +{ + uint64_t id; + evmc::address address; + uint64_t nonce; + intx::uint256 balance; +}; + +struct storage_slot +{ + uint64_t id; + intx::uint256 key; + intx::uint256 value; +}; + + +struct fee_parameters +{ + std::optional gas_price; + std::optional miner_cut; + std::optional ingress_bridge_fee; +}; + +} // 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::balance_and_dust, (balance)(dust)); +FC_REFLECT(evm_test::account_object, (id)(address)(nonce)(balance)) +FC_REFLECT(evm_test::storage_slot, (id)(key)(value)) +FC_REFLECT(evm_test::fee_parameters, (gas_price)(miner_cut)(ingress_bridge_fee)) + +namespace evm_test { +class evm_eoa +{ public: - basic_evm_tester() { - create_accounts({"evm"_n}); + explicit evm_eoa(std::basic_string optional_private_key = {}); + + std::string address_0x() const; + + key256_t address_key256() const; + + void sign(silkworm::Transaction& trx); + + ~evm_eoa(); + + evmc::address address; + uint64_t next_nonce = 0; + +private: + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + std::array private_key; + std::basic_string public_key; +}; + +class basic_evm_tester : public testing::validating_tester +{ +public: + static constexpr name token_account_name = "eosio.token"_n; + static constexpr name faucet_account_name = "faucet"_n; + static constexpr name evm_account_name = "evm"_n; + + static constexpr uint64_t evm_chain_id = 15555; + + // Sensible values for fee parameters passed into init: + static constexpr uint64_t suggested_gas_price = 150'000'000'000; // 150 gwei + static constexpr uint32_t suggested_miner_cut = 10'000; // 10% + static constexpr uint64_t suggested_ingress_bridge_fee_amount = 70; // 0.0070 EOS + + const symbol native_symbol; + + static evmc::address make_reserved_address(uint64_t account); - set_code("evm"_n, testing::contracts::evm_runtime_wasm()); - set_abi("evm"_n, testing::contracts::evm_runtime_abi().data()); + explicit basic_evm_tester(std::string native_symbol_str = "4,EOS"); + + asset make_asset(int64_t amount) const; + + transaction_trace_ptr transfer_token(name from, name to, asset quantity, std::string memo = ""); + + void init(const uint64_t chainid = evm_chain_id, + const uint64_t gas_price = suggested_gas_price, + const uint32_t miner_cut = suggested_miner_cut, + const std::optional ingress_bridge_fee = std::nullopt, + const bool also_prepare_self_balance = true); + + void prepare_self_balance(uint64_t fund_amount = 100'0000); + + config_table_row get_config() const; + + void setfeeparams(const fee_parameters& fee_params); + + silkworm::Transaction + generate_tx(const evmc::address& to, const intx::uint256& value, uint64_t gas_limit = 21000) const; + + void pushtx(const silkworm::Transaction& trx, name miner = evm_account_name); + evmc::address deploy_contract(evm_eoa& eoa, evmc::bytes bytecode); + + void addegress(const std::vector& accounts); + void removeegress(const std::vector& accounts); + + void open(name owner); + void close(name owner); + void withdraw(name owner, asset quantity); + + balance_and_dust vault_balance(name owner) const; + std::optional evm_balance(const evmc::address& address) const; + std::optional evm_balance(const evm_eoa& account) const; + + template + void scan_table(eosio::chain::name table_name, eosio::chain::name scope_name, Visitor&& visitor) const + { + const auto& db = control->db(); + + const auto* t_id = db.find( + boost::make_tuple(evm_account_name, scope_name, table_name)); + + if (!t_id) { + return; + } + + const auto& idx = db.get_index(); + + for (auto itr = idx.lower_bound(boost::make_tuple(t_id->id)); itr != idx.end() && itr->t_id == t_id->id; ++itr) { + T row{}; + fc::datastream ds(itr->value.data(), itr->value.size()); + fc::raw::unpack(ds, row); + if (visitor(std::move(row))) { + // Returning true from visitor means the visitor is no longer interested in continuing the scan. + return; + } + } } - void init(const uint64_t chainid) { - push_action("evm"_n, "init"_n, "evm"_n, mvo()("chainid", chainid)); + bool scan_accounts(std::function visitor) const; + std::optional scan_for_account_by_address(const evmc::address& address) const; + std::optional find_account_by_address(const evmc::address& address) const; + bool scan_account_storage(uint64_t account_id, std::function visitor) const; +}; + +inline constexpr intx::uint256 operator"" _wei(const char* s) { return intx::from_string(s); } + +inline constexpr intx::uint256 operator"" _kwei(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 3_u256); +} + +inline constexpr intx::uint256 operator"" _mwei(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 6_u256); +} + +inline constexpr intx::uint256 operator"" _gwei(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 9_u256); +} + +inline constexpr intx::uint256 operator"" _szabo(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 12_u256); +} + +inline constexpr intx::uint256 operator"" _finney(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 15_u256); +} + +inline constexpr intx::uint256 operator"" _ether(const char* s) +{ + return intx::from_string(s) * intx::exp(10_u256, 18_u256); +} + +template +class speculative_block_starter +{ +public: + // Assumes user will not abort or finish blocks using the tester passed into the constructor for the lifetime of this + // object. + explicit speculative_block_starter(Tester& tester, uint32_t time_gap_sec = 0) : t(tester) + { + t.control->start_block(t.control->head_block_time() + fc::milliseconds(500 + 1000 * time_gap_sec), 0); } -}; \ No newline at end of file + + speculative_block_starter(speculative_block_starter&& other) : t(other.t) { other.canceled = true; } + + speculative_block_starter(const speculative_block_starter&) = delete; + + ~speculative_block_starter() + { + if (!canceled) { + t.control->abort_block(); // Undo side-effects and go back to state just prior to constructor + } + } + + speculative_block_starter& operator=(speculative_block_starter&&) = delete; + speculative_block_starter& operator=(const speculative_block_starter&) = delete; + + void cancel() { canceled = true; } + +private: + Tester& t; + bool canceled = false; +}; + +} // namespace evm_test \ No newline at end of file diff --git a/contract/tests/blockhash_tests.cpp b/contract/tests/blockhash_tests.cpp new file mode 100644 index 00000000..7c562496 --- /dev/null +++ b/contract/tests/blockhash_tests.cpp @@ -0,0 +1,98 @@ +#include "basic_evm_tester.hpp" +#include + +using namespace evm_test; +struct blockhash_evm_tester : basic_evm_tester { + blockhash_evm_tester() { + create_accounts({"alice"_n}); + transfer_token(faucet_account_name, "alice"_n, make_asset(10000'0000)); + init(); + } +}; + +BOOST_AUTO_TEST_SUITE(blockhash_evm_tests) +BOOST_FIXTURE_TEST_CASE(blockhash_tests, blockhash_evm_tester) try { + + // tests/leap/nodeos_trust_evm_server/contracts/Blockhash.sol + const std::string blockhash_bytecode = + "608060405234801561001057600080fd5b506102e8806100206000396000f3fe608060405234801561001057600080fd5b5060" + "04361061007c5760003560e01c8063c059239b1161005b578063c059239b146100c7578063c835de3c146100e5578063edb572" + "a814610103578063f9943638146101215761007c565b80627da6cb146100815780630f59f83a1461009f5780638603136b1461" + "00a9575b600080fd5b61008961013f565b6040516100969190610200565b60405180910390f35b6100a7610149565b005b6100" + "b16101b6565b6040516100be9190610200565b60405180910390f35b6100cf6101c0565b6040516100dc9190610200565b6040" + "5180910390f35b6100ed6101ca565b6040516100fa9190610200565b60405180910390f35b61010b6101d4565b604051610118" + "9190610200565b60405180910390f35b6101296101de565b6040516101369190610234565b60405180910390f35b6000600454" + "905090565b4360008190555060014361015d919061027e565b40600181905550600243610171919061027e565b406002819055" + "50600343610185919061027e565b40600381905550600443610199919061027e565b406004819055506005436101ad91906102" + "7e565b40600581905550565b6000600154905090565b6000600554905090565b6000600354905090565b600060025490509056" + "5b60008054905090565b6000819050919050565b6101fa816101e7565b82525050565b60006020820190506102156000830184" + "6101f1565b92915050565b6000819050919050565b61022e8161021b565b82525050565b600060208201905061024960008301" + "84610225565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160" + "045260246000fd5b60006102898261021b565b91506102948361021b565b92508282039050818111156102ac576102ab61024f" + "565b5b9291505056fea2646970667358221220c437dcf9206268de785ce81fef2e29e3da1e58070205eb27bc943f73938bd292" + "64736f6c63430008110033"; + + // Fund evm1 address with 100 EOS + evm_eoa evm1; + const int64_t to_bridge = 1000000; + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()); + + // Deploy blockhash contract and get its address and account ID + auto contract_addr = deploy_contract(evm1, evmc::from_hex(blockhash_bytecode).value()); + uint64_t contract_account_id = find_account_by_address(contract_addr).value().id; + + // Generate some blocks + produce_blocks(50); + + // Call method "go" on blockhash contract (sha3('go()') = 0x0f59f83a) + // This method will store the current EVM block number and the 5 previous block hashes + auto txn = generate_tx(contract_addr, 0, 500'000); + txn.data = evmc::from_hex("0x0f59f83a").value(); + evm1.sign(txn); + pushtx(txn); + + // blockhash(n) = sha256(0x00 || n || chain_id) + auto generate_block_hash = [](uint64_t height) { + char buffer[1+8+8]; + datastream ds(buffer, sizeof(buffer)); + ds << uint8_t{0}; + ds << uint64_t{height}; + ds << uint64_t{15555}; + auto h = fc::sha256::hash(buffer, sizeof(buffer)); + return fc::to_hex(h.data(), h.data_size()); + }; + + // Retrieve 6 slots from contract storage where block number and hashes were saved + // ... + // contract Blockhash { + // + // uint256 curr_block; //slot0 + // bytes32 prev1; //slot1 + // bytes32 prev2; //slot2 + // bytes32 prev3; //slot3 + // bytes32 prev4; //slot4 + // bytes32 prev5; //slot5 + // .. + std::map slots; + bool successful_scan = scan_account_storage(contract_account_id, [&](storage_slot&& slot) -> bool { + if(slot.key <= 5) { + auto key_u64 = static_cast(slot.key); + BOOST_REQUIRE(slots.count(key_u64) == 0); + slots[key_u64] = slot.value; + return false; + } + BOOST_ERROR("unexpected storage in contract"); + return true; + }); + + BOOST_REQUIRE(slots.size() == 6); + auto curr_block = static_cast(slots[0]); + + for (auto n : { 1, 2, 3, 4, 5 }) { + auto retrieved_block_hash = intx::hex(slots[n]); + auto calculated_block_hash = generate_block_hash(curr_block-n); + BOOST_REQUIRE(calculated_block_hash == retrieved_block_hash); + } + +} FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/contract/tests/contracts/solidity/BlockNumTimestamp.sol b/contract/tests/contracts/solidity/BlockNumTimestamp.sol new file mode 100644 index 00000000..2e57061d --- /dev/null +++ b/contract/tests/contracts/solidity/BlockNumTimestamp.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.2 <0.9.0; + +contract BlockNumTimestamp { + uint256 public blockNumber; + uint256 public timestamp; + + constructor() { + blockNumber = block.number; + timestamp = block.timestamp; + } +} \ No newline at end of file diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 341f2bcb..fc49e654 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -118,6 +118,24 @@ const table_id_object& find_or_create_table( chainbase::database& db, name code, }); } +template +static std::optional get_by_primary_key(chainbase::database& db, const name& scope, const T& o) { + const auto& tab = find_or_create_table( + db, "evm"_n, scope, Object::table_name(), "evm"_n + ); + + const auto* kv_obj = db.find( + boost::make_tuple(tab.id, o) + ); + + BOOST_REQUIRE( kv_obj != nullptr ); + + return fc::raw::unpack( + kv_obj->value.data(), + kv_obj->value.size() + ); +} + template static std::optional get_by_index(chainbase::database& db, const name& scope, const name& inx, const T& o) { @@ -237,10 +255,8 @@ struct account { bytes eth_address; uint64_t nonce; bytes balance; - bytes code; - bytes code_hash; + std::optional code_id; - bytes old_code_hash; struct by_address { typedef index256_object index_object; @@ -249,39 +265,17 @@ struct account { } }; - struct by_codehash { - typedef index256_object index_object; - static name index_name() { - return account::index_name("by.codehash"_n); - } - }; - evmc::uint256be get_balance()const { evmc::uint256be res; std::copy(balance.begin(), balance.end(), res.bytes); return res; } - evmc::bytes32 get_code_hash()const { - evmc::bytes32 res; - std::copy(code_hash.begin(), code_hash.end(), res.bytes); - return res; - } - static name table_name() { return "account"_n; } static name index_name(const name& n) { uint64_t index_table_name = table_name().to_uint64_t() & 0xFFFFFFFFFFFFFFF0ULL; - //0=>by.address, 1=>by.codehash - if( n == "by.address"_n ) { - return name{index_table_name | 0}; - } else if( n == "by.codehash"_n ) { - return name{index_table_name | 1}; - } - - dlog("index name not found: ${a}", ("a",n.to_string())); - BOOST_REQUIRE(false); - return name{0}; + return name{index_table_name | 0}; } static name index_name(uint64_t n) { @@ -290,27 +284,50 @@ struct account { static std::optional get_by_address(chainbase::database& db, const evmc::address& address) { auto r = get_by_index(db, "evm"_n, "by.address"_n, address); - if(r) r->old_code_hash = r->code_hash; return r; } - static std::optional get_by_code_hash(chainbase::database& db, const evmc::bytes32& code_hash) { - auto r = get_by_index(db, "evm"_n, "by.codehash"_n, code_hash); - if(r) r->old_code_hash = r->code_hash; - return r; +}; +FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_id)); + +struct account_code { + uint64_t id; + uint32_t ref_count; + bytes code; + bytes code_hash; + + + struct by_codehash { + typedef index256_object index_object; + static name index_name() { + return account_code::index_name("by.codehash"_n); + } + }; + + evmc::bytes32 get_code_hash()const { + evmc::bytes32 res; + std::copy(code_hash.begin(), code_hash.end(), res.bytes); + return res; } - Account as_silkworm_account() { - return Account{ - nonce, - intx::be::load(get_balance()), - get_code_hash(), - 0 //TODO: ?? - }; + static name table_name() { return "accountcode"_n; } + static name index_name(const name& n) { + uint64_t index_table_name = table_name().to_uint64_t() & 0xFFFFFFFFFFFFFFF0ULL; + + return name{index_table_name | 0}; + } + + static name index_name(uint64_t n) { + return index_name(name{n}); + } + + static std::optional get_by_code_hash(chainbase::database& db, const evmc::bytes32& code_hash) { + auto r = get_by_index(db, "evm"_n, "by.codehash"_n, code_hash); + return r; } }; -FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code)(code_hash)); +FC_REFLECT(account_code, (id)(ref_count)(code)(code_hash)); struct storage { uint64_t id; @@ -407,7 +424,7 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { } BOOST_REQUIRE_EQUAL( success(), push_action(eosio::chain::config::system_account_name, "wasmcfg"_n, mvo()("settings", "high")) ); - create_account_with_resources(ME, system_account_name, 5000000); + create_account_with_resources(ME, system_account_name, 6000000); set_authority( ME, "active"_n, {1, {{get_public_key(ME,"active"),1}}, {{{ME,"eosio.code"_n},1}}} ); set_code(ME, contracts::evm_runtime_wasm()); @@ -418,7 +435,17 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true); evm_runtime_abi.set_abi(abi, abi_serializer_max_time); - base_tester::push_action(ME, "init"_n, ME, mvo()("chainid", 15555)); + base_tester::push_action(ME, + "init"_n, + ME, + mvo() // + ("chainid", 15555) // + ("fee_params", // + mvo() // + ("gas_price", 150'000'000'000) // + ("miner_cut", 10'000) // + ("ingress_bridge_fee", fc::variant())) // + ); } std::string to_str(const fc::variant& o) { @@ -557,24 +584,49 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { ); } + action_result testbaldust( name testname ) { + return call(ME, "testbaldust"_n, mvo() + ("test", testname) + ); + } + //------ silkworm state impl std::optional read_account(const evmc::address& address) const noexcept { auto& db = const_cast(control->db()); auto accnt = account::get_by_address(db, address); if(!accnt) return {}; - return accnt->as_silkworm_account(); + + if (accnt->code_id.has_value()) { + auto r = get_by_primary_key(db, "evm"_n, accnt->code_id.value()); + if (r) { + evmc::bytes32 res; + std::copy(r.value().code_hash.begin(), r.value().code_hash.end(), res.bytes); + return Account{ + accnt->nonce, + intx::be::load(accnt->get_balance()), + res, + 0 //TODO: ?? + }; + } + } + return Account{ + accnt->nonce, + intx::be::load(accnt->get_balance()), + kEmptyHash, + 0 //TODO: ?? + }; }; mutable bytes read_code_buffer; ByteView read_code(const evmc::bytes32& code_hash) const noexcept { auto& db = const_cast(control->db()); - auto accnt = account::get_by_code_hash(db, code_hash); - if(!accnt) { + auto accntcode = account_code::get_by_code_hash(db, code_hash); + if(!accntcode) { dlog("no code for hash ${ch}", ("ch",to_bytes(code_hash))); return ByteView{}; } //dlog("${a} ${c} ${ch} ${ch2}", ("a",accnt->eth_address)("c",accnt->code)("ch2",accnt->code_hash)("ch",to_bytes(code_hash))); - read_code_buffer = accnt->code; + read_code_buffer = accntcode->code; return ByteView{(const uint8_t*)read_code_buffer.data(), read_code_buffer.size()}; } @@ -873,6 +925,8 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { const auto nonce_str{j["nonce"].get()}; account.nonce = std::stoull(nonce_str, nullptr, /*base=*/16); + update_account(address, /*initial=*/std::nullopt, account); + const Bytes code{from_hex(j["code"].get()).value()}; if (!code.empty()) { account.incarnation = kDefaultIncarnation; @@ -883,8 +937,6 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { update_account_code(address, account.incarnation, account.code_hash, code); } - update_account(address, /*initial=*/std::nullopt, account); - for (const auto& storage : j["storage"].items()) { Bytes key{from_hex(storage.key()).value()}; Bytes value{from_hex(storage.value().get()).value()}; @@ -1088,4 +1140,25 @@ BOOST_FIXTURE_TEST_CASE( GeneralStateTests, evm_runtime_tester ) try { BOOST_REQUIRE_EQUAL(total_failed, 0); } FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE( balance_and_dust_tests, evm_runtime_tester ) try { + BOOST_REQUIRE_EQUAL(testbaldust("basic"_n), success()); + + BOOST_REQUIRE_EQUAL(testbaldust("underflow1"_n), error("assertion failure with message: decrementing more than available")); + BOOST_REQUIRE_EQUAL(testbaldust("underflow2"_n), error("assertion failure with message: decrementing more than available")); + BOOST_REQUIRE_EQUAL(testbaldust("underflow3"_n), error("assertion failure with message: decrementing more than available")); + BOOST_REQUIRE_EQUAL(testbaldust("underflow4"_n), error("assertion failure with message: decrementing more than available")); + BOOST_REQUIRE_EQUAL(testbaldust("underflow5"_n), error("assertion failure with message: decrementing more than available")); + + BOOST_REQUIRE_EQUAL(testbaldust("overflow1"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflow2"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflow3"_n), success()); + BOOST_REQUIRE_EQUAL(testbaldust("overflow4"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflow5"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflowa"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflowb"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflowc"_n), error("assertion failure with message: accumulation overflow")); + BOOST_REQUIRE_EQUAL(testbaldust("overflowd"_n), error("assertion failure with message: accumulation overflow")); +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END() diff --git a/contract/tests/gas_fee_tests.cpp b/contract/tests/gas_fee_tests.cpp new file mode 100644 index 00000000..bab9980e --- /dev/null +++ b/contract/tests/gas_fee_tests.cpp @@ -0,0 +1,287 @@ +#include "basic_evm_tester.hpp" + +using namespace eosio::testing; +using namespace evm_test; + +struct gas_fee_evm_tester : basic_evm_tester +{ + evm_eoa faucet_eoa; + + static constexpr name miner_account_name = "alice"_n; + + gas_fee_evm_tester() : + faucet_eoa(evmc::from_hex("a3f1b69da92a0233ce29485d3049a4ace39e8d384bbc2557e3fc60940ce4e954").value()) + { + create_accounts({miner_account_name}); + transfer_token(faucet_account_name, miner_account_name, make_asset(100'0000)); + } + + void fund_evm_faucet() + { + transfer_token(faucet_account_name, evm_account_name, make_asset(100'0000), faucet_eoa.address_0x()); + } +}; + +BOOST_AUTO_TEST_SUITE(gas_fee_evm_tests) + +BOOST_FIXTURE_TEST_CASE(check_init_required_gas_fee_parameters, gas_fee_evm_tester) +try { + + auto suggested_ingress_bridge_fee = make_asset(suggested_ingress_bridge_fee_amount); + + mvo missing_gas_price; + missing_gas_price // + ("gas_price", fc::variant()) // + ("miner_cut", suggested_miner_cut) // + ("ingress_bridge_fee", suggested_ingress_bridge_fee); // + + mvo missing_miner_cut; + missing_miner_cut // + ("gas_price", suggested_gas_price) // + ("miner_cut", fc::variant()) // + ("ingress_bridge_fee", suggested_ingress_bridge_fee); // + + mvo missing_ingress_bridge_fee; + missing_ingress_bridge_fee // + ("gas_price", suggested_gas_price) // + ("miner_cut", suggested_miner_cut) // + ("ingress_bridge_fee", fc::variant()); // + + // gas_price must be provided during init + BOOST_REQUIRE_EXCEPTION( + push_action( + evm_account_name, "init"_n, evm_account_name, mvo()("chainid", evm_chain_id)("fee_params", missing_gas_price)), + eosio_assert_message_exception, + eosio_assert_message_is("All required fee parameters not specified: missing gas_price")); + + // miner_cut must be provided during init + BOOST_REQUIRE_EXCEPTION( + push_action( + evm_account_name, "init"_n, evm_account_name, mvo()("chainid", evm_chain_id)("fee_params", missing_miner_cut)), + eosio_assert_message_exception, + eosio_assert_message_is("All required fee parameters not specified: missing miner_cut")); + + // It is acceptable for the ingress_bridge_fee to not be provided during init. + push_action(evm_account_name, + "init"_n, + evm_account_name, + mvo()("chainid", evm_chain_id)("fee_params", missing_ingress_bridge_fee)); +} +FC_LOG_AND_RETHROW() + + +BOOST_FIXTURE_TEST_CASE(set_fee_parameters, gas_fee_evm_tester) +try { + uint64_t starting_gas_price = 5'000'000'000; + uint32_t starting_miner_cut = 50'000; + int64_t starting_ingress_bridge_fee_amount = 3; + + init(evm_chain_id, starting_gas_price, starting_miner_cut, make_asset(starting_ingress_bridge_fee_amount)); + + const auto& conf1 = get_config(); + + BOOST_CHECK_EQUAL(conf1.gas_price, starting_gas_price); + BOOST_CHECK_EQUAL(conf1.miner_cut, starting_miner_cut); + BOOST_CHECK_EQUAL(conf1.ingress_bridge_fee, make_asset(starting_ingress_bridge_fee_amount)); + + // Cannot set miner_cut to above 100%. + BOOST_REQUIRE_EXCEPTION(setfeeparams({.miner_cut = 100'001}), + eosio_assert_message_exception, + eosio_assert_message_is("miner_cut cannot exceed 100,000 (100%)")); + + // Change only miner_cut to 100%. + setfeeparams({.miner_cut = 100'000}); + + const auto& conf2 = get_config(); + + BOOST_CHECK_EQUAL(conf2.gas_price, conf1.gas_price); + BOOST_CHECK_EQUAL(conf2.miner_cut, 100'000); + BOOST_CHECK_EQUAL(conf2.ingress_bridge_fee, conf1.ingress_bridge_fee); + + // Change only gas_price to 0 + setfeeparams({.gas_price = 0}); + + const auto& conf3 = get_config(); + + BOOST_CHECK_EQUAL(conf3.gas_price, 0); + BOOST_CHECK_EQUAL(conf3.miner_cut, conf2.miner_cut); + BOOST_CHECK_EQUAL(conf3.ingress_bridge_fee, conf2.ingress_bridge_fee); + + // Change only ingress_bridge_fee to 0.0040 EOS + setfeeparams({.ingress_bridge_fee = make_asset(40)}); + + const auto& conf4 = get_config(); + + BOOST_CHECK_EQUAL(conf4.gas_price, conf3.gas_price); + BOOST_CHECK_EQUAL(conf4.miner_cut, conf3.miner_cut); + BOOST_CHECK_EQUAL(conf4.ingress_bridge_fee, make_asset(40)); +} +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(reject_low_gas_price, gas_fee_evm_tester) +try { + init(evm_chain_id, suggested_gas_price, suggested_miner_cut, make_asset(suggested_ingress_bridge_fee_amount)); + fund_evm_faucet(); + + evm_eoa recipient; + + { + // Low gas price is rejected + + static_assert(suggested_gas_price >= 2); + + auto restore_nonce = faucet_eoa.next_nonce; + + silkworm::Transaction tx{ + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = suggested_gas_price - 1, + .max_fee_per_gas = suggested_gas_price - 1, + .gas_limit = 21000, + .to = recipient.address, + .value = 1, + }; + faucet_eoa.sign(tx); + + BOOST_REQUIRE_EXCEPTION( + pushtx(tx), eosio_assert_message_exception, eosio_assert_message_is("gas price is too low")); + + faucet_eoa.next_nonce = restore_nonce; + } + + { + // Exactly matching gas price is accepted + + silkworm::Transaction tx{ + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = suggested_gas_price, + .max_fee_per_gas = suggested_gas_price, + .gas_limit = 21000, + .to = recipient.address, + .value = 1, + }; + faucet_eoa.sign(tx); + pushtx(tx); + } + + { + // Higher gas price is also okay + + silkworm::Transaction tx{ + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = suggested_gas_price + 1, + .max_fee_per_gas = suggested_gas_price + 1, + .gas_limit = 21000, + .to = recipient.address, + .value = 1, + }; + faucet_eoa.sign(tx); + pushtx(tx); + } +} +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(miner_cut_calculation, gas_fee_evm_tester) +try { + produce_block(); + control->abort_block(); + + static constexpr uint32_t hundred_percent = 100'000; + + evm_eoa recipient; + + auto dump_accounts = [&]() { + scan_accounts([](evm_test::account_object&& account) -> bool { + idump((account)); + return false; + }); + }; + + struct gas_fee_data + { + uint64_t gas_price; + uint32_t miner_cut; + uint64_t expected_gas_fee_miner_portion; + uint64_t expected_gas_fee_contract_portion; + }; + + std::vector gas_fee_trials = { + {0, 50'000, 0, 0 }, + {1'000'000'000, 0, 0, 21'000'000'000'000}, + {1'000'000'000, 10'000, 2'100'000'000'000, 18'900'000'000'000}, + {1'000'000'000, 100'000, 21'000'000'000'000, 0 }, + }; + + // EVM contract account acts as the miner + auto run_test_with_contract_as_miner = [this, &recipient](const gas_fee_data& trial) { + speculative_block_starter sb{*this}; + + init(evm_chain_id, trial.gas_price, trial.miner_cut); + fund_evm_faucet(); + + const auto gas_fee = intx::uint256{trial.gas_price * 21000}; + + BOOST_CHECK_EQUAL(gas_fee, + intx::uint256(trial.expected_gas_fee_miner_portion + trial.expected_gas_fee_contract_portion)); + + const intx::uint256 special_balance_before{vault_balance(evm_account_name)}; + const intx::uint256 faucet_before = evm_balance(faucet_eoa).value(); + + auto tx = generate_tx(recipient.address, 1_gwei); + faucet_eoa.sign(tx); + pushtx(tx); + + BOOST_CHECK_EQUAL(*evm_balance(faucet_eoa), (faucet_before - tx.value - gas_fee)); + BOOST_REQUIRE(evm_balance(recipient).has_value()); + BOOST_CHECK_EQUAL(*evm_balance(recipient), tx.value); + BOOST_CHECK_EQUAL(static_cast(vault_balance(evm_account_name)), + (special_balance_before + gas_fee)); + + faucet_eoa.next_nonce = 0; + }; + + for (const auto& trial : gas_fee_trials) { + run_test_with_contract_as_miner(trial); + } + + // alice acts as the miner + auto run_test_with_alice_as_miner = [this, &recipient](const gas_fee_data& trial) { + speculative_block_starter sb{*this}; + + init(evm_chain_id, trial.gas_price, trial.miner_cut); + fund_evm_faucet(); + open(miner_account_name); + + const auto gas_fee = intx::uint256{trial.gas_price * 21000}; + const auto gas_fee_miner_portion = (gas_fee * trial.miner_cut) / hundred_percent; + + BOOST_CHECK_EQUAL(gas_fee_miner_portion, intx::uint256(trial.expected_gas_fee_miner_portion)); + + const auto gas_fee_contract_portion = gas_fee - gas_fee_miner_portion; + BOOST_CHECK_EQUAL(gas_fee_contract_portion, intx::uint256(trial.expected_gas_fee_contract_portion)); + + const intx::uint256 special_balance_before{vault_balance(evm_account_name)}; + const intx::uint256 miner_balance_before{vault_balance(miner_account_name)}; + const intx::uint256 faucet_before = evm_balance(faucet_eoa).value(); + + auto tx = generate_tx(recipient.address, 1_gwei); + faucet_eoa.sign(tx); + pushtx(tx, miner_account_name); + + BOOST_CHECK_EQUAL(*evm_balance(faucet_eoa), (faucet_before - tx.value - gas_fee)); + BOOST_REQUIRE(evm_balance(recipient).has_value()); + BOOST_CHECK_EQUAL(*evm_balance(recipient), tx.value); + BOOST_CHECK_EQUAL(static_cast(vault_balance(evm_account_name)), + (special_balance_before + gas_fee - gas_fee_miner_portion)); + BOOST_CHECK_EQUAL(static_cast(vault_balance(miner_account_name)), + (miner_balance_before + gas_fee_miner_portion)); + + faucet_eoa.next_nonce = 0; + }; + + for (const auto& trial : gas_fee_trials) { + run_test_with_alice_as_miner(trial); + } +} +FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/contract/tests/init_tests.cpp b/contract/tests/init_tests.cpp index 83b1f95f..04415aa8 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -1,14 +1,26 @@ #include "basic_evm_tester.hpp" +using namespace evm_test; + BOOST_AUTO_TEST_SUITE(evm_init_tests) BOOST_FIXTURE_TEST_CASE(check_init, basic_evm_tester) try { //all of these should fail since the contract hasn't been init'd yet - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("ram_payer", "evm"_n)("rlptx", bytes())), + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("miner", "evm"_n)("rlptx", bytes())), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("ram_payer", "evm"_n)), + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "setfeeparams"_n, "evm"_n, mvo()("fee_params", mvo()("gas_price", 1)("miner_cut", 0)("ingress_bridge_fee", fc::variant()))), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "addegress"_n, "evm"_n, mvo()("accounts", std::vector())), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "removeegress"_n, "evm"_n, mvo()("accounts", std::vector())), + eosio_assert_message_exception, + [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); + + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("miner", "evm"_n)), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract not initialized");}); BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "close"_n, "evm"_n, mvo()("owner", "evm"_n)), @@ -70,11 +82,11 @@ BOOST_FIXTURE_TEST_CASE(check_freeze, basic_evm_tester) try { push_action("evm"_n, "freeze"_n, "evm"_n, mvo()("value", true)); - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("ram_payer", "evm"_n)("rlptx", bytes())), + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("miner", "evm"_n)("rlptx", bytes())), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract is frozen");}); - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("ram_payer", "evm"_n)), + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("miner", "evm"_n)), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: contract is frozen");}); BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "close"_n, "evm"_n, mvo()("owner", "evm"_n)), @@ -120,17 +132,18 @@ BOOST_FIXTURE_TEST_CASE(check_freeze, basic_evm_tester) try { push_action("evm"_n, "freeze"_n, "evm"_n, mvo()("value", false)); produce_block(); - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("ram_payer", "evm"_n)("rlptx", bytes())), + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("miner", "evm"_n)("rlptx", bytes())), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: unable to decode transaction");}); - BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "withdraw"_n, "evm"_n, mvo()("owner", "evm"_n)("quantity", asset())), + create_account("spoon"_n); + BOOST_REQUIRE_EXCEPTION(push_action("evm"_n, "withdraw"_n, "spoon"_n, mvo()("owner", "spoon"_n)("quantity", asset())), eosio_assert_message_exception, [](const eosio_assert_message_exception& e) {return testing::expect_assert_message(e, "assertion failure with message: account is not open");}); - push_action("evm"_n, "open"_n, "evm"_n, mvo()("owner", "evm"_n)("ram_payer", "evm"_n)); + push_action("evm"_n, "open"_n, "spoon"_n, mvo()("owner", "spoon"_n)); - push_action("evm"_n, "close"_n, "evm"_n, mvo()("owner", "evm"_n)); + push_action("evm"_n, "close"_n, "spoon"_n, mvo()("owner", "spoon"_n)); // Test of transfer notification w/o init is handled in native_token_evm_tests/transfer_notifier_without_init test as it requires additional setup diff --git a/contract/tests/mapping_tests.cpp b/contract/tests/mapping_tests.cpp index b121cc69..ce664060 100644 --- a/contract/tests/mapping_tests.cpp +++ b/contract/tests/mapping_tests.cpp @@ -2,51 +2,54 @@ #include -#include - using namespace eosio::testing; +using namespace evm_test; + +struct mapping_evm_tester : basic_evm_tester +{ + + // Compiled from BlockNumTimestamp.sol + static constexpr const char* blocknum_timestamp_contract_bytecode_hex = + "608060405234801561001057600080fd5b50436000819055504260018190555060dd8061002d6000396000f3fe6080604052348015600f" + "57600080fd5b506004361060325760003560e01c806357e871e7146037578063b80777ea146051575b600080fd5b603d606b565b604051" + "60489190608e565b60405180910390f35b60576071565b60405160629190608e565b60405180910390f35b60005481565b60015481565b" + "6000819050919050565b6088816077565b82525050565b600060208201905060a160008301846081565b9291505056fea2646970667358" + "221220574648f86a6daeaf2c309ad6d05df02cfd0d83cdcb153889194852dea07068dc64736f6c63430008120033"; -using eosio::chain::time_point; -using eosio::chain::time_point_sec; -using eosio::chain::unsigned_int; + evm_eoa faucet_eoa; -struct mapping_evm_tester : basic_evm_tester { - static constexpr eosio::chain::name evm_account_name = "evm"_n; + mapping_evm_tester() : + faucet_eoa(evmc::from_hex("a3f1b69da92a0233ce29485d3049a4ace39e8d384bbc2557e3fc60940ce4e954").value()) + {} - bool produce_blocks_until_timestamp_satisfied(std::function cond, unsigned int limit = 10) { - for (unsigned int blocks_produced = 0; blocks_produced < limit && !cond(control->pending_block_time()); ++blocks_produced) { + bool produce_blocks_until_timestamp_satisfied(std::function cond, unsigned int limit = 10) + { + for (unsigned int blocks_produced = 0; blocks_produced < limit && !cond(control->pending_block_time()); + ++blocks_produced) { produce_block(); } return cond(control->pending_block_time()); } - struct config_table_row { - unsigned_int version; - uint64_t chainid; - time_point_sec genesis_time; - uint32_t status; - }; + time_point_sec get_genesis_time() { return get_config().genesis_time; } - time_point_sec get_genesis_time() { - static constexpr eosio::chain::name config_singleton_name = "config"_n; - const vector d = - get_row_by_account(evm_account_name, evm_account_name, config_singleton_name, config_singleton_name); - return fc::raw::unpack(d).genesis_time; + void fund_evm_faucet() + { + transfer_token(faucet_account_name, evm_account_name, make_asset(100'0000), faucet_eoa.address_0x()); } }; -FC_REFLECT(mapping_evm_tester::config_table_row, (version)(chainid)(genesis_time)(status)) - BOOST_AUTO_TEST_SUITE(mapping_evm_tests) -BOOST_AUTO_TEST_CASE(block_mapping_tests) try { +BOOST_AUTO_TEST_CASE(block_mapping_tests) +try { using evm_common::block_mapping; { // Tests using default 1 second block interval block_mapping bm(10); - + BOOST_CHECK_EQUAL(bm.block_interval, 1); // Block interval should default to 1 second. BOOST_CHECK_EQUAL(bm.genesis_timestamp, 10); @@ -56,18 +59,18 @@ BOOST_AUTO_TEST_CASE(block_mapping_tests) try { BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(11'000'000), 2); BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(0), 10); - // An EVM transaction in an Antelope block with timestamp 10 ends up in EVM block 1 which has timestamp 11. This is intentional. - // It means that an EVM transaction included in the same block immediately after the init action will end up in a block - // that has a timestamp greater than the genesis timestamp. - BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(1), 11); - BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(2), 12); + // An EVM transaction in an Antelope block with timestamp 10 ends up in EVM block 1 which has timestamp 11. This + // is intentional. It means that an EVM transaction included in the same block immediately after the init action + // will end up in a block that has a timestamp greater than the genesis timestamp. + BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(1), 11); + BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(2), 12); } { // Tests using a 5 second block interval block_mapping bm(123, 5); - + BOOST_CHECK_EQUAL(bm.block_interval, 5); BOOST_CHECK_EQUAL(bm.genesis_timestamp, 123); @@ -83,38 +86,222 @@ BOOST_AUTO_TEST_CASE(block_mapping_tests) try { BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(1), 128); BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(2), 133); } - -} FC_LOG_AND_RETHROW() +} +FC_LOG_AND_RETHROW() -BOOST_FIXTURE_TEST_CASE(init_on_second_boundary, mapping_evm_tester) try { +BOOST_FIXTURE_TEST_CASE(init_on_second_boundary, mapping_evm_tester) +try { auto timestamp_at_second_boundary = [](time_point timestamp) -> bool { return timestamp.time_since_epoch().count() % 1'000'000 == 0; }; BOOST_REQUIRE(produce_blocks_until_timestamp_satisfied(timestamp_at_second_boundary)); - init(15555); + init(); time_point_sec expected_genesis_time = control->pending_block_time(); // Rounds down to nearest second. - + time_point_sec actual_genesis_time = get_genesis_time(); ilog("Genesis time: ${time}", ("time", actual_genesis_time)); - + BOOST_REQUIRE_EQUAL(actual_genesis_time.sec_since_epoch(), expected_genesis_time.sec_since_epoch()); -} FC_LOG_AND_RETHROW() +} +FC_LOG_AND_RETHROW() -BOOST_FIXTURE_TEST_CASE(init_not_on_second_boundary, mapping_evm_tester) try { +BOOST_FIXTURE_TEST_CASE(init_not_on_second_boundary, mapping_evm_tester) +try { auto timestamp_not_at_second_boundary = [](time_point timestamp) -> bool { return timestamp.time_since_epoch().count() % 1'000'000 != 0; }; BOOST_REQUIRE(produce_blocks_until_timestamp_satisfied(timestamp_not_at_second_boundary)); - init(15555); + init(); time_point_sec expected_genesis_time = control->pending_block_time(); // Rounds down to nearest second. time_point_sec actual_genesis_time = get_genesis_time(); ilog("Genesis time: ${time}", ("time", actual_genesis_time)); BOOST_REQUIRE_EQUAL(actual_genesis_time.sec_since_epoch(), expected_genesis_time.sec_since_epoch()); -} FC_LOG_AND_RETHROW() +} +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(values_seen_by_contracts, mapping_evm_tester) +try { + produce_block(); + + auto timestamp_not_at_second_boundary = [](time_point timestamp) -> bool { + return timestamp.time_since_epoch().count() % 1'000'000 != 0; + }; + BOOST_REQUIRE(produce_blocks_until_timestamp_satisfied(timestamp_not_at_second_boundary)); + + evm_common::block_mapping bm(control->pending_block_time().sec_since_epoch()); + ilog("Genesis timestamp should be ${genesis_timestamp} since init action is targetting block at timestamp " + "${block_time}", + ("genesis_timestamp", bm.genesis_timestamp)("block_time", control->pending_block_time())); + control->abort_block(); + + auto dump_accounts = [&]() { + scan_accounts([](evm_test::account_object&& account) -> bool { + idump((account)); + return false; + }); + }; + + auto dump_account_storage = [&](uint64_t account_id) { + scan_account_storage(account_id, [](storage_slot&& slot) -> bool { + idump((slot)); + return false; + }); + }; + + auto get_stored_blocknum_and_timestamp = [&](uint64_t account_id) -> std::pair { + uint64_t block_number = 0; + uint32_t block_timestamp = 0; + + static const intx::uint256 threshold = (1_u256 << 64); + + unsigned int ordinal = 0; + bool successful_scan = scan_account_storage(account_id, [&](storage_slot&& slot) -> bool { + if (ordinal == 0) { + // The first storage slot of the contract is the block number. + + // ilog("block number = ${bn}", ("bn", slot.value)); + + BOOST_REQUIRE(slot.key == 0); + BOOST_REQUIRE(slot.value < threshold); + block_number = slot.value[0]; + } else if (ordinal == 1) { + // The second storage slot of the contract is the block timestamp. + + // ilog("timestamp = ${time}", ("time", slot.value)); + + BOOST_REQUIRE(slot.key == 1); + BOOST_REQUIRE(slot.value < threshold); + block_timestamp = slot.value[0]; + } else { + BOOST_ERROR("unexpected storage in contract"); + return true; + } + ++ordinal; + return false; + }); + BOOST_REQUIRE(successful_scan); + BOOST_REQUIRE_EQUAL(ordinal, 2); + + BOOST_CHECK(block_number <= std::numeric_limits::max()); + + return {static_cast(block_number), block_timestamp}; + }; + + auto blocknum_timestamp_contract_bytecode = evmc::from_hex(blocknum_timestamp_contract_bytecode_hex).value(); + + { + // Test transaction within virtual block 1. (Same block as init action.) + speculative_block_starter sb{*this}; + + ilog("Initial test within speculative block at time: ${time} (${time_sec})", + ("time", control->pending_block_time())("time_sec", control->pending_block_time().sec_since_epoch())); + + uint32_t expected_evm_block_num = + bm.timestamp_to_evm_block_num(control->pending_block_time().time_since_epoch().count()); + uint64_t expected_evm_timestamp = bm.evm_block_num_to_evm_timestamp(expected_evm_block_num); + + init(); + + BOOST_REQUIRE_EQUAL(expected_evm_block_num, 1); + BOOST_REQUIRE_EQUAL(expected_evm_timestamp, bm.genesis_timestamp + 1); + + fund_evm_faucet(); + + dump_accounts(); + + auto contract_address = deploy_contract(faucet_eoa, blocknum_timestamp_contract_bytecode); + ilog("address of deployed contract is ${address}", ("address", contract_address)); + + dump_accounts(); + + auto contract_account = find_account_by_address(contract_address); + + BOOST_REQUIRE(contract_account.has_value()); + + { + // Scan for account should also find the same account + auto acct = scan_for_account_by_address(contract_address); + BOOST_REQUIRE(acct.has_value()); + BOOST_CHECK_EQUAL(acct->id, contract_account->id); + } + + dump_account_storage(contract_account->id); + + auto [block_num, timestamp] = get_stored_blocknum_and_timestamp(contract_account->id); + + BOOST_CHECK_EQUAL(block_num, 1); + BOOST_CHECK_EQUAL(timestamp, bm.genesis_timestamp + 1); + + faucet_eoa.next_nonce = 0; + } + + // Now actually commit the init action into a block to do further testing on top of that state in later blocks. + { + control->start_block(control->head_block_time() + fc::milliseconds(500), 0); + BOOST_REQUIRE_EQUAL(control->pending_block_time().sec_since_epoch(), bm.genesis_timestamp); + + init(); + auto init_block_num = control->pending_block_num(); + + BOOST_REQUIRE_EQUAL(get_genesis_time().sec_since_epoch(), bm.genesis_timestamp); + + // Also fund the from account prior to completing the block. + + fund_evm_faucet(); + + produce_block(); + BOOST_REQUIRE_EQUAL(control->head_block_num(), init_block_num); + + ilog("Timestamp of block containing init action: ${time} (${time_sec})", + ("time", control->head_block_time())("time_sec", control->head_block_time().sec_since_epoch())); + + BOOST_REQUIRE_EQUAL(control->head_block_time().sec_since_epoch(), bm.genesis_timestamp); + + // Produce one more block so the next block with closest allowed timestamp has a timestamp exactly one second + // after the timestamp of the block containing the init action. This is not necessary and the furhter tests + // below would also work if this second produce_block was commented out. + produce_block(); + control->abort_block(); + } + + auto start_speculative_block = [tester = this, &bm](uint32_t time_gap_sec) { + speculative_block_starter sbs{*tester, time_gap_sec}; + + uint32_t expected_evm_block_num = + bm.timestamp_to_evm_block_num(tester->control->pending_block_time().time_since_epoch().count()); + uint64_t expected_evm_timestamp = bm.evm_block_num_to_evm_timestamp(expected_evm_block_num); + + BOOST_CHECK_EQUAL(expected_evm_block_num, 2 + time_gap_sec); + BOOST_CHECK_EQUAL(expected_evm_timestamp, bm.genesis_timestamp + 2 + time_gap_sec); + + return sbs; + }; + + + for (uint32_t vbn = 2; vbn <= 4; ++vbn) { + // Test transaction within virtual block vbn. + auto sb = start_speculative_block(vbn - 2); + ilog("Speculative block time: ${time} (${time_sec})", + ("time", control->pending_block_time())("time_sec", control->pending_block_time().sec_since_epoch())); + + auto contract_address = deploy_contract(faucet_eoa, blocknum_timestamp_contract_bytecode); + auto contract_account = find_account_by_address(contract_address); + + BOOST_REQUIRE(contract_account.has_value()); + + auto [block_num, timestamp] = get_stored_blocknum_and_timestamp(contract_account->id); + + BOOST_CHECK_EQUAL(block_num, vbn); + BOOST_CHECK_EQUAL(timestamp, bm.genesis_timestamp + vbn); + + faucet_eoa.next_nonce = 0; + } +} +FC_LOG_AND_RETHROW() BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/contract/tests/native_token_tester.hpp b/contract/tests/native_token_tester.hpp new file mode 100644 index 00000000..720386ec --- /dev/null +++ b/contract/tests/native_token_tester.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "basic_evm_tester.hpp" + +#include + +using namespace eosio::testing; +using namespace evm_test; +struct native_token_evm_tester : basic_evm_tester { + enum class init_mode + { + do_not_init, + init_without_ingress_bridge_fee, + init_with_ingress_bridge_fee, + }; + + native_token_evm_tester(std::string native_symbol_str, init_mode mode, uint64_t ingress_bridge_fee_amount = 0) : + basic_evm_tester(std::move(native_symbol_str)) + { + std::vector new_accounts = {"alice"_n, "bob"_n, "carol"_n}; + + create_accounts(new_accounts); + + for(const name& recipient : new_accounts) { + transfer_token(faucet_account_name, recipient, make_asset(100'0000)); + } + + if (mode != init_mode::do_not_init) { + std::optional ingress_bridge_fee; + if (mode == init_mode::init_with_ingress_bridge_fee) { + ingress_bridge_fee.emplace(make_asset(ingress_bridge_fee_amount)); + } + + init(evm_chain_id, + suggested_gas_price, + suggested_miner_cut, + ingress_bridge_fee, + mode == init_mode::init_with_ingress_bridge_fee); + } + + produce_block(); + } + + int64_t native_balance(name owner) const { + return get_currency_balance(token_account_name, native_symbol, owner).get_amount(); + } + + int64_t vault_balance_token(name owner) const { + return vault_balance(owner).balance.get_amount(); + } + uint64_t vault_balance_dust(name owner) const { + return vault_balance(owner).dust; + } + + balance_and_dust inevm() const + { + return fc::raw::unpack(get_row_by_account("evm"_n, "evm"_n, "inevm"_n, "inevm"_n)); + } +}; + +struct native_token_evm_tester_EOS : native_token_evm_tester { + native_token_evm_tester_EOS() : native_token_evm_tester("4,EOS", init_mode::init_with_ingress_bridge_fee) {} +}; +struct native_token_evm_tester_SPOON : native_token_evm_tester { + native_token_evm_tester_SPOON() : native_token_evm_tester("4,SPOON", init_mode::init_without_ingress_bridge_fee) {} +}; +struct native_token_evm_tester_noinit : native_token_evm_tester { + native_token_evm_tester_noinit() : native_token_evm_tester("4,EOS", init_mode::do_not_init) {} +}; diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 5d2a23be..09eb074a 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -1,83 +1,13 @@ -#include "basic_evm_tester.hpp" +#include "native_token_tester.hpp" -#include - -using namespace eosio::testing; - -struct native_token_evm_tester : basic_evm_tester { - native_token_evm_tester(std::string native_smybol_str, bool doinit) : native_symbol(symbol::from_string(native_smybol_str)) { - if(doinit) - init(15555); - create_accounts({"eosio.token"_n, "alice"_n, "bob"_n, "carol"_n}); - produce_block(); - - set_code("eosio.token"_n, contracts::eosio_token_wasm()); - set_abi("eosio.token"_n, contracts::eosio_token_abi().data()); - - push_action("eosio.token"_n, "create"_n, "eosio.token"_n, mvo()("issuer", "eosio.token"_n) - ("maximum_supply", asset(1'000'000'0000, native_symbol))); - for(const name& n : {"alice"_n, "bob"_n, "carol"_n}) - push_action("eosio.token"_n, "issue"_n, "eosio.token"_n, mvo()("to", n) - ("quantity", asset(100'0000, native_symbol)) - ("memo", "")); - } - - transaction_trace_ptr transfer_token(name from, name to, asset quantity, std::string memo) { - return push_action("eosio.token"_n, "transfer"_n, from, mvo()("from", from) - ("to", to) - ("quantity", quantity) - ("memo", memo)); - } - - int64_t native_balance(name owner) const { - return get_currency_balance("eosio.token"_n, native_symbol, owner).get_amount(); - } - - std::tuple evm_balance(name owner) const { - const vector d = get_row_by_account("evm"_n, "evm"_n, "accounts"_n, owner); - FC_ASSERT(d.size(), "EVM not open"); - auto [_, amount, dust] = fc::raw::unpack(d); - return std::make_tuple(amount, dust); - } - int64_t evm_balance_token(name owner) const { - return std::get<0>(evm_balance(owner)).get_amount(); - } - int64_t evm_balance_dust(name owner) const { - return std::get<1>(evm_balance(owner)); - } - - transaction_trace_ptr open(name owner, name ram_payer) { - return push_action("evm"_n, "open"_n, ram_payer, mvo()("owner", owner)("ram_payer", ram_payer)); - } - transaction_trace_ptr close(name owner) { - return push_action("evm"_n, "close"_n, owner, mvo()("owner", owner)); - } - transaction_trace_ptr withdraw(name owner, asset quantity) { - return push_action("evm"_n, "withdraw"_n, owner, mvo()("owner", owner)("quantity", quantity)); - } - - symbol native_symbol; - asset make_asset(int64_t amount) { - return asset(amount, native_symbol); - } - - struct evm_account_row { - name owner; - asset balance; - uint64_t dust = 0; - }; -}; -FC_REFLECT(native_token_evm_tester::evm_account_row, (owner)(balance)(dust)) - -struct native_token_evm_tester_EOS : native_token_evm_tester { - native_token_evm_tester_EOS() : native_token_evm_tester("4,EOS", true) {} -}; -struct native_token_evm_tester_SPOON : native_token_evm_tester { - native_token_evm_tester_SPOON() : native_token_evm_tester("4,SPOON", true) {} -}; -struct native_token_evm_tester_noinit : native_token_evm_tester { - native_token_evm_tester_noinit() : native_token_evm_tester("4,EOS", false) {} -}; +static const char do_nothing_wast[] = R"=====( +(module + (export "apply" (func $apply)) + (func $apply (param $0 i64) (param $1 i64) (param $2 i64) + ;; nothing + ) +) +)====="; BOOST_AUTO_TEST_SUITE(native_token_evm_tests) @@ -87,28 +17,28 @@ BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try eosio_assert_message_exception, eosio_assert_message_is("receiving account has not been opened")); - open("alice"_n, "alice"_n); + open("alice"_n); //alice sends her own tokens in to her EVM balance { const int64_t to_transfer = 1'0000; const int64_t alice_native_before = native_balance("alice"_n); - const int64_t alice_evm_before = evm_balance_token("alice"_n); + const int64_t alice_evm_before = vault_balance_token("alice"_n); transfer_token("alice"_n, "evm"_n, make_asset(to_transfer), "alice"); BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_transfer); - BOOST_REQUIRE_EQUAL(evm_balance_token("alice"_n), to_transfer); + BOOST_REQUIRE_EQUAL(vault_balance_token("alice"_n), to_transfer); } //bob sends his tokens in to alice's EVM balance { const int64_t to_transfer = 1'0000; const int64_t bob_native_before = native_balance("bob"_n); - const int64_t alice_evm_before = evm_balance_token("alice"_n); + const int64_t alice_evm_before = vault_balance_token("alice"_n); transfer_token("bob"_n, "evm"_n, make_asset(to_transfer), "alice"); BOOST_REQUIRE_EQUAL(bob_native_before - native_balance("bob"_n), to_transfer); - BOOST_REQUIRE_EQUAL(evm_balance_token("alice"_n) - alice_evm_before, to_transfer); + BOOST_REQUIRE_EQUAL(vault_balance_token("alice"_n) - alice_evm_before, to_transfer); } //carol can't send tokens to bob's balance because bob isn't open @@ -126,17 +56,17 @@ BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try { const int64_t to_withdraw = 5000; const int64_t alice_native_before = native_balance("alice"_n); - const int64_t alice_evm_before = evm_balance_token("alice"_n); + const int64_t alice_evm_before = vault_balance_token("alice"_n); withdraw("alice"_n, make_asset(to_withdraw)); BOOST_REQUIRE_EQUAL(native_balance("alice"_n) - alice_native_before, to_withdraw); - BOOST_REQUIRE_EQUAL(alice_evm_before - evm_balance_token("alice"_n), to_withdraw); + BOOST_REQUIRE_EQUAL(alice_evm_before - vault_balance_token("alice"_n), to_withdraw); } //try and withdraw more than alice has { const int64_t to_withdraw = 2'0000; - BOOST_REQUIRE_GT(to_withdraw, evm_balance_token("alice"_n)); + BOOST_REQUIRE_GT(to_withdraw, vault_balance_token("alice"_n)); BOOST_REQUIRE_EXCEPTION(withdraw("alice"_n, make_asset(to_withdraw)), eosio_assert_message_exception, eosio_assert_message_is("overdrawn balance")); } @@ -152,18 +82,18 @@ BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try { const int64_t to_withdraw = 1'5000; const int64_t alice_native_before = native_balance("alice"_n); - const int64_t alice_evm_before = evm_balance_token("alice"_n); + const int64_t alice_evm_before = vault_balance_token("alice"_n); withdraw("alice"_n, make_asset(to_withdraw)); BOOST_REQUIRE_EQUAL(native_balance("alice"_n) - alice_native_before, to_withdraw); - BOOST_REQUIRE_EQUAL(alice_evm_before - evm_balance_token("alice"_n), to_withdraw); + BOOST_REQUIRE_EQUAL(alice_evm_before - vault_balance_token("alice"_n), to_withdraw); } produce_block(); //now alice can close out close("alice"_n); - BOOST_REQUIRE_EXCEPTION(evm_balance_token("alice"_n), + BOOST_REQUIRE_EXCEPTION(vault_balance_token("alice"_n), fc::assert_exception, fc_assert_exception_message_is("EVM not open")); //make sure alice can't deposit any more @@ -179,31 +109,23 @@ BOOST_FIXTURE_TEST_CASE(weird_names, native_token_evm_tester_EOS) try { eosio_assert_message_exception, eosio_assert_message_is("character is not in allowed character set for names")); BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "loooooooooooooooooong"), - eosio_assert_message_exception, eosio_assert_message_is("string is too long to be a valid name")); + eosio_assert_message_exception, eosio_assert_message_is("memo must be either 0x EVM address or already opened account name to credit deposit to")); BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), " "), eosio_assert_message_exception, eosio_assert_message_is("character is not in allowed character set for names")); BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), ""), - eosio_assert_message_exception, eosio_assert_message_is("memo must be already opened account name to credit deposit to")); - -} FC_LOG_AND_RETHROW() - -BOOST_FIXTURE_TEST_CASE(non_existing_account, native_token_evm_tester_EOS) try { - //can only open for accounts that exist - - BOOST_REQUIRE_EXCEPTION(open("spoon"_n, "alice"_n), - eosio_assert_message_exception, eosio_assert_message_is("owner account does not exist")); + eosio_assert_message_exception, eosio_assert_message_is("memo must be either 0x EVM address or already opened account name to credit deposit to")); } FC_LOG_AND_RETHROW() BOOST_FIXTURE_TEST_CASE(non_standard_native_symbol, native_token_evm_tester_SPOON) try { //the symbol 4,EOS is fixed as the expected native symbol. try transfering in a different symbol from eosio.token - open("alice"_n, "alice"_n); + open("alice"_n); BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(1'0000), "alice"), - eosio_assert_message_exception, eosio_assert_message_is("attempt to add asset with different symbol")); + eosio_assert_message_exception, eosio_assert_message_is("received unexpected token")); } FC_LOG_AND_RETHROW() @@ -215,4 +137,278 @@ BOOST_FIXTURE_TEST_CASE(transfer_notifier_without_init, native_token_evm_tester_ } FC_LOG_AND_RETHROW() -BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file +BOOST_FIXTURE_TEST_CASE(basic_eos_evm_bridge, native_token_evm_tester_EOS) try { + evm_eoa evm1, evm2; + + //reminder: .0001 EOS is 100 szabos + const intx::uint256 smallest = 100_szabo; + + balance_and_dust expected_inevm = { + make_asset(0) + }; + BOOST_REQUIRE(expected_inevm == inevm()); + + auto initial_special_balance = vault_balance("evm"_n); + + //to start with, there is no ingress bridge fee. should be 1->1 + + //transfer 1.0000 EOS from alice to evm1 account + { + const int64_t to_bridge = 1'0000; + const int64_t alice_native_before = native_balance("alice"_n); + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_bridge); + BOOST_REQUIRE(evm_balance(evm1) == smallest * to_bridge); + + expected_inevm.balance += make_asset(to_bridge); + BOOST_REQUIRE(expected_inevm == inevm()); + } + + //transfer 0.5000 EOS from alice to evm2 account + { + const int64_t to_bridge = 5000; + const int64_t alice_native_before = native_balance("alice"_n); + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm2.address_0x()); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_bridge); + BOOST_REQUIRE(evm_balance(evm2) == smallest * to_bridge); + + expected_inevm.balance += make_asset(to_bridge); + BOOST_REQUIRE(expected_inevm == inevm()); + } + + //transfer 0.1234 EOS from alice to evm1 account + { + const int64_t to_bridge = 1234; + const int64_t alice_native_before = native_balance("alice"_n); + BOOST_REQUIRE(!!evm_balance(evm1)); + const intx::uint256 evm1_before = *evm_balance(evm1); + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_bridge); + BOOST_REQUIRE(evm_balance(evm1) == evm1_before + smallest * to_bridge); + + expected_inevm.balance += make_asset(to_bridge); + BOOST_REQUIRE(expected_inevm == inevm()); + } + + //try to transfer 1000.0000 EOS from alice to evm1 account. can't do that alice! + { + const int64_t to_bridge = 1000'0000; + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()), + eosio_assert_message_exception, eosio_assert_message_is("overdrawn balance")); + + BOOST_REQUIRE(expected_inevm == inevm()); + } + + //set the bridge free to 0.1000 EOS + const int64_t bridge_fee = 1000; + setfeeparams(fee_parameters{.ingress_bridge_fee = make_asset(bridge_fee)}); + + //transferring less than the bridge fee isn't allowed + { + const int64_t to_bridge = 900; + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()), + eosio_assert_message_exception, eosio_assert_message_is("must bridge more than ingress bridge fee")); + } + + //transferring exact amount of bridge fee isn't allowed + { + const int64_t to_bridge = 1000; + BOOST_REQUIRE_EXCEPTION(transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()), + eosio_assert_message_exception, eosio_assert_message_is("must bridge more than ingress bridge fee")); + } + + BOOST_REQUIRE(expected_inevm == inevm()); + + //nothing should have accumulated in contract's special balance yet + BOOST_REQUIRE(vault_balance("evm"_n) == initial_special_balance); + + //transfer 2.0000 EOS from alice to evm1 account, expect 1.9000 to be delivered to evm1 account, 0.1000 to contract balance + { + const int64_t to_bridge = 2'0000; + const int64_t alice_native_before = native_balance("alice"_n); + BOOST_REQUIRE(!!evm_balance(evm1)); + const intx::uint256 evm1_before = *evm_balance(evm1); + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_bridge); + BOOST_REQUIRE(evm_balance(evm1) == evm1_before + smallest * (to_bridge - bridge_fee)); + + expected_inevm.balance += make_asset(to_bridge - bridge_fee); + BOOST_REQUIRE(expected_inevm == inevm()); + + intx::uint256 new_special_balance{initial_special_balance}; + new_special_balance += smallest * bridge_fee; + BOOST_REQUIRE_EQUAL(static_cast(vault_balance("evm"_n)), new_special_balance); + } + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(disallow_bridge_sigs_outside_bridge_trx, native_token_evm_tester_EOS) try { + evm_eoa evm1; + + //r == 0 indicates a bridge signature. These are only allowed in contract-initiated (i.e. inline) EVM actions + BOOST_REQUIRE_EXCEPTION(pushtx(generate_tx(evm1.address, 11111111_u256)), + eosio_assert_message_exception, eosio_assert_message_is("bridge signature used outside of bridge transaction")); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { + evm_eoa evm1, evm2; + + //reminder: .0001 EOS is 100 szabos + const intx::uint256 smallest = 100_szabo; + + const auto gas_fee = intx::uint256{get_config().gas_price} * 21000; + + //alice transfers in 10.0000 EOS to evm1 + { + const int64_t to_bridge = 10'0000; + const int64_t alice_native_before = native_balance("alice"_n); + transfer_token("alice"_n, "evm"_n, make_asset(to_bridge), evm1.address_0x()); + + BOOST_REQUIRE_EQUAL(alice_native_before - native_balance("alice"_n), to_bridge); + BOOST_REQUIRE(!!evm_balance(evm1)); + BOOST_REQUIRE(*evm_balance(evm1) == smallest * to_bridge); + } + + //evm1 then transfers 2.0000 EOS to evm2 + { + const int64_t to_transfer = 2'0000; + const intx::uint256 evm1_before = *evm_balance(evm1); + const intx::uint256 special_balance_before{vault_balance("evm"_n)}; + + auto txn = generate_tx(evm2.address, 100_szabo * to_transfer); + evm1.sign(txn); + pushtx(txn); + + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); + BOOST_REQUIRE(!!evm_balance(evm2)); + BOOST_REQUIRE(*evm_balance(evm2) == txn.value); + BOOST_REQUIRE_EQUAL(static_cast(vault_balance("evm"_n)), (special_balance_before + gas_fee)); + } + + //evm1 is going to egress 1.0000 EOS to alice. alice does not have an open balance, so this goes direct inline to native EOS balance + { + const int64_t to_bridge = 1'0000; + const intx::uint256 evm1_before = *evm_balance(evm1); + const int64_t alice_native_before = native_balance("alice"_n); + + auto txn = generate_tx(make_reserved_address("alice"_n.to_uint64_t()), 100_szabo * to_bridge); + evm1.sign(txn); + pushtx(txn); + + BOOST_REQUIRE_EQUAL(alice_native_before + to_bridge, native_balance("alice"_n)); + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); + } + + //evm1 is now going to try to egress 1.00001 EOS to alice. Since this includes dust without an open balance for alice, this fails + { + const int64_t to_bridge = 1'0000; + const intx::uint256 evm1_before = *evm_balance(evm1); + const int64_t alice_native_before = native_balance("alice"_n); + + auto txn = generate_tx(make_reserved_address("alice"_n.to_uint64_t()), 100_szabo * to_bridge + 10_szabo); + evm1.sign(txn); + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("egress bridging to non-open accounts must not contain dust")); + + BOOST_REQUIRE_EQUAL(alice_native_before, native_balance("alice"_n)); + BOOST_REQUIRE(evm_balance(evm1) == evm1_before); + + //alice will now open a balance + open("alice"_n); + //and try again + pushtx(txn); + + BOOST_REQUIRE_EQUAL(alice_native_before, native_balance("alice"_n)); // native balance unchanged + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); // EVM balance decreased + BOOST_REQUIRE(vault_balance("alice"_n) == + (balance_and_dust{make_asset(1'0000), + 10'000'000'000'000UL})); // vault balance increased to 1.0000 EOS, 10 szabo + } + + //install some code on bob's account + set_code("bob"_n, do_nothing_wast); + + //evm1 is going to try to egress 1.0000 EOS to bob. bob is neither open nor on allow list, but bob has code so egress is disallowed + { + const int64_t to_bridge = 1'0000; + const intx::uint256 evm1_before = *evm_balance(evm1); + + auto txn = generate_tx(make_reserved_address("bob"_n.to_uint64_t()), 100_szabo * to_bridge); + evm1.sign(txn); + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("non-open accounts containing contract code must be on allow list for egress bridging")); + + //open up bob's balance + open("bob"_n); + //and now it'll go through + pushtx(txn); + BOOST_REQUIRE_EQUAL(vault_balance_token("bob"_n), to_bridge); + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); + } + + //install some code on carol's account + set_code("carol"_n, do_nothing_wast); + + //evm1 is going to try to egress 1.0000 EOS to carol. carol is neither open nor on allow list, but carol has code so egress is disallowed + { + const int64_t to_bridge = 1'0000; + const int64_t carol_native_before = native_balance("carol"_n); + const intx::uint256 evm1_before = *evm_balance(evm1); + + auto txn = generate_tx(make_reserved_address("carol"_n.to_uint64_t()), 100_szabo * to_bridge); + evm1.sign(txn); + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("non-open accounts containing contract code must be on allow list for egress bridging")); + + //add carol to the egress allow list + addegress({"carol"_n}); + //and now it'll go through + pushtx(txn); + BOOST_REQUIRE_EQUAL(carol_native_before + to_bridge, native_balance("carol"_n)); + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); + + //remove carol from egress allow list + removeegress({"carol"_n}); + evm1.sign(txn); + //and it won't go through again + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("non-open accounts containing contract code must be on allow list for egress bridging")); + } + + //Warning for future tests: evm1 nonce left incorrect here + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(evm_eos_nonexistant, native_token_evm_tester_EOS) try { + evm_eoa evm1; + + transfer_token("alice"_n, "evm"_n, make_asset(10'0000), evm1.address_0x()); + + //trying to bridge to a non-existing account is not allowed + { + const int64_t to_bridge = 1'0000; + + auto txn = generate_tx(make_reserved_address("spoon"_n.to_uint64_t()), 100_szabo * to_bridge); + evm1.sign(txn); + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("can only egress bridge to existing accounts")); + } +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(evm_eos_disallow_reserved_zero, native_token_evm_tester_EOS) try { + evm_eoa evm1; + + transfer_token("alice"_n, "evm"_n, make_asset(10'0000), evm1.address_0x()); + + //doing anything with the reserved-zero address should fail; in this case just send an empty message to it + auto txn = generate_tx(make_reserved_address(0u), 0); + evm1.sign(txn); + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + eosio_assert_message_exception, eosio_assert_message_is("reserved 0 address cannot be used")); +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/contract/tests/silkworm/core/silkworm/common/util.hpp b/contract/tests/silkworm/core/silkworm/common/util.hpp index 5ed6dc88..67de428c 100644 --- a/contract/tests/silkworm/core/silkworm/common/util.hpp +++ b/contract/tests/silkworm/core/silkworm/common/util.hpp @@ -106,6 +106,20 @@ 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 evmc::address make_reserved_address(uint64_t account) { + return evmc_address({0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + static_cast(account >> 56), + static_cast(account >> 48), + static_cast(account >> 40), + static_cast(account >> 32), + static_cast(account >> 24), + static_cast(account >> 16), + static_cast(account >> 8), + static_cast(account >> 0)}); +} + } // namespace silkworm #endif // SILKWORM_COMMON_UTIL_HPP_ \ No newline at end of file diff --git a/docs/local_testnet_deployment_plan.md b/docs/local_testnet_deployment_plan.md index 82d9995e..8ba6be84 100644 --- a/docs/local_testnet_deployment_plan.md +++ b/docs/local_testnet_deployment_plan.md @@ -39,8 +39,6 @@ The compilation result should be these two files: - evm_runtime.wasm - evm_runtime.abi - Ensure action "setbal" exists in evm_runtime.abi - Compiled binaries from this repo: - trustevm-node: silkworm node process that receive data from the main Antelope chain and convert to the EVM chain @@ -359,26 +357,25 @@ Deploy evm_runtime contract, wasm and abi file, to account evmevmevmevm: ./cleos set abi evmevmevmevm ../TrustEVM/contract/build/evm_runtime/evm_runtime.abi ``` -Set chain ID & native token configuration +Set chain ID & native token configuration (in this example, gas price is 150 Gwei, miner_cut is 10%) ``` -./cleos push action evmevmevmevm init '{"chainid": 15555}' +./cleos push action evmevmevmevm init "{\"chainid\":15555,\"fee_params\":{\"gas_price\":150000000000,\"miner_cut\":10000,\"ingress_bridge_fee\":\"0.0100 EOS +\"}}" -p evmevmevmevm ``` -#### Set Initial Balance For Genesis ETH Accounts - -In this document we use `0x2787b98fc4e731d0456b3941f0b3fe2e01439961` (private key `a3f1b69da92a0233ce29485d3049a4ace39e8d384bbc2557e3fc60940ce4e954` as genesis. Developers can use one or more other genesis ETH accounts. - -Notice that the balance string must be in hex and must be exactly 64 bytes long (representing a full 256-bit integer value). Failure to meet such criteria will result in incorrect balance calculation in transfers. - -```shell -./cleos push action evmevmevmevm setbal '{"addy":"2787b98fc4e731d0456b3941f0b3fe2e01439961", "bal":"0000000000000000000000000000000100000000000000000000000000000000"}' -p evmevmevmevm +after the init action we need a small amount of token (1 EOS) to be transferred into the contract account (with memo=contract account), for example: +``` +./cleos transfer eosio evmevmevmevm "1.0000 EOS" "evmevmevmevm" ``` +Now EVM initialization is completed. -Repeat this action for all genesis accounts. -Now EVM initialization is completed. +#### Bridging tokens (EOS->EVM) and Verify EVM account balances -#### Verify EVM account balances +to bridge in token (EOS->EVM), use native Antelope transfer with memo equals to ETH address, for example: +``` +./cleos transfer eosio evmevmevmevm "1000000.0000 EOS" "0x2787b98fc4e731d0456b3941f0b3fe2e01439961" +``` To verify all EVM account balances directly on the Antelope node run the following command and replace your contract name "evmevmevmevm" if needed: @@ -429,6 +426,11 @@ We use `a123` for example (public key EOS8kE63z4NcZatvVWY4jxYdtLg6UEA123raMGwS6Q ./cleos create account eosio a123 EOS8kE63z4NcZatvVWY4jxYdtLg6UEA123raMGwS6QDKwpQ69eGcP EOS8kE63z4NcZatvVWY4jxYdtLg6UEA123raMGwS6QDKwpQ69eGcP ``` +run the open action on evm contract to open the account balance row: +``` +./cleos push action evmevmevmevm open '{"owner":"a123"}' -p a123 +``` + #### Prepare The .env File Prepare the `.env` file to configure Antelope RPC endpoint, listening port, EVM contract account, sender account and other details: @@ -882,7 +884,27 @@ Antelope block 8 & 9 -> EVM virtual block 4 #### Set The Correct EVM Genesis -Once we have decided the starting block number, the next step is to build up the correct genesis for the virtual Ethereum chain. Take this as example. +check the current config table: +``` +./cleos get table evmevmevmevm evmevmevmevm config +{ + "rows": [{ + "version": 0, + "chainid": 15555, + "genesis_time": "2022-11-18T07:58:34", + "ingress_bridge_fee": "0.0100 EOS", + "gas_price": "150000000000", + "miner_cut": 10000, + "status": 0 + } + ], + "more": false, + "next_key": "" +} +``` + +take the above example, we need to findout the Antelope block number x whose the timestamp equals to 2022-11-18T07:58:34. +Once we have decided the starting block number x, the next step is to build up the correct genesis for the virtual Ethereum chain. Take this as example. Antelope block 2: @@ -920,22 +942,22 @@ This determines the value of the "timestamp" field in EVM genesis. Set the "mixHash" field to be "0x + Antelope starting block id", e.g. "0x000000026d392f1bfeddb000555bcb03ca6e31a54c0cf9edc23cede42bda17e6" -Set the "nonce" field with "0x3e8". This is re-purposed to be the block time (in mill-second) of the EVM chain. +Set the "nonce" field to be the hex encoding of the value of the Antelope name of the account on which the EVM contract is deployed. So if the `evmevmevmevm` account name is used, then set the nonce to "0x56e4adc95b92b720". If the `eosio.evm` account name is used, then set the nonce to "0x56e4adc95b92b720". This is re-purposed to be the block time (in mill-second) of the EVM chain. -In the "alloc" part, setup the genesis EVM account balance. +In the "alloc" part, setup the genesis EVM account balance (should be all zeros) Final EVM genesis example: ```json - { +{ "alloc": { - "2787b98fc4e731d0456b3941f0b3fe2e01439961": { - "balance": "0x100000000000000000000000000000000" + "0x0000000000000000000000000000000000000000": { + "balance": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, "coinbase": "0x0000000000000000000000000000000000000000", "config": { - "chainId": 15555, + "chainId": 15556, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, @@ -943,15 +965,16 @@ Final EVM genesis example: "constantinopleBlock": 0, "petersburgBlock": 0, "istanbulBlock": 0, - "noproof": {} + "trust": {} }, "difficulty": "0x01", "extraData": "TrustEVM", "gasLimit": "0x7ffffffffff", "mixHash": "0x000000026d392f1bfeddb000555bcb03ca6e31a54c0cf9edc23cede42bda17e6", - "nonce": "0x3e8", + "nonce": "0x56e4adc95b92b720", "timestamp": "0x63773b2a" } + ``` #### Start The TrustEVM Process diff --git a/evm.abi b/evm.abi deleted file mode 100644 index 549e8810..00000000 --- a/evm.abi +++ /dev/null @@ -1,82 +0,0 @@ -{ - "version": "eosio::abi/1.2", - "types": [], - "structs": [{ - "name": "account", - "base": "", - "fields": [{ - "name": "id", - "type": "uint64" - },{ - "name": "eth_address", - "type": "bytes" - },{ - "name": "nonce", - "type": "uint64" - },{ - "name": "balance", - "type": "bytes" - },{ - "name": "eos_account", - "type": "name" - },{ - "name": "code", - "type": "bytes" - },{ - "name": "code_hash", - "type": "bytes" - } - ] - },{ - "name": "pushtx", - "base": "", - "fields": [{ - "name": "ram_payer", - "type": "name" - },{ - "name": "rlptx", - "type": "bytes" - } - ] - },{ - "name": "storage", - "base": "", - "fields": [{ - "name": "id", - "type": "uint64" - },{ - "name": "key", - "type": "bytes" - },{ - "name": "value", - "type": "bytes" - } - ] - } - ], - "actions": [{ - "name": "pushtx", - "type": "pushtx", - "ricardian_contract": "" - } - ], - "tables": [{ - "name": "account", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "account" - },{ - "name": "storage", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "storage" - } - ], - "ricardian_clauses": [], - "error_messages": [], - "abi_extensions": [], - "variants": [], - "action_results": [] -} diff --git a/peripherals/proxy/Dockerfile b/peripherals/proxy/Dockerfile index 68b14a63..d6c9ad4c 100644 --- a/peripherals/proxy/Dockerfile +++ b/peripherals/proxy/Dockerfile @@ -1,29 +1,6 @@ -FROM debian:jessie - -RUN apt-get update && apt-get install -y build-essential curl libssl-dev libluajit-5.1-dev libpcre3-dev zlib1g-dev - -RUN mkdir /var/log/nginx - -RUN mkdir /opt/src \ - && curl -L http://nginx.org/download/nginx-1.11.7.tar.gz 2> /dev/null > /opt/src/nginx-1.11.7.tar.gz \ - && curl -L https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz 2> /dev/null > /opt/src/ngx_devel_kit-0.3.0.tar.gz \ - && curl -L https://github.com/openresty/lua-nginx-module/archive/v0.10.7.tar.gz 2> /dev/null > /opt/src/lua-nginx-module-0.10.7.tar.gz \ - && curl -L https://github.com/mpx/lua-cjson/archive/2.1.0.tar.gz 2> /dev/null > /opt/src/lua-cjson-2.1.0.tar.gz - -RUN cd /opt/src && tar xfz lua-cjson-2.1.0.tar.gz && cd lua-cjson-2.1.0 \ - && sed -i.bak 's/LUA_INCLUDE_DIR =.*/LUA_INCLUDE_DIR = \/usr\/include\/luajit-2.0/g' Makefile \ - && sed -i.bak 's/LUA_MODULE_DIR =.*/LUA_MODULE_DIR = \/usr\/share\/luajit-2.0.3\/jitg/g' Makefile \ - && make && make install - -RUN cd /opt/src && tar xfz nginx-1.11.7.tar.gz && tar xfz ngx_devel_kit-0.3.0.tar.gz && tar xfz lua-nginx-module-0.10.7.tar.gz - -RUN cd /opt/src/nginx-1.11.7 && ./configure --prefix=/opt/nginx --with-ld-opt="-Wl,-rpath,/usr/local/lib" --add-module=/opt/src/ngx_devel_kit-0.3.0 --add-module=/opt/src/lua-nginx-module-0.10.7 --with-http_ssl_module \ - && make && make install \ - && rm /opt/src/*.tar.gz - -ADD nginx.conf /etc/nginx.conf -ADD eth-jsonrpc-access.lua /opt/nginx/eth-jsonrpc-access.lua +FROM openresty/openresty:alpine +COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf +COPY eth-jsonrpc-access.lua /usr/local/openresty/nginx/eth-jsonrpc-access.lua EXPOSE 80 443 -CMD ["/opt/nginx/sbin/nginx", "-c", "/etc/nginx.conf"] diff --git a/peripherals/proxy/README.md b/peripherals/proxy/README.md index 520ee71e..5ab29b16 100644 --- a/peripherals/proxy/README.md +++ b/peripherals/proxy/README.md @@ -1,6 +1,6 @@ # evm_proxy_nginx -Sample config: +Sample config for location: ``` location / { set $jsonrpc_write_calls 'eth_sendRawTransaction'; @@ -9,3 +9,19 @@ location / { proxy_pass http://$proxy; } ``` + +**The access log and error log are directed to stdout and sterr by default.** + +To build +``` +sudo docker build -t evm/tx_proxy . +``` + +To run +``` +mkdir -p logs + +sudo docker run -p 80:80 -v ${PWD}/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf evm/tx_proxy:latest > ./logs/access.log 2>./logs/error.log & +``` + +Or use -d instead of the & approach. Make sure logs are handled properly in that case. \ No newline at end of file diff --git a/peripherals/proxy/nginx.conf b/peripherals/proxy/nginx.conf index af690fb0..3c98d170 100644 --- a/peripherals/proxy/nginx.conf +++ b/peripherals/proxy/nginx.conf @@ -1,7 +1,4 @@ worker_processes 5; -daemon off; -error_log /var/log/nginx/error.log; -pid /var/log/nginx/nginx.pid; worker_rlimit_nofile 8192; events { @@ -9,7 +6,7 @@ events { } http { - include /opt/nginx/conf/mime.types; + include /usr/local/openresty/nginx/conf/mime.types; index index.html index.htm index.php; upstream write { @@ -24,7 +21,6 @@ http { log_format main '$remote_addr - $remote_user [$time_local] $status ' '"$request" $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; sendfile on; tcp_nopush on; diff --git a/peripherals/token_distribution/distribute_to_accounts.py b/peripherals/token_distribution/distribute_to_accounts.py index 5289f594..29bf796f 100644 --- a/peripherals/token_distribution/distribute_to_accounts.py +++ b/peripherals/token_distribution/distribute_to_accounts.py @@ -91,7 +91,7 @@ def queryNonce(): batch_end = i + batch_size print("distribute {} to account {}, nonce {}".format(to_acc_bals[i][1], to_acc_bals[i][0], starting_nonce + i)) - act_data = {"ram_payer":EOS_SENDER, "rlptx":to_acc_bals[i][2]} + act_data = {"miner":EOS_SENDER, "rlptx":to_acc_bals[i][2]} result = subprocess.run(["./cleos", "-u", NODEOS_ENDPOINT, "push", "action", EVM_CONTRACT, "pushtx", json.dumps(act_data), "-p", EOS_SENDER, "-s", "-j", "-d"], capture_output=True, text=True) txn_json = json.loads(result.stdout) i = i + 1 @@ -99,7 +99,7 @@ def queryNonce(): while i < batch_end and i < len(to_acc_bals): print("distribute {} to account {}, nonce {}".format(to_acc_bals[i][1], to_acc_bals[i][0], starting_nonce + i)) - act_data = {"ram_payer":EOS_SENDER, "rlptx":to_acc_bals[i][2]} + act_data = {"miner":EOS_SENDER, "rlptx":to_acc_bals[i][2]} act_json = json.loads(json.dumps(txn_json["actions"][0])) act_json["data"] = act_data txn_json["actions"].append(act_json) diff --git a/peripherals/tx_wrapper/index.js b/peripherals/tx_wrapper/index.js index e2e4f02d..e9dcce50 100644 --- a/peripherals/tx_wrapper/index.js +++ b/peripherals/tx_wrapper/index.js @@ -1,6 +1,7 @@ const { Api, JsonRpc, RpcError } = require("eosjs"); const { JsSignatureProvider } = require("eosjs/dist/eosjs-jssig"); // development only -const fetch = require("node-fetch"); // node only; not needed in browsers +// const fetch = require("node-fetch"); // node only; not needed in browsers +const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); const { TextEncoder, TextDecoder } = require("util"); // node only; native TextEncoder/Decoder const RpcServer = require("http-jsonrpc-server"); @@ -77,8 +78,8 @@ async function push_tx(strRlptx) { } ], data: { - ram_payer : process.env.EOS_SENDER, - rlptx : strRlptx + miner : process.env.EOS_SENDER, + rlptx : strRlptx }, }, ], @@ -100,9 +101,28 @@ async function eth_sendRawTransaction(params) { return '0x'+keccak256(Buffer.from(rlptx, "hex")).toString("hex"); } +var lastGetTableCallTime = 0 +var gasPrice = "0x1"; async function eth_gasPrice(params) { - // TODO: get price from somewhere - return "0x2540BE400"; + if ( (new Date() - lastGetTableCallTime) >= 500 ) { + try { + const result = await rpc.get_table_rows({ + json: true, // Get the response as json + code: process.env.EOS_EVM_ACCOUNT, // Contract that we target + scope: process.env.EOS_EVM_ACCOUNT, // Account that owns the data + table: 'config', // Table name + limit: 1, // Maximum number of rows that we want to get + reverse: false, // Optional: Get reversed data + show_payer: false // Optional: Show ram payer + }); + console.log("result:", result); + gasPrice = "0x" + parseInt(result.rows[0].gas_price).toString(16); + lastGetTableCallTime = new Date(); + } catch(e) { + console.log("Error getting gas price from nodeos: " + e); + } + } + return gasPrice; } function zero_pad(hexstr) { @@ -111,47 +131,6 @@ function zero_pad(hexstr) { return res.length % 2 ? '0'+res : res; } -async function eth_estimateGas(params) { - // TODO: get estimation from evm runtime - const data = { - from : params[0].from.substr(2), - to : params[0].to.substr(2), - value : zero_pad(params[0].value), - data : params[0].data || "" - }; - - try { - const result = await api.transact( - { - actions: [ - { - account: process.env.EOS_EVM_ACCOUNT, - name: "estimate", - authorization: [{ - actor : process.env.EOS_EVM_ACCOUNT, - permission : "active", - } - ], - data: data - }, - ], - }, - { - blocksBehind: 3, - expireSeconds: 3000, - } - ); - } catch(err) { - const m = err.details[0].message.match(/assertion failure with message: GAS:\[(\d+),(\d+)\]/); - if(m) { - console.log("estimated: ", m[2]); - return '0x'+parseInt(m[2]).toString(16); - } - console.log("default: 21k"); - return "0x5208"; //21000 - } -} - // Setting up the RPC server const rpcServer = new RpcServer({ path: "/", @@ -171,7 +150,6 @@ const rpcServer = new RpcServer({ rpcServer.setMethod("eth_sendRawTransaction", eth_sendRawTransaction); rpcServer.setMethod("eth_gasPrice", eth_gasPrice); -rpcServer.setMethod("eth_estimateGas", eth_estimateGas); // Main loop rpcServer.listen(+process.env.PORT, process.env.HOST).then(() => { diff --git a/silkrpc/json/types.cpp b/silkrpc/json/types.cpp index 8407bf45..c6cf510d 100644 --- a/silkrpc/json/types.cpp +++ b/silkrpc/json/types.cpp @@ -33,6 +33,10 @@ namespace silkrpc { std::string to_hex_no_leading_zeros(silkworm::ByteView bytes) { static const char* kHexDigits{"0123456789abcdef"}; + if (bytes.length() == 0) { + return "0"; + } + std::string out{}; out.reserve(2 * bytes.length()); @@ -69,6 +73,9 @@ std::string to_quantity(silkworm::ByteView bytes) { } std::string to_quantity(uint64_t number) { + if (number == 0) { + return "0x0"; + } return "0x" + to_hex_no_leading_zeros(number); } @@ -317,7 +324,7 @@ void from_json(const nlohmann::json& json, Call& call) { if (json.count("value") != 0) { call.value = json.at("value").get(); } - if (json.count("data") != 0) { + if (json.count("data") != 0 && !json.at("data").is_null()) { const auto json_data = json.at("data").get(); call.data = silkworm::from_hex(json_data); } diff --git a/silkworm b/silkworm index b52315cd..0ed53623 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit b52315cd0ce000bc26384618f87c8cf93e491572 +Subproject commit 0ed5362304a1cca16bf0a0cbd6cc7693bcfa1668 diff --git a/tests/leap/antelope_name.py b/tests/leap/antelope_name.py new file mode 100644 index 00000000..2334e051 --- /dev/null +++ b/tests/leap/antelope_name.py @@ -0,0 +1,33 @@ +def convert_name_to_value(name): + def value_of_char(c: str): + assert len(c) == 1 + if c >= 'a' and c <= 'z': + return ord(c) - ord('a') + 6 + if c >= '1' and c <= '5': + return ord(c) - ord('1') + 1 + if c == '.': + return 0 + raise ValueError("invalid Antelope name: character '{0}' is not allowed".format(c)) + + name_length = len(name) + + if name_length > 13: + raise ValueError("invalid Antelope name: cannot exceed 13 characters") + + thirteen_char_value = 0 + if name_length == 13: + thirteen_char_value = value_of_char(name[12]) + if thirteen_char_value > 15: + raise ValueError("invalid Antelope name: 13th character cannot be letter past j") + + normalized_name = name[:12].ljust(12,'.') # truncate/extend to at exactly 12 characters since the 13th character is handled differently + + def convert_to_value(str): + value = 0 + for c in str: + value <<= 5 + value |= value_of_char(c) + + return value + + return (convert_to_value(normalized_name) << 4) | thirteen_char_value diff --git a/tests/leap/nodeos_trust_evm_server.py b/tests/leap/nodeos_trust_evm_server.py index 9feb6954..b2083cf3 100755 --- a/tests/leap/nodeos_trust_evm_server.py +++ b/tests/leap/nodeos_trust_evm_server.py @@ -21,8 +21,10 @@ from TestHarness import Cluster, TestHelper, Utils, WalletMgr from TestHarness.TestHelper import AppArgs +from TestHarness.testUtils import ReturnType from core_symbol import CORE_SYMBOL +from antelope_name import convert_name_to_value ############################################################### # nodeos_trust_evm_server @@ -170,6 +172,7 @@ for account in accounts: Print("Create new account %s via %s with private key: %s" % (account.name, cluster.eosioAccount.name, account.activePrivateKey)) trans=nonProdNode.createInitializeAccount(account, cluster.eosioAccount, stakedDeposit=0, waitForTransBlock=True, stakeNet=10000, stakeCPU=10000, buyRAM=10000000, exitOnError=True) + # max supply 1000000000.0000 (1 Billion) transferAmount="100000000.0000 {0}".format(CORE_SYMBOL) Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, account.name)) nonProdNode.transferFunds(cluster.eosioAccount, account, transferAmount, "test transfer", waitForTransBlock=True) @@ -201,8 +204,13 @@ abiFile="evm_runtime.abi" Utils.Print("Publish evm_runtime contract") prodNode.publishContract(evmAcc, contractDir, wasmFile, abiFile, waitForTransBlock=True) - - trans = prodNode.pushMessage(evmAcc.name, "init", '{"chainid":15555}', '-p evmevmevmevm') + + # add eosio.code permission + cmd="set account permission evmevmevmevm active --add-code -p evmevmevmevm@active" + prodNode.processCleosCmd(cmd, cmd, silentErrors=True, returnType=ReturnType.raw) + + trans = prodNode.pushMessage(evmAcc.name, "init", '{"chainid":15555, "fee_params": {"gas_price": "150000000000", "miner_cut": 10000, "ingress_bridge_fee": null}}', '-p evmevmevmevm') + prodNode.waitForTransBlockIfNeeded(trans[1], True) transId=prodNode.getTransId(trans[1]) @@ -212,7 +220,9 @@ Utils.Print("Block timestamp: ", block["timestamp"]) genesis_info = { - "alloc": {}, + "alloc": { + "0x0000000000000000000000000000000000000000" : {"balance":"0x00"} + }, "coinbase": "0x0000000000000000000000000000000000000000", "config": { "chainId": 15555, @@ -229,10 +239,15 @@ "extraData": "TrustEVM", "gasLimit": "0x7ffffffffff", "mixHash": "0x"+block["id"], - "nonce": hex(1000), + "nonce": f'{convert_name_to_value(evmAcc.name):#0x}', "timestamp": hex(int(calendar.timegm(datetime.strptime(block["timestamp"].split(".")[0], '%Y-%m-%dT%H:%M:%S').timetuple()))) } + Utils.Print("Send small balance to special balance to allow the bridge to work") + transferAmount="1.0000 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, evmAcc.name)) + nonProdNode.transferFunds(cluster.eosioAccount, evmAcc, transferAmount, evmAcc.name, waitForTransBlock=True) + # accounts: { # mnemonic: "test test test test test test test test test test test junk", # path: "m/44'/60'/0'/0", @@ -324,12 +339,14 @@ "0x9E126C57330FA71556628e0aabd6B6B6783d99fA":"0x034d7b61c8dd53a761ab44d1e06be6b1338de4095c620112494b8830792c84f64b,0xba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0" } + # init with 100,000 EOS for i,k in enumerate(addys): print("addys: [{0}] [{1}] [{2}]".format(i,k[2:].lower(), len(k[2:]))) - trans = prodNode.pushMessage(evmAcc.name, "setbal", '{"addy":"' + k[2:].lower() + '", "bal":"0000000000000000000000000000000000100000000000000000000000000000"}', '-p evmevmevmevm') - genesis_info["alloc"][k.lower()] = {"balance":"0x100000000000000000000000000000"} + transferAmount="100000.0000 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, evmAcc.name)) + trans = prodNode.transferFunds(cluster.eosioAccount, evmAcc, transferAmount, "0x" + k[2:].lower(), waitForTransBlock=False) if not (i+1) % 20: time.sleep(1) - prodNode.waitForTransBlockIfNeeded(trans[1], True) + prodNode.waitForTransBlockIfNeeded(trans, True) if gensisJson[0] != '/': gensisJson = os.path.realpath(gensisJson) f=open(gensisJson,"w") @@ -386,7 +403,7 @@ def restore(): def default(): def forward_request(req): if req['method'] == "eth_sendRawTransaction": - actData = {"ram_payer":"evmevmevmevm", "rlptx":req['params'][0][2:]} + actData = {"miner":"evmevmevmevm", "rlptx":req['params'][0][2:]} prodNode1.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm') return { "id": req['id'], diff --git a/tests/leap/nodeos_trust_evm_server/contracts/Blockhash.sol b/tests/leap/nodeos_trust_evm_server/contracts/Blockhash.sol new file mode 100644 index 00000000..262989f7 --- /dev/null +++ b/tests/leap/nodeos_trust_evm_server/contracts/Blockhash.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.8.2 <0.9.0; + +contract Blockhash { + + uint256 curr_block; + bytes32 prev1; + bytes32 prev2; + bytes32 prev3; + bytes32 prev4; + bytes32 prev5; + + function go() public { + curr_block = block.number; + prev1 = blockhash(block.number-1); + prev2 = blockhash(block.number-2); + prev3 = blockhash(block.number-3); + prev4 = blockhash(block.number-4); + prev5 = blockhash(block.number-5); + } + + function r_curr_block() public view returns (uint256){ + return curr_block; + } + function r_prev1() public view returns (bytes32){ + return prev1; + } + function r_prev2() public view returns (bytes32){ + return prev2; + } + function r_prev3() public view returns (bytes32){ + return prev3; + } + function r_prev4() public view returns (bytes32){ + return prev4; + } + function r_prev5() public view returns (bytes32){ + return prev5; + } + +} \ No newline at end of file diff --git a/tests/leap/nodeos_trust_evm_server/hardhat.config.js b/tests/leap/nodeos_trust_evm_server/hardhat.config.js index d925aa30..6ba8c1c6 100644 --- a/tests/leap/nodeos_trust_evm_server/hardhat.config.js +++ b/tests/leap/nodeos_trust_evm_server/hardhat.config.js @@ -83,6 +83,17 @@ task("emit-event", "Emit event") console.log("############################################ EMIT #######"); }); +task("test-blockhash", "Test blockhash") + .addParam("contract", "Blockhash contract address") + .setAction(async (taskArgs) => { + const Blockhash = await ethers.getContractFactory('Blockhash') + const blockhash = Blockhash.attach(taskArgs.contract).connect(await ethers.getSigner(0)); + + const res = await blockhash.go(); + console.log("############################################ GO #######"); + console.log(res); +}); + task("storage-loop", "Store incremental values to the storage contract") .addParam("contract", "Token contract address") .setAction(async (taskArgs) => { diff --git a/tests/leap/nodeos_trust_evm_server/scripts/deploy-blockhash.js b/tests/leap/nodeos_trust_evm_server/scripts/deploy-blockhash.js new file mode 100644 index 00000000..a39d5bd9 --- /dev/null +++ b/tests/leap/nodeos_trust_evm_server/scripts/deploy-blockhash.js @@ -0,0 +1,25 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node