From 16c1ae03537b4ab78871f0981806d36d3157f586 Mon Sep 17 00:00:00 2001 From: Areg Hayrapetian Date: Tue, 14 Mar 2023 12:04:27 -0700 Subject: [PATCH] Use common block mapping code between contract and EVM node. --- cmd/block_conversion_plugin.cpp | 74 +++++------ .../evm_common/block_mapping.hpp | 52 ++++++++ contract/src/CMakeLists.txt | 1 + contract/src/actions.cpp | 10 +- contract/tests/CMakeLists.txt | 2 + contract/tests/mapping_tests.cpp | 120 ++++++++++++++++++ 6 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 cmd/contract_common/evm_common/block_mapping.hpp create mode 100644 contract/tests/mapping_tests.cpp diff --git a/cmd/block_conversion_plugin.cpp b/cmd/block_conversion_plugin.cpp index 27ceab1a..056f89b8 100644 --- a/cmd/block_conversion_plugin.cpp +++ b/cmd/block_conversion_plugin.cpp @@ -2,6 +2,7 @@ #include "channels.hpp" #include "abi_utils.hpp" #include "utils.hpp" +#include "contract_common/evm_common/block_mapping.hpp" #include @@ -9,38 +10,22 @@ #include #include -struct block_mapping { - - uint64_t genesis_timestamp = 1667688089000000; //us - uint64_t block_interval = 1000000; //us - - void set_genesis_timestamp(uint64_t timestamp){ - genesis_timestamp = timestamp; - } - - void set_block_interval(uint64_t interval) { - block_interval = interval; - } - - inline uint32_t timestamp_to_evm_block(uint64_t timestamp) { - if( timestamp < genesis_timestamp ) { - SILK_CRIT << "Invalid timestamp [" << timestamp << ", genesis: " << genesis_timestamp; - assert(timestamp >= genesis_timestamp); - } - return 1 + (timestamp - genesis_timestamp)/block_interval; - } - - inline uint64_t evm_block_to_timestamp(uint32_t block_num) { - return genesis_timestamp + block_num * block_interval; - } - -}; using sys = sys_plugin; class block_conversion_plugin_impl : std::enable_shared_from_this { public: block_conversion_plugin_impl() : evm_blocks_channel(appbase::app().get_channel()){} + + uint32_t timestamp_to_evm_block_num(uint64_t timestamp) const { + if (timestamp < bm.value().genesis_timestamp) { + SILK_CRIT << "Invalid timestamp " << timestamp << ", genesis: " << bm->genesis_timestamp; + assert(timestamp >= bm->genesis_timestamp); + } + + return bm->timestamp_to_evm_block_num(timestamp); + } + void load_head() { auto head_header = appbase::app().get_plugin().get_head_canonical_header(); if (!head_header) { @@ -67,11 +52,20 @@ class block_conversion_plugin_impl : std::enable_shared_from_thisnonce.data())*1e3); - bm.set_genesis_timestamp(genesis_header->timestamp*1e6); + uint64_t block_interval_ms = silkworm::endian::load_big_u64(genesis_header->nonce.data()); + // TODO: Consider redefining genesis nonce to be in units of seconds rather than milliseconds? Or just hardcode to 1 second? + + if (block_interval_ms == 0 || block_interval_ms % 1000 != 0) { + SILK_CRIT << "Genesis nonce is invalid. Must be a positive multiple of 1000 representing the block interval in milliseconds. " + "Instead got: " << block_interval_ms; + sys::error("Invalid genesis nonce"); + return; + } + + bm.emplace(genesis_header->timestamp, block_interval_ms/1e3); - SILK_INFO << "Block interval: " << bm.block_interval; - SILK_INFO << "Genesis timestamp: " << bm.genesis_timestamp; + SILK_INFO << "Block interval: " << bm->block_interval; + SILK_INFO << "Genesis timestamp: " << bm->genesis_timestamp; } evmc::bytes32 compute_transaction_root(const silkworm::BlockBody& body) { @@ -94,7 +88,7 @@ class block_conversion_plugin_impl : std::enable_shared_from_thisblock_num; // Keep the last block before genesis timestamp - if (b->timestamp <= bm.genesis_timestamp) { - SILK_DEBUG << "Before genesis: " << bm.genesis_timestamp << " Block #" << b->block_num << " timestamp: " << b->timestamp; + if (b->timestamp <= bm.value().genesis_timestamp) { + SILK_DEBUG << "Before genesis: " << bm->genesis_timestamp << " Block #" << b->block_num << " timestamp: " << b->timestamp; native_blocks.clear(); native_blocks.push_back(*b); return; } - + // Add received native block to our local reversible chain. // If the received native block can't be linked we remove the forked blocks until the fork point. // Also we remove the forked block transactions from the corresponding evm block as they where previously included @@ -151,7 +145,7 @@ class block_conversion_plugin_impl : std::enable_shared_from_thisblock_num; const auto& forked_block = native_blocks.back(); - while( bm.timestamp_to_evm_block(forked_block.timestamp) < evm_blocks.back().header.number ) + while( timestamp_to_evm_block_num(forked_block.timestamp) < evm_blocks.back().header.number ) evm_blocks.pop_back(); auto& last_evm_block = evm_blocks.back(); @@ -160,7 +154,7 @@ class block_conversion_plugin_impl : std::enable_shared_from_thistimestamp); + auto evm_num = timestamp_to_evm_block_num(b->timestamp); //SILK_INFO << "Expecting evm number: " << evm_num; while(evm_blocks.back().header.number < evm_num) { @@ -213,11 +207,11 @@ class block_conversion_plugin_impl : std::enable_shared_from_this evm_blocks; channels::evm_blocks::channel_type& evm_blocks_channel; channels::native_blocks::channel_type::handle native_blocks_subscription; - block_mapping bm; + std::optional bm; }; block_conversion_plugin::block_conversion_plugin() : my(new block_conversion_plugin_impl()) {} diff --git a/cmd/contract_common/evm_common/block_mapping.hpp b/cmd/contract_common/evm_common/block_mapping.hpp new file mode 100644 index 00000000..b865e0b0 --- /dev/null +++ b/cmd/contract_common/evm_common/block_mapping.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +namespace evm_common { + +struct block_mapping { + + /** + * @brief Construct object that maps from Antelope timestamp to EVM block number and timestamp + * + * @param genesis_timestamp_sec - the EVM genesis timestamp in seconds + * @param block_interval_sec - time interval between consecutive EVM blocks in seconds (must be positive) + * + * @note The timestamp of the Antelope block containing the init action rounded down to the nearest second must equal genesis_timestamp_sec. + */ + block_mapping(uint64_t genesis_timestamp_sec, uint32_t block_interval_sec = 1) + : block_interval(block_interval_sec == 0 ? 1 : block_interval_sec), + genesis_timestamp(genesis_timestamp_sec) + {} + + const uint64_t block_interval; // seconds + const uint64_t genesis_timestamp; // seconds + + /** + * @brief Map Antelope timestamp to EVM block num + * + * @param timestamp - Antelope timestamp in microseconds + * @return mapped EVM block number (returns 0 for all timestamps prior to the genesis timestamp) + */ + inline uint32_t timestamp_to_evm_block_num(uint64_t timestamp_us) const { + uint64_t timestamp = timestamp_us / 1e6; // map Antelope block timestamp to EVM block timestamp + if( timestamp < genesis_timestamp ) { + // There should not be any transactions prior to the init action. + // But any entity with an associated timestamp prior to the genesis timestamp can be considered as part of the genesis block. + return 0; + } + return 1 + (timestamp - genesis_timestamp) / block_interval; + } + + /** + * @brief Map EVM block num to EVM block timestamp + * + * @param block_num - EVM block number + * @return EVM block timestamp associated with the given EVM block number + */ + inline uint64_t evm_block_num_to_evm_timestamp(uint32_t block_num) const { + return genesis_timestamp + block_num * block_interval; + } +}; + +} // namespace evm_common \ No newline at end of file diff --git a/contract/src/CMakeLists.txt b/contract/src/CMakeLists.txt index 84896371..832bcf1c 100644 --- a/contract/src/CMakeLists.txt +++ b/contract/src/CMakeLists.txt @@ -71,6 +71,7 @@ add_contract( evm_contract evm_runtime ${SOURCES}) target_include_directories( evm_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../../cmd/contract_common ${CMAKE_CURRENT_SOURCE_DIR}/../external/intx/include ${CMAKE_CURRENT_SOURCE_DIR}/../external/ethash/include ${CMAKE_CURRENT_SOURCE_DIR}/../external/GSL/include diff --git a/contract/src/actions.cpp b/contract/src/actions.cpp index 3b344981..032b63ab 100644 --- a/contract/src/actions.cpp +++ b/contract/src/actions.cpp @@ -8,6 +8,8 @@ #include #include +#include + #include // included here so NDEBUG is defined to disable assert macro #include @@ -46,7 +48,7 @@ void evm_contract::init(const uint64_t chainid) { _config.set({ .version = 0, .chainid = chainid, - .genesis_time = current_time_point() + .genesis_time = eosio::current_time_point() // implicitly converts from Antelope timestamp to EVM compatible timestamp }, get_self()); } @@ -121,11 +123,13 @@ void evm_contract::pushtx( eosio::name ram_payer, const bytes& rlptx ) { 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()); + Block block; block.header.difficulty = 1; block.header.gas_limit = 0x7ffffffffff; - block.header.timestamp = eosio::current_time_point().sec_since_epoch(); - block.header.number = 1 + (block.header.timestamp - _config.get().genesis_time.sec_since_epoch()); // same logic with block_mapping in TrustEVM + 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); silkworm::consensus::TrustEngine engine{*found_chain_config->second}; push_trx( ram_payer, block, rlptx, engine, *found_chain_config->second ); diff --git a/contract/tests/CMakeLists.txt b/contract/tests/CMakeLists.txt index 541036ca..4a33f34b 100644 --- a/contract/tests/CMakeLists.txt +++ b/contract/tests/CMakeLists.txt @@ -9,6 +9,7 @@ configure_file(${CMAKE_SOURCE_DIR}/contracts.hpp.in ${CMAKE_BINARY_DIR}/contract include_directories( ${CMAKE_BINARY_DIR} + ${CMAKE_SOURCE_DIR}/../../cmd/contract_common ${CMAKE_SOURCE_DIR}/silkworm/node ${CMAKE_SOURCE_DIR}/silkworm/core ${CMAKE_SOURCE_DIR}/../../silkworm/third_party/evmone/lib @@ -27,6 +28,7 @@ add_eosio_test_executable( unit_test ${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}/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/mapping_tests.cpp b/contract/tests/mapping_tests.cpp new file mode 100644 index 00000000..b121cc69 --- /dev/null +++ b/contract/tests/mapping_tests.cpp @@ -0,0 +1,120 @@ +#include "basic_evm_tester.hpp" + +#include + +#include + +using namespace eosio::testing; + +using eosio::chain::time_point; +using eosio::chain::time_point_sec; +using eosio::chain::unsigned_int; + +struct mapping_evm_tester : basic_evm_tester { + static constexpr eosio::chain::name evm_account_name = "evm"_n; + + bool produce_blocks_until_timestamp_satisfied(std::function cond, unsigned int limit = 10) { + for (unsigned int blocks_produced = 0; blocks_produced < limit && !cond(control->pending_block_time()); ++blocks_produced) { + produce_block(); + } + return cond(control->pending_block_time()); + } + + struct config_table_row { + unsigned_int version; + uint64_t chainid; + time_point_sec genesis_time; + uint32_t status; + }; + + time_point_sec get_genesis_time() { + 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; + } +}; + +FC_REFLECT(mapping_evm_tester::config_table_row, (version)(chainid)(genesis_time)(status)) + +BOOST_AUTO_TEST_SUITE(mapping_evm_tests) + +BOOST_AUTO_TEST_CASE(block_mapping_tests) try { + 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); + + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(9'500'000), 0); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(10'000'000), 1); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(10'500'000), 1); + 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); + } + + { + // 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); + + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(100'000'000), 0); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(122'000'000), 0); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(123'000'000), 1); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(124'000'000), 1); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(127'000'000), 1); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(127'999'999), 1); + BOOST_CHECK_EQUAL(bm.timestamp_to_evm_block_num(128'000'000), 2); + + BOOST_CHECK_EQUAL(bm.evm_block_num_to_evm_timestamp(0), 123); + 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() + + +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); + 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() + +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); + 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() + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file