From 3be0104abd7612874e40aed35bd092e7bf089d10 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Mon, 13 Feb 2023 15:34:29 -0500 Subject: [PATCH 01/70] misc contract changes to support bridging --- contract/include/evm_runtime/evm_contract.hpp | 36 +++-- contract/include/evm_runtime/tables.hpp | 88 ++++++++++++ contract/include/evm_runtime/types.hpp | 9 ++ contract/src/actions.cpp | 126 ++++++++++++++---- contract/tests/init_tests.cpp | 10 ++ contract/tests/native_token_tests.cpp | 44 +++--- 6 files changed, 253 insertions(+), 60 deletions(-) diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index 6ca9efba..ae04aac0 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -22,6 +22,15 @@ CONTRACT evm_contract : public contract { [[eosio::action]] void init(const uint64_t chainid); + [[eosio::action]] + void setingressfee(asset ingress_bridge_fee); + + [[eosio::action]] + void addegress(const std::vector& accounts); + + [[eosio::action]] + void removeegress(const std::vector& accounts); + [[eosio::action]] void pushtx(eosio::name ram_payer, const bytes& rlptx); @@ -31,7 +40,7 @@ CONTRACT evm_contract : public contract { [[eosio::action]] void close(eosio::name owner); - [[eosio::on_notify("eosio.token::transfer")]] + [[eosio::on_notify(TOKEN_ACCOUNT_NAME "::transfer")]] void transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo); [[eosio::action]] @@ -52,22 +61,14 @@ CONTRACT evm_contract : public contract { 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; - struct [[eosio::table]] [[eosio::contract("evm_contract")]] config { - eosio::unsigned_int version; //placeholder for future variant index - uint64_t chainid = 0; + 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); + + EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)); }; - EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)); eosio::singleton<"config"_n, config> _config{get_self(), get_self().value}; @@ -77,6 +78,13 @@ CONTRACT evm_contract : public contract { } void push_trx(eosio::name ram_payer, silkworm::Block& block, const bytes& rlptx); + + 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(const eosio::asset& quantity, const std::string& memo); }; diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 0ef274d6..0243254c 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include namespace evm_runtime { @@ -75,4 +77,90 @@ 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); + } + + void accumulate(const intx::uint256& amount) { + //asset::max_amount is conservative at 2^62-1, this means two 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 + check(amount/min_asset_bn <= asset::max_amount, "accumulation overflow"); + + const int64_t base_amount = (amount/min_asset_bn)[0]; + check(balance.amount + base_amount < asset::max_amount, "accumulation overflow"); + balance.amount += base_amount; + dust += (amount%min_asset_bn)[0]; + + if(dust > min_asset) { + balance.amount++; + dust -= min_asset; + } + } + + void decrement(const intx::uint256& amount) { + check(amount/min_asset_bn <= balance.amount, "decrementing more than available"); + balance.amount -= (amount/min_asset_bn)[0]; + dust -= (amount%min_asset_bn)[0]; + + if(dust & (UINT64_C(1) << 63)) { + balance.amount--; + dust += min_asset; + check(balance.amount >= 0, "decrementing more than available"); + } + } + + static constexpr intx::uint256 min_asset_bn = intx::exp(10_u256, intx::uint256(evm_precision - token_symbol.precision())); + static constexpr uint64_t min_asset = min_asset_bn[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; + +struct [[eosio::table]] [[eosio::contract("evm_contract")]] stats { + balance_with_dust in_evm; + + EOSLIB_SERIALIZE(stats, (in_evm)); +}; + +typedef eosio::singleton<"stats"_n, stats> stats_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..b6c6ba6a 100644 --- a/contract/include/evm_runtime/types.hpp +++ b/contract/include/evm_runtime/types.hpp @@ -1,11 +1,19 @@ #pragma once #include +#include +#include #include #include #include +#define TOKEN_ACCOUNT_NAME "eosio.token" + namespace evm_runtime { + constexpr unsigned evm_precision = 18; + constexpr eosio::name token_account(eosio::name(TOKEN_ACCOUNT_NAME)); + constexpr eosio::symbol token_symbol("EOS", 4u); + static_assert(token_symbol.precision() <= evm_precision); typedef intx::uint<256> uint256; typedef intx::uint<512> uint512; @@ -27,6 +35,7 @@ namespace evm_runtime { evmc::bytes32 to_bytes32(const bytes& data); uint256 to_uint256(const bytes& value); + using intx::operator""_u256; } //namespace evm_runtime namespace eosio { diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 7651f09e..33d8f230 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -29,8 +29,7 @@ namespace silkworm { } } -static constexpr eosio::name token_account("eosio.token"_n); -static constexpr eosio::symbol token_symbol("EOS", 4u); +extern "C" __attribute__((eosio_wasm_import)) uint32_t get_code_hash(uint64_t account, uint32_t struct_version, char* data, uint32_t size); namespace evm_runtime { @@ -47,6 +46,43 @@ void evm_contract::init(const uint64_t chainid) { .chainid = chainid, .genesis_time = current_time_point() }, get_self()); + + stats_singleton(get_self(), get_self().value).get_or_create(get_self()); + + open(get_self(), get_self()); +} + +void evm_contract::setingressfee(asset ingress_bridge_fee) { + assert_inited(); + + check( ingress_bridge_fee.symbol == token_symbol, "unexpected bridge symbol" ); + check( ingress_bridge_fee.amount >= 0, "ingress bridge fee cannot be negative"); + + config current_config = _config.get(); + current_config.ingress_bridge_fee = ingress_bridge_fee; + _config.set(current_config, get_self()); +} + +void evm_contract::addegress(const std::vector& accounts) { + assert_inited(); + + 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(); + + 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 check_result( ValidationResult r, const Transaction& txn, const char* desc ) { @@ -123,11 +159,16 @@ void evm_contract::open(eosio::name owner, eosio::name ram_payer) { require_auth(ram_payer); check(is_account(owner), "owner account does not exist"); - accounts account_table(get_self(), get_self().value); - if(account_table.find(owner.value) == account_table.end()) - account_table.emplace(ram_payer, [&](account& a) { + balances balance_table(get_self(), get_self().value); + if(balance_table.find(owner.value) == balance_table.end()) + balance_table.emplace(ram_payer, [&](balance& a) { + a.owner = owner; + }); + + nextnonces nextnonce_table(get_self(), get_self().value); + if(nextnonce_table.find(owner.value) == nextnonce_table.end()) + nextnonce_table.emplace(ram_payer, [&](nextnonce& a) { a.owner = owner; - a.balance = asset(0, token_symbol); }); } @@ -135,41 +176,78 @@ void evm_contract::close(eosio::name owner) { assert_inited(); 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"); - eosio::check(owner_account.balance.amount == 0 && owner_account.dust == 0, "cannot close because balance is not zero"); - account_table.erase(owner_account); + 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); + + 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_inited(); +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; +} - eosio::check(!memo.empty(), "memo must be already opened account name to credit deposit to"); +checksum256 evm_contract::get_code_hash(name account) const { + char buff[64]; + eosio::check(::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::transfer(eosio::name from, eosio::name to, eosio::asset quantity, std::string memo) { + assert_inited(); + 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') + eosio::check(false, "unsupported"); + 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_inited(); 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}}); diff --git a/contract/tests/init_tests.cpp b/contract/tests/init_tests.cpp index de749f59..b81ed479 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -8,6 +8,16 @@ BOOST_FIXTURE_TEST_CASE(check_init, basic_evm_tester) try { 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, "setingressfee"_n, "evm"_n, mvo()("ingress_bridge_fee", asset(0, symbol(4, "EOS")))), + 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)("ram_payer", "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");}); diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 5d2a23be..6d5e2347 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -33,17 +33,17 @@ struct native_token_evm_tester : basic_evm_tester { 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); + std::tuple vault_balance(name owner) const { + const vector d = get_row_by_account("evm"_n, "evm"_n, "balances"_n, owner); FC_ASSERT(d.size(), "EVM not open"); - auto [_, amount, dust] = fc::raw::unpack(d); + 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 vault_balance_token(name owner) const { + return std::get<0>(vault_balance(owner)).get_amount(); } - int64_t evm_balance_dust(name owner) const { - return std::get<1>(evm_balance(owner)); + int64_t vault_balance_dust(name owner) const { + return std::get<1>(vault_balance(owner)); } transaction_trace_ptr open(name owner, name ram_payer) { @@ -61,13 +61,13 @@ struct native_token_evm_tester : basic_evm_tester { return asset(amount, native_symbol); } - struct evm_account_row { + struct vault_balance_row { name owner; asset balance; uint64_t dust = 0; }; }; -FC_REFLECT(native_token_evm_tester::evm_account_row, (owner)(balance)(dust)) +FC_REFLECT(native_token_evm_tester::vault_balance_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) {} @@ -93,22 +93,22 @@ BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try { 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 +126,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 +152,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,13 +179,13 @@ 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")); + 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() @@ -203,7 +203,7 @@ BOOST_FIXTURE_TEST_CASE(non_standard_native_symbol, native_token_evm_tester_SPOO open("alice"_n, "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() From f46e2b77f4124016017cca23525056afbe2cfeb8 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Wed, 15 Feb 2023 19:51:23 -0500 Subject: [PATCH 02/70] minor changes * move get_code_hash import to existing intrinsics.hpp * add missing require_auth()s * get rid of balance_and_dust's functions for operators instead --- contract/include/evm_runtime/intrinsics.hpp | 3 +++ contract/include/evm_runtime/tables.hpp | 8 ++++++-- contract/src/actions.cpp | 7 ++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/contract/include/evm_runtime/intrinsics.hpp b/contract/include/evm_runtime/intrinsics.hpp index f1e832c3..3f483ebb 100644 --- a/contract/include/evm_runtime/intrinsics.hpp +++ b/contract/include/evm_runtime/intrinsics.hpp @@ -5,6 +5,9 @@ namespace eosio { __attribute__((eosio_wasm_import)) uint32_t get_block_num(); + __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 0243254c..f85879a8 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -88,7 +88,7 @@ struct balance_with_dust { return !(*this == o); } - void accumulate(const intx::uint256& amount) { + balance_with_dust& operator+=(const intx::uint256& amount) { //asset::max_amount is conservative at 2^62-1, this means two 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 @@ -103,9 +103,11 @@ struct balance_with_dust { balance.amount++; dust -= min_asset; } + + return *this; } - void decrement(const intx::uint256& amount) { + balance_with_dust& operator-=(const intx::uint256& amount) { check(amount/min_asset_bn <= balance.amount, "decrementing more than available"); balance.amount -= (amount/min_asset_bn)[0]; dust -= (amount%min_asset_bn)[0]; @@ -115,6 +117,8 @@ struct balance_with_dust { dust += min_asset; check(balance.amount >= 0, "decrementing more than available"); } + + return *this; } static constexpr intx::uint256 min_asset_bn = intx::exp(10_u256, intx::uint256(evm_precision - token_symbol.precision())); diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 33d8f230..0b5481ae 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -29,8 +29,6 @@ namespace silkworm { } } -extern "C" __attribute__((eosio_wasm_import)) uint32_t get_code_hash(uint64_t account, uint32_t struct_version, char* data, uint32_t size); - namespace evm_runtime { using namespace silkworm; @@ -54,6 +52,7 @@ void evm_contract::init(const uint64_t chainid) { void evm_contract::setingressfee(asset ingress_bridge_fee) { assert_inited(); + require_auth(get_self()); check( ingress_bridge_fee.symbol == token_symbol, "unexpected bridge symbol" ); check( ingress_bridge_fee.amount >= 0, "ingress bridge fee cannot be negative"); @@ -65,6 +64,7 @@ void evm_contract::setingressfee(asset ingress_bridge_fee) { void evm_contract::addegress(const std::vector& accounts) { assert_inited(); + require_auth(get_self()); egresslist egresslist_table(get_self(), get_self().value); @@ -77,6 +77,7 @@ void evm_contract::addegress(const std::vector& accounts) { void evm_contract::removeegress(const std::vector& accounts) { assert_inited(); + require_auth(get_self()); egresslist egresslist_table(get_self(), get_self().value); @@ -205,7 +206,7 @@ uint64_t evm_contract::get_and_increment_nonce(const name owner) { checksum256 evm_contract::get_code_hash(name account) const { char buff[64]; - eosio::check(::get_code_hash(account.value, 0, buff, sizeof(buff)) <= sizeof(buff), "get_code_hash() too big"); + 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)); From 8fcee4df85c8ac20a26c7b19aa5518a3b3f04c6c Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Wed, 15 Feb 2023 20:05:08 -0500 Subject: [PATCH 03/70] change 'stats' singleton to 'inevm' singleton having the singleton contain just a balance_and_dust makes modifications a little easier --- contract/include/evm_runtime/tables.hpp | 8 +------- contract/src/actions.cpp | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index f85879a8..b6c86165 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -138,13 +138,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] balance { typedef eosio::multi_index<"balances"_n, balance> balances; -struct [[eosio::table]] [[eosio::contract("evm_contract")]] stats { - balance_with_dust in_evm; - - EOSLIB_SERIALIZE(stats, (in_evm)); -}; - -typedef eosio::singleton<"stats"_n, stats> stats_singleton; +typedef eosio::singleton<"inevm"_n, balance_with_dust> inevm_singleton; struct [[eosio::table]] [[eosio::contract("evm_contract")]] nextnonce { name owner; diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 032149ad..da2c74cb 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -46,7 +46,7 @@ void evm_contract::init(const uint64_t chainid) { .genesis_time = current_time_point() }, get_self()); - stats_singleton(get_self(), get_self().value).get_or_create(get_self()); + inevm_singleton(get_self(), get_self().value).get_or_create(get_self()); open(get_self(), get_self()); } From 924e003ddc902256c163414e5cb4688b7e3886b7 Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Thu, 16 Feb 2023 20:03:56 -0300 Subject: [PATCH 04/70] Allow data field to be null for Call type --- silkrpc/json/types.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/silkrpc/json/types.cpp b/silkrpc/json/types.cpp index 8407bf45..27d56049 100644 --- a/silkrpc/json/types.cpp +++ b/silkrpc/json/types.cpp @@ -317,7 +317,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); } From b2b67ca23340bb744a5759dd9898d4957de56561 Mon Sep 17 00:00:00 2001 From: yarkin Date: Wed, 22 Feb 2023 10:23:09 +0800 Subject: [PATCH 05/70] Seperate code storage from account table. --- contract/include/evm_runtime/tables.hpp | 34 ++++++++--- contract/src/state.cpp | 23 +++++--- contract/tests/evm_runtime_tests.cpp | 76 +++++++++++++++---------- 3 files changed, 89 insertions(+), 44 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 0ef274d6..277773f1 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -13,7 +13,6 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { bytes eth_address; uint64_t nonce; bytes balance; - bytes code; bytes code_hash; uint64_t primary_key()const { return id; } @@ -22,10 +21,6 @@ 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); @@ -38,14 +33,37 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { return res; } - EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code)(code_hash)); + EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code_hash)); }; typedef multi_index< "account"_n, account, - indexed_by<"by.address"_n, const_mem_fun>, - indexed_by<"by.codehash"_n, const_mem_fun> + indexed_by<"by.address"_n, const_mem_fun> > account_table; +struct [[eosio::table]] [[eosio::contract("evm_contract")]] codestore { + uint64_t id; + 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; + } + + EOSLIB_SERIALIZE(codestore, (id)(code)(code_hash)); +}; + +typedef multi_index< "codestore"_n, codestore, + indexed_by<"by.codehash"_n, const_mem_fun> +> codestore_table; + struct [[eosio::table]] [[eosio::contract("evm_contract")]] storage { uint64_t id; bytes key; diff --git a/contract/src/state.cpp b/contract/src/state.cpp index 8e38992d..f1857581 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -19,7 +19,6 @@ std::optional state::read_account(const evmc::address& address) const n auto code_hash = itr->get_code_hash(); addr2id[address] = itr->id; - addr2code[code_hash] = itr->code; return Account{itr->nonce, intx::be::load(itr->get_balance()), code_hash, 0}; } @@ -31,11 +30,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>(); + codestore_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{}; } @@ -141,13 +139,25 @@ bool state::gc(uint32_t max) { } void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { + codestore_table codes(_self, _self.value); + auto inxc = codes.get_index<"by.codehash"_n>(); + auto itrc = inxc.find(make_key(code_hash)); + if(itrc == inxc.end()) { + codes.emplace(_ram_payer, [&](auto& row){ + row.id = codes.available_primary_key(); + row.code_hash = to_bytes(code_hash); + row.code = bytes{code.begin(), code.end()}; + }); + } else { + // code should be immutable + } + 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); }); ++stats.account.update; @@ -156,7 +166,6 @@ 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); }); ++stats.account.create; diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 123f205a..eda6a62e 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -237,7 +237,6 @@ struct account { bytes eth_address; uint64_t nonce; bytes balance; - bytes code; bytes code_hash; bytes old_code_hash; @@ -249,13 +248,6 @@ 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); @@ -272,16 +264,7 @@ struct account { 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) { @@ -294,12 +277,6 @@ struct account { 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; - } - Account as_silkworm_account() { return Account{ nonce, @@ -310,7 +287,48 @@ struct account { } }; -FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code)(code_hash)); +FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_hash)); + +struct codestore { + uint64_t id; + bytes code; + bytes code_hash; + + bytes old_code_hash; + + + struct by_codehash { + typedef index256_object index_object; + static name index_name() { + return account::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; + } + + static name table_name() { return "codestore"_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); + if(r) r->old_code_hash = r->code_hash; + return r; + } + +}; +FC_REFLECT(codestore, (id)(code)(code_hash)); struct storage { uint64_t id; @@ -407,7 +425,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, 50000000); set_authority( ME, "active"_n, {1, {{get_public_key(ME,"active"),1}}, {{{ME,"eosio.code"_n},1}}} ); set_code(ME, contracts::evm_runtime_wasm()); @@ -568,13 +586,13 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { 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 = codestore::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()}; } From 3bc4fa49d09ddd2f8a124e0d7a229e1f6921ac19 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Wed, 22 Feb 2023 21:38:10 -0500 Subject: [PATCH 06/70] add a constexpr with minimum representable value in configured symbol --- contract/include/evm_runtime/types.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contract/include/evm_runtime/types.hpp b/contract/include/evm_runtime/types.hpp index b6c6ba6a..2f1b4515 100644 --- a/contract/include/evm_runtime/types.hpp +++ b/contract/include/evm_runtime/types.hpp @@ -10,10 +10,13 @@ #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); static_assert(token_symbol.precision() <= evm_precision); + constexpr intx::uint256 minimum_natively_representable = intx::exp(10_u256, intx::uint256(evm_precision - token_symbol.precision())); typedef intx::uint<256> uint256; typedef intx::uint<512> uint512; @@ -34,8 +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); - - using intx::operator""_u256; } //namespace evm_runtime namespace eosio { From 1d76cd938a851594b48b9f8efb02596dd2c35e21 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Wed, 22 Feb 2023 22:16:52 -0500 Subject: [PATCH 07/70] initial bridging --- contract/include/evm_runtime/eosio.token.hpp | 8 + contract/include/evm_runtime/evm_contract.hpp | 18 +- contract/src/CMakeLists.txt | 2 +- contract/src/actions.cpp | 113 +++++- contract/tests/basic_evm_tester.hpp | 153 +++++++- contract/tests/native_token_tests.cpp | 327 +++++++++++++++++- silkworm | 2 +- 7 files changed, 616 insertions(+), 7 deletions(-) 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 cd743c5f..18496b19 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -83,8 +83,22 @@ CONTRACT evm_contract : public contract { checksum256 get_code_hash(name account) const; void handle_account_transfer(const eosio::asset& quantity, const std::string& memo); - void handle_evm_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 ram_payer, const std::basic_string& rlptx); + using pushtx_action = eosio::action_wrapper<"pushtx"_n, &evm_contract::pushtx_bytes>; }; -} //evm_runtime \ No newline at end of file +} //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; +} +} \ No newline at end of file diff --git a/contract/src/CMakeLists.txt b/contract/src/CMakeLists.txt index 84896371..ee8cfa69 100644 --- a/contract/src/CMakeLists.txt +++ b/contract/src/CMakeLists.txt @@ -81,4 +81,4 @@ target_include_directories( evm_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../silkworm/third_party/evmone/evmc/include ) -target_link_options(evm_runtime PUBLIC -stack-size=20480) +target_link_options(evm_runtime PUBLIC -stack-size=20112) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index da2c74cb..47bf1abb 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -108,6 +108,17 @@ void check_result( ValidationResult r, const Transaction& txn, const char* desc } void evm_contract::push_trx( eosio::name ram_payer, Block& block, const bytes& rlptx, silkworm::consensus::IEngine& engine, const silkworm::ChainConfig& chain_config ) { + //when being called as an inline action, clutch in allowance for reserved addresses & signatures by setting from_my_self=true + const bool from_self = get_sender() == get_self(); + + std::optional balance_table; + std::optional inevm; + auto populate_bridge_accessors = [&]() { + if(balance_table) + return; + balance_table.emplace(get_self(), get_self().value); + inevm.emplace(get_self(), get_self().value); + }; Transaction tx; ByteView bv{(const uint8_t*)rlptx.data(), rlptx.size()}; @@ -122,6 +133,26 @@ void evm_contract::push_trx( eosio::name ram_payer, Block& block, const bytes& r evm_runtime::state state{get_self(), ram_payer}; silkworm::ExecutionProcessor ep{block, engine, state, chain_config}; + 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) ?: get_self().value); //reserved-0 used for from-contract + + 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" ); @@ -134,6 +165,52 @@ void evm_contract::push_trx( eosio::name ram_payer, Block& block, const bytes& r engine.finalize(ep.state(), ep.evm().block(), ep.evm().revision()); ep.state().write_to_db(ep.evm().block().header.number); + 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) ?: get_self().value); //egress to reserved-0 goes to contract's bucket; TODO: needs more love w/ gas changes + const Account& reserved_account = *reserved_object.second.current; + + check(reserved_account.code_hash == kEmptyHash, "contracts cannot be created in the reserved address space"); + + 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; + }); + } + 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); + } + LOGTIME("EVM EXECUTE"); } @@ -224,6 +301,40 @@ void evm_contract::handle_account_transfer(const eosio::asset& quantity, const s }); } +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; + }); + + //substract off the ingress bridge fee from the quantity that will be bridged + quantity -= _config.get().ingress_bridge_fee; + eosio::check(quantity.amount > 0, "must bridge more than ingress bridge fee"); + + const std::optional address_bytes = from_hex(memo); + 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 = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = to_evmc_address(*address_bytes), + .value = value + }; + //txn's r == 0 && s == 0 which is a psuedo-signature from reserved address zero + + 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_inited(); eosio::check(quantity.symbol == token_symbol, "received unexpected token"); @@ -232,7 +343,7 @@ void evm_contract::transfer(eosio::name from, eosio::name to, eosio::asset quant return; if(memo.size() == 42 && memo[0] == '0' && memo[1] == 'x') - eosio::check(false, "unsupported"); + handle_evm_transfer(quantity, memo); else if(!memo.empty() && memo.size() <= 13) handle_account_transfer(quantity, memo); else diff --git a/contract/tests/basic_evm_tester.hpp b/contract/tests/basic_evm_tester.hpp index 6b4538b2..038c68b2 100644 --- a/contract/tests/basic_evm_tester.hpp +++ b/contract/tests/basic_evm_tester.hpp @@ -2,8 +2,18 @@ #include #include +#include #include +#include +#include + +#include +#include +#include +#include + +#include #include @@ -11,6 +21,79 @@ using namespace eosio; using namespace eosio::chain; using mvo = fc::mutable_variant_object; +using intx::operator""_u256; + +class evm_eoa { +public: + evm_eoa() { + fc::rand_bytes((char*)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 address_0x() const { + return std::string("0x") + fc::to_hex((char*)address.bytes, sizeof(address.bytes)); + } + + key256_t address_key256() const { + uint8_t buffer[32]={0}; + memcpy(buffer, address.bytes, sizeof(address.bytes)); + return fixed_bytes<32>(buffer).get_array(); + } + + void sign(silkworm::Transaction& trx) { + silkworm::Bytes rlp; + trx.chain_id = 15555; /// TODO: don't hardcode this + 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() { + secp256k1_context_destroy(ctx); + } + + evmc::address address; +private: + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + std::array private_key; + std::basic_string public_key; + + unsigned next_nonce = 0; +}; + +struct balance_and_dust { + asset balance; + uint64_t dust = 0; + + bool operator==(const balance_and_dust& o) const { + return balance == o.balance && dust == o.dust; + } + bool operator!=(const balance_and_dust& o) const { + return !(*this == o); + } +}; +FC_REFLECT(balance_and_dust, (balance)(dust)); + class basic_evm_tester : public testing::validating_tester { public: basic_evm_tester() { @@ -23,4 +106,72 @@ class basic_evm_tester : public testing::validating_tester { void init(const uint64_t chainid) { push_action("evm"_n, "init"_n, "evm"_n, mvo()("chainid", chainid)); } -}; \ No newline at end of file + + void setingressfee(const asset& ingress_bridge_fee) { + push_action("evm"_n, "setingressfee"_n, "evm"_n, mvo()("ingress_bridge_fee", ingress_bridge_fee)); + } + + void pushtx(const silkworm::Transaction& trx) { + 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"_n, "pushtx"_n, "evm"_n, mvo()("ram_payer", "evm"_n)("rlptx", rlp_bytes)); + } + + balance_and_dust inevm() const { + return fc::raw::unpack(get_row_by_account("evm"_n, "evm"_n, "inevm"_n, "inevm"_n)); + } + + std::optional evm_balance(const evm_eoa& account) { + const int64_t account_table_id = get(boost::make_tuple("evm"_n, "evm"_n, "account"_n)).id._id; + + const index256_object* secondary_row = find(boost::make_tuple(account_table_id, account.address_key256())); + if(secondary_row == nullptr) + return std::nullopt; + + const key_value_object& row = get(boost::make_tuple(account_table_id, secondary_row->primary_key)); + + // cheat a little here: uint64_t id, bytes eth_address, uint64_t nonce, bytes balance; 32 bytes of balance must start at byte 38 + return intx::be::unsafe::load((uint8_t*)row.value.data()+38); + } + + void addegress(const std::vector accounts) { + push_action("evm"_n, "addegress"_n, "evm"_n, mvo()("accounts",accounts)); + } + + void removeegress(const std::vector accounts) { + push_action("evm"_n, "removeegress"_n, "evm"_n, mvo()("accounts",accounts)); + } +}; + +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); +} \ No newline at end of file diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 6d5e2347..c19a7807 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -4,6 +4,15 @@ using namespace eosio::testing; +static const char do_nothing_wast[] = R"=====( +(module + (export "apply" (func $apply)) + (func $apply (param $0 i64) (param $1 i64) (param $2 i64) + ;; nothing + ) +) +)====="; + 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) @@ -42,7 +51,7 @@ struct native_token_evm_tester : basic_evm_tester { int64_t vault_balance_token(name owner) const { return std::get<0>(vault_balance(owner)).get_amount(); } - int64_t vault_balance_dust(name owner) const { + uint64_t vault_balance_dust(name owner) const { return std::get<1>(vault_balance(owner)); } @@ -66,6 +75,20 @@ struct native_token_evm_tester : basic_evm_tester { asset balance; uint64_t dust = 0; }; + + evmc::address make_reserved_address(uint64_t account) const { + return evmc_address({0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 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)}); + } }; FC_REFLECT(native_token_evm_tester::vault_balance_row, (owner)(balance)(dust)) @@ -215,4 +238,306 @@ BOOST_FIXTURE_TEST_CASE(transfer_notifier_without_init, native_token_evm_tester_ } FC_LOG_AND_RETHROW() +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()); + + //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; + setingressfee(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) == std::make_tuple(make_asset(0), 0UL)); + + //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()); + + BOOST_REQUIRE_EQUAL(vault_balance_token("evm"_n), bridge_fee); + } + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(disallow_bridge_sigs_outside_bridge_trx, native_token_evm_tester_EOS) try { + evm_eoa evm1; + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = evm1.address, + .value = 11111111_u256, + }; + + //r == 0 indicates a bridge signature. These are only allowed in contract-initiated (i.e. inline) EVM actions + BOOST_REQUIRE_EXCEPTION(pushtx(txn), + 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; + + //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); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = evm2.address, + .value = 100_szabo * to_transfer, + }; + evm1.sign(txn); + pushtx(txn); + + BOOST_REQUIRE(*evm_balance(evm1) == evm1_before - txn.value); + BOOST_REQUIRE(!!evm_balance(evm2)); + BOOST_REQUIRE(*evm_balance(evm2) == txn.value); + } + + //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 valance + { + 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); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address("alice"_n.to_uint64_t()), + .value = 100_szabo * to_bridge, + }; + evm1.sign(txn); + pushtx(txn); + + BOOST_REQUIRE_EQUAL(alice_native_before + to_bridge, native_balance("alice"_n)); + BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); + } + + //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); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address("alice"_n.to_uint64_t()), + .value = 100_szabo * to_bridge + 10_szabo, //dust + }; + 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, "alice"_n); + //and try again + pushtx(txn); + + BOOST_REQUIRE_EQUAL(alice_native_before, native_balance("alice"_n)); //native balance unchanged + BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); //EVM balance decresed + BOOST_REQUIRE(vault_balance("alice"_n) == std::make_tuple(make_asset(1'0000), 10'000'000'000'000UL)); //vault balance increased to 1.0000EOS, 10szabo + } + + //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); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address("bob"_n.to_uint64_t()), + .value = 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, "bob"_n); + //and now it'll go through + pushtx(txn); + BOOST_REQUIRE_EQUAL(vault_balance_token("bob"_n), to_bridge); + BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); + } + + //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); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address("carol"_n.to_uint64_t()), + .value = 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(evm_balance(evm1) == evm1_before - txn.value); + + //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; + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address("spoon"_n.to_uint64_t()), + .value = 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_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/silkworm b/silkworm index 3352c187..9d7d3e96 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit 3352c187ce355b3bd2bafe5dd881ed94ad4f24af +Subproject commit 9d7d3e969b1f4bfd23b72dc474d48d2b89e59ad5 From 174d28b04acb116bb0874731c37dceb7064df947 Mon Sep 17 00:00:00 2001 From: yarkin Date: Thu, 23 Feb 2023 16:47:38 +0800 Subject: [PATCH 08/70] Use std::optional for code_hash --- contract/include/evm_runtime/tables.hpp | 12 +++++++++--- contract/src/state.cpp | 9 ++++++--- contract/tests/evm_runtime_tests.cpp | 18 +++++++++++------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 277773f1..d1e89e83 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -7,13 +7,14 @@ namespace evm_runtime { using namespace eosio; - +// c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 +inline constexpr char kEmptyHashBytes[32] = {-59, -46, 70, 1, -122, -9, 35, 60, -110, 126, 125, -78, -36, -57, 3, -64, -27, 0, -74, 83, -54, -126, 39, 59, 123, -6, -40, 4, 93,-123, -92, 112}; struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { uint64_t id; bytes eth_address; uint64_t nonce; bytes balance; - bytes code_hash; + std::optional code_hash; uint64_t primary_key()const { return id; } @@ -29,7 +30,12 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { bytes32 get_code_hash()const { bytes32 res; - std::copy(code_hash.begin(), code_hash.end(), res.bytes); + if (code_hash.has_value()) { + std::copy(code_hash.value().begin(), code_hash.value().end(), res.bytes); + } + else { + std::copy(kEmptyHashBytes, &kEmptyHashBytes[32], res.bytes); + } return res; } diff --git a/contract/src/state.cpp b/contract/src/state.cpp index f1857581..7506418e 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -82,26 +82,29 @@ 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)); ++stats.account.read; if (current.has_value()) { + const bool zero_code = (std::memcmp(evmc::bytes_view(current->code_hash).data(), + kEmptyHashBytes, sizeof(kEmptyHashBytes)) == 0); + if (itr == inx.end()) { accounts.emplace(_ram_payer, [&](auto& row){ row.id = accounts.available_primary_key(); row.eth_address = to_bytes(address); row.nonce = current->nonce; row.balance = to_bytes(current->balance); - row.code_hash = to_bytes(current->code_hash); + row.code_hash = zero_code ? std::nullopt : std::optional(to_bytes(current->code_hash)); }); ++stats.account.create; } else { accounts.modify(*itr, eosio::same_payer, [&](auto& row){ row.nonce = current->nonce; - row.code_hash = to_bytes(current->code_hash); + row.code_hash = zero_code ? std::nullopt : std::optional(to_bytes(current->code_hash)); row.balance = to_bytes(current->balance); }); ++stats.account.update; diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index eda6a62e..e84f14db 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -231,15 +231,18 @@ struct block_info { }; //FC_REFLECT(block_info, (coinbase)(difficulty)(gasLimit)(number)(timestamp)(base_fee_per_gas)); FC_REFLECT(block_info, (coinbase)(difficulty)(gasLimit)(number)(timestamp)); +inline constexpr char kEmptyHashBytes[32] = {(char)-59, (char)-46, (char)70, (char)1, (char)-122, (char)-9, (char)35, (char)60, + (char)-110, (char)126, (char)125, (char)-78, (char)-36, (char)-57, (char)3, (char)-64, (char)-27, (char)0, + (char)-74, (char)83, (char)-54, (char)-126, (char)39, (char)59, (char)123, (char)-6, + (char)-40, (char)4, (char)93,(char)-123, (char)-92, (char)112}; struct account { uint64_t id; bytes eth_address; uint64_t nonce; bytes balance; - bytes code_hash; + std::optional code_hash; - bytes old_code_hash; struct by_address { typedef index256_object index_object; @@ -256,7 +259,12 @@ struct account { evmc::bytes32 get_code_hash()const { evmc::bytes32 res; - std::copy(code_hash.begin(), code_hash.end(), res.bytes); + if (code_hash.has_value()) { + std::copy(code_hash.value().begin(), code_hash.value().end(), res.bytes); + } + else { + std::copy(kEmptyHashBytes, &kEmptyHashBytes[32], res.bytes); + } return res; } @@ -273,7 +281,6 @@ 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; } @@ -294,8 +301,6 @@ struct codestore { bytes code; bytes code_hash; - bytes old_code_hash; - struct by_codehash { typedef index256_object index_object; @@ -323,7 +328,6 @@ struct codestore { 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; } From f2a156e137ce4e275c368fb59fba6d4524c0a268 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 24 Feb 2023 16:33:46 -0600 Subject: [PATCH 09/70] WIP start of integration test for bridging --- tests/leap/nodeos_trust_evm_test.py | 110 +++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 847786f2..259cb964 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -4,6 +4,10 @@ import os import json import time +import sys +import signal +import calendar +from datetime import datetime import sys from binascii import unhexlify @@ -14,6 +18,7 @@ from TestHarness import Cluster, TestHelper, Utils, WalletMgr from TestHarness.TestHelper import AppArgs +from TestHarness.testUtils import ReturnType from core_symbol import CORE_SYMBOL @@ -31,6 +36,7 @@ # # Example: # cd ~/ext/leap/build +# edit core_symbol.py to be EOS # ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --leave-running # # Launches wallet at port: 9899 @@ -42,7 +48,8 @@ errorExit=Utils.errorExit appArgs=AppArgs() -extraArgs = appArgs.add(flag="--trust-evm-contract-root", type=str, help="TrustEVM contract build dir", default=None) +appArgs.add(flag="--trust-evm-contract-root", type=str, help="TrustEVM contract build dir", default=None) +appArgs.add(flag="--genesis-json", type=str, help="File to save generated genesis json", default="trust-evm-genesis.json") args=TestHelper.parse_args({"--keep-logs","--dump-error-details","-v","--leave-running","--clean-run" }, applicationSpecificArgs=appArgs) debug=args.v @@ -51,6 +58,7 @@ keepLogs=args.keep_logs killAll=args.clean_run trustEvmContractRoot=args.trust_evm_contract_root +gensisJson=args.genesis_json assert trustEvmContractRoot is not None, "--trust-evm-contract-root is required" @@ -123,17 +131,18 @@ def generate_evm_transactions(nonce): prodNode = cluster.getNode(0) nonProdNode = cluster.getNode(1) - accounts=cluster.createAccountKeys(1) + accounts=cluster.createAccountKeys(2) if accounts is None: Utils.errorExit("FAILURE - create keys") evmAcc = accounts[0] evmAcc.name = "evmevmevmevm" + testAcc = accounts[1] testWalletName="test" Print("Creating wallet \"%s\"." % (testWalletName)) - testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0]]) + testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0],accounts[1]]) # create accounts via eosio as otherwise a bid is needed for account in accounts: @@ -157,18 +166,51 @@ def generate_evm_transactions(nonce): Utils.Print("Block Id: ", block["id"]) Utils.Print("Block timestamp: ", block["timestamp"]) - Utils.Print("Set balance") - trans = prodNode.pushMessage(evmAcc.name, "setbal", '{"addy":"2787b98fc4e731d0456b3941f0b3fe2e01439961", "bal":"0000000000000000000000000000000000100000000000000000000000000000"}', '-p evmevmevmevm') + genesis_info = { + "alloc": {}, + "coinbase": "0x0000000000000000000000000000000000000000", + "config": { + "chainId": 15555, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "trust": {} + }, + "difficulty": "0x01", + "extraData": "TrustEVM", + "gasLimit": "0x7ffffffffff", + "mixHash": "0x"+block["id"], + "nonce": hex(1000), + "timestamp": hex(int(calendar.timegm(datetime.strptime(block["timestamp"].split(".")[0], '%Y-%m-%dT%H:%M:%S').timetuple()))) + } + + # add eosio.code permission + cmd="set account permission evmevmevmevm active --add-code -p evmevmevmevm@active" + prodNode.processCleosCmd(cmd, cmd, silentErrors=True, returnType=ReturnType.raw) + Utils.Print("Set balance") + addys = { + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":"0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75,0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + } + + 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"} + if not (i+1) % 20: time.sleep(1) prodNode.waitForTransBlockIfNeeded(trans[1], True) Utils.Print("Send balance") evmChainId = 15555 - fromAdd = "2787b98fc4e731d0456b3941f0b3fe2e01439961" + fromAdd = "f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" toAdd = '0x9edf022004846bc987799d552d1b8485b317b7ed' amount = 100 nonce = 0 - evmSendKey = "a3f1b69da92a0233ce29485d3049a4ace39e8d384bbc2557e3fc60940ce4e954" + evmSendKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, @@ -251,10 +293,60 @@ def generate_evm_transactions(nonce): # } retValue = prodNode.pushMessage(evmAcc.name, "updatecode", '{"address":"2787b98fc4e731d0456b3941f0b3fe2e01430000","incarnation":0,"code_hash":"286e3d498e2178afc91275f11d446e62a0d85b060ce66d6ca75f6ec9d874d560","code":"608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033"}', '-p evmevmevmevm') - testSuccessful=True - generate_evm_transactions(nonce) + if gensisJson[0] != '/': gensisJson = os.path.realpath(gensisJson) + f=open(gensisJson,"w") + f.write(json.dumps(genesis_info)) + f.close() + Utils.Print("#####################################################") + Utils.Print("Generated EVM json genesis file in: %s" % gensisJson) + Utils.Print("") + Utils.Print("You can now run:") + Utils.Print(" trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=/tmp --verbosity=4" % gensisJson) + Utils.Print(" trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=/tmp --api-spec=eth,debug,net,trace") + Utils.Print("") + + # + # Test EOS/EVM Bridge + # + Utils.Print("Test EOS/EVM Bridge") + + expectedAmount="60000000.0000 {0}".format(CORE_SYMBOL) + evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tAccount balances: evm %s, test %s", (evmAccAcutalAmount,testAccActualAmount)) + if expectedAmount != evmAccAcutalAmount or expectedAmount != testAccActualAmount: + Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccAcutalAmount, testAccActualAmount)) + + # set ingress bridge fee + contract="currency1111" + action="create" + data="[\"0.0100 EOS\"]" + opts="--permission evmevmevmevm@active" + trans=prodNode.pushMessage("evmevmevmevm", "setingressfee", data, opts) + + rows=prodNode.getTable(evmAcc.name, evmAcc.name, "balances") + Utils.Print("\tBefore transfer table rows:", rows) + + transferAmount="97.5321 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) + prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0xF0cE7BaB13C99bA0565f426508a7CD8f4C247E5a", waitForTransBlock=True) + + row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) + Utils.Print("\tAfter transfer table row:", row0) + assert(row0["balance"]["balance"] == "0.0100 {0}".format(CORE_SYMBOL)) # should have fee at end of transaction + + evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + expectedAmount="60000097.5321 {0}".format(CORE_SYMBOL) + if expectedAmount != evmAccAcutalAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) + + row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + assert(row4["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) + + testSuccessful=True finally: TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, killEosInstances=killEosInstances, killWallet=killEosInstances, keepLogs=keepLogs, cleanRun=killAll, dumpErrorDetails=dumpErrorDetails) From 5052a147ccc33ed825d78543898a6ca118fc33e0 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 27 Feb 2023 14:06:57 -0600 Subject: [PATCH 10/70] Add launching of trustevm-node and trustevm-rpc --- tests/leap/nodeos_trust_evm_test.py | 69 ++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 259cb964..2999d4ef 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -6,6 +6,7 @@ import time import sys import signal +import shutil import calendar from datetime import datetime @@ -37,7 +38,7 @@ # Example: # cd ~/ext/leap/build # edit core_symbol.py to be EOS -# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --leave-running +# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/cmake-build-release-gcc --leave-running # # Launches wallet at port: 9899 # Example: bin/cleos --wallet-url http://127.0.0.1:9899 ... @@ -49,6 +50,7 @@ appArgs=AppArgs() appArgs.add(flag="--trust-evm-contract-root", type=str, help="TrustEVM contract build dir", default=None) +appArgs.add(flag="--trust-evm-build-root", type=str, help="TrustEVM build dir", default=None) appArgs.add(flag="--genesis-json", type=str, help="File to save generated genesis json", default="trust-evm-genesis.json") args=TestHelper.parse_args({"--keep-logs","--dump-error-details","-v","--leave-running","--clean-run" }, applicationSpecificArgs=appArgs) @@ -58,9 +60,11 @@ keepLogs=args.keep_logs killAll=args.clean_run trustEvmContractRoot=args.trust_evm_contract_root +trustEvmBuildRoot=args.trust_evm_build_root gensisJson=args.genesis_json assert trustEvmContractRoot is not None, "--trust-evm-contract-root is required" +assert trustEvmBuildRoot is not None, "--trust-evm-build-root is required" seed=1 @@ -299,12 +303,13 @@ def generate_evm_transactions(nonce): f=open(gensisJson,"w") f.write(json.dumps(genesis_info)) f.close() + Utils.Print("#####################################################") Utils.Print("Generated EVM json genesis file in: %s" % gensisJson) Utils.Print("") Utils.Print("You can now run:") - Utils.Print(" trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=/tmp --verbosity=4" % gensisJson) - Utils.Print(" trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=/tmp --api-spec=eth,debug,net,trace") + Utils.Print(" trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=/tmp/data --verbosity=5" % gensisJson) + Utils.Print(" trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=/tmp/data --api-spec=eth,debug,net,trace") Utils.Print("") # @@ -312,6 +317,7 @@ def generate_evm_transactions(nonce): # Utils.Print("Test EOS/EVM Bridge") + # Verify starting values expectedAmount="60000000.0000 {0}".format(CORE_SYMBOL) evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) @@ -320,8 +326,6 @@ def generate_evm_transactions(nonce): Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccAcutalAmount, testAccActualAmount)) # set ingress bridge fee - contract="currency1111" - action="create" data="[\"0.0100 EOS\"]" opts="--permission evmevmevmevm@active" trans=prodNode.pushMessage("evmevmevmevm", "setingressfee", data, opts) @@ -329,6 +333,7 @@ def generate_evm_transactions(nonce): rows=prodNode.getTable(evmAcc.name, evmAcc.name, "balances") Utils.Print("\tBefore transfer table rows:", rows) + # EOS -> EVM transferAmount="97.5321 {0}".format(CORE_SYMBOL) Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0xF0cE7BaB13C99bA0565f426508a7CD8f4C247E5a", waitForTransBlock=True) @@ -346,8 +351,60 @@ def generate_evm_transactions(nonce): row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test assert(row4["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) + # EOS -> EVM to the same address + transferAmount="10.0000 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) + prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0xF0cE7BaB13C99bA0565f426508a7CD8f4C247E5a", waitForTransBlock=True) + + row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) + Utils.Print("\tAfter transfer table row:", row0) + assert(row0["balance"]["balance"] == "0.0200 {0}".format(CORE_SYMBOL)) # should have fee from both transfers + + evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + expectedAmount="60000107.5321 {0}".format(CORE_SYMBOL) + if expectedAmount != evmAccAcutalAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) - testSuccessful=True + row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + assert(row4["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5D407B55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) + + # Launch trustevm-node & trustevm-rpc + dataDir = Utils.DataDir + "/eos_evm" + nodeStdOutDir = dataDir + "/trustevm-node.stdout" + nodeStdErrDir = dataDir + "/trustevm-node.stderr" + nodeRPCStdOutDir = dataDir + "/trustevm-rpc.stdout" + nodeRPCStdErrDir = dataDir + "/trustevm-rpc.stderr" + shutil.rmtree(dataDir, ignore_errors=True) + os.makedirs(dataDir) + outFile = open(nodeStdOutDir, "w") + errFile = open(nodeStdErrDir, "w") + outRPCFile = open(nodeRPCStdOutDir, "w") + errRPCFile = open(nodeRPCStdErrDir, "w") + cmd = "%s/cmd/trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=%s --verbosity=5 --nocolor=1" % (trustEvmBuildRoot, gensisJson, dataDir) + Utils.Print("Launching: %s", cmd) + popen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) + + cmdRPC = "%s/cmd/trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=%s --api-spec=eth,debug,net,trace" % (trustEvmBuildRoot, dataDir) + Utils.Print("Launching: %s", cmdRPC) + popenRPC=Utils.delayedCheckOutput(cmdRPC, stdout=outRPCFile, stderr=errRPCFile) + + time.sleep(5) # allow time to sync trxs + + Utils.Print("\n") + foundErr = False + stdErrFile = open(nodeStdErrDir, "r") + lines = stdErrFile.readlines() + for line in lines: + if line.find("ERROR") != -1 or line.find("CRIT") != -1: + Utils.Print(" Found ERROR in trustevm log: ", line) + foundErr = True + + if killEosInstances: + popenRPC.kill() + popen.kill() + + testSuccessful= not foundErr finally: TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, killEosInstances=killEosInstances, killWallet=killEosInstances, keepLogs=keepLogs, cleanRun=killAll, dumpErrorDetails=dumpErrorDetails) From 0fb67ec1b97a84f952584be8f98154367a0719ba Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Tue, 28 Feb 2023 21:10:00 -0600 Subject: [PATCH 11/70] Add EVM -> EOS transfer --- tests/leap/nodeos_trust_evm_test.py | 88 ++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 2999d4ef..2d116837 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -9,6 +9,7 @@ import shutil import calendar from datetime import datetime +from ctypes import c_uint8 import sys from binascii import unhexlify @@ -66,7 +67,7 @@ assert trustEvmContractRoot is not None, "--trust-evm-contract-root is required" assert trustEvmBuildRoot is not None, "--trust-evm-build-root is required" - +szabo = 1000000000000 seed=1 Utils.Debug=debug testSuccessful=False @@ -104,6 +105,41 @@ def generate_evm_transactions(nonce): time.sleep(1) +def makeReservedEvmAddress(account): + bytearr = [0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + c_uint8(account >> 56).value, + c_uint8(account >> 48).value, + c_uint8(account >> 40).value, + c_uint8(account >> 32).value, + c_uint8(account >> 24).value, + c_uint8(account >> 16).value, + c_uint8(account >> 8).value, + c_uint8(account >> 0).value] + return "0x" + bytes(bytearr).hex() + +def charToSymbol(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 + return 0 + +def nameStrToInt(s: str): + n = 0 + for i, c in enumerate(s): + if i >= 12: + break + n |= (charToSymbol(c) & 0x1f) << (64 - (5 * (i + 1))) + i = i + 1 + + assert i > 0 + if i < len(s) and i == 12: + n |= charToSymbol(s[12]) & 0x0F + return n + try: TestHelper.printSystemInfo("BEGIN") @@ -349,6 +385,7 @@ def generate_evm_transactions(nonce): Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") assert(row4["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) # EOS -> EVM to the same address @@ -367,7 +404,54 @@ def generate_evm_transactions(nonce): Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test - assert(row4["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5D407B55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) + assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") + assert(row4["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5d407b55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) + + # EOS -> EVM to diff address + transferAmount="42.4242 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) + prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0x9E126C57330FA71556628e0aabd6B6B6783d99fA", waitForTransBlock=True) + + row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) + Utils.Print("\tAfter transfer table row:", row0) + assert(row0["balance"]["balance"] == "0.0300 {0}".format(CORE_SYMBOL)) # should have fee from all three transfers + + evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + expectedAmount="60000149.9563 {0}".format(CORE_SYMBOL) + if expectedAmount != evmAccAcutalAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) + + row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test + assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row5["balance"] == "0000000000000000000000000000000000000000000000024c9d822e105f8000") # 0x24c9d822e105f8000 => 42414200000000000000 (42.4242 - 0.0100) + + # EVM -> EOS + # 0x9E126C57330FA71556628e0aabd6B6B6783d99fA private key: 0xba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0 + toAdd = makeReservedEvmAddress(nameStrToInt(testAcc.name)) + evmSendKey = "ba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0" + amount=13.1313 # 13.131313 + nonce = 0 + signed_trx = w3.eth.account.sign_transaction(dict( + nonce=nonce, + # maxFeePerGas=150000000000, #150 GWei + gas=100000, #100k Gas + gasPrice=1, + to=Web3.toChecksumAddress(toAdd), + value=int(amount*10000*szabo*100), + data=b'', + chainId=evmChainId + ), evmSendKey) + + actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + Utils.Print("Send EVM->EOS") + trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + prodNode.waitForTransBlockIfNeeded(trans[1], True) + + row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test + Utils.Print("row5: ", row5) + assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row5["balance"] == "0000000000000000000000000000000000000000000000019661c2670d48edf8") # 0x19661c2670d48edf8 => 29282899999999979000 (42.4242 - 0.0100 - 13.131313 - gas 21000wei) # Launch trustevm-node & trustevm-rpc dataDir = Utils.DataDir + "/eos_evm" From 701f91938c15d2651c0096075e887bc9d66c4e8f Mon Sep 17 00:00:00 2001 From: yarkin Date: Thu, 2 Mar 2023 02:29:10 +0800 Subject: [PATCH 12/70] Switch to use silkworm::kEmptyHash. --- contract/include/evm_runtime/tables.hpp | 17 +++-------------- contract/src/state.cpp | 8 +++----- contract/tests/evm_runtime_tests.cpp | 10 +++------- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index d1e89e83..603125d6 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -3,12 +3,10 @@ #include #include #include - +#include namespace evm_runtime { using namespace eosio; -// c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 -inline constexpr char kEmptyHashBytes[32] = {-59, -46, 70, 1, -122, -9, 35, 60, -110, 126, 125, -78, -36, -57, 3, -64, -27, 0, -74, 83, -54, -126, 39, 59, 123, -6, -40, 4, 93,-123, -92, 112}; struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { uint64_t id; bytes eth_address; @@ -29,14 +27,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { } bytes32 get_code_hash()const { - bytes32 res; - if (code_hash.has_value()) { - std::copy(code_hash.value().begin(), code_hash.value().end(), res.bytes); - } - else { - std::copy(kEmptyHashBytes, &kEmptyHashBytes[32], res.bytes); - } - return res; + return code_hash ? to_bytes32(code_hash.value()) : silkworm::kEmptyHash; } EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code_hash)); @@ -58,9 +49,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] codestore { } 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(codestore, (id)(code)(code_hash)); diff --git a/contract/src/state.cpp b/contract/src/state.cpp index 7506418e..2cf8f73f 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -89,8 +89,6 @@ void state::update_account(const evmc::address& address, std::optional ++stats.account.read; if (current.has_value()) { - const bool zero_code = (std::memcmp(evmc::bytes_view(current->code_hash).data(), - kEmptyHashBytes, sizeof(kEmptyHashBytes)) == 0); if (itr == inx.end()) { accounts.emplace(_ram_payer, [&](auto& row){ @@ -98,13 +96,13 @@ 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 = zero_code ? std::nullopt : std::optional(to_bytes(current->code_hash)); + row.code_hash = current->code_hash == silkworm::kEmptyHash ? std::nullopt : std::optional(to_bytes(current->code_hash)); }); ++stats.account.create; } else { accounts.modify(*itr, eosio::same_payer, [&](auto& row){ row.nonce = current->nonce; - row.code_hash = zero_code ? std::nullopt : std::optional(to_bytes(current->code_hash)); + row.code_hash = current->code_hash == silkworm::kEmptyHash ? std::nullopt : std::optional(to_bytes(current->code_hash)); row.balance = to_bytes(current->balance); }); ++stats.account.update; @@ -200,7 +198,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_hash = to_bytes(silkworm::kEmptyHash); }); ++stats.account.read; } else { diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index e84f14db..4fe7c77b 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -231,10 +231,6 @@ struct block_info { }; //FC_REFLECT(block_info, (coinbase)(difficulty)(gasLimit)(number)(timestamp)(base_fee_per_gas)); FC_REFLECT(block_info, (coinbase)(difficulty)(gasLimit)(number)(timestamp)); -inline constexpr char kEmptyHashBytes[32] = {(char)-59, (char)-46, (char)70, (char)1, (char)-122, (char)-9, (char)35, (char)60, - (char)-110, (char)126, (char)125, (char)-78, (char)-36, (char)-57, (char)3, (char)-64, (char)-27, (char)0, - (char)-74, (char)83, (char)-54, (char)-126, (char)39, (char)59, (char)123, (char)-6, - (char)-40, (char)4, (char)93,(char)-123, (char)-92, (char)112}; struct account { uint64_t id; @@ -258,14 +254,14 @@ struct account { } evmc::bytes32 get_code_hash()const { - evmc::bytes32 res; if (code_hash.has_value()) { + evmc::bytes32 res; std::copy(code_hash.value().begin(), code_hash.value().end(), res.bytes); + return res; } else { - std::copy(kEmptyHashBytes, &kEmptyHashBytes[32], res.bytes); + return kEmptyHash; } - return res; } static name table_name() { return "account"_n; } From bd7a17099c69b6324a960e696e04497c1d4dc936 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 1 Mar 2023 15:05:32 -0600 Subject: [PATCH 13/70] Additional test case and misc cleanup --- tests/leap/nodeos_trust_evm_test.py | 108 ++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 2d116837..5bc32901 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -355,11 +355,11 @@ def nameStrToInt(s: str): # Verify starting values expectedAmount="60000000.0000 {0}".format(CORE_SYMBOL) - evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) - Utils.Print("\tAccount balances: evm %s, test %s", (evmAccAcutalAmount,testAccActualAmount)) - if expectedAmount != evmAccAcutalAmount or expectedAmount != testAccActualAmount: - Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccAcutalAmount, testAccActualAmount)) + Utils.Print("\tAccount balances: EVM %s, Test %s" % (evmAccActualAmount, testAccActualAmount)) + if expectedAmount != evmAccActualAmount or expectedAmount != testAccActualAmount: + Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccActualAmount, testAccActualAmount)) # set ingress bridge fee data="[\"0.0100 EOS\"]" @@ -377,13 +377,16 @@ def nameStrToInt(s: str): row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) assert(row0["balance"]["balance"] == "0.0100 {0}".format(CORE_SYMBOL)) # should have fee at end of transaction - - evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) - Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + testAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s" % testAccActualAmount) expectedAmount="60000097.5321 {0}".format(CORE_SYMBOL) - if expectedAmount != evmAccAcutalAmount: - Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) - + if expectedAmount != testAccActualAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) + expectedAmount="59999902.4679 {0}".format(CORE_SYMBOL) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tTest Account balance %s" % testAccActualAmount) + if testAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") assert(row4["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) @@ -392,17 +395,19 @@ def nameStrToInt(s: str): transferAmount="10.0000 {0}".format(CORE_SYMBOL) Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0xF0cE7BaB13C99bA0565f426508a7CD8f4C247E5a", waitForTransBlock=True) - row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) assert(row0["balance"]["balance"] == "0.0200 {0}".format(CORE_SYMBOL)) # should have fee from both transfers - - evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) - Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) expectedAmount="60000107.5321 {0}".format(CORE_SYMBOL) - if expectedAmount != evmAccAcutalAmount: - Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) - + if expectedAmount != evmAccActualAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccActualAmount)) + expectedAmount="59999892.4679 {0}".format(CORE_SYMBOL) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tTest Account balance %s" % testAccActualAmount) + if testAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") assert(row4["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5d407b55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) @@ -411,17 +416,19 @@ def nameStrToInt(s: str): transferAmount="42.4242 {0}".format(CORE_SYMBOL) Print("Transfer funds %s from account %s to %s" % (transferAmount, testAcc.name, evmAcc.name)) prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0x9E126C57330FA71556628e0aabd6B6B6783d99fA", waitForTransBlock=True) - row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) assert(row0["balance"]["balance"] == "0.0300 {0}".format(CORE_SYMBOL)) # should have fee from all three transfers - - evmAccAcutalAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) - Utils.Print("\tEVM Account balance %s", evmAccAcutalAmount) + evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) expectedAmount="60000149.9563 {0}".format(CORE_SYMBOL) - if expectedAmount != evmAccAcutalAmount: - Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccAcutalAmount)) - + if expectedAmount != evmAccActualAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, evmAccActualAmount)) + expectedAmount="59999850.0437 {0}".format(CORE_SYMBOL) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tTest Account balance %s" % testAccActualAmount) + if testAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") assert(row5["balance"] == "0000000000000000000000000000000000000000000000024c9d822e105f8000") # 0x24c9d822e105f8000 => 42414200000000000000 (42.4242 - 0.0100) @@ -430,7 +437,9 @@ def nameStrToInt(s: str): # 0x9E126C57330FA71556628e0aabd6B6B6783d99fA private key: 0xba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0 toAdd = makeReservedEvmAddress(nameStrToInt(testAcc.name)) evmSendKey = "ba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0" - amount=13.1313 # 13.131313 + amount=13.1313 + transferAmount="13.1313 {0}".format(CORE_SYMBOL) + Print("Transfer EVM->EOS funds %s from account %s to %s" % (transferAmount, evmAcc.name, testAcc.name)) nonce = 0 signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, @@ -442,16 +451,57 @@ def nameStrToInt(s: str): data=b'', chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - Utils.Print("Send EVM->EOS") trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) prodNode.waitForTransBlockIfNeeded(trans[1], True) - row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test - Utils.Print("row5: ", row5) + Utils.Print("\taccount row5: ", row5) assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") assert(row5["balance"] == "0000000000000000000000000000000000000000000000019661c2670d48edf8") # 0x19661c2670d48edf8 => 29282899999999979000 (42.4242 - 0.0100 - 13.131313 - gas 21000wei) + expectedAmount="60000136.8250 {0}".format(CORE_SYMBOL) + evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) + if evmAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) + expectedAmount="59999863.1750 {0}".format(CORE_SYMBOL) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tTest Account balance %s" % testAccActualAmount) + if testAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) + + # EVM->EOS from same address + amount=1.0000 + transferAmount="1.000 {0}".format(CORE_SYMBOL) + Print("Transfer EVM->EOS funds %s from account %s to %s" % (transferAmount, evmAcc.name, testAcc.name)) + nonce = nonce + 1 + signed_trx = w3.eth.account.sign_transaction(dict( + nonce=nonce, + # maxFeePerGas=150000000000, #150 GWei + gas=100000, #100k Gas + gasPrice=1, + to=Web3.toChecksumAddress(toAdd), + value=int(amount*10000*szabo*100), + data=b'', + chainId=evmChainId + ), evmSendKey) + actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + prodNode.waitForTransBlockIfNeeded(trans[1], True) + row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test + Utils.Print("\taccount row5: ", row5) + assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row5["balance"] == "00000000000000000000000000000000000000000000000188810bb365e49bf0") # 0x188810bb365e49bf0 => 28282899999999958000 (42.4242 - 0.0100 - 13.131313 - 1.0000 - 2*gas 21000wei) + expectedAmount="60000135.8250 {0}".format(CORE_SYMBOL) + evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) + Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) + if evmAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) + expectedAmount="59999864.1750 {0}".format(CORE_SYMBOL) + testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) + Utils.Print("\tTest Account balance %s" % testAccActualAmount) + if testAccActualAmount != expectedAmount: + Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) + # Launch trustevm-node & trustevm-rpc dataDir = Utils.DataDir + "/eos_evm" From ef69763ccfdbc7bf10969732ec4ebf6515167b7d Mon Sep 17 00:00:00 2001 From: yarkin Date: Thu, 2 Mar 2023 10:29:41 +0800 Subject: [PATCH 14/70] codestore to account_code --- contract/include/evm_runtime/tables.hpp | 10 +++++----- contract/src/state.cpp | 4 ++-- contract/tests/evm_runtime_tests.cpp | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 603125d6..c9708341 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -37,7 +37,7 @@ typedef multi_index< "account"_n, account, indexed_by<"by.address"_n, const_mem_fun> > account_table; -struct [[eosio::table]] [[eosio::contract("evm_contract")]] codestore { +struct [[eosio::table]] [[eosio::contract("evm_contract")]] account_code { uint64_t id; bytes code; bytes code_hash; @@ -52,12 +52,12 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] codestore { return to_bytes32(code_hash); } - EOSLIB_SERIALIZE(codestore, (id)(code)(code_hash)); + EOSLIB_SERIALIZE(account_code, (id)(code)(code_hash)); }; -typedef multi_index< "codestore"_n, codestore, - indexed_by<"by.codehash"_n, const_mem_fun> -> codestore_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; diff --git a/contract/src/state.cpp b/contract/src/state.cpp index 2cf8f73f..351028c4 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -30,7 +30,7 @@ ByteView state::read_code(const evmc::bytes32& code_hash) const noexcept { return ByteView{(const uint8_t*)code.data(), code.size()}; } - codestore_table codes(_self, _self.value); + account_code_table codes(_self, _self.value); auto inx = codes.get_index<"by.codehash"_n>(); auto itr = inx.find(make_key(code_hash)); @@ -140,7 +140,7 @@ bool state::gc(uint32_t max) { } void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { - codestore_table codes(_self, _self.value); + account_code_table codes(_self, _self.value); auto inxc = codes.get_index<"by.codehash"_n>(); auto itrc = inxc.find(make_key(code_hash)); if(itrc == inxc.end()) { diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 4fe7c77b..780d8ae8 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -292,7 +292,7 @@ struct account { }; FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_hash)); -struct codestore { +struct account_code { uint64_t id; bytes code; bytes code_hash; @@ -311,7 +311,7 @@ struct codestore { return res; } - static name table_name() { return "codestore"_n; } + 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; @@ -322,13 +322,13 @@ struct codestore { 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); + 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(codestore, (id)(code)(code_hash)); +FC_REFLECT(account_code, (id)(code)(code_hash)); struct storage { uint64_t id; @@ -586,7 +586,7 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { mutable bytes read_code_buffer; ByteView read_code(const evmc::bytes32& code_hash) const noexcept { auto& db = const_cast(control->db()); - auto accntcode = codestore::get_by_code_hash(db, code_hash); + 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{}; From 7d1924b64c3b5a1402893d00bba2bfed8195438e Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Thu, 2 Mar 2023 10:17:55 -0600 Subject: [PATCH 15/70] terminate rpc node since normal kill leaves it running --- tests/leap/nodeos_trust_evm_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 5bc32901..34a03f52 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -447,7 +447,7 @@ def nameStrToInt(s: str): gas=100000, #100k Gas gasPrice=1, to=Web3.toChecksumAddress(toAdd), - value=int(amount*10000*szabo*100), + value=int(amount*10000*szabo*100), # .0001 EOS is 100 szabos data=b'', chainId=evmChainId ), evmSendKey) @@ -535,7 +535,7 @@ def nameStrToInt(s: str): foundErr = True if killEosInstances: - popenRPC.kill() + popenRPC.terminate() popen.kill() testSuccessful= not foundErr From fa66af5df863d8f07a10fc86021b4c7937cf7f8c Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 6 Mar 2023 08:26:58 -0600 Subject: [PATCH 16/70] Update comment --- tests/leap/nodeos_trust_evm_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 34a03f52..dbabaea7 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -32,7 +32,7 @@ # Need to install: # web3 - pip install web3 # -# --turst-evm-root should point to the root of TrustEVM build dir +# --trust-evm-build-root should point to the root of TrustEVM build dir # --trust-evm-contract-root should point to root of TrustEVM contract build dir # contracts should be built with -DWITH_TEST_ACTIONS=On # From 7a67d2e949b4da38335922705a16fb07fa460592 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 6 Mar 2023 08:29:13 -0600 Subject: [PATCH 17/70] Update comment --- tests/leap/nodeos_trust_evm_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index dbabaea7..7caa52bb 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -38,7 +38,7 @@ # # Example: # cd ~/ext/leap/build -# edit core_symbol.py to be EOS +# edit tests/core_symbol.py to be EOS # ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/cmake-build-release-gcc --leave-running # # Launches wallet at port: 9899 From c02169a3c9eead6aae2ba564ff01aeff0b24e3de Mon Sep 17 00:00:00 2001 From: yarkin Date: Mon, 6 Mar 2023 23:04:00 +0800 Subject: [PATCH 18/70] Add ref_count support. --- contract/include/evm_runtime/tables.hpp | 14 +++++++- contract/src/state.cpp | 42 +++++++++++++++++++++++- contract/tests/evm_runtime_tests.cpp | 43 ++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index c9708341..26ea2ca5 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -39,6 +39,7 @@ typedef multi_index< "account"_n, account, struct [[eosio::table]] [[eosio::contract("evm_contract")]] account_code { uint64_t id; + uint64_t ref_count; bytes code; bytes code_hash; @@ -52,7 +53,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account_code { return to_bytes32(code_hash); } - EOSLIB_SERIALIZE(account_code, (id)(code)(code_hash)); + EOSLIB_SERIALIZE(account_code, (id)(ref_count)(code)(code_hash)); }; typedef multi_index< "accountcode"_n, account_code, @@ -88,4 +89,15 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] gcstore { typedef multi_index< "gcstore"_n, gcstore> gc_store_table; +struct [[eosio::table]] [[eosio::contract("evm_contract")]] gc_code { + uint64_t id; + uint64_t code_id; + + uint64_t primary_key()const { return id; } + + EOSLIB_SERIALIZE(gc_code, (id)(code_id)); +}; + +typedef multi_index< "gccode"_n, gc_code> gc_code_table; + } //namespace evm_runtime \ No newline at end of file diff --git a/contract/src/state.cpp b/contract/src/state.cpp index 351028c4..8cfb3c2b 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -116,6 +116,28 @@ void state::update_account(const evmc::address& address, std::optional row.id = gc.available_primary_key(); row.storage_id = itr->id; }); + // Decrease ref_count for code + if (itr->code_hash ) { + account_code_table codes(_self, _self.value); + auto inxc = codes.get_index<"by.codehash"_n>(); + auto itrc = inxc.find(make_key(itr->code_hash.value())); + if(itrc == inxc.end()) { + // SHOULD NOT REACH HERE! + // But we ignore this for robustness. + } else { + if (itrc->ref_count <= 1) { + gc_code_table gccode(_self, _self.value); + gccode.emplace(_ram_payer, [&](auto& row){ + row.id = gccode.available_primary_key(); + row.code_id = itrc->id; + }); + } + codes.modify(*itrc, eosio::same_payer, [&](auto& row){ + if (row.ref_count > 0) + row.ref_count --; + }); + } + } accounts.erase(*itr); ++stats.account.remove; } @@ -136,7 +158,21 @@ bool state::gc(uint32_t max) { i = gc.erase(i); --max; } - return gc.begin() == gc.end(); + + gc_code_table gccode(_self, _self.value); + auto j = gccode.begin(); + while( max && j != gccode.end() ) { + account_code_table codes(_self, _self.value); + auto citr = codes.find(j->code_id); + if ( max && citr != codes.end() && citr->ref_count == 0) { + citr = codes.erase(citr); + --max; + } + if( !max ) break; + j = gccode.erase(j); + --max; + } + return gc.begin() == gc.end() && gccode.begin() == gccode.end(); } void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { @@ -148,9 +184,13 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev row.id = codes.available_primary_key(); 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 ++; + }); } account_table accounts(_self, _self.value); diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 780d8ae8..4b497e44 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -294,6 +294,7 @@ FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_hash)); struct account_code { uint64_t id; + uint64_t ref_count; bytes code; bytes code_hash; @@ -328,7 +329,7 @@ struct account_code { } }; -FC_REFLECT(account_code, (id)(code)(code_hash)); +FC_REFLECT(account_code, (id)(ref_count)(code)(code_hash)); struct storage { uint64_t id; @@ -388,6 +389,22 @@ struct gcstore { }; FC_REFLECT(gcstore, (id)(storage_id)); +struct gc_code { + uint64_t id; + uint64_t code_id; + + static name table_name() { return "gccode"_n; } + static name index_name(const name& n) { + BOOST_REQUIRE(false); + return name{0}; + } + + static name index_name(uint64_t n) { + return index_name(name{n}); + } +}; +FC_REFLECT(gc_code, (id)(code_id)); + struct assert_message_check { string _expected; assert_message_check(const string& expected) { @@ -709,6 +726,25 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return count; } + size_t gc_code_size() { + auto& db = const_cast(control->db()); + + const auto* tid = db.find( + boost::make_tuple("evm"_n, "evm"_n,"gccode"_n) + ); + + if(tid == nullptr) return 0; + + const auto& idx = db.get_index(); + auto itr = idx.lower_bound( boost::make_tuple( tid->id) ); + size_t count=0; + while ( itr != idx.end() && itr->t_id == tid->id ) { + ++itr; + ++count; + } + return count; + } + size_t state_storage_size(const evmc::address& address, uint64_t incarnation) { auto& db = const_cast(control->db()); auto accnt = account::get_by_address(db, address); @@ -968,6 +1004,11 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return false; } + if( gc_code_size() != 0 ) { + std::cout << "gc_code is not empty: " << gc_code_size() << std::endl; + return false; + } + return true; } From a1ff105594b5f36e4f74e43bde6530963770c24e Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Tue, 7 Mar 2023 02:38:03 -0300 Subject: [PATCH 19/70] update silkworm --- silkworm | 2 +- tests/leap/nodeos_trust_evm_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/silkworm b/silkworm index 9d7d3e96..e2092489 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit 9d7d3e969b1f4bfd23b72dc474d48d2b89e59ad5 +Subproject commit e2092489ed8aaefae520470dbb6c6720f9958ac7 diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 7caa52bb..96cd7b4a 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -362,7 +362,7 @@ def nameStrToInt(s: str): Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccActualAmount, testAccActualAmount)) # set ingress bridge fee - data="[\"0.0100 EOS\"]" + data="[\"0.0100 {0}\"]".format(CORE_SYMBOL) opts="--permission evmevmevmevm@active" trans=prodNode.pushMessage("evmevmevmevm", "setingressfee", data, opts) From b3d1c24b373281f106795215f070655eeeda6205 Mon Sep 17 00:00:00 2001 From: kayan Date: Wed, 8 Mar 2023 14:43:07 +0800 Subject: [PATCH 20/70] fix zero representation of r & s, required by blockscout --- silkrpc/json/types.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/silkrpc/json/types.cpp b/silkrpc/json/types.cpp index 8407bf45..c5e95b8e 100644 --- a/silkrpc/json/types.cpp +++ b/silkrpc/json/types.cpp @@ -65,10 +65,14 @@ std::string to_hex_no_leading_zeros(uint64_t number) { } std::string to_quantity(silkworm::ByteView bytes) { - return "0x" + to_hex_no_leading_zeros(bytes); + std::string r = "0x" + to_hex_no_leading_zeros(bytes); + return (r == "0x") ? "0x0" : r; } std::string to_quantity(uint64_t number) { + if (number == 0) { + return "0x0"; + } return "0x" + to_hex_no_leading_zeros(number); } From 22fb0b159db60c7edf8946e67f37570528daf217 Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Wed, 8 Mar 2023 03:53:13 -0300 Subject: [PATCH 21/70] - Create storage contract by sending a transaction so it can be applied on the node side also. - Validate that all account balances are the same on both sides (contract/node) --- tests/leap/nodeos_trust_evm_test.py | 59 ++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 96cd7b4a..daffaf5d 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -14,6 +14,7 @@ import sys from binascii import unhexlify from web3 import Web3 +import rlp sys.path.append(os.getcwd()) sys.path.append(os.path.join(os.getcwd(), "tests")) @@ -79,20 +80,19 @@ pnodes=1 total_nodes=pnodes + 2 -def generate_evm_transactions(nonce): +def interact_with_storage_contract(dest, nonce): for i in range(1, 5): # execute a few Utils.Print("Execute ETH contract") nonce += 1 - toAdd = "2787b98fc4e731d0456b3941f0b3fe2e01430000" amount = 0 signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=1, - to=Web3.toChecksumAddress(toAdd), + to=Web3.toChecksumAddress(dest), value=amount, - data=unhexlify("6057361d000000000000000000000000000000000000000000000000000000000000007b"), + data=unhexlify("6057361d00000000000000000000000000000000000000000000000000000000000000%02x" % nonce), chainId=evmChainId ), evmSendKey) @@ -104,6 +104,24 @@ def generate_evm_transactions(nonce): Utils.Print("\tTable row:", row0) time.sleep(1) + return nonce + +def normalize_address(x, allow_blank=False): + if allow_blank and x == '': + return '' + if len(x) in (42, 50) and x[:2] == '0x': + x = x[2:] + if len(x) in (40, 48): + x = unhexlify(x) + if len(x) == 24: + assert len(x) == 24 and sha3(x[:20])[:4] == x[-4:] + x = x[:20] + if len(x) != 20: + raise Exception("Invalid address format: %r" % x) + return x + +def makeContractAddress(sender, nonce): + return Web3.toHex(Web3.keccak(rlp.encode([normalize_address(sender), nonce]))[12:]) def makeReservedEvmAddress(account): bytearr = [0xff, 0xff, 0xff, 0xff, @@ -143,7 +161,7 @@ def nameStrToInt(s: str): try: TestHelper.printSystemInfo("BEGIN") - w3 = Web3() + w3 = Web3(Web3.HTTPProvider("http://localhost:8881")) cluster.setWalletMgr(walletMgr) @@ -331,9 +349,21 @@ def nameStrToInt(s: str): # return number; # } # } - retValue = prodNode.pushMessage(evmAcc.name, "updatecode", '{"address":"2787b98fc4e731d0456b3941f0b3fe2e01430000","incarnation":0,"code_hash":"286e3d498e2178afc91275f11d446e62a0d85b060ce66d6ca75f6ec9d874d560","code":"608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033"}', '-p evmevmevmevm') + nonce += 1 + evmChainId = 15555 + signed_trx = w3.eth.account.sign_transaction(dict( + nonce=nonce, + # maxFeePerGas=150000000000, #150 GWei + gas=1000000, #5M Gas + gasPrice=1, + data=Web3.toBytes(hexstr='608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea2646970667358fe12209ffe32fe5779018f7ee58886c856a4cfdf550f2df32cec944f57716a3abf4a5964736f6c63430008110033'), + chainId=evmChainId + ), evmSendKey) - generate_evm_transactions(nonce) + actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + assert retValue[0], f"push trx should have succeeded: {retValue}" + nonce = interact_with_storage_contract(makeContractAddress(fromAdd, nonce), nonce) if gensisJson[0] != '/': gensisJson = os.path.realpath(gensisJson) f=open(gensisJson,"w") @@ -504,7 +534,7 @@ def nameStrToInt(s: str): # Launch trustevm-node & trustevm-rpc - dataDir = Utils.DataDir + "/eos_evm" + dataDir = Utils.DataDir + "eos_evm" nodeStdOutDir = dataDir + "/trustevm-node.stdout" nodeStdErrDir = dataDir + "/trustevm-node.stderr" nodeRPCStdOutDir = dataDir + "/trustevm-rpc.stdout" @@ -515,16 +545,19 @@ def nameStrToInt(s: str): errFile = open(nodeStdErrDir, "w") outRPCFile = open(nodeRPCStdOutDir, "w") errRPCFile = open(nodeRPCStdErrDir, "w") - cmd = "%s/cmd/trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=%s --verbosity=5 --nocolor=1" % (trustEvmBuildRoot, gensisJson, dataDir) + cmd = "%s/cmd/trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=%s --verbosity=5 --nocolor=1 --plugin=rpc_plugin --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --api-spec=eth,debug,net,trace --chaindata=%s" % (trustEvmBuildRoot, gensisJson, dataDir, dataDir) Utils.Print("Launching: %s", cmd) popen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) - cmdRPC = "%s/cmd/trustevm-rpc --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --chaindata=%s --api-spec=eth,debug,net,trace" % (trustEvmBuildRoot, dataDir) - Utils.Print("Launching: %s", cmdRPC) - popenRPC=Utils.delayedCheckOutput(cmdRPC, stdout=outRPCFile, stderr=errRPCFile) - time.sleep(5) # allow time to sync trxs + # Validate all balances are the same on both sides + rows=prodNode.getTable(evmAcc.name, evmAcc.name, "account") + for row in rows['rows']: + Utils.Print("0x{0} balance".format(row['eth_address'])) + r = w3.eth.get_balance(Web3.toChecksumAddress('0x'+row['eth_address'])) + assert r == int(row['balance'],16), row['eth_address'] + Utils.Print("\n") foundErr = False stdErrFile = open(nodeStdErrDir, "r") From f387b07f66cf6cf8b311e8855f90d8e0e69922e6 Mon Sep 17 00:00:00 2001 From: kayan Date: Wed, 8 Mar 2023 16:57:48 +0800 Subject: [PATCH 22/70] fix to_hex_no_leading_zeros --- silkrpc/json/types.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/silkrpc/json/types.cpp b/silkrpc/json/types.cpp index c5e95b8e..57b45553 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()); @@ -65,8 +69,7 @@ std::string to_hex_no_leading_zeros(uint64_t number) { } std::string to_quantity(silkworm::ByteView bytes) { - std::string r = "0x" + to_hex_no_leading_zeros(bytes); - return (r == "0x") ? "0x0" : r; + return "0x" + to_hex_no_leading_zeros(bytes); } std::string to_quantity(uint64_t number) { From e04ca03311040483320cb9570817667c50ba37e7 Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Wed, 8 Mar 2023 11:00:10 -0300 Subject: [PATCH 23/70] remove trustevm-rpc references --- tests/leap/nodeos_trust_evm_test.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index daffaf5d..669a7678 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -533,18 +533,14 @@ def nameStrToInt(s: str): Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) - # Launch trustevm-node & trustevm-rpc + # Launch trustevm-node dataDir = Utils.DataDir + "eos_evm" nodeStdOutDir = dataDir + "/trustevm-node.stdout" nodeStdErrDir = dataDir + "/trustevm-node.stderr" - nodeRPCStdOutDir = dataDir + "/trustevm-rpc.stdout" - nodeRPCStdErrDir = dataDir + "/trustevm-rpc.stderr" shutil.rmtree(dataDir, ignore_errors=True) os.makedirs(dataDir) outFile = open(nodeStdOutDir, "w") errFile = open(nodeStdErrDir, "w") - outRPCFile = open(nodeRPCStdOutDir, "w") - errRPCFile = open(nodeRPCStdErrDir, "w") cmd = "%s/cmd/trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=%s --verbosity=5 --nocolor=1 --plugin=rpc_plugin --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --api-spec=eth,debug,net,trace --chaindata=%s" % (trustEvmBuildRoot, gensisJson, dataDir, dataDir) Utils.Print("Launching: %s", cmd) popen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) @@ -554,11 +550,10 @@ def nameStrToInt(s: str): # Validate all balances are the same on both sides rows=prodNode.getTable(evmAcc.name, evmAcc.name, "account") for row in rows['rows']: - Utils.Print("0x{0} balance".format(row['eth_address'])) + Utils.Print("Checking 0x{0} balance".format(row['eth_address'])) r = w3.eth.get_balance(Web3.toChecksumAddress('0x'+row['eth_address'])) assert r == int(row['balance'],16), row['eth_address'] - Utils.Print("\n") foundErr = False stdErrFile = open(nodeStdErrDir, "r") lines = stdErrFile.readlines() @@ -568,7 +563,6 @@ def nameStrToInt(s: str): foundErr = True if killEosInstances: - popenRPC.terminate() popen.kill() testSuccessful= not foundErr From ccbb26049b4eb492b0e8e54495e1f3a7dfe79e3a Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Thu, 9 Mar 2023 13:39:54 -0500 Subject: [PATCH 24/70] discrete unit tests for balance_and_dust --- contract/include/evm_runtime/evm_contract.hpp | 1 + contract/include/evm_runtime/tables.hpp | 37 +++-- contract/include/evm_runtime/types.hpp | 2 +- contract/src/actions.cpp | 142 ++++++++++++++++++ contract/tests/evm_runtime_tests.cpp | 27 ++++ 5 files changed, 195 insertions(+), 14 deletions(-) diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index cd743c5f..99c9d866 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -58,6 +58,7 @@ CONTRACT evm_contract : public contract { ACTION clearall(); ACTION dumpall(); ACTION setbal(const bytes& addy, const bytes& bal); + ACTION testbaldust(const name test); #endif private: struct [[eosio::table]] [[eosio::contract("evm_contract")]] config { diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index b6c86165..b295b14a 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -89,28 +89,40 @@ struct balance_with_dust { } balance_with_dust& operator+=(const intx::uint256& amount) { - //asset::max_amount is conservative at 2^62-1, this means two 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 - check(amount/min_asset_bn <= asset::max_amount, "accumulation overflow"); + const intx::div_result div_result = udivrem(amount, minimum_natively_representable); - const int64_t base_amount = (amount/min_asset_bn)[0]; - check(balance.amount + base_amount < asset::max_amount, "accumulation overflow"); + //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 += (amount%min_asset_bn)[0]; + dust += div_result.rem[0]; - if(dust > min_asset) { + 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) { - check(amount/min_asset_bn <= balance.amount, "decrementing more than available"); - balance.amount -= (amount/min_asset_bn)[0]; - dust -= (amount%min_asset_bn)[0]; + 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--; @@ -121,8 +133,7 @@ struct balance_with_dust { return *this; } - static constexpr intx::uint256 min_asset_bn = intx::exp(10_u256, intx::uint256(evm_precision - token_symbol.precision())); - static constexpr uint64_t min_asset = min_asset_bn[0]; + static constexpr uint64_t min_asset = minimum_natively_representable[0]; EOSLIB_SERIALIZE(balance_with_dust, (balance)(dust)); }; diff --git a/contract/include/evm_runtime/types.hpp b/contract/include/evm_runtime/types.hpp index 2f1b4515..903f1e18 100644 --- a/contract/include/evm_runtime/types.hpp +++ b/contract/include/evm_runtime/types.hpp @@ -15,8 +15,8 @@ namespace evm_runtime { constexpr unsigned evm_precision = 18; constexpr eosio::name token_account(eosio::name(TOKEN_ACCOUNT_NAME)); constexpr eosio::symbol token_symbol("EOS", 4u); - static_assert(token_symbol.precision() <= evm_precision); 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; diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index da2c74cb..e0d5397e 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -467,6 +467,148 @@ ACTION evm_contract::setbal(const bytes& addy, const bytes& bal) { }); } } + +ACTION 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/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 123f205a..91fd310c 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -557,6 +557,12 @@ 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()); @@ -1083,4 +1089,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() From 5779f6c26e5228ef57031ee4cec584aca64f9bcf Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Thu, 9 Mar 2023 14:56:19 -0500 Subject: [PATCH 25/70] remove ram_payer from open action --- contract/include/evm_runtime/evm_contract.hpp | 2 +- contract/src/actions.cpp | 10 +++++----- contract/tests/native_token_tests.cpp | 16 ++++------------ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index 99c9d866..d861f61b 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -34,7 +34,7 @@ CONTRACT evm_contract : public contract { void pushtx(eosio::name ram_payer, const bytes& rlptx); [[eosio::action]] - void open(eosio::name owner, eosio::name ram_payer); + void open(eosio::name owner); [[eosio::action]] void close(eosio::name owner); diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index e0d5397e..68a668e0 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -48,7 +48,7 @@ void evm_contract::init(const uint64_t chainid) { inevm_singleton(get_self(), get_self().value).get_or_create(get_self()); - open(get_self(), get_self()); + open(get_self()); } void evm_contract::setingressfee(asset ingress_bridge_fee) { @@ -155,20 +155,20 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { push_trx( ram_payer, block, rlptx, engine, *found_chain_config->second ); } -void evm_contract::open(eosio::name owner, eosio::name ram_payer) { +void evm_contract::open(eosio::name owner) { assert_inited(); - require_auth(ram_payer); + require_auth(owner); check(is_account(owner), "owner account does not exist"); balances balance_table(get_self(), get_self().value); if(balance_table.find(owner.value) == balance_table.end()) - balance_table.emplace(ram_payer, [&](balance& a) { + balance_table.emplace(owner, [&](balance& a) { a.owner = owner; }); nextnonces nextnonce_table(get_self(), get_self().value); if(nextnonce_table.find(owner.value) == nextnonce_table.end()) - nextnonce_table.emplace(ram_payer, [&](nextnonce& a) { + nextnonce_table.emplace(owner, [&](nextnonce& a) { a.owner = owner; }); } diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 6d5e2347..38f33bef 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -46,8 +46,8 @@ struct native_token_evm_tester : basic_evm_tester { return std::get<1>(vault_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 open(name owner) { + return push_action("evm"_n, "open"_n, owner, mvo()("owner", owner)); } transaction_trace_ptr close(name owner) { return push_action("evm"_n, "close"_n, owner, mvo()("owner", owner)); @@ -87,7 +87,7 @@ 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 { @@ -189,18 +189,10 @@ BOOST_FIXTURE_TEST_CASE(weird_names, native_token_evm_tester_EOS) try { } 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")); - -} 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("received unexpected token")); From 7febfbddad2859b099b9342e797ad6e766d55c17 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:39:21 -0500 Subject: [PATCH 26/70] no longer need to check if account existed on open() either --- contract/src/actions.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 35e07b54..7456fbbe 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -171,7 +171,6 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { void evm_contract::open(eosio::name owner) { assert_unfrozen(); require_auth(owner); - check(is_account(owner), "owner account does not exist"); balances balance_table(get_self(), get_self().value); if(balance_table.find(owner.value) == balance_table.end()) From fd55df74d31bf6c798d24a0e442cdbd528c1507c Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 10 Mar 2023 15:40:18 -0600 Subject: [PATCH 27/70] GH-349 tx_wrapper retrieve gas price from nodeos --- contract/include/evm_runtime/evm_contract.hpp | 3 +- peripherals/tx_wrapper/index.js | 26 ++- tests/leap/nodeos_trust_evm_test.py | 157 ++++++++++++++++-- 3 files changed, 165 insertions(+), 21 deletions(-) diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index 18496b19..3baf66a4 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -65,8 +65,9 @@ CONTRACT evm_contract : public contract { uint64_t chainid = 0; time_point_sec genesis_time; asset ingress_bridge_fee = asset(0, token_symbol); + uint64_t gas_price = 10000000000; - EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)); + EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)); }; eosio::singleton<"config"_n, config> _config{get_self(), get_self().value}; diff --git a/peripherals/tx_wrapper/index.js b/peripherals/tx_wrapper/index.js index e2e4f02d..bbfe001b 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"); @@ -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) { diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 669a7678..3027a668 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -11,6 +11,10 @@ from datetime import datetime from ctypes import c_uint8 +import urllib.request +import urllib.parse +import urllib.error + import sys from binascii import unhexlify from web3 import Web3 @@ -22,9 +26,9 @@ from TestHarness import Cluster, TestHelper, Utils, WalletMgr from TestHarness.TestHelper import AppArgs from TestHarness.testUtils import ReturnType +from TestHarness.testUtils import unhandledEnumType from core_symbol import CORE_SYMBOL - ############################################################### # nodeos_trust_evm_test # @@ -33,6 +37,17 @@ # Need to install: # web3 - pip install web3 # +# --use-tx-wrapper path_to_tx_wrapper +# if specified then uses tx_wrapper to get gas price. +# Requires tx_wrapper dependencies installed: nodejs, eosjs, ethereumjs-util +# sudo apt install nodejs +# sudo apt install npm +# npm install eosjs +# npm install ethereumjs-util +# npm install node-fetch +# npm install http-jsonrpc-server +# npm install dotenv +# npm install is-valid-hostname # --trust-evm-build-root should point to the root of TrustEVM build dir # --trust-evm-contract-root should point to root of TrustEVM contract build dir # contracts should be built with -DWITH_TEST_ACTIONS=On @@ -40,7 +55,7 @@ # Example: # cd ~/ext/leap/build # edit tests/core_symbol.py to be EOS -# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/cmake-build-release-gcc --leave-running +# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/build --use-tx-wrapper --leave-running # # Launches wallet at port: 9899 # Example: bin/cleos --wallet-url http://127.0.0.1:9899 ... @@ -54,6 +69,7 @@ appArgs.add(flag="--trust-evm-contract-root", type=str, help="TrustEVM contract build dir", default=None) appArgs.add(flag="--trust-evm-build-root", type=str, help="TrustEVM build dir", default=None) appArgs.add(flag="--genesis-json", type=str, help="File to save generated genesis json", default="trust-evm-genesis.json") +appArgs.add(flag="--use-tx-wrapper", type=str, help="tx_wrapper to use to send trx to nodeos", default=None) args=TestHelper.parse_args({"--keep-logs","--dump-error-details","-v","--leave-running","--clean-run" }, applicationSpecificArgs=appArgs) debug=args.v @@ -64,6 +80,7 @@ trustEvmContractRoot=args.trust_evm_contract_root trustEvmBuildRoot=args.trust_evm_build_root gensisJson=args.genesis_json +useTrxWrapper=args.use_tx_wrapper assert trustEvmContractRoot is not None, "--trust-evm-contract-root is required" assert trustEvmBuildRoot is not None, "--trust-evm-build-root is required" @@ -79,17 +96,19 @@ pnodes=1 total_nodes=pnodes + 2 +evmNodePOpen = None def interact_with_storage_contract(dest, nonce): for i in range(1, 5): # execute a few Utils.Print("Execute ETH contract") nonce += 1 amount = 0 + gasP=getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(dest), value=amount, data=unhexlify("6057361d00000000000000000000000000000000000000000000000000000000000000%02x" % nonce), @@ -106,6 +125,83 @@ def interact_with_storage_contract(dest, nonce): return nonce +def writeTxWrapperEnv(): + with open(".env", 'w') as envFile: + env = \ +f''' +EOS_RPC="http://127.0.0.1:8888" +EOS_KEY="{txWrapAcc.activePrivateKey}" +HOST="127.0.0.1" +PORT="18888" +EOS_EVM_ACCOUNT="evmevmevmevm" +EOS_SENDER="{txWrapAcc.name}" +''' + envFile.write(env) + +def processUrllibRequest(endpoint, payload={}, silentErrors=False, exitOnError=False, exitMsg=None, returnType=ReturnType.json): + cmd = f"{endpoint}" + req = urllib.request.Request(cmd, method="POST") + req.add_header('Content-Type', 'application/json') + req.add_header('Accept', 'application/json') + data = payload + data = json.dumps(data) + data = data.encode() + if Utils.Debug: Utils.Print("cmd: %s" % (cmd)) + rtn=None + start=time.perf_counter() + try: + response = urllib.request.urlopen(req, data=data) + if returnType==ReturnType.json: + rtn = {} + rtn["code"] = response.getcode() + rtn["payload"] = json.load(response) + elif returnType==ReturnType.raw: + rtn = response.read() + else: + unhandledEnumType(returnType) + + if Utils.Debug: + end=time.perf_counter() + Utils.Print("cmd Duration: %.3f sec" % (end-start)) + printReturn=json.dumps(rtn) if returnType==ReturnType.json else rtn + Utils.Print("cmd returned: %s" % (printReturn[:1024])) + except urllib.error.HTTPError as ex: + if not silentErrors: + end=time.perf_counter() + msg=ex.msg + errorMsg="Exception during \"%s\". %s. cmd Duration=%.3f sec." % (cmd, msg, end-start) + if exitOnError: + Utils.cmdError(errorMsg) + Utils.errorExit(errorMsg) + else: + Utils.Print("ERROR: %s" % (errorMsg)) + if returnType==ReturnType.json: + rtn = json.load(ex) + elif returnType==ReturnType.raw: + rtn = ex.read() + else: + unhandledEnumType(returnType) + else: + return None + + if exitMsg is not None: + exitMsg=": " + exitMsg + else: + exitMsg="" + if exitOnError and rtn is None: + Utils.cmdError("could not \"%s\" - %s" % (cmd,exitMsg)) + Utils.errorExit("Failed to \"%s\"" % (cmd)) + + return rtn + +def getGasPrice(): + if useTrxWrapper is None: + return 1 + else: + result = processUrllibRequest("http://127.0.0.1:18888", payload={"method":"eth_gasPrice","params":[],"id":1,"jsonrpc":"2.0"}) + Utils.Print("result: ", result) + return result["payload"]["result"] + def normalize_address(x, allow_blank=False): if allow_blank and x == '': return '' @@ -147,6 +243,7 @@ def charToSymbol(c: str): def nameStrToInt(s: str): n = 0 + i = 0 for i, c in enumerate(s): if i >= 12: break @@ -189,18 +286,19 @@ def nameStrToInt(s: str): prodNode = cluster.getNode(0) nonProdNode = cluster.getNode(1) - accounts=cluster.createAccountKeys(2) + accounts=cluster.createAccountKeys(3) if accounts is None: Utils.errorExit("FAILURE - create keys") evmAcc = accounts[0] evmAcc.name = "evmevmevmevm" testAcc = accounts[1] + txWrapAcc = accounts[2] testWalletName="test" Print("Creating wallet \"%s\"." % (testWalletName)) - testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0],accounts[1]]) + testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0],accounts[1],accounts[2]]) # create accounts via eosio as otherwise a bid is needed for account in accounts: @@ -250,6 +348,23 @@ def nameStrToInt(s: str): cmd="set account permission evmevmevmevm active --add-code -p evmevmevmevm@active" prodNode.processCleosCmd(cmd, cmd, silentErrors=True, returnType=ReturnType.raw) + # + # Setup tx_wrapper + # + txWrapPOpen = None + if useTrxWrapper is not None: + writeTxWrapperEnv() + dataDir = Utils.DataDir + "tx_wrap" + outDir = dataDir + "/tx_wrapper.stdout" + errDir = dataDir + "/tx_wrapper.stderr" + shutil.rmtree(dataDir, ignore_errors=True) + os.makedirs(dataDir) + outFile = open(outDir, "w") + errFile = open(errDir, "w") + cmd = "node %s/index.js" % (useTrxWrapper) + Utils.Print("Launching: %s" % cmd) + txWrapPOpen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) + Utils.Print("Set balance") addys = { "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":"0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75,0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" @@ -270,11 +385,12 @@ def nameStrToInt(s: str): nonce = 0 evmSendKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), value=amount, data=b'', @@ -298,11 +414,12 @@ def nameStrToInt(s: str): # correct nonce nonce += 1 + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), value=amount, data=b'', @@ -317,11 +434,12 @@ def nameStrToInt(s: str): # incorrect chainid nonce += 1 evmChainId = 8888 + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), value=amount, data=b'', @@ -351,11 +469,12 @@ def nameStrToInt(s: str): # } nonce += 1 evmChainId = 15555 + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=1000000, #5M Gas - gasPrice=1, + gasPrice=gasP, data=Web3.toBytes(hexstr='608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea2646970667358fe12209ffe32fe5779018f7ee58886c856a4cfdf550f2df32cec944f57716a3abf4a5964736f6c63430008110033'), chainId=evmChainId ), evmSendKey) @@ -471,11 +590,12 @@ def nameStrToInt(s: str): transferAmount="13.1313 {0}".format(CORE_SYMBOL) Print("Transfer EVM->EOS funds %s from account %s to %s" % (transferAmount, evmAcc.name, testAcc.name)) nonce = 0 + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), value=int(amount*10000*szabo*100), # .0001 EOS is 100 szabos data=b'', @@ -504,11 +624,12 @@ def nameStrToInt(s: str): transferAmount="1.000 {0}".format(CORE_SYMBOL) Print("Transfer EVM->EOS funds %s from account %s to %s" % (transferAmount, evmAcc.name, testAcc.name)) nonce = nonce + 1 + gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas - gasPrice=1, + gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), value=int(amount*10000*szabo*100), data=b'', @@ -542,8 +663,8 @@ def nameStrToInt(s: str): outFile = open(nodeStdOutDir, "w") errFile = open(nodeStdErrDir, "w") cmd = "%s/cmd/trustevm-node --plugin=blockchain_plugin --ship-endpoint=127.0.0.1:8999 --genesis-json=%s --chain-data=%s --verbosity=5 --nocolor=1 --plugin=rpc_plugin --trust-evm-node=127.0.0.1:8080 --http-port=0.0.0.0:8881 --api-spec=eth,debug,net,trace --chaindata=%s" % (trustEvmBuildRoot, gensisJson, dataDir, dataDir) - Utils.Print("Launching: %s", cmd) - popen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) + Utils.Print("Launching: %s" % cmd) + evmNodePOpen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) time.sleep(5) # allow time to sync trxs @@ -552,7 +673,7 @@ def nameStrToInt(s: str): for row in rows['rows']: Utils.Print("Checking 0x{0} balance".format(row['eth_address'])) r = w3.eth.get_balance(Web3.toChecksumAddress('0x'+row['eth_address'])) - assert r == int(row['balance'],16), row['eth_address'] + assert r == int(row['balance'],16), f"{row['eth_address']} {r} != {int(row['balance'],16)}" foundErr = False stdErrFile = open(nodeStdErrDir, "r") @@ -562,12 +683,14 @@ def nameStrToInt(s: str): Utils.Print(" Found ERROR in trustevm log: ", line) foundErr = True - if killEosInstances: - popen.kill() - testSuccessful= not foundErr finally: TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, killEosInstances=killEosInstances, killWallet=killEosInstances, keepLogs=keepLogs, cleanRun=killAll, dumpErrorDetails=dumpErrorDetails) + if killEosInstances: + if evmNodePOpen is not None: + evmNodePOpen.kill() + if txWrapPOpen is not None: + txWrapPOpen.kill() exitCode = 0 if testSuccessful else 1 exit(exitCode) From 8214319814c95eaf7e420029d7a4cffe242650d3 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 10 Mar 2023 15:50:29 -0600 Subject: [PATCH 28/70] GH-349 Update example cmd --- tests/leap/nodeos_trust_evm_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 3027a668..77e1064f 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -55,7 +55,7 @@ # Example: # cd ~/ext/leap/build # edit tests/core_symbol.py to be EOS -# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/build --use-tx-wrapper --leave-running +# ~/ext/TrustEVM/tests/leap/nodeos_trust_evm_test.py --trust-evm-contract-root ~/ext/TrustEVM/contract/build --trust-evm-build-root ~/ext/TrustEVM/build --use-tx-wrapper ~/ext/TrustEVM/peripherals/tx_wrapper --leave-running # # Launches wallet at port: 9899 # Example: bin/cleos --wallet-url http://127.0.0.1:9899 ... From 7299531a82c8a5b76d7b7c0508cabb7c1de60c33 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Sun, 12 Mar 2023 19:21:08 -0400 Subject: [PATCH 29/70] update tests for lack of open ram_payer --- contract/tests/native_token_tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index e950d7aa..130cc3f9 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -432,7 +432,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { BOOST_REQUIRE(evm_balance(evm1) == evm1_before); //alice will now open a balance - open("alice"_n, "alice"_n); + open("alice"_n); //and try again pushtx(txn); @@ -462,7 +462,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { 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, "bob"_n); + open("bob"_n); //and now it'll go through pushtx(txn); BOOST_REQUIRE_EQUAL(vault_balance_token("bob"_n), to_bridge); From 53c214e141273b9b71ddc45657d95490a0c9d53d Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Sun, 12 Mar 2023 23:45:55 -0400 Subject: [PATCH 30/70] disallow reserved 0 address usage --- contract/src/actions.cpp | 10 ++++++---- contract/tests/native_token_tests.cpp | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index d9b36d98..7ee037f7 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -148,7 +148,7 @@ void evm_contract::push_trx( eosio::name ram_payer, Block& block, const bytes& r 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) ?: get_self().value); //reserved-0 used for from-contract + 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"); @@ -188,10 +188,11 @@ void evm_contract::push_trx( eosio::name ram_payer, Block& block, const bytes& r for(const auto& reserved_object : ep.state().reserved_objects()) { const evmc::address& address = reserved_object.first; - const name egress_account(*extract_reserved_address(address) ?: get_self().value); //egress to reserved-0 goes to contract's bucket; TODO: needs more love w/ gas changes + 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; @@ -337,9 +338,10 @@ void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& .max_fee_per_gas = 0, .gas_limit = 21000, .to = to_evmc_address(*address_bytes), - .value = value + .value = value, + .r = 0u, // r == 0 is pseudo signature that resolves to reserved address range + .s = get_self().value }; - //txn's r == 0 && s == 0 which is a psuedo-signature from reserved address zero Bytes rlp; rlp::encode(rlp, txn); diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 130cc3f9..22285551 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -532,4 +532,22 @@ BOOST_FIXTURE_TEST_CASE(evm_eos_nonexistant, native_token_evm_tester_EOS) try { } } 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 + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 0, + .gas_limit = 21000, + .to = make_reserved_address(0u) + }; + 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() \ No newline at end of file From d1f86126f38f961a0df5644525f05e7860336581 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:01:16 -0400 Subject: [PATCH 31/70] minor fixups to check_freeze required from other prior changes --- contract/tests/init_tests.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contract/tests/init_tests.cpp b/contract/tests/init_tests.cpp index 3d2c49cc..c413dc6e 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -134,13 +134,14 @@ BOOST_FIXTURE_TEST_CASE(check_freeze, basic_evm_tester) try { 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 From 7e1e5a3f18e5cefa62286932591029c76e62dbfd Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Tue, 14 Mar 2023 17:36:57 -0400 Subject: [PATCH 32/70] bump up account RAM available during consensus tests --- contract/tests/evm_runtime_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index b0450657..a6273fd0 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -407,7 +407,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()); From c5387f5a43eba0e1851860f68dc3c073cf19b223 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Tue, 14 Mar 2023 21:33:45 -0400 Subject: [PATCH 33/70] remove unused (and newly incorrect) config status in BM tests --- contract/tests/mapping_tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contract/tests/mapping_tests.cpp b/contract/tests/mapping_tests.cpp index b121cc69..8edc5247 100644 --- a/contract/tests/mapping_tests.cpp +++ b/contract/tests/mapping_tests.cpp @@ -24,7 +24,7 @@ struct mapping_evm_tester : basic_evm_tester { unsigned_int version; uint64_t chainid; time_point_sec genesis_time; - uint32_t status; + //additional fields ignored in this test }; time_point_sec get_genesis_time() { @@ -35,7 +35,7 @@ struct mapping_evm_tester : basic_evm_tester { } }; -FC_REFLECT(mapping_evm_tester::config_table_row, (version)(chainid)(genesis_time)(status)) +FC_REFLECT(mapping_evm_tester::config_table_row, (version)(chainid)(genesis_time)) BOOST_AUTO_TEST_SUITE(mapping_evm_tests) From 987cc387aff983ae2a84482ff7f868edf48f8c4a Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Tue, 14 Mar 2023 22:52:45 -0400 Subject: [PATCH 34/70] change reserved address prefix to 0xbb... --- contract/tests/native_token_tests.cpp | 6 +++--- silkworm | 2 +- tests/leap/nodeos_trust_evm_test.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 22285551..33ebe13d 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -77,9 +77,9 @@ struct native_token_evm_tester : basic_evm_tester { }; evmc::address make_reserved_address(uint64_t account) const { - return evmc_address({0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, + 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), diff --git a/silkworm b/silkworm index f6a3f8db..0f19bcca 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit f6a3f8dba3ddc2a4e021ff570f44bb6b863359e3 +Subproject commit 0f19bcca56ed0cfe9b191d90fd5e0463e9b312e6 diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 77e1064f..57fec7ab 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -220,9 +220,9 @@ def makeContractAddress(sender, nonce): return Web3.toHex(Web3.keccak(rlp.encode([normalize_address(sender), nonce]))[12:]) def makeReservedEvmAddress(account): - bytearr = [0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, + bytearr = [0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, + 0xbb, 0xbb, 0xbb, 0xbb, c_uint8(account >> 56).value, c_uint8(account >> 48).value, c_uint8(account >> 40).value, From f4f7935ee2f6d78b4770129b98e210f830505e53 Mon Sep 17 00:00:00 2001 From: yarkin Date: Wed, 15 Mar 2023 10:58:47 +0800 Subject: [PATCH 35/70] use uint32_t instead of uint64_t for ref_count. --- contract/include/evm_runtime/tables.hpp | 2 +- contract/tests/evm_runtime_tests.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index 26ea2ca5..ea940133 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -39,7 +39,7 @@ typedef multi_index< "account"_n, account, struct [[eosio::table]] [[eosio::contract("evm_contract")]] account_code { uint64_t id; - uint64_t ref_count; + uint32_t ref_count; bytes code; bytes code_hash; diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index bc726411..12bf8cd0 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -294,7 +294,7 @@ FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_hash)); struct account_code { uint64_t id; - uint64_t ref_count; + uint32_t ref_count; bytes code; bytes code_hash; From 595ebd8186cb7e9c42abd342ae3d5103a5d25a18 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Tue, 14 Mar 2023 23:02:44 -0400 Subject: [PATCH 36/70] add -no-missing-ricardian-clause to contract build --- contract/src/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contract/src/CMakeLists.txt b/contract/src/CMakeLists.txt index 2675e535..e1f2c149 100644 --- a/contract/src/CMakeLists.txt +++ b/contract/src/CMakeLists.txt @@ -82,6 +82,8 @@ 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() From ddf14363b0d618d5da911ab35ce1548b5f73f6e1 Mon Sep 17 00:00:00 2001 From: yarkin Date: Wed, 15 Mar 2023 16:55:17 +0800 Subject: [PATCH 37/70] Store code_id instead of code_hash in account table. Fix minor issues in test. --- contract/include/evm_runtime/tables.hpp | 8 +-- contract/src/actions.cpp | 2 +- contract/src/state.cpp | 40 +++++++++++---- contract/tests/evm_runtime_tests.cpp | 67 ++++++++++++++++--------- 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index ea940133..b031b6f3 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -12,7 +12,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { bytes eth_address; uint64_t nonce; bytes balance; - std::optional code_hash; + std::optional code_id; uint64_t primary_key()const { return id; } @@ -26,11 +26,7 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] account { return res; } - bytes32 get_code_hash()const { - return code_hash ? to_bytes32(code_hash.value()) : silkworm::kEmptyHash; - } - - EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code_hash)); + EOSLIB_SERIALIZE(account, (id)(eth_address)(nonce)(balance)(code_id)); }; typedef multi_index< "account"_n, account, diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 07f5f729..6635c4f6 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -400,7 +400,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; }); diff --git a/contract/src/state.cpp b/contract/src/state.cpp index e5add9eb..96243401 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -17,9 +17,24 @@ std::optional state::read_account(const evmc::address& address) const n return {}; } - auto code_hash = itr->get_code_hash(); addr2id[address] = itr->id; + 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}; } @@ -93,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 = current->code_hash == silkworm::kEmptyHash ? std::nullopt : std::optional(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 = current->code_hash == silkworm::kEmptyHash ? std::nullopt : std::optional(to_bytes(current->code_hash)); + // Codes are not supposed to changed in this call. }; auto remove_account = [&](auto& itr) { @@ -111,11 +127,10 @@ void state::update_account(const evmc::address& address, std::optional row.storage_id = itr->id; }); // Decrease ref_count for code - if (itr->code_hash ) { + if (itr->code_id ) { account_code_table codes(_self, _self.value); - auto inxc = codes.get_index<"by.codehash"_n>(); - auto itrc = inxc.find(make_key(itr->code_hash.value())); - if(itrc == inxc.end()) { + auto itrc = codes.find(itr->code_id.value()); + if(itrc == codes.end()) { // SHOULD NOT REACH HERE! // But we ignore this for robustness. } else { @@ -192,9 +207,11 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev 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 = codes.available_primary_key(); + row.id = code_id; row.code_hash = to_bytes(code_hash); row.code = bytes{code.begin(), code.end()}; row.ref_count = 1; @@ -204,6 +221,7 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev codes.modify(*itrc, eosio::same_payer, [&](auto& row){ row.ref_count ++; }); + code_id = itrc->id; } account_table accounts(_self, _self.value); @@ -212,7 +230,7 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev ++stats.account.read; if( itr != inx.end() ) { accounts.modify(*itr, eosio::same_payer, [&](auto& row){ - row.code_hash = to_bytes(code_hash); + row.code_id = code_id; }); ++stats.account.update; } else { @@ -220,7 +238,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_hash = to_bytes(code_hash); + row.code_id = code_id; }); ++stats.account.create; } @@ -251,7 +269,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(silkworm::kEmptyHash); + row.code_id = std::nullopt; }); ++stats.account.read; } else { diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 12bf8cd0..08f07ba2 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,7 +255,7 @@ struct account { bytes eth_address; uint64_t nonce; bytes balance; - std::optional code_hash; + std::optional code_id; struct by_address { @@ -253,17 +271,6 @@ struct account { return res; } - evmc::bytes32 get_code_hash()const { - if (code_hash.has_value()) { - evmc::bytes32 res; - std::copy(code_hash.value().begin(), code_hash.value().end(), res.bytes); - return res; - } - else { - return kEmptyHash; - } - } - 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; @@ -280,17 +287,8 @@ struct account { return r; } - Account as_silkworm_account() { - return Account{ - nonce, - intx::be::load(get_balance()), - get_code_hash(), - 0 //TODO: ?? - }; - } - }; -FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_hash)); +FC_REFLECT(account, (id)(eth_address)(nonce)(balance)(code_id)); struct account_code { uint64_t id; @@ -302,7 +300,7 @@ struct account_code { struct by_codehash { typedef index256_object index_object; static name index_name() { - return account::index_name("by.codehash"_n); + return account_code::index_name("by.codehash"_n); } }; @@ -442,7 +440,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, 50000000); + create_account_with_resources(ME, system_account_name, 7000000); set_authority( ME, "active"_n, {1, {{get_public_key(ME,"active"),1}}, {{{ME,"eosio.code"_n},1}}} ); set_code(ME, contracts::evm_runtime_wasm()); @@ -597,7 +595,26 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { 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; From d08e73422c45819adf9f2b267d2bb0c13da5d109 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 15 Mar 2023 08:05:21 -0500 Subject: [PATCH 38/70] GH-352 Remove setbal call --- tests/leap/nodeos_trust_evm_test.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 77e1064f..5996c708 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -300,14 +300,25 @@ def nameStrToInt(s: str): Print("Creating wallet \"%s\"." % (testWalletName)) testWallet=walletMgr.create(testWalletName, [cluster.eosioAccount,accounts[0],accounts[1],accounts[2]]) + addys = { + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":"0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75,0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + } + + numAddys = len(addys) + # create accounts via eosio as otherwise a bid is needed for account in accounts: Print("Create new account %s via %s" % (account.name, cluster.eosioAccount.name)) trans=nonProdNode.createInitializeAccount(account, cluster.eosioAccount, stakedDeposit=0, waitForTransBlock=True, stakeNet=10000, stakeCPU=10000, buyRAM=10000000, exitOnError=True) - transferAmount="100000000.0000 {0}".format(CORE_SYMBOL) + # max supply 1000000000.0000 (1 Billion) + transferAmount="100000000.0000 {0}".format(CORE_SYMBOL) # 100 Million 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) - trans=nonProdNode.delegatebw(account, 20000000.0000, 20000000.0000, waitForTransBlock=True, exitOnError=True) + if account.name == evmAcc.name: + # stake more for evmAcc so it has a smaller balance, during setup of addys below the difference will be transferred in + trans=nonProdNode.delegatebw(account, 20000000.0000 + numAddys*1000000.0000, 20000000.0000, waitForTransBlock=True, exitOnError=True) + else: + trans=nonProdNode.delegatebw(account, 20000000.0000, 20000000.0000, waitForTransBlock=True, exitOnError=True) contractDir=trustEvmContractRoot + "/evm_runtime" wasmFile="evm_runtime.wasm" @@ -366,16 +377,15 @@ def nameStrToInt(s: str): txWrapPOpen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) Utils.Print("Set balance") - addys = { - "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266":"0x038318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed75,0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - } + # init with 1 Million 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="1000000.0000 {0}".format(CORE_SYMBOL) + Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, evmAcc.name)) + prodNode.transferFunds(cluster.eosioAccount, evmAcc, transferAmount, "0x" + k[2:].lower(), waitForTransBlock=True) + genesis_info["alloc"][k.lower()] = {"balance":"0x0000000000d3c21bcecceda1000000"} # 1 Million EOS if not (i+1) % 20: time.sleep(1) - prodNode.waitForTransBlockIfNeeded(trans[1], True) Utils.Print("Send balance") evmChainId = 15555 @@ -508,7 +518,7 @@ def nameStrToInt(s: str): testAccActualAmount=prodNode.getAccountEosBalanceStr(testAcc.name) Utils.Print("\tAccount balances: EVM %s, Test %s" % (evmAccActualAmount, testAccActualAmount)) if expectedAmount != evmAccActualAmount or expectedAmount != testAccActualAmount: - Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual" % (expectedAmount, evmAccActualAmount, testAccActualAmount)) + Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual %s" % (expectedAmount, evmAccActualAmount, testAccActualAmount)) # set ingress bridge fee data="[\"0.0100 {0}\"]".format(CORE_SYMBOL) From a96386e957f1483d4aa4352f5c15275dfef5d56c Mon Sep 17 00:00:00 2001 From: yarkin Date: Thu, 16 Mar 2023 00:01:43 +0800 Subject: [PATCH 39/70] Clean up code table during clearall action. --- contract/src/actions.cpp | 7 +++++++ contract/tests/evm_runtime_tests.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 6635c4f6..2b66299c 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -329,6 +329,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()); diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 08f07ba2..b49fe6cb 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -440,7 +440,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, 7000000); + create_account_with_resources(ME, system_account_name, 5000000); set_authority( ME, "active"_n, {1, {{get_public_key(ME,"active"),1}}, {{{ME,"eosio.code"_n},1}}} ); set_code(ME, contracts::evm_runtime_wasm()); From 53e34660560eaf7e3ebac5894ad8ece32adb67f6 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 15 Mar 2023 11:54:29 -0500 Subject: [PATCH 40/70] GH-352 Remove `alloc` section --- tests/leap/nodeos_trust_evm_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 5996c708..3c279d20 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -334,7 +334,6 @@ def nameStrToInt(s: str): Utils.Print("Block timestamp: ", block["timestamp"]) genesis_info = { - "alloc": {}, "coinbase": "0x0000000000000000000000000000000000000000", "config": { "chainId": 15555, @@ -384,7 +383,6 @@ def nameStrToInt(s: str): transferAmount="1000000.0000 {0}".format(CORE_SYMBOL) Print("Transfer funds %s from account %s to %s" % (transferAmount, cluster.eosioAccount.name, evmAcc.name)) prodNode.transferFunds(cluster.eosioAccount, evmAcc, transferAmount, "0x" + k[2:].lower(), waitForTransBlock=True) - genesis_info["alloc"][k.lower()] = {"balance":"0x0000000000d3c21bcecceda1000000"} # 1 Million EOS if not (i+1) % 20: time.sleep(1) Utils.Print("Send balance") @@ -676,7 +674,7 @@ def nameStrToInt(s: str): Utils.Print("Launching: %s" % cmd) evmNodePOpen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) - time.sleep(5) # allow time to sync trxs + time.sleep(10) # allow time to sync trxs # Validate all balances are the same on both sides rows=prodNode.getTable(evmAcc.name, evmAcc.name, "account") From 4e1f1d9f230c8ad6fb5afe2d1a86644da981324d Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Wed, 15 Mar 2023 16:17:46 -0300 Subject: [PATCH 41/70] GH-352 add zero addres with empty balance --- tests/leap/nodeos_trust_evm_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 3c279d20..39b9a568 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -334,6 +334,9 @@ def nameStrToInt(s: str): Utils.Print("Block timestamp: ", block["timestamp"]) genesis_info = { + "alloc": { + "0x0000000000000000000000000000000000000000" : {"balance":"0x00"} + }, "coinbase": "0x0000000000000000000000000000000000000000", "config": { "chainId": 15555, From 9f346af4257cc0c96d617e5f7aaec08cd6cd90a0 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 15 Mar 2023 15:14:53 -0500 Subject: [PATCH 42/70] GH-352 Clean up comments --- tests/leap/nodeos_trust_evm_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_test.py b/tests/leap/nodeos_trust_evm_test.py index 39b9a568..01e4c44f 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -50,7 +50,6 @@ # npm install is-valid-hostname # --trust-evm-build-root should point to the root of TrustEVM build dir # --trust-evm-contract-root should point to root of TrustEVM contract build dir -# contracts should be built with -DWITH_TEST_ACTIONS=On # # Example: # cd ~/ext/leap/build @@ -378,7 +377,7 @@ def nameStrToInt(s: str): Utils.Print("Launching: %s" % cmd) txWrapPOpen=Utils.delayedCheckOutput(cmd, stdout=outFile, stderr=errFile) - Utils.Print("Set balance") + Utils.Print("Transfer initial balances") # init with 1 Million EOS for i,k in enumerate(addys): @@ -408,8 +407,6 @@ def nameStrToInt(s: str): chainId=evmChainId ), evmSendKey) - Utils.Print("raw: ", signed_trx.rawTransaction) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm') prodNode.waitForTransBlockIfNeeded(trans[1], True) From 3f72e8e81dd3e28ada02ef601f8049fd1f09ebe8 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Wed, 15 Mar 2023 15:15:28 -0500 Subject: [PATCH 43/70] GH-352 Remove setbal calls and use bridging instead --- tests/leap/nodeos_trust_evm_server.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/leap/nodeos_trust_evm_server.py b/tests/leap/nodeos_trust_evm_server.py index 9feb6954..c271d30a 100755 --- a/tests/leap/nodeos_trust_evm_server.py +++ b/tests/leap/nodeos_trust_evm_server.py @@ -21,6 +21,7 @@ from TestHarness import Cluster, TestHelper, Utils, WalletMgr from TestHarness.TestHelper import AppArgs +from TestHarness.testUtils import ReturnType from core_symbol import CORE_SYMBOL @@ -170,6 +171,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) @@ -205,6 +207,10 @@ trans = prodNode.pushMessage(evmAcc.name, "init", '{"chainid":15555}', '-p evmevmevmevm') prodNode.waitForTransBlockIfNeeded(trans[1], True) + # add eosio.code permission + cmd="set account permission evmevmevmevm active --add-code -p evmevmevmevm@active" + prodNode.processCleosCmd(cmd, cmd, silentErrors=True, returnType=ReturnType.raw) + transId=prodNode.getTransId(trans[1]) blockNum = prodNode.getBlockNumByTransId(transId) block = prodNode.getBlock(blockNum) @@ -212,7 +218,9 @@ Utils.Print("Block timestamp: ", block["timestamp"]) genesis_info = { - "alloc": {}, + "alloc": { + "0x0000000000000000000000000000000000000000" : {"balance":"0x00"} + }, "coinbase": "0x0000000000000000000000000000000000000000", "config": { "chainId": 15555, @@ -324,12 +332,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") From 34c4522115df34acfac5ca3c944cb785aee68dbc Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Wed, 15 Mar 2023 19:35:54 -0300 Subject: [PATCH 44/70] state: simplify code removal --- contract/include/evm_runtime/tables.hpp | 11 ------- contract/src/state.cpp | 40 ++++++------------------- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/contract/include/evm_runtime/tables.hpp b/contract/include/evm_runtime/tables.hpp index b031b6f3..dd244c5c 100644 --- a/contract/include/evm_runtime/tables.hpp +++ b/contract/include/evm_runtime/tables.hpp @@ -85,15 +85,4 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] gcstore { typedef multi_index< "gcstore"_n, gcstore> gc_store_table; -struct [[eosio::table]] [[eosio::contract("evm_contract")]] gc_code { - uint64_t id; - uint64_t code_id; - - uint64_t primary_key()const { return id; } - - EOSLIB_SERIALIZE(gc_code, (id)(code_id)); -}; - -typedef multi_index< "gccode"_n, gc_code> gc_code_table; - } //namespace evm_runtime \ No newline at end of file diff --git a/contract/src/state.cpp b/contract/src/state.cpp index 96243401..ce24f875 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -126,25 +126,16 @@ void state::update_account(const evmc::address& address, std::optional row.id = gc.available_primary_key(); row.storage_id = itr->id; }); - // Decrease ref_count for code - if (itr->code_id ) { + // Remove code if necessary + if (itr->code_id) { account_code_table codes(_self, _self.value); - auto itrc = codes.find(itr->code_id.value()); - if(itrc == codes.end()) { - // SHOULD NOT REACH HERE! - // But we ignore this for robustness. - } else { - if (itrc->ref_count <= 1) { - gc_code_table gccode(_self, _self.value); - gccode.emplace(_ram_payer, [&](auto& row){ - row.id = gccode.available_primary_key(); - row.code_id = itrc->id; - }); - } - codes.modify(*itrc, eosio::same_payer, [&](auto& row){ - if (row.ref_count > 0) - row.ref_count --; + auto itrc = codes.get(itr->code_id.value()); + if(--itrc.ref_count) { + codes.modify(itrc, eosio::same_payer, [&](auto& row){ + row.ref_count--; }); + } else { + codes.erase(itrc); } } accounts.erase(*itr); @@ -187,20 +178,7 @@ bool state::gc(uint32_t max) { --max; } - gc_code_table gccode(_self, _self.value); - auto j = gccode.begin(); - while( max && j != gccode.end() ) { - account_code_table codes(_self, _self.value); - auto citr = codes.find(j->code_id); - if ( max && citr != codes.end() && citr->ref_count == 0) { - citr = codes.erase(citr); - --max; - } - if( !max ) break; - j = gccode.erase(j); - --max; - } - return gc.begin() == gc.end() && gccode.begin() == gccode.end(); + return gc.begin() == gc.end(); } void state::update_account_code(const evmc::address& address, uint64_t, const evmc::bytes32& code_hash, ByteView code) { From 1616e2a2c31f74d9cc9d6ef493f92a577e08e235 Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Wed, 15 Mar 2023 21:19:49 -0300 Subject: [PATCH 45/70] state: fix update_account --- contract/src/state.cpp | 6 +++--- contract/tests/evm_runtime_tests.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contract/src/state.cpp b/contract/src/state.cpp index ce24f875..25799d8b 100644 --- a/contract/src/state.cpp +++ b/contract/src/state.cpp @@ -129,8 +129,8 @@ void state::update_account(const evmc::address& address, std::optional // Remove code if necessary if (itr->code_id) { account_code_table codes(_self, _self.value); - auto itrc = codes.get(itr->code_id.value()); - if(--itrc.ref_count) { + 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--; }); @@ -197,7 +197,7 @@ void state::update_account_code(const evmc::address& address, uint64_t, const ev } else { // code should be immutable codes.modify(*itrc, eosio::same_payer, [&](auto& row){ - row.ref_count ++; + row.ref_count++; }); code_id = itrc->id; } diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index 08f07ba2..f57ccbb7 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -944,6 +944,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; @@ -954,8 +956,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()}; From a4e4e6d7e2ad8a9c041ec5a402279eabf94f1810 Mon Sep 17 00:00:00 2001 From: yarkin Date: Thu, 16 Mar 2023 09:01:00 +0800 Subject: [PATCH 46/70] Clean up test code after the removal of gc_code table. --- contract/tests/evm_runtime_tests.cpp | 40 ---------------------------- 1 file changed, 40 deletions(-) diff --git a/contract/tests/evm_runtime_tests.cpp b/contract/tests/evm_runtime_tests.cpp index b1a8d15a..b52b6ae0 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -387,22 +387,6 @@ struct gcstore { }; FC_REFLECT(gcstore, (id)(storage_id)); -struct gc_code { - uint64_t id; - uint64_t code_id; - - static name table_name() { return "gccode"_n; } - static name index_name(const name& n) { - BOOST_REQUIRE(false); - return name{0}; - } - - static name index_name(uint64_t n) { - return index_name(name{n}); - } -}; -FC_REFLECT(gc_code, (id)(code_id)); - struct assert_message_check { string _expected; assert_message_check(const string& expected) { @@ -743,25 +727,6 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return count; } - size_t gc_code_size() { - auto& db = const_cast(control->db()); - - const auto* tid = db.find( - boost::make_tuple("evm"_n, "evm"_n,"gccode"_n) - ); - - if(tid == nullptr) return 0; - - const auto& idx = db.get_index(); - auto itr = idx.lower_bound( boost::make_tuple( tid->id) ); - size_t count=0; - while ( itr != idx.end() && itr->t_id == tid->id ) { - ++itr; - ++count; - } - return count; - } - size_t state_storage_size(const evmc::address& address, uint64_t incarnation) { auto& db = const_cast(control->db()); auto accnt = account::get_by_address(db, address); @@ -1030,11 +995,6 @@ struct evm_runtime_tester : eosio_system_tester, silkworm::State { return false; } - if( gc_code_size() != 0 ) { - std::cout << "gc_code is not empty: " << gc_code_size() << std::endl; - return false; - } - return true; } From 8504abb61d1ea1e7efa74080ce2811a0101e886c Mon Sep 17 00:00:00 2001 From: Areg Hayrapetian Date: Thu, 16 Mar 2023 03:03:45 -0700 Subject: [PATCH 47/70] Update block_conversion_plugin.cpp evm_block_num_to_evm_timestamp already returns in seconds so no need to divide --- cmd/block_conversion_plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/block_conversion_plugin.cpp b/cmd/block_conversion_plugin.cpp index 056f89b8..c6c157a1 100644 --- a/cmd/block_conversion_plugin.cpp +++ b/cmd/block_conversion_plugin.cpp @@ -88,7 +88,7 @@ class block_conversion_plugin_impl : std::enable_shared_from_this Date: Thu, 16 Mar 2023 13:50:35 -0700 Subject: [PATCH 48/70] Implement gas fee feature. setingressfee action replaced by more general setfeeparams action which also allows changing the gas_price and the newly introduced miner_cut within the config singleton. The init action now also takes an additional argument which is the same argument that the setfeeparams action takes. This enables the gas_price and miner_cut to be set during initialization (and in fact values for both must be provided). The ingress_bridge_fee does not need to be provided during init; if it is not provided it will go with the default value of zero ingress bridge fee. The pushtx action has been modified to take miner as its first argument instead of ram_payer. The type is the same but its semantic meaning has now changed. The RAM costs are now always paid by the contract. And therefore no authorizer is required on the pushtx action. The purpose of the miner argument is to specify the EOS account that will receive the miner portion of the gas fees paid for the EVM transaction. The miner portion is determined by miner_cut which acts as a percentage between 0% and 100%. Additionally, the pushtx action now enforces that the transaction provided as an argument to the action sets a gas price that is no less than the gas price set in the contract state. Unit tests have been added for the gas fee feature. Some minor tweaks were made to the bridging tests so that their checks properly account for a non-zero gas price provided in the test transactions. Additionally an extra test case has been added to the unit tests for the block mapping and timestamp changes. Changes have not yet been made to the integration tests to reflect the change to the signature of the contract actions. --- contract/include/evm_runtime/evm_contract.hpp | 200 ++++---- contract/src/actions.cpp | 199 +++++--- contract/tests/CMakeLists.txt | 3 + contract/tests/basic_evm_tester.cpp | 442 ++++++++++++++++++ contract/tests/basic_evm_tester.hpp | 311 +++++++----- .../contracts/solidity/BlockNumTimestamp.sol | 12 + contract/tests/evm_runtime_tests.cpp | 12 +- contract/tests/gas_fee_tests.cpp | 287 ++++++++++++ contract/tests/init_tests.cpp | 14 +- contract/tests/mapping_tests.cpp | 267 +++++++++-- contract/tests/native_token_tests.cpp | 208 +++------ 11 files changed, 1524 insertions(+), 431 deletions(-) create mode 100644 contract/tests/basic_evm_tester.cpp create mode 100644 contract/tests/contracts/solidity/BlockNumTimestamp.sol create mode 100644 contract/tests/gas_fee_tests.cpp diff --git a/contract/include/evm_runtime/evm_contract.hpp b/contract/include/evm_runtime/evm_contract.hpp index 861742c3..c13e1c84 100644 --- a/contract/include/evm_runtime/evm_contract.hpp +++ b/contract/include/evm_runtime/evm_contract.hpp @@ -14,106 +14,144 @@ using namespace eosio; namespace evm_runtime { -CONTRACT evm_contract : public contract { - public: - using contract::contract; - - [[eosio::action]] - void init(const uint64_t chainid); - - [[eosio::action]] - void setingressfee(asset ingress_bridge_fee); - - [[eosio::action]] - void addegress(const std::vector& accounts); - - [[eosio::action]] - void removeegress(const std::vector& accounts); - - [[eosio::action]] - void freeze(bool value); - - [[eosio::action]] - void pushtx(eosio::name ram_payer, 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); +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); #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); - ACTION testbaldust(const name test); + [[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 - private: - enum class status_flags : uint32_t { - frozen = 0x1 - }; - 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 = 10000000000; - uint32_t status = 0; // <- bit mask values from status_flags + struct [[eosio::table]] [[eosio::contract("evm_contract")]] config + { + unsigned_int version; // placeholder for future variant index + uint64_t chainid = 0; + time_point_sec genesis_time; + asset ingress_bridge_fee = asset(0, token_symbol); + uint64_t gas_price = 0; + uint32_t miner_cut = 0; + uint32_t status = 0; // <- bit mask values from status_flags + + EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(miner_cut)(status)); + }; - EOSLIB_SERIALIZE(config, (version)(chainid)(genesis_time)(ingress_bridge_fee)(gas_price)(status)); - }; +private: + enum class status_flags : uint32_t + { + frozen = 0x1 + }; - eosio::singleton<"config"_n, config> _config{get_self(), get_self().value}; + 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_inited() + { + check(_config.exists(), "contract not initialized"); + check(_config.get().version == 0u, "unsupported configuration singleton"); + } - void assert_unfrozen() { - assert_inited(); - check((_config.get().status & static_cast(status_flags::frozen)) == 0, "contract is frozen"); - } + void assert_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); + uint64_t get_and_increment_nonce(const name owner); - checksum256 get_code_hash(name account) const; + 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); + 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 ram_payer, const std::basic_string& rlptx); - using pushtx_action = eosio::action_wrapper<"pushtx"_n, &evm_contract::pushtx_bytes>; + // 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) { +template +DataStream& operator<<(DataStream& ds, const std::basic_string& bs) +{ ds << (unsigned_int)bs.size(); - if(bs.size()) + if (bs.size()) ds.write((const char*)bs.data(), bs.size()); return ds; } -} +} // namespace std diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index a0b4af73..b5ebb32b 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -34,35 +34,72 @@ namespace silkworm { 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; + } +} - check( !_config.exists(), "contract already initialized" ); - check( !!lookup_known_chain(chainid), "unknown chainid" ); +void evm_contract::init(const uint64_t chainid, const fee_parameters& fee_params) +{ + eosio::require_auth(get_self()); - _config.set({ - .version = 0, - .chainid = chainid, - .genesis_time = eosio::current_time_point() // implicitly converts from Antelope timestamp to EVM compatible timestamp - }, get_self()); + check(!_config.exists(), "contract already initialized"); + check(!!lookup_known_chain(chainid), "unknown chainid"); - inevm_singleton(get_self(), get_self().value).get_or_create(get_self()); + // 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(); - open(get_self()); -} + config new_config = { + .version = 0, + .chainid = chainid, + .genesis_time = genesis_time, + }; -void evm_contract::setingressfee(asset ingress_bridge_fee) { - assert_inited(); - require_auth(get_self()); + // 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. + + 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()); +} - check( ingress_bridge_fee.symbol == token_symbol, "unexpected bridge symbol" ); - check( ingress_bridge_fee.amount >= 0, "ingress bridge fee cannot be negative"); +void evm_contract::setfeeparams(const fee_parameters& fee_params) +{ + assert_inited(); + require_auth(get_self()); - config current_config = _config.get(); - current_config.ingress_bridge_fee = ingress_bridge_fee; - _config.set(current_config, 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) { @@ -122,24 +159,31 @@ 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(); - std::optional balance_table; + 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(balance_table) + if(inevm) return; - balance_table.emplace(get_self(), get_self().value); inevm.emplace(get_self(), get_self().value); }; - 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"); - tx.from.reset(); tx.recover_sender(); eosio::check(tx.from.has_value(), "unable to recover sender"); @@ -154,7 +198,7 @@ Receipt evm_contract::execute_tx( Block& block, const bytes& rlptx, silkworm::Ex 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){ + 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); @@ -174,6 +218,17 @@ 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 = ep.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"); @@ -194,9 +249,14 @@ Receipt evm_contract::execute_tx( Block& block, const bytes& rlptx, silkworm::Ex 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){ + 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 { @@ -221,32 +281,53 @@ Receipt evm_contract::execute_tx( Block& block, const bytes& rlptx, silkworm::Ex 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); + block.header.beneficiary = make_reserved_address(get_self().value); + block.header.difficulty = 1; + block.header.number = bm.timestamp_to_evm_block_num(eosio::current_time_point().time_since_epoch().count()); + block.header.gas_limit = 0x7ffffffffff; + block.header.timestamp = bm.evm_block_num_to_evm_timestamp(block.header.number); 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); @@ -327,8 +408,10 @@ void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& b.balance.balance += quantity; }); - //substract off the ingress bridge fee from the quantity that will be bridged - quantity -= _config.get().ingress_bridge_fee; + 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); @@ -340,8 +423,8 @@ void evm_contract::handle_evm_transfer(eosio::asset quantity, const std::string& const Transaction txn { .type = Transaction::Type::kLegacy, .nonce = get_and_increment_nonce(get_self()), - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, + .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, @@ -394,7 +477,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()); @@ -407,13 +490,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()); @@ -447,7 +534,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()); @@ -497,7 +584,7 @@ ACTION evm_contract::dumpall() { } -ACTION evm_contract::clearall() { +[[eosio::action]] void evm_contract::clearall() { assert_unfrozen(); eosio::require_auth(get_self()); @@ -532,7 +619,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()); @@ -542,7 +629,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()); @@ -559,7 +646,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()); @@ -583,7 +670,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()); @@ -606,7 +693,7 @@ ACTION evm_contract::setbal(const bytes& addy, const bytes& bal) { } } -ACTION evm_contract::testbaldust(const name test) { +[[eosio::action]] void evm_contract::testbaldust(const name test) { if(test == "basic"_n) { balance_with_dust b; // ↱minimum EOS diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index c4a901e7..f16f1c12 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -25,10 +25,12 @@ 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}/main.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/encode.cpp ${CMAKE_SOURCE_DIR}/silkworm/core/silkworm/rlp/decode.cpp @@ -38,6 +40,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 038c68b2..a4db6066 100644 --- a/contract/tests/basic_evm_tester.hpp +++ b/contract/tests/basic_evm_tester.hpp @@ -12,9 +12,12 @@ #include #include #include +#include #include +#include + #include using namespace eosio; @@ -23,155 +26,249 @@ using mvo = fc::mutable_variant_object; using intx::operator""_u256; -class evm_eoa { -public: - evm_eoa() { - fc::rand_bytes((char*)private_key.data(), private_key.size()); - public_key.resize(65); +namespace intx { - secp256k1_pubkey pubkey; - BOOST_REQUIRE(secp256k1_ec_pubkey_create(ctx, &pubkey, private_key.data())); +inline std::ostream& operator<<(std::ostream& ds, const intx::uint256& num) +{ + ds << intx::to_string(num, 10); + return ds; +} - size_t serialized_result_sz = public_key.size(); - secp256k1_ec_pubkey_serialize(ctx, public_key.data(), &serialized_result_sz, &pubkey, SECP256K1_EC_UNCOMPRESSED); +} // namespace intx - std::optional addr = silkworm::ecdsa::public_key_to_address(public_key); - BOOST_REQUIRE(!!addr); - address = *addr; - } +namespace fc { - std::string address_0x() const { - return std::string("0x") + fc::to_hex((char*)address.bytes, sizeof(address.bytes)); - } +void to_variant(const intx::uint256& o, fc::variant& v); +void to_variant(const evmc::address& o, fc::variant& v); - key256_t address_key256() const { - uint8_t buffer[32]={0}; - memcpy(buffer, address.bytes, sizeof(address.bytes)); - return fixed_bytes<32>(buffer).get_array(); - } +} // namespace fc - void sign(silkworm::Transaction& trx) { - silkworm::Bytes rlp; - trx.chain_id = 15555; /// TODO: don't hardcode this - 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; - } +namespace evm_test { - ~evm_eoa() { - secp256k1_context_destroy(ctx); - } +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: + 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; - - unsigned next_nonce = 0; }; -struct balance_and_dust { - asset balance; - uint64_t dust = 0; +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; - bool operator==(const balance_and_dust& o) const { - return balance == o.balance && dust == o.dust; - } - bool operator!=(const balance_and_dust& o) const { - return !(*this == o); - } -}; -FC_REFLECT(balance_and_dust, (balance)(dust)); + static constexpr uint64_t evm_chain_id = 15555; -class basic_evm_tester : public testing::validating_tester { -public: - basic_evm_tester() { - create_accounts({"evm"_n}); + // 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 - set_code("evm"_n, testing::contracts::evm_runtime_wasm()); - set_abi("evm"_n, testing::contracts::evm_runtime_abi().data()); - } + const symbol native_symbol; - void init(const uint64_t chainid) { - push_action("evm"_n, "init"_n, "evm"_n, mvo()("chainid", chainid)); - } + static evmc::address make_reserved_address(uint64_t account); - void setingressfee(const asset& ingress_bridge_fee) { - push_action("evm"_n, "setingressfee"_n, "evm"_n, mvo()("ingress_bridge_fee", ingress_bridge_fee)); - } + explicit basic_evm_tester(std::string native_symbol_str = "4,EOS"); - void pushtx(const silkworm::Transaction& trx) { - silkworm::Bytes rlp; - silkworm::rlp::encode(rlp, trx); + asset make_asset(int64_t amount) const; - bytes rlp_bytes; - rlp_bytes.resize(rlp.size()); - memcpy(rlp_bytes.data(), rlp.data(), rlp.size()); + transaction_trace_ptr transfer_token(name from, name to, asset quantity, std::string memo = ""); - push_action("evm"_n, "pushtx"_n, "evm"_n, mvo()("ram_payer", "evm"_n)("rlptx", rlp_bytes)); - } + 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); - balance_and_dust inevm() const { - return fc::raw::unpack(get_row_by_account("evm"_n, "evm"_n, "inevm"_n, "inevm"_n)); - } + void prepare_self_balance(uint64_t fund_amount = 100'0000); - std::optional evm_balance(const evm_eoa& account) { - const int64_t account_table_id = get(boost::make_tuple("evm"_n, "evm"_n, "account"_n)).id._id; + config_table_row get_config() const; - const index256_object* secondary_row = find(boost::make_tuple(account_table_id, account.address_key256())); - if(secondary_row == nullptr) - return std::nullopt; + void setfeeparams(const fee_parameters& fee_params); - const key_value_object& row = get(boost::make_tuple(account_table_id, secondary_row->primary_key)); + silkworm::Transaction + generate_tx(const evmc::address& to, const intx::uint256& value, uint64_t gas_limit = 21000) const; - // cheat a little here: uint64_t id, bytes eth_address, uint64_t nonce, bytes balance; 32 bytes of balance must start at byte 38 - return intx::be::unsafe::load((uint8_t*)row.value.data()+38); - } + 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) { - push_action("evm"_n, "addegress"_n, "evm"_n, mvo()("accounts",accounts)); - } + 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(); - void removeegress(const std::vector accounts) { - push_action("evm"_n, "removeegress"_n, "evm"_n, mvo()("accounts",accounts)); + 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; + } + } } + + 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"" _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"" _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"" _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"" _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"" _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"" _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); } -inline constexpr intx::uint256 operator"" _ether(const char* s) { - return intx::from_string(s) * intx::exp(10_u256, 18_u256); -} \ No newline at end of file +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); + } + + 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/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 a6273fd0..8d48dea2 100644 --- a/contract/tests/evm_runtime_tests.cpp +++ b/contract/tests/evm_runtime_tests.cpp @@ -418,7 +418,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) { 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 c413dc6e..04415aa8 100644 --- a/contract/tests/init_tests.cpp +++ b/contract/tests/init_tests.cpp @@ -1,14 +1,16 @@ #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, "setingressfee"_n, "evm"_n, mvo()("ingress_bridge_fee", asset(0, symbol(4, "EOS")))), + 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())), @@ -18,7 +20,7 @@ BOOST_FIXTURE_TEST_CASE(check_init, basic_evm_tester) try { 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, "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)), @@ -80,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)), @@ -130,7 +132,7 @@ 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");}); diff --git a/contract/tests/mapping_tests.cpp b/contract/tests/mapping_tests.cpp index 8edc5247..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; - //additional fields ignored in this test - }; + 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)) - 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_tests.cpp b/contract/tests/native_token_tests.cpp index 33ebe13d..795b9558 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -3,6 +3,7 @@ #include using namespace eosio::testing; +using namespace evm_test; static const char do_nothing_wast[] = R"=====( (module @@ -14,92 +15,65 @@ static const char do_nothing_wast[] = R"=====( )====="; 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(); + enum class init_mode + { + do_not_init, + init_without_ingress_bridge_fee, + init_with_ingress_bridge_fee, + }; - set_code("eosio.token"_n, contracts::eosio_token_wasm()); - set_abi("eosio.token"_n, contracts::eosio_token_abi().data()); + 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}; - 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", "")); - } + create_accounts(new_accounts); + + for(const name& recipient : new_accounts) { + transfer_token(faucet_account_name, recipient, make_asset(100'0000)); + } - 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)); + 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("eosio.token"_n, native_symbol, owner).get_amount(); + return get_currency_balance(token_account_name, native_symbol, owner).get_amount(); } - std::tuple vault_balance(name owner) const { - const vector d = get_row_by_account("evm"_n, "evm"_n, "balances"_n, owner); - FC_ASSERT(d.size(), "EVM not open"); - auto [_, amount, dust] = fc::raw::unpack(d); - return std::make_tuple(amount, dust); - } int64_t vault_balance_token(name owner) const { - return std::get<0>(vault_balance(owner)).get_amount(); + return vault_balance(owner).balance.get_amount(); } uint64_t vault_balance_dust(name owner) const { - return std::get<1>(vault_balance(owner)); - } - - transaction_trace_ptr open(name owner) { - return push_action("evm"_n, "open"_n, owner, mvo()("owner", owner)); + return vault_balance(owner).dust; } - 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 vault_balance_row { - name owner; - asset balance; - uint64_t dust = 0; - }; - evmc::address make_reserved_address(uint64_t account) const { - 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)}); + balance_and_dust inevm() const + { + return fc::raw::unpack(get_row_by_account("evm"_n, "evm"_n, "inevm"_n, "inevm"_n)); } }; -FC_REFLECT(native_token_evm_tester::vault_balance_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) {} + 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", true) {} + 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", false) {} + native_token_evm_tester_noinit() : native_token_evm_tester("4,EOS", init_mode::do_not_init) {} }; BOOST_AUTO_TEST_SUITE(native_token_evm_tests) @@ -241,6 +215,8 @@ BOOST_FIXTURE_TEST_CASE(basic_eos_evm_bridge, native_token_evm_tester_EOS) try { }; 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 @@ -295,7 +271,7 @@ BOOST_FIXTURE_TEST_CASE(basic_eos_evm_bridge, native_token_evm_tester_EOS) try { //set the bridge free to 0.1000 EOS const int64_t bridge_fee = 1000; - setingressfee(make_asset(bridge_fee)); + setfeeparams(fee_parameters{.ingress_bridge_fee = make_asset(bridge_fee)}); //transferring less than the bridge fee isn't allowed { @@ -314,7 +290,7 @@ BOOST_FIXTURE_TEST_CASE(basic_eos_evm_bridge, native_token_evm_tester_EOS) try { BOOST_REQUIRE(expected_inevm == inevm()); //nothing should have accumulated in contract's special balance yet - BOOST_REQUIRE(vault_balance("evm"_n) == std::make_tuple(make_asset(0), 0UL)); + 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 { @@ -330,24 +306,18 @@ BOOST_FIXTURE_TEST_CASE(basic_eos_evm_bridge, native_token_evm_tester_EOS) try { expected_inevm.balance += make_asset(to_bridge - bridge_fee); BOOST_REQUIRE(expected_inevm == inevm()); - BOOST_REQUIRE_EQUAL(vault_balance_token("evm"_n), bridge_fee); + 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; - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = evm1.address, - .value = 11111111_u256, - }; //r == 0 indicates a bridge signature. These are only allowed in contract-initiated (i.e. inline) EVM actions - BOOST_REQUIRE_EXCEPTION(pushtx(txn), + 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() @@ -357,6 +327,8 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { //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; @@ -372,42 +344,30 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { { 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)}; - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = evm2.address, - .value = 100_szabo * to_transfer, - }; + auto txn = generate_tx(evm2.address, 100_szabo * to_transfer); evm1.sign(txn); pushtx(txn); - BOOST_REQUIRE(*evm_balance(evm1) == evm1_before - txn.value); + 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 valance + //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); - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address("alice"_n.to_uint64_t()), - .value = 100_szabo * to_bridge, - }; + 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(evm_balance(evm1) == evm1_before - txn.value); + 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 @@ -416,14 +376,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { const intx::uint256 evm1_before = *evm_balance(evm1); const int64_t alice_native_before = native_balance("alice"_n); - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address("alice"_n.to_uint64_t()), - .value = 100_szabo * to_bridge + 10_szabo, //dust - }; + 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")); @@ -436,9 +389,11 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { //and try again pushtx(txn); - BOOST_REQUIRE_EQUAL(alice_native_before, native_balance("alice"_n)); //native balance unchanged - BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); //EVM balance decresed - BOOST_REQUIRE(vault_balance("alice"_n) == std::make_tuple(make_asset(1'0000), 10'000'000'000'000UL)); //vault balance increased to 1.0000EOS, 10szabo + 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 @@ -449,14 +404,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { const int64_t to_bridge = 1'0000; const intx::uint256 evm1_before = *evm_balance(evm1); - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address("bob"_n.to_uint64_t()), - .value = 100_szabo * to_bridge, - }; + 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")); @@ -466,7 +414,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { //and now it'll go through pushtx(txn); BOOST_REQUIRE_EQUAL(vault_balance_token("bob"_n), to_bridge); - BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); } //install some code on carol's account @@ -478,14 +426,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { const int64_t carol_native_before = native_balance("carol"_n); const intx::uint256 evm1_before = *evm_balance(evm1); - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address("carol"_n.to_uint64_t()), - .value = 100_szabo * to_bridge, - }; + 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")); @@ -495,7 +436,7 @@ BOOST_FIXTURE_TEST_CASE(basic_evm_eos_bridge, native_token_evm_tester_EOS) try { //and now it'll go through pushtx(txn); BOOST_REQUIRE_EQUAL(carol_native_before + to_bridge, native_balance("carol"_n)); - BOOST_REQUIRE(evm_balance(evm1) == evm1_before - txn.value); + BOOST_REQUIRE_EQUAL(*evm_balance(evm1), (evm1_before - txn.value - gas_fee)); //remove carol from egress allow list removeegress({"carol"_n}); @@ -518,14 +459,7 @@ BOOST_FIXTURE_TEST_CASE(evm_eos_nonexistant, native_token_evm_tester_EOS) try { { const int64_t to_bridge = 1'0000; - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address("spoon"_n.to_uint64_t()), - .value = 100_szabo * to_bridge, - }; + 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")); @@ -538,13 +472,7 @@ BOOST_FIXTURE_TEST_CASE(evm_eos_disallow_reserved_zero, native_token_evm_tester_ 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 - silkworm::Transaction txn { - .type = silkworm::Transaction::Type::kLegacy, - .max_priority_fee_per_gas = 0, - .max_fee_per_gas = 0, - .gas_limit = 21000, - .to = make_reserved_address(0u) - }; + 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")); From 8a32b147203dce70e27e5b331728f59fff7014a6 Mon Sep 17 00:00:00 2001 From: Matt Witherspoon <32485495+spoonincode@users.noreply.github.com> Date: Thu, 16 Mar 2023 18:12:53 -0400 Subject: [PATCH 49/70] bump silkworm submodule to latest HEAD w/ bridging changes --- silkworm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/silkworm b/silkworm index 0f19bcca..7ae7e568 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit 0f19bcca56ed0cfe9b191d90fd5e0463e9b312e6 +Subproject commit 7ae7e56860ac3a46d7e2857a53d7743d7f037b8a From 7515f4d1a73ecafa2b2e835893221d6768cd259c Mon Sep 17 00:00:00 2001 From: Areg Hayrapetian Date: Thu, 16 Mar 2023 23:46:17 -0700 Subject: [PATCH 50/70] Fix Python integration scripts and tests. Also cleanup in other places to deal with changes to pushtx action. --- cmd/block_conversion_plugin.hpp | 4 +- evm.abi | 82 -------------- .../distribute_to_accounts.py | 4 +- peripherals/tx_wrapper/index.js | 4 +- tests/leap/nodeos_trust_evm_server.py | 14 ++- tests/leap/nodeos_trust_evm_test.py | 103 +++++++++--------- 6 files changed, 69 insertions(+), 142 deletions(-) delete mode 100644 evm.abi 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/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/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 bbfe001b..89ddd5be 100644 --- a/peripherals/tx_wrapper/index.js +++ b/peripherals/tx_wrapper/index.js @@ -78,8 +78,8 @@ async function push_tx(strRlptx) { } ], data: { - ram_payer : process.env.EOS_SENDER, - rlptx : strRlptx + miner : process.env.EOS_SENDER, + rlptx : strRlptx }, }, ], diff --git a/tests/leap/nodeos_trust_evm_server.py b/tests/leap/nodeos_trust_evm_server.py index c271d30a..979f04ef 100755 --- a/tests/leap/nodeos_trust_evm_server.py +++ b/tests/leap/nodeos_trust_evm_server.py @@ -203,14 +203,15 @@ 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') - prodNode.waitForTransBlockIfNeeded(trans[1], True) # 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]) blockNum = prodNode.getBlockNumByTransId(transId) block = prodNode.getBlock(blockNum) @@ -241,6 +242,11 @@ "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", @@ -396,7 +402,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_test.py b/tests/leap/nodeos_trust_evm_test.py index 2c481ed8..01afdeff 100755 --- a/tests/leap/nodeos_trust_evm_test.py +++ b/tests/leap/nodeos_trust_evm_test.py @@ -105,7 +105,6 @@ def interact_with_storage_contract(dest, nonce): gasP=getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(dest), @@ -114,8 +113,8 @@ def interact_with_storage_contract(dest, nonce): chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm') + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name)) assert retValue[0], "pushtx to ETH contract failed." Utils.Print("\tBlock#", retValue[1]["processed"]["block_num"]) row0=prodNode.getTableRow(evmAcc.name, 3, "storage", 0) @@ -293,6 +292,7 @@ def nameStrToInt(s: str): evmAcc.name = "evmevmevmevm" testAcc = accounts[1] txWrapAcc = accounts[2] + minerAcc = txWrapAcc testWalletName="test" @@ -315,7 +315,7 @@ def nameStrToInt(s: str): nonProdNode.transferFunds(cluster.eosioAccount, account, transferAmount, "test transfer", waitForTransBlock=True) if account.name == evmAcc.name: # stake more for evmAcc so it has a smaller balance, during setup of addys below the difference will be transferred in - trans=nonProdNode.delegatebw(account, 20000000.0000 + numAddys*1000000.0000, 20000000.0000, waitForTransBlock=True, exitOnError=True) + trans=nonProdNode.delegatebw(account, 20000000.0000 + numAddys*1000000.0000, 20000001.0000, waitForTransBlock=True, exitOnError=True) else: trans=nonProdNode.delegatebw(account, 20000000.0000, 20000000.0000, waitForTransBlock=True, exitOnError=True) @@ -324,7 +324,12 @@ def nameStrToInt(s: str): 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": "10000000000", "miner_cut": 100000, "ingress_bridge_fee": "0.0000 {0}"}}}}'.format(CORE_SYMBOL), '-p evmevmevmevm') prodNode.waitForTransBlockIfNeeded(trans[1], True) transId=prodNode.getTransId(trans[1]) blockNum = prodNode.getBlockNumByTransId(transId) @@ -356,9 +361,13 @@ def nameStrToInt(s: str): "timestamp": hex(int(calendar.timegm(datetime.strptime(block["timestamp"].split(".")[0], '%Y-%m-%dT%H:%M:%S').timetuple()))) } - # add eosio.code permission - cmd="set account permission evmevmevmevm active --add-code -p evmevmevmevm@active" - prodNode.processCleosCmd(cmd, cmd, silentErrors=True, returnType=ReturnType.raw) + 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) + + Utils.Print("Open balance for miner") + trans=prodNode.pushMessage(evmAcc.name, "open", '[{0}]'.format(minerAcc.name), '-p {0}'.format(minerAcc.name)) # # Setup tx_wrapper @@ -398,7 +407,6 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, -# maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), @@ -407,8 +415,8 @@ def nameStrToInt(s: str): chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm') + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name)) prodNode.waitForTransBlockIfNeeded(trans[1], True) # @@ -417,7 +425,7 @@ def nameStrToInt(s: str): # incorrect nonce Utils.Print("Send balance again, should fail with wrong nonce") - retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) assert not retValue[0], f"push trx should have failed: {retValue}" # correct nonce @@ -425,7 +433,6 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), @@ -434,9 +441,9 @@ def nameStrToInt(s: str): chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} Utils.Print("Send balance again, with correct nonce") - retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) assert retValue[0], f"push trx should have succeeded: {retValue}" # incorrect chainid @@ -445,7 +452,6 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), @@ -454,9 +460,9 @@ def nameStrToInt(s: str): chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} Utils.Print("Send balance again, with invalid chainid") - retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) assert not retValue[0], f"push trx should have failed: {retValue}" # correct values for continuing @@ -480,15 +486,14 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=1000000, #5M Gas gasPrice=gasP, data=Web3.toBytes(hexstr='608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea2646970667358fe12209ffe32fe5779018f7ee58886c856a4cfdf550f2df32cec944f57716a3abf4a5964736f6c63430008110033'), chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + retValue = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) assert retValue[0], f"push trx should have succeeded: {retValue}" nonce = interact_with_storage_contract(makeContractAddress(fromAdd, nonce), nonce) @@ -519,9 +524,9 @@ def nameStrToInt(s: str): Utils.errorExit("Unexpected starting conditions. Excepted %s, evm actual: %s, test actual %s" % (expectedAmount, evmAccActualAmount, testAccActualAmount)) # set ingress bridge fee - data="[\"0.0100 {0}\"]".format(CORE_SYMBOL) - opts="--permission evmevmevmevm@active" - trans=prodNode.pushMessage("evmevmevmevm", "setingressfee", data, opts) + Utils.Print("Set ingress bridge fee") + data='[{{"gas_price": null, "miner_cut": null, "ingress_bridge_fee": "0.0100 {}"}}]'.format(CORE_SYMBOL) + trans=prodNode.pushMessage(evmAcc.name, "setfeeparams", data, '-p {0}'.format(evmAcc.name)) rows=prodNode.getTable(evmAcc.name, evmAcc.name, "balances") Utils.Print("\tBefore transfer table rows:", rows) @@ -533,7 +538,7 @@ def nameStrToInt(s: str): row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) - assert(row0["balance"]["balance"] == "0.0100 {0}".format(CORE_SYMBOL)) # should have fee at end of transaction + assert(row0["balance"]["balance"] == "1.0100 {0}".format(CORE_SYMBOL)) # should have fee at end of transaction testAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) Utils.Print("\tEVM Account balance %s" % testAccActualAmount) expectedAmount="60000097.5321 {0}".format(CORE_SYMBOL) @@ -544,9 +549,9 @@ def nameStrToInt(s: str): Utils.Print("\tTest Account balance %s" % testAccActualAmount) if testAccActualAmount != expectedAmount: Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) - row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test - assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") - assert(row4["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) + row3=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 3) # 3rd balance of this integration test + assert(row3["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") + assert(row3["balance"] == "000000000000000000000000000000000000000000000005496419417a1f4000") # 0x5496419417a1f4000 => 97522100000000000000 (97.5321 - 0.0100) # EOS -> EVM to the same address transferAmount="10.0000 {0}".format(CORE_SYMBOL) @@ -554,7 +559,7 @@ def nameStrToInt(s: str): prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0xF0cE7BaB13C99bA0565f426508a7CD8f4C247E5a", waitForTransBlock=True) row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) - assert(row0["balance"]["balance"] == "0.0200 {0}".format(CORE_SYMBOL)) # should have fee from both transfers + assert(row0["balance"]["balance"] == "1.0200 {0}".format(CORE_SYMBOL)) # should have fee from both transfers evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) expectedAmount="60000107.5321 {0}".format(CORE_SYMBOL) @@ -565,9 +570,9 @@ def nameStrToInt(s: str): Utils.Print("\tTest Account balance %s" % testAccActualAmount) if testAccActualAmount != expectedAmount: Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) - row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test - assert(row4["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") - assert(row4["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5d407b55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) + row3=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 3) # 3rd balance of this integration test + assert(row3["eth_address"] == "f0ce7bab13c99ba0565f426508a7cd8f4c247e5a") + assert(row3["balance"] == "000000000000000000000000000000000000000000000005d407b55394464000") # 0x5d407b55394464000 => 107512100000000000000 (97.5321 + 10.000 - 0.0100 - 0.0100) # EOS -> EVM to diff address transferAmount="42.4242 {0}".format(CORE_SYMBOL) @@ -575,7 +580,7 @@ def nameStrToInt(s: str): prodNode.transferFunds(testAcc, evmAcc, transferAmount, "0x9E126C57330FA71556628e0aabd6B6B6783d99fA", waitForTransBlock=True) row0=prodNode.getTableRow(evmAcc.name, evmAcc.name, "balances", 0) Utils.Print("\tAfter transfer table row:", row0) - assert(row0["balance"]["balance"] == "0.0300 {0}".format(CORE_SYMBOL)) # should have fee from all three transfers + assert(row0["balance"]["balance"] == "1.0300 {0}".format(CORE_SYMBOL)) # should have fee from all three transfers evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) expectedAmount="60000149.9563 {0}".format(CORE_SYMBOL) @@ -586,9 +591,9 @@ def nameStrToInt(s: str): Utils.Print("\tTest Account balance %s" % testAccActualAmount) if testAccActualAmount != expectedAmount: Utils.errorExit("Transfer verification failed. Excepted %s, actual: %s" % (expectedAmount, testAccActualAmount)) - row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test - assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") - assert(row5["balance"] == "0000000000000000000000000000000000000000000000024c9d822e105f8000") # 0x24c9d822e105f8000 => 42414200000000000000 (42.4242 - 0.0100) + row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + assert(row4["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row4["balance"] == "0000000000000000000000000000000000000000000000024c9d822e105f8000") # 0x24c9d822e105f8000 => 42414200000000000000 (42.4242 - 0.0100) # EVM -> EOS # 0x9E126C57330FA71556628e0aabd6B6B6783d99fA private key: 0xba8c9ff38e4179748925335a9891b969214b37dc3723a1754b8b849d3eea9ac0 @@ -601,7 +606,6 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), @@ -609,13 +613,13 @@ def nameStrToInt(s: str): data=b'', chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) prodNode.waitForTransBlockIfNeeded(trans[1], True) - row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test - Utils.Print("\taccount row5: ", row5) - assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") - assert(row5["balance"] == "0000000000000000000000000000000000000000000000019661c2670d48edf8") # 0x19661c2670d48edf8 => 29282899999999979000 (42.4242 - 0.0100 - 13.131313 - gas 21000wei) + row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + Utils.Print("\taccount row4: ", row4) + assert(row4["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row4["balance"] == "000000000000000000000000000000000000000000000001966103689de22000") # 0x1966103689de22000 => 29282690000000000000 (42.4242 - 0.0100 - 13.1313 - 21000 * 10^10) expectedAmount="60000136.8250 {0}".format(CORE_SYMBOL) evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) @@ -635,7 +639,6 @@ def nameStrToInt(s: str): gasP = getGasPrice() signed_trx = w3.eth.account.sign_transaction(dict( nonce=nonce, - # maxFeePerGas=150000000000, #150 GWei gas=100000, #100k Gas gasPrice=gasP, to=Web3.toChecksumAddress(toAdd), @@ -643,13 +646,13 @@ def nameStrToInt(s: str): data=b'', chainId=evmChainId ), evmSendKey) - actData = {"ram_payer":"evmevmevmevm", "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} - trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p evmevmevmevm', silentErrors=True, force=True) + actData = {"miner":minerAcc.name, "rlptx":Web3.toHex(signed_trx.rawTransaction)[2:]} + trans = prodNode.pushMessage(evmAcc.name, "pushtx", json.dumps(actData), '-p {0}'.format(minerAcc.name), silentErrors=True, force=True) prodNode.waitForTransBlockIfNeeded(trans[1], True) - row5=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 5) # 5th balance of this integration test - Utils.Print("\taccount row5: ", row5) - assert(row5["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") - assert(row5["balance"] == "00000000000000000000000000000000000000000000000188810bb365e49bf0") # 0x188810bb365e49bf0 => 28282899999999958000 (42.4242 - 0.0100 - 13.131313 - 1.0000 - 2*gas 21000wei) + row4=prodNode.getTableRow(evmAcc.name, evmAcc.name, "account", 4) # 4th balance of this integration test + Utils.Print("\taccount row4: ", row4) + assert(row4["eth_address"] == "9e126c57330fa71556628e0aabd6b6b6783d99fa") + assert(row4["balance"] == "000000000000000000000000000000000000000000000001887f8db687170000") # 0x1887f8db687170000 => 28282480000000000000 (42.4242 - 0.0100 - 13.1313 - 1.0000 - 2 * 21000 * 10^10) expectedAmount="60000135.8250 {0}".format(CORE_SYMBOL) evmAccActualAmount=prodNode.getAccountEosBalanceStr(evmAcc.name) Utils.Print("\tEVM Account balance %s" % evmAccActualAmount) From ef25965a4fd67ecf39c4844d6714fcc4e6a3840a Mon Sep 17 00:00:00 2001 From: yarkin Date: Fri, 17 Mar 2023 16:49:48 +0800 Subject: [PATCH 51/70] Add sanity check before processing error message. --- peripherals/tx_wrapper/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/peripherals/tx_wrapper/index.js b/peripherals/tx_wrapper/index.js index e2e4f02d..6cbac612 100644 --- a/peripherals/tx_wrapper/index.js +++ b/peripherals/tx_wrapper/index.js @@ -142,11 +142,14 @@ async function eth_estimateGas(params) { } ); } 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); - } + if (err.details && err.details[0] && err.details[0].messge) { + 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 } From 3e417518aa63b930aad6e4c509491a7bcfddcefb Mon Sep 17 00:00:00 2001 From: Matias Romeo Date: Thu, 16 Mar 2023 19:18:02 -0300 Subject: [PATCH 52/70] Add tests for BLOCKHASH instruction --- contract/tests/CMakeLists.txt | 1 + contract/tests/blockhash_tests.cpp | 115 ++++++++++++++++++ contract/tests/native_token_tester.hpp | 95 +++++++++++++++ contract/tests/native_token_tests.cpp | 64 +--------- .../contracts/Blockhash.sol | 42 +++++++ .../nodeos_trust_evm_server/hardhat.config.js | 11 ++ .../scripts/deploy-blockhash.js | 25 ++++ 7 files changed, 290 insertions(+), 63 deletions(-) create mode 100644 contract/tests/blockhash_tests.cpp create mode 100644 contract/tests/native_token_tester.hpp create mode 100644 tests/leap/nodeos_trust_evm_server/contracts/Blockhash.sol create mode 100644 tests/leap/nodeos_trust_evm_server/scripts/deploy-blockhash.js diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index f16f1c12..59ec8224 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -31,6 +31,7 @@ add_eosio_test_executable( unit_test ${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 diff --git a/contract/tests/blockhash_tests.cpp b/contract/tests/blockhash_tests.cpp new file mode 100644 index 00000000..9f709a54 --- /dev/null +++ b/contract/tests/blockhash_tests.cpp @@ -0,0 +1,115 @@ +#include "native_token_tester.hpp" +#include + +struct blockhash_evm_tester : native_token_evm_tester_EOS { + static constexpr eosio::chain::name evm_account_name = "evm"_n; +}; + +struct evm_account { + uint64_t id; + std::vector eth_address; + uint64_t nonce; + std::vector balance; + std::vector code; + std::vector code_hash; +}; +FC_REFLECT(evm_account, (id)(eth_address)(nonce)(balance)(code)(code_hash)); + +struct evm_storage { + uint64_t id; + std::vector key; + std::vector value; +}; +FC_REFLECT(evm_storage, (id)(key)(value)); + +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 = "608060405234801561001057600080fd5b506102e8806100206000396000f3fe608060405234801561001057600080fd5b506004361061007c5760003560e01c8063c059239b1161005b578063c059239b146100c7578063c835de3c146100e5578063edb572a814610103578063f9943638146101215761007c565b80627da6cb146100815780630f59f83a1461009f5780638603136b146100a9575b600080fd5b61008961013f565b6040516100969190610200565b60405180910390f35b6100a7610149565b005b6100b16101b6565b6040516100be9190610200565b60405180910390f35b6100cf6101c0565b6040516100dc9190610200565b60405180910390f35b6100ed6101ca565b6040516100fa9190610200565b60405180910390f35b61010b6101d4565b6040516101189190610200565b60405180910390f35b6101296101de565b6040516101369190610234565b60405180910390f35b6000600454905090565b4360008190555060014361015d919061027e565b40600181905550600243610171919061027e565b40600281905550600343610185919061027e565b40600381905550600443610199919061027e565b406004819055506005436101ad919061027e565b40600581905550565b6000600154905090565b6000600554905090565b6000600354905090565b6000600254905090565b60008054905090565b6000819050919050565b6101fa816101e7565b82525050565b600060208201905061021560008301846101f1565b92915050565b6000819050919050565b61022e8161021b565b82525050565b60006020820190506102496000830184610225565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102898261021b565b91506102948361021b565b92508282039050818111156102ac576102ab61024f565b5b9291505056fea2646970667358221220c437dcf9206268de785ce81fef2e29e3da1e58070205eb27bc943f73938bd29264736f6c63430008110033"; + + // 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()); + + silkworm::Transaction txn { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 1, + .gas_limit = 500000, + .data = silkworm::from_hex(blockhash_bytecode).value() + }; + + evm1.sign(txn); + pushtx(txn); + + produce_blocks(50); + + // Get blockhash contract address + auto contract_addr = silkworm::create_address(evm1.address, 0); + + // Call method "go" on blockhash contract (sha3('go()') = 0x0f59f83a) + // This method will store the current EVM block number and the 5 previous block hashes + silkworm::Transaction txn2 { + .type = silkworm::Transaction::Type::kLegacy, + .max_priority_fee_per_gas = 0, + .max_fee_per_gas = 1, + .gas_limit = 500000, + .to = contract_addr, + .data = silkworm::from_hex("0x0f59f83a").value() + }; + + evm1.sign(txn2); + pushtx(txn2); + + // NOTE/TODO: Hacky way to query contract storage. We should change this by having a new contract action + // that executes readonly functions, or refactor evm_runtime_tests and extract the silkworm::State + // implementation that works agains the leap db + + uint64_t contract_account_id=0; + while(true) { + auto a = fc::raw::unpack(get_row_by_account("evm"_n, "evm"_n, "account"_n, eosio::chain::name{contract_account_id})); + if(fc::to_hex((char*)a.eth_address.data(), a.eth_address.size()) == fc::to_hex((char*)contract_addr.bytes, 20)) break; + contract_account_id++; + } + + // 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 + // .. + + // 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()); + }; + + uint64_t curr_block; + for(size_t slot=0; slot<6; ++slot) { + auto sid = 5 - slot; + auto s = fc::raw::unpack(get_row_by_account("evm"_n, eosio::chain::name{contract_account_id}, "storage"_n, eosio::chain::name{sid})); + dlog("slot: ${slot}, k: ${k}, v: ${v}", ("slot",slot)("k",s.key)("v",s.value)); + + if(slot == 0) { + curr_block = static_cast(intx::be::unsafe::load((const uint8_t*)s.value.data())); + } else { + auto retrieved_block_hash = fc::to_hex(s.value.data(), s.value.size()); + auto calculated_block_hash = generate_block_hash(curr_block-slot); + 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/native_token_tester.hpp b/contract/tests/native_token_tester.hpp new file mode 100644 index 00000000..fef28e62 --- /dev/null +++ b/contract/tests/native_token_tester.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include "basic_evm_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 vault_balance(name owner) const { + const vector d = get_row_by_account("evm"_n, "evm"_n, "balances"_n, owner); + FC_ASSERT(d.size(), "EVM not open"); + auto [_, amount, dust] = fc::raw::unpack(d); + return std::make_tuple(amount, dust); + } + int64_t vault_balance_token(name owner) const { + return std::get<0>(vault_balance(owner)).get_amount(); + } + uint64_t vault_balance_dust(name owner) const { + return std::get<1>(vault_balance(owner)); + } + + transaction_trace_ptr open(name owner) { + return push_action("evm"_n, "open"_n, owner, mvo()("owner", owner)); + } + 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 vault_balance_row { + name owner; + asset balance; + uint64_t dust = 0; + }; + + evmc::address make_reserved_address(uint64_t account) const { + 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)}); + } +}; +FC_REFLECT(native_token_evm_tester::vault_balance_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) {} +}; diff --git a/contract/tests/native_token_tests.cpp b/contract/tests/native_token_tests.cpp index 795b9558..e35f3087 100644 --- a/contract/tests/native_token_tests.cpp +++ b/contract/tests/native_token_tests.cpp @@ -14,68 +14,6 @@ static const char do_nothing_wast[] = R"=====( ) )====="; -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) {} -}; - BOOST_AUTO_TEST_SUITE(native_token_evm_tests) BOOST_FIXTURE_TEST_CASE(basic_deposit_withdraw, native_token_evm_tester_EOS) try { @@ -478,4 +416,4 @@ BOOST_FIXTURE_TEST_CASE(evm_eos_disallow_reserved_zero, native_token_evm_tester_ eosio_assert_message_exception, eosio_assert_message_is("reserved 0 address cannot be used")); } FC_LOG_AND_RETHROW() -BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file +BOOST_AUTO_TEST_SUITE_END() 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