diff --git a/include/evm_runtime/bridge.hpp b/include/evm_runtime/bridge.hpp new file mode 100644 index 00000000..d931cecd --- /dev/null +++ b/include/evm_runtime/bridge.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include + +namespace evm_runtime { namespace bridge { + +struct message_v0 { + static constexpr uint32_t id = 0xf781185b; //sha3('bridgeMsgV0(string,bool,bytes)')[:4] + + string account; + bool force_atomic; //currently only atomic is supported + bytes data; + + name get_account_as_name() const { + if(!account_.has_value()) { + account_ = name{account}; + eosio::check(account_.value().to_string() == account, "invalid account name"); + } + return *account_; + } + + mutable std::optional account_; +}; + +using message = std::variant; + +message_v0 decode_message_v0(eosio::datastream& ds) { + // offset_p1 (32) + p2_value (32) + offset_p3 (32) + // p1_len (32) + p1_data ((p1_len+31)/32*32) + // p3_len (32) + p3_data ((p2_len+31)/32*32) + uint256 offset_p1, value_p2, offset_p3; + + ds >> offset_p1; + eosio::check(offset_p1 == 0x60, "invalid p1 offset"); + ds >> value_p2; + eosio::check(value_p2 <= 1, "invalid p2 value"); + ds >> offset_p3; + eosio::check(offset_p3 == 0xA0, "invalid p3 offset"); + + message_v0 res; + res.force_atomic = value_p2 ? true : false; + + auto get_length=[&]() -> uint32_t { + uint256 len; + ds >> len; + eosio::check(len < std::numeric_limits::max(), "invalid length"); + return static_cast(len); + }; + + uint32_t p1_len = get_length(); + auto p1_len_32 = (p1_len+31)/32*32; + res.account.resize(p1_len_32); + ds.read(res.account.data(), p1_len_32); + res.account.resize(p1_len); + + uint32_t p3_len = get_length(); + auto p3_len_32 = (p3_len+31)/32*32; + res.data.resize(p3_len_32); + ds.read(res.data.data(), p3_len_32); + res.data.resize(p3_len); + + return res; +} + +std::optional decode_message(ByteView bv) { + // method_id (4) + eosio::datastream ds(bv.data(), bv.size()); + uint32_t method_id; + ds >> method_id; + + if(method_id == __builtin_bswap32(message_v0::id)) return decode_message_v0(ds); + return {}; +} + +} //namespace bridge +} //namespace evm_runtime diff --git a/include/evm_runtime/evm_contract.hpp b/include/evm_runtime/evm_contract.hpp index e52669fa..5ae8b204 100644 --- a/include/evm_runtime/evm_contract.hpp +++ b/include/evm_runtime/evm_contract.hpp @@ -88,6 +88,9 @@ class [[eosio::contract]] evm_contract : public contract [[eosio::action]] void call(eosio::name from, const bytes& to, const bytes& value, const bytes& data, uint64_t gas_limit); [[eosio::action]] void admincall(const bytes& from, const bytes& to, const bytes& value, const bytes& data, uint64_t gas_limit); + [[eosio::action]] void bridgereg(eosio::name receiver, const eosio::asset& min_fee); + [[eosio::action]] void bridgeunreg(eosio::name receiver); + #ifdef WITH_TEST_ACTIONS [[eosio::action]] void testtx(const std::optional& orlptx, const evm_runtime::test::block_info& bi); [[eosio::action]] void @@ -136,6 +139,7 @@ class [[eosio::contract]] evm_contract : public contract } silkworm::Receipt execute_tx(eosio::name miner, silkworm::Block& block, silkworm::Transaction& tx, silkworm::ExecutionProcessor& ep, bool enforce_chain_id); + void process_filtered_messages(const std::vector& filtered_messages); uint64_t get_and_increment_nonce(const name owner); diff --git a/include/evm_runtime/print_tracer.hpp b/include/evm_runtime/print_tracer.hpp index ef92a023..6f7594ec 100644 --- a/include/evm_runtime/print_tracer.hpp +++ b/include/evm_runtime/print_tracer.hpp @@ -1,8 +1,9 @@ #pragma once #include +#include namespace evm_runtime { -struct print_tracer : EvmTracer { +struct print_tracer : silkworm::EvmTracer { const char* const* opcode_names_ = nullptr; @@ -19,27 +20,28 @@ struct print_tracer : EvmTracer { } void on_instruction_start(uint32_t pc, const intx::uint256* stack_top, int stack_height, - const evmone::ExecutionState& state, - const IntraBlockState& intra_block_state) noexcept override { - const auto opcode = state.code[pc]; + int64_t gas, const evmone::ExecutionState& state, + const silkworm::IntraBlockState& intra_block_state) override { + + const auto opcode = state.original_code[pc]; auto opcode_name = get_opcode_name(opcode_names_, opcode); eosio::print(opcode_name.c_str(), "\n"); } - void on_execution_end(const evmc_result& result, const IntraBlockState& intra_block_state) noexcept override { + void on_execution_end(const evmc_result& result, const silkworm::IntraBlockState& intra_block_state) noexcept override { eosio::print("TRACE: end\n"); } - void on_creation_completed(const evmc_result& result, const IntraBlockState& intra_block_state) noexcept override { + void on_creation_completed(const evmc_result& result, const silkworm::IntraBlockState& intra_block_state) noexcept override { } void on_precompiled_run(const evmc_result& result, int64_t gas, - const IntraBlockState& intra_block_state) noexcept override { + const silkworm::IntraBlockState& intra_block_state) noexcept override { } - void on_reward_granted(const CallResult& result, const IntraBlockState& intra_block_state) noexcept override { + void on_reward_granted(const silkworm::CallResult& result, const silkworm::IntraBlockState& intra_block_state) noexcept override { } diff --git a/include/evm_runtime/tables.hpp b/include/evm_runtime/tables.hpp index 205a2d4c..c44b6768 100644 --- a/include/evm_runtime/tables.hpp +++ b/include/evm_runtime/tables.hpp @@ -182,4 +182,24 @@ struct [[eosio::table]] [[eosio::contract("evm_contract")]] allowed_egress_accou typedef eosio::multi_index<"egresslist"_n, allowed_egress_account> egresslist; -} //namespace evm_runtime \ No newline at end of file +struct [[eosio::table]] [[eosio::contract("evm_contract")]] message_receiver { + + enum flag : uint32_t { + FORCE_ATOMIC = 0x1 + }; + + name account; + asset min_fee; + uint32_t flags; + + uint64_t primary_key() const { return account.value; } + bool has_flag(flag f) const { + return (flags & f) != 0; + } + + EOSLIB_SERIALIZE(message_receiver, (account)(min_fee)(flags)); +}; + +typedef eosio::multi_index<"msgreceiver"_n, message_receiver> message_receiver_table; + +} //namespace evm_runtime diff --git a/include/evm_runtime/types.hpp b/include/evm_runtime/types.hpp index aa050640..50a6293c 100644 --- a/include/evm_runtime/types.hpp +++ b/include/evm_runtime/types.hpp @@ -29,7 +29,7 @@ namespace evm_runtime { eosio::checksum256 make_key(bytes data); eosio::checksum256 make_key(const evmc::address& addr); eosio::checksum256 make_key(const evmc::bytes32& data); - + bytes to_bytes(const uint256& val); bytes to_bytes(const evmc::bytes32& val); bytes to_bytes(const evmc::address& addr); @@ -63,6 +63,18 @@ namespace evm_runtime { EOSLIB_SERIALIZE(exec_output, (status)(data)(context)); }; + struct bridge_message_v0 { + eosio::name receiver; + bytes sender; + eosio::time_point timestamp; + bytes value; + bytes data; + + EOSLIB_SERIALIZE(bridge_message_v0, (receiver)(sender)(timestamp)(value)(data)); + }; + + using bridge_message = std::variant; + } //namespace evm_runtime namespace eosio { diff --git a/silkworm b/silkworm index d661be9a..77e88a62 160000 --- a/silkworm +++ b/silkworm @@ -1 +1 @@ -Subproject commit d661be9a624505fb769e9c964ab5ba995e56c94a +Subproject commit 77e88a620a8ba3421c7d7ffd2080e700352e6ba1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 32f09944..b6256d19 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,5 +85,5 @@ target_compile_options(evm_runtime PUBLIC --no-missing-ricardian-clause) if (WITH_LARGE_STACK) target_link_options(evm_runtime PUBLIC --stack-size=50000000) else() - target_link_options(evm_runtime PUBLIC --stack-size=38560) + target_link_options(evm_runtime PUBLIC --stack-size=38432) endif() diff --git a/src/actions.cpp b/src/actions.cpp index 116bf5c9..8f325b7f 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -384,6 +385,61 @@ void evm_contract::exec(const exec_input& input, const std::optional& filtered_messages ) { + + intx::uint256 accumulated_value; + for(const auto& rawmsg : filtered_messages) { + auto msg = bridge::decode_message(ByteView{rawmsg.data}); + eosio::check(msg.has_value(), "unable to decode bridge message"); + + auto& msg_v0 = std::get(msg.value()); + eosio::print("FIL MESSAGE: ", uint64_t(msg_v0.force_atomic), "\n"); + + const auto& receiver = msg_v0.get_account_as_name(); + eosio::check(eosio::is_account(receiver), "receiver is not account"); + + message_receiver_table message_receivers(get_self(), get_self().value); + auto it = message_receivers.find(receiver.value); + eosio::check(it != message_receivers.end(), "receiver not registered"); + + eosio::check(msg_v0.force_atomic == false || it->has_flag(message_receiver::FORCE_ATOMIC), "unable to process message"); + + intx::uint256 min_fee((uint64_t)it->min_fee.amount); + min_fee *= minimum_natively_representable; + + auto value = intx::be::unsafe::load(rawmsg.value.bytes); + eosio::check(value >= min_fee, "min_fee not covered"); + + balances balance_table(get_self(), get_self().value); + const balance& receiver_account = balance_table.get(msg_v0.get_account_as_name().value, "receiver account is not open"); + + action(std::vector{}, msg_v0.get_account_as_name(), "onbridgemsg"_n, + bridge_message{ bridge_message_v0 { + .receiver = msg_v0.get_account_as_name(), + .sender = to_bytes(rawmsg.sender), + .timestamp = eosio::current_time_point(), + .value = to_bytes(rawmsg.value), + .data = msg_v0.data + } } + ).send(); + + balance_table.modify(receiver_account, eosio::same_payer, [&](balance& row) { + row.balance += value; + }); + + accumulated_value += value; + } + + if(accumulated_value > 0) { + balances balance_table(get_self(), get_self().value); + const balance& self_balance = balance_table.get(get_self().value); + balance_table.modify(self_balance, eosio::same_payer, [&](balance& row) { + row.balance -= accumulated_value; + }); + } + +} + void evm_contract::pushtx( eosio::name miner, const bytes& rlptx ) { LOGTIME("EVM START"); @@ -415,8 +471,17 @@ void evm_contract::pushtx( eosio::name miner, const bytes& rlptx ) { 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"); + // Filter EVM messages (with data) that are sent to the reserved address + // corresponding to the EOS account holding the contract (self) + ep.set_evm_message_filter([&](const evmc_message& message) -> bool { + static auto me = make_reserved_address(get_self().value); + return message.recipient == me && message.input_size > 0; + }); + auto receipt = execute_tx(miner, block, tx, ep, true); + process_filtered_messages(ep.state().filtered_messages()); + engine.finalize(ep.state(), ep.evm().block()); ep.state().write_to_db(ep.evm().block().header.number); LOGTIME("EVM END"); @@ -634,6 +699,42 @@ void evm_contract::admincall(const bytes& from, const bytes& to, const bytes& va call_(s, to, v, data, gas_limit, nonce); } +void evm_contract::bridgereg(eosio::name receiver, const eosio::asset& min_fee) { + assert_unfrozen(); + require_auth(receiver); + require_auth(get_self()); // to temporarily prevent registration of unauthorized accounts + + eosio::check(min_fee.symbol == token_symbol, "unexpected symbol"); + eosio::check(min_fee.amount >= 0, "min_fee cannot be negative"); + + auto update_row = [&](auto& row) { + row.account = receiver; + row.min_fee = min_fee; + row.flags = message_receiver::FORCE_ATOMIC; + }; + + message_receiver_table message_receivers(get_self(), get_self().value); + auto it = message_receivers.find(receiver.value); + + if(it == message_receivers.end()) { + message_receivers.emplace(receiver, update_row); + } else { + message_receivers.modify(*it, eosio::same_payer, update_row); + } + + open(receiver); +} + +void evm_contract::bridgeunreg(eosio::name receiver) { + assert_unfrozen(); + require_auth(receiver); + + message_receiver_table message_receivers(get_self(), get_self().value); + auto it = message_receivers.find(receiver.value); + eosio::check(it != message_receivers.end(), "receiver not found"); + message_receivers.erase(*it); +} + #ifdef WITH_TEST_ACTIONS [[eosio::action]] void evm_contract::testtx( const std::optional& orlptx, const evm_runtime::test::block_info& bi ) { assert_unfrozen(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0c91558..2b1ead9f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_eosio_test_executable( unit_test ${CMAKE_SOURCE_DIR}/exec_tests.cpp ${CMAKE_SOURCE_DIR}/call_tests.cpp ${CMAKE_SOURCE_DIR}/chainid_tests.cpp + ${CMAKE_SOURCE_DIR}/bridge_message_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/tests/basic_evm_tester.cpp b/tests/basic_evm_tester.cpp index 5f7121ca..2e3e9352 100644 --- a/tests/basic_evm_tester.cpp +++ b/tests/basic_evm_tester.cpp @@ -139,6 +139,10 @@ evmc::address basic_evm_tester::make_reserved_address(uint64_t account) // clang-format on } +evmc::address basic_evm_tester::make_reserved_address(name account) { + return make_reserved_address(account.to_uint64_t()); +} + basic_evm_tester::basic_evm_tester(std::string native_symbol_str) : native_symbol(symbol::from_string(native_symbol_str)) { @@ -329,7 +333,18 @@ void basic_evm_tester::admincall(const evmc::bytes& from, const evmc::bytes& to, push_action(evm_account_name, "admincall"_n, actor, mvo()("from", from_bytes)("to", to_bytes)("value", value_bytes)("data", data_bytes)("gas_limit", gas_limit)); } -void basic_evm_tester::pushtx(const silkworm::Transaction& trx, name miner) +transaction_trace_ptr basic_evm_tester::bridgereg(name receiver, asset min_fee, vector extra_signers) { + extra_signers.push_back(receiver); + return basic_evm_tester::push_action(evm_account_name, "bridgereg"_n, extra_signers, + mvo()("receiver", receiver)("min_fee", min_fee)); +} + +transaction_trace_ptr basic_evm_tester::bridgeunreg(name receiver) { + return basic_evm_tester::push_action(evm_account_name, "bridgeunreg"_n, receiver, + mvo()("receiver", receiver)); +} + +transaction_trace_ptr basic_evm_tester::pushtx(const silkworm::Transaction& trx, name miner) { silkworm::Bytes rlp; silkworm::rlp::encode(rlp, trx); @@ -338,7 +353,7 @@ void basic_evm_tester::pushtx(const silkworm::Transaction& trx, name miner) 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)); + return 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) @@ -380,6 +395,10 @@ 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::inevm() const { + return fc::raw::unpack(get_row_by_account(evm_account_name, evm_account_name, "inevm"_n, "inevm"_n)); +} + 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); @@ -525,4 +544,38 @@ bool basic_evm_tester::scan_account_storage(uint64_t account_id, std::function visitor) const { + static constexpr eosio::chain::name balances_table_name = "balances"_n; + scan_table( + balances_table_name, evm_account_name, [this, &visitor](vault_balance_row&& row) { + return visitor(row); + } + ); +} + +asset basic_evm_tester::get_eos_balance( const account_name& act ) { + vector data = get_row_by_account( "eosio.token"_n, act, "accounts"_n, name(native_symbol.to_symbol_code().value) ); + return data.empty() ? asset(0, native_symbol) : fc::raw::unpack(data); +} + +void basic_evm_tester::check_balances() { + intx::uint256 total_in_evm_accounts; + scan_accounts([&](account_object&& account) -> bool { + total_in_evm_accounts += account.balance; + return false; + }); + + auto in_evm = intx::uint256(inevm()); + BOOST_REQUIRE(total_in_evm_accounts == in_evm); + + intx::uint256 total_in_accounts; + scan_balances([&](vault_balance_row&& row) -> bool { + total_in_accounts += intx::uint256(balance_and_dust{.balance=row.balance, .dust=row.dust}); + return false; + }); + + auto evm_eos_balance = intx::uint256(balance_and_dust{.balance=get_eos_balance(evm_account_name), .dust=0}); + BOOST_REQUIRE(evm_eos_balance == total_in_accounts+total_in_evm_accounts); +} + +} // namespace evm_test diff --git a/tests/basic_evm_tester.hpp b/tests/basic_evm_tester.hpp index 64303da7..b6e5d1f0 100644 --- a/tests/basic_evm_tester.hpp +++ b/tests/basic_evm_tester.hpp @@ -109,6 +109,22 @@ struct exec_output { std::optional context; }; +struct message_receiver { + name account; + asset min_fee; + uint32_t flags; +}; + +struct bridge_message_v0 { + name receiver; + bytes sender; + time_point timestamp; + bytes value; + bytes data; +}; + +using bridge_message = std::variant; + } // namespace evm_test @@ -123,6 +139,9 @@ FC_REFLECT(evm_test::exec_input, (context)(from)(to)(data)(value)) FC_REFLECT(evm_test::exec_callback, (contract)(action)) FC_REFLECT(evm_test::exec_output, (status)(data)(context)) +FC_REFLECT(evm_test::message_receiver, (account)(min_fee)(flags)); +FC_REFLECT(evm_test::bridge_message_v0, (receiver)(sender)(timestamp)(value)(data)); + namespace evm_test { class evm_eoa { @@ -147,6 +166,7 @@ class evm_eoa std::basic_string public_key; }; +struct vault_balance_row; class basic_evm_tester : public testing::validating_tester { public: @@ -166,6 +186,7 @@ class basic_evm_tester : public testing::validating_tester const symbol native_symbol; static evmc::address make_reserved_address(uint64_t account); + static evmc::address make_reserved_address(name account); explicit basic_evm_tester(std::string native_symbol_str = "4,EOS"); @@ -198,8 +219,10 @@ class basic_evm_tester : public testing::validating_tester silkworm::Transaction generate_tx(const evmc::address& to, const intx::uint256& value, uint64_t gas_limit = 21000) const; + transaction_trace_ptr bridgereg(name receiver, asset min_fee, vector extra_signers={evm_account_name}); + transaction_trace_ptr bridgeunreg(name receiver); transaction_trace_ptr exec(const exec_input& input, const std::optional& callback); - void pushtx(const silkworm::Transaction& trx, name miner = evm_account_name); + transaction_trace_ptr pushtx(const silkworm::Transaction& trx, name miner = evm_account_name); void call(name from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); void admincall(const evmc::bytes& from, const evmc::bytes& to, const evmc::bytes& value, evmc::bytes& data, uint64_t gas_limit, name actor); evmc::address deploy_contract(evm_eoa& eoa, evmc::bytes bytecode); @@ -211,10 +234,15 @@ class basic_evm_tester : public testing::validating_tester void close(name owner); void withdraw(name owner, asset quantity); + balance_and_dust inevm() const; 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; + asset get_eos_balance( const account_name& act ); + + void check_balances(); + template void scan_table(eosio::chain::name table_name, eosio::chain::name scope_name, Visitor&& visitor) const { @@ -244,6 +272,7 @@ class basic_evm_tester : public testing::validating_tester 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; + void scan_balances(std::function visitor) const; }; inline constexpr intx::uint256 operator"" _wei(const char* s) { return intx::from_string(s); } @@ -310,4 +339,4 @@ class speculative_block_starter bool canceled = false; }; -} // namespace evm_test \ No newline at end of file +} // namespace evm_test diff --git a/tests/bridge_message_tests.cpp b/tests/bridge_message_tests.cpp new file mode 100644 index 00000000..c1868dd0 --- /dev/null +++ b/tests/bridge_message_tests.cpp @@ -0,0 +1,328 @@ +#include "basic_evm_tester.hpp" +#include + +#include "utils.hpp" + +using intx::operator""_u256; + +using namespace evm_test; +using eosio::testing::eosio_assert_message_is; +using eosio::testing::expect_assert_message; + +struct bridge_message_tester : basic_evm_tester { + + static constexpr const char* bridgeMsgV0_method_id = "f781185b"; + + bridge_message_tester() { + create_accounts({"alice"_n}); + transfer_token(faucet_account_name, "alice"_n, make_asset(10000'0000)); + init(); + open("alice"_n); + } + + ~bridge_message_tester() { + dlog("~bridge_message_tester()"); + check_balances(); + } + + std::string int_str32(uint32_t x) { + std::stringstream hex_ss; + hex_ss << std::hex << x; + int hex_length = hex_ss.str().length(); + + std::stringstream ss; + ss << std::setfill('0') << std::setw(64 - hex_length) << 0 << std::hex << std::uppercase << x; + return ss.str(); + } + + std::string str_to_hex(const std::string& str) { + std::stringstream ss; + for (char c : str) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(c); + } + return ss.str(); + } + + std::string data_str32(const std::string& str) { + std::stringstream ss; + ss << str; + int ps = 64 - (str.length() % 64); + if (ps == 64) {ps = 0;} + ss << std::setw(ps) << std::setfill('0') << ""; + return ss.str(); + } + + transaction_trace_ptr send_bridge_message(evm_eoa& eoa, const std::string& receiver, const intx::uint256& value, const std::string& str_data) { + + silkworm::Bytes data; + data += evmc::from_hex(bridgeMsgV0_method_id).value(); + data += evmc::from_hex(int_str32(96)).value(); //offset param1 (receiver: string) + data += evmc::from_hex(int_str32(1)).value(); //param2 (true) (force_atomic: bool) + data += evmc::from_hex(int_str32(160)).value(); //offset param3 (data: bytes) + data += evmc::from_hex(int_str32(receiver.length())).value(); //param1 length + data += evmc::from_hex(data_str32(str_to_hex(receiver))).value(); //param1 data + data += evmc::from_hex(int_str32(str_data.size()/2)).value(); //param3 length + data += evmc::from_hex(data_str32(str_data)).value(); //param3 data + + return send_raw_message(eoa, make_reserved_address(evm_account_name), value, data); + } + + transaction_trace_ptr send_raw_message(evm_eoa& eoa, const evmc::address& dest, const intx::uint256& value, const silkworm::Bytes& data) { + auto txn = generate_tx(dest, value, 250'000); + txn.data = data; + eoa.sign(txn); + return pushtx(txn, "alice"_n); + } +}; + +BOOST_AUTO_TEST_SUITE(bridge_message_tests) +BOOST_FIXTURE_TEST_CASE(bridge_register_test, bridge_message_tester) try { + + create_accounts({"rec1"_n}); + + // evm auth is needed + BOOST_REQUIRE_EXCEPTION(bridgereg("rec1"_n, make_asset(-1), {}), + missing_auth_exception, [](const missing_auth_exception& e) { return expect_assert_message(e, "missing authority of evm"); }); + + // min fee only accepts EOS + BOOST_REQUIRE_EXCEPTION(bridgereg("rec1"_n, asset::from_string("1.0000 TST")), + eosio_assert_message_exception, eosio_assert_message_is("unexpected symbol")); + + // min fee must be non-negative + BOOST_REQUIRE_EXCEPTION(bridgereg("rec1"_n, make_asset(-1)), + eosio_assert_message_exception, eosio_assert_message_is("min_fee cannot be negative")); + + bridgereg("rec1"_n, make_asset(0)); + + auto row = fc::raw::unpack(get_row_by_account( evm_account_name, evm_account_name, "msgreceiver"_n, "rec1"_n)); + BOOST_REQUIRE(row.account == "rec1"_n); + BOOST_REQUIRE(row.min_fee == make_asset(0)); + BOOST_REQUIRE(row.flags == 0x1); + + // Register again changing min fee + bridgereg("rec1"_n, make_asset(1)); + + row = fc::raw::unpack(get_row_by_account( evm_account_name, evm_account_name, "msgreceiver"_n, "rec1"_n)); + BOOST_REQUIRE(row.account == "rec1"_n); + BOOST_REQUIRE(row.min_fee == make_asset(1)); + BOOST_REQUIRE(row.flags == 0x1); + + // Unregister rec1 + bridgeunreg("rec1"_n); + const auto& d = get_row_by_account( evm_account_name, evm_account_name, "msgreceiver"_n, "rec1"_n); + BOOST_REQUIRE(d.size() == 0); + produce_block(); + + // Try to unregister again + BOOST_REQUIRE_EXCEPTION(bridgeunreg("rec1"_n), + eosio_assert_message_exception, eosio_assert_message_is("receiver not found")); + +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(basic_tests, bridge_message_tester) try { + + // Create destination account + create_accounts({"rec1"_n}); + set_code("rec1"_n, testing::contracts::evm_bridge_receiver_wasm()); + set_abi("rec1"_n, testing::contracts::evm_bridge_receiver_abi().data()); + + // Register rec1 with 1000.0000 EOS as min_fee + bridgereg("rec1"_n, make_asset(1000'0000)); + + // Fund evm1 address with 1001 EOS + evm_eoa evm1; + const int64_t to_bridge = 1001'0000; + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + // Check rec1 balance before sending the message + BOOST_REQUIRE(vault_balance("rec1"_n) == (balance_and_dust{make_asset(0), 0})); + + // Emit message + auto res = send_bridge_message(evm1, "rec1", 1000_ether, "0102030405060708090a"); + + // Check rec1 balance after sending the message + BOOST_REQUIRE(vault_balance("rec1"_n) == (balance_and_dust{make_asset(1000'0000), 0})); + + // Recover message form the return value of rec1 contract + BOOST_CHECK(res->action_traces[1].receiver == "rec1"_n); + auto msgout = fc::raw::unpack(res->action_traces[1].return_value); + auto out = std::get(msgout); + + BOOST_CHECK(out.receiver == "rec1"_n); + BOOST_CHECK(out.sender == to_bytes(evm1.address)); + BOOST_CHECK(out.timestamp.time_since_epoch() == control->pending_block_time().time_since_epoch()); + BOOST_CHECK(out.value == to_bytes(1000_ether)); + BOOST_CHECK(out.data == to_bytes(evmc::from_hex("0102030405060708090a").value())); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(test_bridge_errors, bridge_message_tester) try { + + auto emv_reserved_address = make_reserved_address(evm_account_name); + + // Fund evm1 address with 100 EOS + evm_eoa evm1; + const int64_t to_bridge = 1000000; + transfer_token("alice"_n, evm_account_name, make_asset(to_bridge), evm1.address_0x()); + + // Send message without call data [ok] + send_raw_message(evm1, emv_reserved_address, 0, silkworm::Bytes{}); + + // Send message with 1 byte call data (we need at least 4 bytes for the function signature) + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, evmc::from_hex("01").value()), + eosio_assert_message_exception, eosio_assert_message_is("datastream attempted to read past the end")); + evm1.next_nonce--; + + // Send message with 4 bytes NOT matching the 'bridgeMsgV0(string,bool,bytes)' signature + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, evmc::from_hex("01020304").value()), + eosio_assert_message_exception, eosio_assert_message_is("unable to decode bridge message")); + evm1.next_nonce--; + + // Send message with 4 bytes matching the 'bridgeMsgV0(string,bool,bytes)' signature + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, evmc::from_hex(bridgeMsgV0_method_id).value()), + eosio_assert_message_exception, eosio_assert_message_is("datastream attempted to read past the end")); + evm1.next_nonce--; + + // Wrong p1 offset + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(11)).value()), + eosio_assert_message_exception, eosio_assert_message_is("invalid p1 offset")); + evm1.next_nonce--; + + // Wrong p2 value + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value() + + evmc::from_hex(int_str32(11)).value()), + eosio_assert_message_exception, eosio_assert_message_is("invalid p2 value")); + evm1.next_nonce--; + + // Wrong p3 offset + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value()+ + evmc::from_hex(int_str32(1)).value()+ + evmc::from_hex(int_str32(11)).value()), + eosio_assert_message_exception, eosio_assert_message_is("invalid p3 offset")); + evm1.next_nonce--; + + // abcd is not an account + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value() + + evmc::from_hex(int_str32(1)).value() + + evmc::from_hex(int_str32(160)).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("abcd"))).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("data"))).value()), + eosio_assert_message_exception, eosio_assert_message_is("receiver is not account")); + evm1.next_nonce--; + + // Create destination account + create_accounts({"abcd"_n}); + + // abcd not registered + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value() + + evmc::from_hex(int_str32(1)).value() + + evmc::from_hex(int_str32(160)).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("abcd"))).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("data"))).value()), + eosio_assert_message_exception, eosio_assert_message_is("receiver not registered")); + evm1.next_nonce--; + + // Register abcd + bridgereg("abcd"_n, make_asset(1)); + + // min fee not covered + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 0, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value() + + evmc::from_hex(int_str32(1)).value() + + evmc::from_hex(int_str32(160)).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("abcd"))).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("data"))).value()), + eosio_assert_message_exception, eosio_assert_message_is("min_fee not covered")); + evm1.next_nonce--; + + // Close abcd balance + close("abcd"_n); + + // receiver account is not open + BOOST_REQUIRE_EXCEPTION(send_raw_message(evm1, emv_reserved_address, 1e14, + evmc::from_hex(bridgeMsgV0_method_id).value() + + evmc::from_hex(int_str32(96)).value() + + evmc::from_hex(int_str32(1)).value() + + evmc::from_hex(int_str32(160)).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("abcd"))).value() + + evmc::from_hex(int_str32(4)).value() + + evmc::from_hex(data_str32(str_to_hex("data"))).value()), + eosio_assert_message_exception, eosio_assert_message_is("receiver account is not open")); +} FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(test_send_message_from_solidity, bridge_message_tester) try { + // // SPDX-License-Identifier: GPL-3.0 + // pragma solidity >=0.7.0 <0.9.0; + // contract Emiter { + // function go(string memory destination, bool force_atomic, uint256 n) public { + // address eosevm = 0xBbBBbbBBbBbbbBBBbBbbBBbB56E4000000000000; + // for(uint i=0; i go('rec1', true, 3) + auto txn = generate_tx(contract_addr, 0, 500'000); + txn.data = evmc::from_hex("e1963a31").value(); + txn.data += evmc::from_hex(int_str32(96)).value(); //offset of param1 + txn.data += evmc::from_hex(int_str32(1)).value(); //param2 + txn.data += evmc::from_hex(int_str32(3)).value(); //param3 + txn.data += evmc::from_hex(int_str32(4)).value(); //param1 size + txn.data += evmc::from_hex(data_str32(str_to_hex("rec1"))).value(); //param1 data + evm1.sign(txn); + + auto res = pushtx(txn); + BOOST_CHECK(res->action_traces.size() == 4); + + for(int i=0; i<3; ++i) { + auto msgout = fc::raw::unpack(res->action_traces[1+i].return_value); + auto out = std::get(msgout); + + BOOST_CHECK(out.receiver == "rec1"_n); + BOOST_CHECK(out.sender == to_bytes(contract_addr)); + BOOST_CHECK(out.timestamp.time_since_epoch() == control->pending_block_time().time_since_epoch()); + BOOST_CHECK(out.value == to_bytes(0_ether)); + BOOST_CHECK(out.data == to_bytes(evmc::from_hex("00000000000000000000000000000000000000000000000000000000FFFFFF0"+std::to_string(i)).value())); + } +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/tests/contracts.hpp.in b/tests/contracts.hpp.in index 7b308181..7610a3f9 100644 --- a/tests/contracts.hpp.in +++ b/tests/contracts.hpp.in @@ -40,6 +40,9 @@ struct contracts { static std::vector evm_read_callback_wasm() { return read_wasm("${CMAKE_CURRENT_SOURCE_DIR}/contracts/evm_read_callback/evm_read_callback.wasm"); } static std::vector evm_read_callback_abi() { return read_abi("${CMAKE_CURRENT_SOURCE_DIR}/contracts/evm_read_callback/evm_read_callback.abi"); } + static std::vector evm_bridge_receiver_wasm() { return read_wasm("${CMAKE_CURRENT_SOURCE_DIR}/contracts/evm_bridge_receiver/evm_bridge_receiver.wasm"); } + static std::vector evm_bridge_receiver_abi() { return read_abi("${CMAKE_CURRENT_SOURCE_DIR}/contracts/evm_bridge_receiver/evm_bridge_receiver.abi"); } + static std::string eth_test_folder() { return "${CMAKE_CURRENT_SOURCE_DIR}/../silkworm/third_party/tests"; } diff --git a/tests/contracts/evm_bridge_receiver/README.txt b/tests/contracts/evm_bridge_receiver/README.txt new file mode 100644 index 00000000..5bd9b7bb --- /dev/null +++ b/tests/contracts/evm_bridge_receiver/README.txt @@ -0,0 +1,4 @@ + --- evm_bridge_receiver Project --- + + - How to Build - + - run the command 'cdt-cpp -abigen -o evm_bridge_receiver.wasm evm_bridge_receiver.cpp' diff --git a/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.abi b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.abi new file mode 100644 index 00000000..d3c52c8b --- /dev/null +++ b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.abi @@ -0,0 +1,64 @@ +{ + "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", + "version": "eosio::abi/1.2", + "types": [ + { + "new_type_name": "bridge_message", + "type": "variant_bridge_message_v0" + } + ], + "structs": [ + { + "name": "bridge_message_v0", + "base": "", + "fields": [ + { + "name": "receiver", + "type": "name" + }, + { + "name": "sender", + "type": "bytes" + }, + { + "name": "timestamp", + "type": "time_point" + }, + { + "name": "value", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + } + ] + }, + { + "name": "onbridgemsg", + "base": "", + "fields": [ + { + "name": "msg", + "type": "bridge_message" + } + ] + } + ], + "actions": [ + { + "name": "onbridgemsg", + "type": "onbridgemsg", + "ricardian_contract": "" + } + ], + "tables": [], + "ricardian_clauses": [], + "variants": [ + { + "name": "variant_bridge_message_v0", + "types": ["bridge_message_v0"] + } + ], + "action_results": [] +} \ No newline at end of file diff --git a/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.contracts.md b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.contracts.md new file mode 100644 index 00000000..5c7d6556 --- /dev/null +++ b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.contracts.md @@ -0,0 +1,3 @@ +

hi

+ +Stub for hi action's ricardian contract \ No newline at end of file diff --git a/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.cpp b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.cpp new file mode 100644 index 00000000..92854950 --- /dev/null +++ b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.cpp @@ -0,0 +1,11 @@ +#include "evm_bridge_receiver.hpp" + +extern "C" { +__attribute__((eosio_wasm_import)) +void set_action_return_value(void*, size_t); +} + +void evm_bridge_receiver::onbridgemsg(const bridge_message& msg) { + auto output_bin = eosio::pack(msg); + set_action_return_value(output_bin.data(), output_bin.size()); +} diff --git a/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.hpp b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.hpp new file mode 100644 index 00000000..af914e11 --- /dev/null +++ b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.hpp @@ -0,0 +1,25 @@ +#include +#include +#include + +using namespace eosio; + +typedef std::vector bytes; + +struct bridge_message_v0 { + name receiver; + bytes sender; + time_point timestamp; + bytes value; + bytes data; + + EOSLIB_SERIALIZE(bridge_message_v0, (receiver)(sender)(timestamp)(value)(data)); +}; + +using bridge_message = std::variant; + +CONTRACT evm_bridge_receiver : public contract { + public: + using contract::contract; + [[eosio::action]] void onbridgemsg(const bridge_message& msg); +}; \ No newline at end of file diff --git a/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.wasm b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.wasm new file mode 100755 index 00000000..c112091f Binary files /dev/null and b/tests/contracts/evm_bridge_receiver/evm_bridge_receiver.wasm differ diff --git a/tests/native_token_tester.hpp b/tests/native_token_tester.hpp index 720386ec..2f8b267f 100644 --- a/tests/native_token_tester.hpp +++ b/tests/native_token_tester.hpp @@ -51,11 +51,6 @@ struct native_token_evm_tester : basic_evm_tester { 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 { diff --git a/tests/utils.hpp b/tests/utils.hpp new file mode 100644 index 00000000..ce9f6a4f --- /dev/null +++ b/tests/utils.hpp @@ -0,0 +1,46 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +using namespace eosio; +using namespace std; +typedef intx::uint<256> u256; + +key256_t to_key256(const uint8_t* ptr, size_t len); +key256_t to_key256(const evmc::address& addr); +key256_t to_key256(const evmc::bytes32& data); +key256_t to_key256(const bytes& data); +bytes to_bytes(const u256& val); +bytes to_bytes(const silkworm::Bytes& b); +bytes to_bytes(const silkworm::ByteView& b); +bytes to_bytes(const evmc::bytes32& val); +bytes to_bytes(const evmc::address& addr); +bytes to_bytes(const key256_t& k); +evmc::address to_address(const bytes& addr); + \ No newline at end of file