diff --git a/plugins/producer_plugin/include/eosio/producer_plugin/block_timing_util.hpp b/plugins/producer_plugin/include/eosio/producer_plugin/block_timing_util.hpp index 8238e55fae..b4e3741874 100644 --- a/plugins/producer_plugin/include/eosio/producer_plugin/block_timing_util.hpp +++ b/plugins/producer_plugin/include/eosio/producer_plugin/block_timing_util.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include namespace eosio { @@ -8,6 +9,34 @@ enum class pending_block_mode { producing, speculating }; namespace block_timing_util { + // Store watermarks + // Watermarks are recorded times that the specified producer has produced. + // Used by calculate_producer_wake_up_time to skip over already produced blocks avoiding duplicate production. + class producer_watermarks { + public: + void consider_new_watermark(chain::account_name producer, uint32_t block_num, chain::block_timestamp_type timestamp) { + auto itr = _producer_watermarks.find(producer); + if (itr != _producer_watermarks.end()) { + itr->second.first = std::max(itr->second.first, block_num); + itr->second.second = std::max(itr->second.second, timestamp); + } else { + _producer_watermarks.emplace(producer, std::make_pair(block_num, timestamp)); + } + } + + using producer_watermark = std::pair; + std::optional get_watermark(chain::account_name producer) const { + auto itr = _producer_watermarks.find(producer); + + if (itr == _producer_watermarks.end()) + return {}; + + return itr->second; + } + private: + std::map _producer_watermarks; + }; + // Calculate when a producer can start producing a given block represented by its block_time // // In the past, a producer would always start a block `config::block_interval_us` ahead of its block time. However, @@ -15,7 +44,7 @@ namespace block_timing_util { // received it and start producing on schedule. To mitigate the problem, we leave no time gap in block producing. For // example, given block_interval=500 ms and cpu effort=400 ms, assuming the our round start at time point 0; in the // past, the block start time points would be at time point -500, 0, 500, 1000, 1500, 2000 .... With this new - // approach, the block time points would become -500, -100, 300, 700, 1200 ... + // approach, the block time points would become -500, -100, 300, 700, 1100 ... inline fc::time_point production_round_block_start_time(uint32_t cpu_effort_us, chain::block_timestamp_type block_time) { uint32_t block_slot = block_time.slot; uint32_t production_round_start_block_slot = @@ -25,22 +54,95 @@ namespace block_timing_util { fc::microseconds(cpu_effort_us * production_round_index); } - inline fc::time_point calculate_block_deadline(uint32_t cpu_effort_us, pending_block_mode mode, chain::block_timestamp_type block_time) { - const auto hard_deadline = - block_time.to_time_point() - fc::microseconds(chain::config::block_interval_us - cpu_effort_us); - if (mode == pending_block_mode::producing) { - auto estimated_deadline = production_round_block_start_time(cpu_effort_us, block_time) + fc::microseconds(cpu_effort_us); - auto now = fc::time_point::now(); - if (estimated_deadline > now) { - return estimated_deadline; + inline fc::time_point calculate_producing_block_deadline(uint32_t cpu_effort_us, chain::block_timestamp_type block_time) { + auto estimated_deadline = production_round_block_start_time(cpu_effort_us, block_time) + fc::microseconds(cpu_effort_us); + auto now = fc::time_point::now(); + if (estimated_deadline > now) { + return estimated_deadline; + } else { + // This could only happen when the producer stop producing and then comes back alive in the middle of its own + // production round. In this case, we just use the hard deadline. + const auto hard_deadline = block_time.to_time_point() - fc::microseconds(chain::config::block_interval_us - cpu_effort_us); + return std::min(hard_deadline, now + fc::microseconds(cpu_effort_us)); + } + } + + namespace detail { + inline uint32_t calculate_next_block_slot(const chain::account_name& producer_name, uint32_t current_block_slot, uint32_t block_num, + size_t producer_index, size_t active_schedule_size, const producer_watermarks& prod_watermarks) { + uint32_t minimum_offset = 1; // must at least be the "next" block + + // account for a watermark in the future which is disqualifying this producer for now + // this is conservative assuming no blocks are dropped. If blocks are dropped the watermark will + // disqualify this producer for longer but it is assumed they will wake up, determine that they + // are disqualified for longer due to skipped blocks and re-calculate their next block with better + // information then + auto current_watermark = prod_watermarks.get_watermark(producer_name); + if (current_watermark) { + const auto watermark = *current_watermark; + if (watermark.first > block_num) { + // if I have a watermark block number then I need to wait until after that watermark + minimum_offset = watermark.first - block_num + 1; + } + if (watermark.second.slot > current_block_slot) { + // if I have a watermark block timestamp then I need to wait until after that watermark timestamp + minimum_offset = std::max(minimum_offset, watermark.second.slot - current_block_slot + 1); + } + } + + // this producers next opportunity to produce is the next time its slot arrives after or at the calculated minimum + uint32_t minimum_slot = current_block_slot + minimum_offset; + size_t minimum_slot_producer_index = + (minimum_slot % (active_schedule_size * chain::config::producer_repetitions)) / chain::config::producer_repetitions; + if (producer_index == minimum_slot_producer_index) { + // this is the producer for the minimum slot, go with that + return minimum_slot; } else { - // This could only happen when the producer stop producing and then comes back alive in the middle of its own - // production round. In this case, we just use the hard deadline. - return std::min(hard_deadline, now + fc::microseconds(cpu_effort_us)); + // calculate how many rounds are between the minimum producer and the producer in question + size_t producer_distance = producer_index - minimum_slot_producer_index; + // check for unsigned underflow + if (producer_distance > producer_index) { + producer_distance += active_schedule_size; + } + + // align the minimum slot to the first of its set of reps + uint32_t first_minimum_producer_slot = minimum_slot - (minimum_slot % chain::config::producer_repetitions); + + // offset the aligned minimum to the *earliest* next set of slots for this producer + uint32_t next_block_slot = first_minimum_producer_slot + (producer_distance * chain::config::producer_repetitions); + return next_block_slot; } - } else { - return hard_deadline; } } -}; + + // Return the *next* block start time according to its block time slot. + // Returns empty optional if no producers are in the active_schedule. + // block_num is only used for watermark minimum offset. + inline std::optional calculate_producer_wake_up_time(uint32_t cpu_effort_us, uint32_t block_num, + const chain::block_timestamp_type& ref_block_time, + const std::set& producers, + const std::vector& active_schedule, + const producer_watermarks& prod_watermarks) { + auto ref_block_slot = ref_block_time.slot; + // if we have any producers then we should at least set a timer for our next available slot + uint32_t wake_up_slot = UINT32_MAX; + for (const auto& p : producers) { + // determine if this producer is in the active schedule and if so, where + auto itr = std::find_if(active_schedule.begin(), active_schedule.end(), [&](const auto& asp) { return asp.producer_name == p; }); + if (itr == active_schedule.end()) { + continue; + } + size_t producer_index = itr - active_schedule.begin(); + + auto next_producer_block_slot = detail::calculate_next_block_slot(p, ref_block_slot, block_num, producer_index, active_schedule.size(), prod_watermarks); + wake_up_slot = std::min(next_producer_block_slot, wake_up_slot); + } + if (wake_up_slot == UINT32_MAX) { + return {}; + } + + return production_round_block_start_time(cpu_effort_us, chain::block_timestamp_type(wake_up_slot)); + } + +} // namespace block_timing_util } // namespace eosio \ No newline at end of file diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index dd0b944b0a..a97557ee0e 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -382,7 +382,6 @@ class producer_plugin_impl : public std::enable_shared_from_this()) , _ro_timer(io) {} - uint32_t calculate_next_block_slot(const account_name& producer_name, uint32_t current_block_slot) const; void schedule_production_loop(); void schedule_maybe_produce_block(bool exhausted); void produce_block(); @@ -513,8 +512,7 @@ class producer_plugin_impl : public std::enable_shared_from_this _signature_providers; std::set _producers; boost::asio::deadline_timer _timer; - using producer_watermark = std::pair; - std::map _producer_watermarks; + block_timing_util::producer_watermarks _producer_watermarks; pending_block_mode _pending_block_mode = pending_block_mode::speculating; unapplied_transaction_queue _unapplied_transactions; size_t _thread_pool_size = config::default_controller_thread_pool_size; @@ -634,25 +632,6 @@ class producer_plugin_impl : public std::enable_shared_from_this next); - void consider_new_watermark(account_name producer, uint32_t block_num, block_timestamp_type timestamp) { - auto itr = _producer_watermarks.find(producer); - if (itr != _producer_watermarks.end()) { - itr->second.first = std::max(itr->second.first, block_num); - itr->second.second = std::max(itr->second.second, timestamp); - } else if (_producers.count(producer) > 0) { - _producer_watermarks.emplace(producer, std::make_pair(block_num, timestamp)); - } - } - - std::optional get_watermark(account_name producer) const { - auto itr = _producer_watermarks.find(producer); - - if (itr == _producer_watermarks.end()) - return {}; - - return itr->second; - } - void on_block(const block_state_ptr& bsp) { auto& chain = chain_plug->chain(); auto before = _unapplied_transactions.size(); @@ -663,7 +642,10 @@ class producer_plugin_impl : public std::enable_shared_from_thisheader.producer, bsp->block_num, bsp->block->timestamp); } + void on_block_header(const block_state_ptr& bsp) { + if (_producers.contains(bsp->header.producer)) + _producer_watermarks.consider_new_watermark(bsp->header.producer, bsp->block_num, bsp->block->timestamp); + } void on_irreversible_block(const signed_block_ptr& lib) { const chain::controller& chain = chain_plug->chain(); @@ -999,7 +981,6 @@ class producer_plugin_impl : public std::enable_shared_from_this& weak_this, std::optional wake_up_time); - std::optional calculate_producer_wake_up_time( const block_timestamp_type& ref_block_time ) const; bool in_producing_mode() const { return _pending_block_mode == pending_block_mode::producing; } bool in_speculating_mode() const { return _pending_block_mode == pending_block_mode::speculating; } @@ -1811,69 +1792,6 @@ producer_plugin::get_unapplied_transactions_result producer_plugin::get_unapplie return result; } - -uint32_t producer_plugin_impl::calculate_next_block_slot(const account_name& producer_name, uint32_t current_block_slot) const { - chain::controller& chain = chain_plug->chain(); - const auto& hbs = chain.head_block_state(); - const auto& active_schedule = hbs->active_schedule.producers; - - // determine if this producer is in the active schedule and if so, where - auto itr = - std::find_if(active_schedule.begin(), active_schedule.end(), [&](const auto& asp) { return asp.producer_name == producer_name; }); - if (itr == active_schedule.end()) { - // this producer is not in the active producer set - return UINT32_MAX; - } - - size_t producer_index = itr - active_schedule.begin(); - uint32_t minimum_offset = 1; // must at least be the "next" block - - // account for a watermark in the future which is disqualifying this producer for now - // this is conservative assuming no blocks are dropped. If blocks are dropped the watermark will - // disqualify this producer for longer but it is assumed they will wake up, determine that they - // are disqualified for longer due to skipped blocks and re-calculate their next block with better - // information then - auto current_watermark = get_watermark(producer_name); - if (current_watermark) { - const auto watermark = *current_watermark; - auto block_num = chain.head_block_state()->block_num; - if (chain.is_building_block()) { - ++block_num; - } - if (watermark.first > block_num) { - // if I have a watermark block number then I need to wait until after that watermark - minimum_offset = watermark.first - block_num + 1; - } - if (watermark.second.slot > current_block_slot) { - // if I have a watermark block timestamp then I need to wait until after that watermark timestamp - minimum_offset = std::max(minimum_offset, watermark.second.slot - current_block_slot + 1); - } - } - - // this producers next opportunity to produce is the next time its slot arrives after or at the calculated minimum - uint32_t minimum_slot = current_block_slot + minimum_offset; - size_t minimum_slot_producer_index = - (minimum_slot % (active_schedule.size() * config::producer_repetitions)) / config::producer_repetitions; - if (producer_index == minimum_slot_producer_index) { - // this is the producer for the minimum slot, go with that - return minimum_slot; - } else { - // calculate how many rounds are between the minimum producer and the producer in question - size_t producer_distance = producer_index - minimum_slot_producer_index; - // check for unsigned underflow - if (producer_distance > producer_index) { - producer_distance += active_schedule.size(); - } - - // align the minimum slot to the first of its set of reps - uint32_t first_minimum_producer_slot = minimum_slot - (minimum_slot % config::producer_repetitions); - - // offset the aligned minimum to the *earliest* next set of slots for this producer - uint32_t next_block_slot = first_minimum_producer_slot + (producer_distance * config::producer_repetitions); - return next_block_slot; - } -} - block_timestamp_type producer_plugin_impl::calculate_pending_block_time() const { const chain::controller& chain = chain_plug->chain(); const fc::time_point now = fc::time_point::now(); @@ -1912,7 +1830,7 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { // Not our turn const auto& scheduled_producer = hbs->get_scheduled_producer(block_time); - const auto current_watermark = get_watermark(scheduled_producer.producer_name); + const auto current_watermark = _producer_watermarks.get_watermark(scheduled_producer.producer_name); size_t num_relevant_signatures = 0; scheduled_producer.for_each_key([&](const public_key_type& key) { @@ -1961,24 +1879,42 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { } if (in_speculating_mode()) { - auto head_block_age = now - chain.head_block_time(); - if (head_block_age > fc::seconds(5)) - return start_block_result::waiting_for_block; + static fc::time_point last_start_block_time = fc::time_point::maximum(); // always start with speculative block + // Determine if we are syncing: if we have recently started an old block then assume we are syncing + if (last_start_block_time < now + fc::microseconds(config::block_interval_us)) { + auto head_block_age = now - chain.head_block_time(); + if (head_block_age > fc::seconds(5)) + return start_block_result::waiting_for_block; // if syncing no need to create a block just to immediately abort it + } + last_start_block_time = now; } - _pending_block_deadline = block_timing_util::calculate_block_deadline(_cpu_effort_us, _pending_block_mode, block_time); - auto preprocess_deadline = _pending_block_deadline; - uint32_t production_round_index = block_timestamp_type(block_time).slot % chain::config::producer_repetitions; - if (production_round_index == 0) { - // first block of our round, wait for block production window - const auto start_block_time = block_time.to_time_point() - fc::microseconds(config::block_interval_us); - if (now < start_block_time) { - fc_dlog(_log, "Not starting block until ${bt}", ("bt", start_block_time)); - schedule_delayed_production_loop(weak_from_this(), start_block_time); - return start_block_result::waiting_for_production; + // create speculative blocks at regular intervals, so we create blocks with "current" block time + _pending_block_deadline = now + fc::microseconds(config::block_interval_us); + if (in_producing_mode()) { + uint32_t production_round_index = block_timestamp_type(block_time).slot % chain::config::producer_repetitions; + if (production_round_index == 0) { + // first block of our round, wait for block production window + const auto start_block_time = block_time.to_time_point() - fc::microseconds(config::block_interval_us); + if (now < start_block_time) { + fc_dlog(_log, "Not starting block until ${bt}", ("bt", start_block_time)); + schedule_delayed_production_loop(weak_from_this(), start_block_time); + return start_block_result::waiting_for_production; + } } + + _pending_block_deadline = block_timing_util::calculate_producing_block_deadline(_cpu_effort_us, block_time); + } else if (!_producers.empty()) { + // cpu effort percent doesn't matter for the first block of the round, use max (block_interval_us) for cpu effort + auto wake_time = block_timing_util::calculate_producer_wake_up_time(config::block_interval_us, chain.head_block_num(), chain.head_block_time(), + _producers, chain.head_block_state()->active_schedule.producers, + _producer_watermarks); + if (wake_time) + _pending_block_deadline = std::min(*wake_time, _pending_block_deadline); } + const auto& preprocess_deadline = _pending_block_deadline; + fc_dlog(_log, "Starting block #${n} at ${time} producer ${p}", ("n", pending_block_num)("time", now)("p", scheduled_producer.producer_name)); try { @@ -2668,8 +2604,12 @@ void producer_plugin_impl::schedule_production_loop() { })); } else if (result == start_block_result::waiting_for_block) { if (!_producers.empty() && !production_disabled_by_policy()) { + chain::controller& chain = chain_plug->chain(); fc_dlog(_log, "Waiting till another block is received and scheduling Speculative/Production Change"); - schedule_delayed_production_loop(weak_from_this(), calculate_producer_wake_up_time(calculate_pending_block_time())); + auto wake_time = block_timing_util::calculate_producer_wake_up_time(_cpu_effort_us, chain.head_block_num(), calculate_pending_block_time(), + _producers, chain.head_block_state()->active_schedule.producers, + _producer_watermarks); + schedule_delayed_production_loop(weak_from_this(), wake_time); } else { fc_tlog(_log, "Waiting till another block is received"); // nothing to do until more blocks arrive @@ -2685,7 +2625,10 @@ void producer_plugin_impl::schedule_production_loop() { chain::controller& chain = chain_plug->chain(); fc_dlog(_log, "Speculative Block Created; Scheduling Speculative/Production Change"); EOS_ASSERT(chain.is_building_block(), missing_pending_block_state, "speculating without pending_block_state"); - schedule_delayed_production_loop(weak_from_this(), calculate_producer_wake_up_time(chain.pending_block_timestamp())); + auto wake_time = block_timing_util::calculate_producer_wake_up_time(_cpu_effort_us, chain.pending_block_num(), chain.pending_block_timestamp(), + _producers, chain.head_block_state()->active_schedule.producers, + _producer_watermarks); + schedule_delayed_production_loop(weak_from_this(), wake_time); } else { fc_dlog(_log, "Speculative Block Created"); } @@ -2696,9 +2639,10 @@ void producer_plugin_impl::schedule_production_loop() { void producer_plugin_impl::schedule_maybe_produce_block(bool exhausted) { chain::controller& chain = chain_plug->chain(); + assert(in_producing_mode()); // we succeeded but block may be exhausted static const boost::posix_time::ptime epoch(boost::gregorian::date(1970, 1, 1)); - auto deadline = block_timing_util::calculate_block_deadline(_cpu_effort_us, _pending_block_mode, chain.pending_block_time()); + auto deadline = block_timing_util::calculate_producing_block_deadline(_cpu_effort_us, chain.pending_block_time()); if (!exhausted && deadline > fc::time_point::now()) { // ship this block off no later than its deadline @@ -2728,24 +2672,6 @@ void producer_plugin_impl::schedule_maybe_produce_block(bool exhausted) { })); } - - -std::optional producer_plugin_impl::calculate_producer_wake_up_time(const block_timestamp_type& ref_block_time) const { - auto ref_block_slot = ref_block_time.slot; - // if we have any producers then we should at least set a timer for our next available slot - uint32_t wake_up_slot = UINT32_MAX; - for (const auto& p : _producers) { - auto next_producer_block_slot = calculate_next_block_slot(p, ref_block_slot); - wake_up_slot = std::min(next_producer_block_slot, wake_up_slot); - } - if (wake_up_slot == UINT32_MAX) { - fc_dlog(_log, "Not Scheduling Speculative/Production, no local producers had valid wake up times"); - return {}; - } - - return block_timing_util::production_round_block_start_time(_cpu_effort_us, block_timestamp_type(wake_up_slot)); -} - void producer_plugin_impl::schedule_delayed_production_loop(const std::weak_ptr& weak_this, std::optional wake_up_time) { if (wake_up_time) { @@ -2759,6 +2685,8 @@ void producer_plugin_impl::schedule_delayed_production_loop(const std::weak_ptr< self->schedule_production_loop(); } })); + } else { + fc_dlog(_log, "Not Scheduling Speculative/Production, no local producers had valid wake up times"); } } diff --git a/plugins/producer_plugin/test/test_block_timing_util.cpp b/plugins/producer_plugin/test/test_block_timing_util.cpp index c2d1e43e78..efb045b477 100644 --- a/plugins/producer_plugin/test/test_block_timing_util.cpp +++ b/plugins/producer_plugin/test/test_block_timing_util.cpp @@ -1,9 +1,11 @@ + #include #include #include namespace fc { std::ostream& boost_test_print_type(std::ostream& os, const time_point& t) { return os << t.to_iso_string(); } +std::ostream& boost_test_print_type(std::ostream& os, const std::optional& t) { return os << (t ? t->to_iso_string() : "null"); } } // namespace fc static_assert(eosio::chain::config::block_interval_ms == 500); @@ -34,17 +36,6 @@ BOOST_AUTO_TEST_CASE(test_calculate_block_deadline) { { // Scenario 1: - // In speculating mode, the deadline of a block will always be ahead of its block_time by 100 ms, - // These deadlines are referred as hard deadlines. - for (int i = 0; i < eosio::chain::config::producer_repetitions; ++i) { - auto block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + i); - auto expected_deadline = block_time.to_time_point() - fc::milliseconds(100); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::speculating, block_time), - expected_deadline); - } - } - { - // Scenario 2: // In producing mode, the deadline of a block will be ahead of its block_time from 100, 200, 300, ...ms, // depending on the its index to the starting block of a production round. These deadlines are referred // as optimized deadlines. @@ -52,31 +43,31 @@ BOOST_AUTO_TEST_CASE(test_calculate_block_deadline) { for (int i = 0; i < eosio::chain::config::producer_repetitions; ++i) { auto block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + i); auto expected_deadline = block_time.to_time_point() - fc::milliseconds((i + 1) * 100); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, block_time), expected_deadline); fc::mock_time_traits::set_now(expected_deadline); } } { - // Scenario 3: + // Scenario 2: // In producing mode and it is already too late to meet the optimized deadlines, // the returned deadline can never be later than the hard deadlines. auto second_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 1); fc::mock_time_traits::set_now(second_block_time.to_time_point() - fc::milliseconds(200)); auto second_block_hard_deadline = second_block_time.to_time_point() - fc::milliseconds(100); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, second_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, second_block_time), second_block_hard_deadline); // use previous deadline as now fc::mock_time_traits::set_now(second_block_hard_deadline); auto third_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 2); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, third_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, third_block_time), third_block_time.to_time_point() - fc::milliseconds(300)); // use previous deadline as now fc::mock_time_traits::set_now(third_block_time.to_time_point() - fc::milliseconds(300)); auto forth_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 3); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, forth_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, forth_block_time), forth_block_time.to_time_point() - fc::milliseconds(400)); /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -84,23 +75,180 @@ BOOST_AUTO_TEST_CASE(test_calculate_block_deadline) { auto seventh_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 6); fc::mock_time_traits::set_now(seventh_block_time.to_time_point() - fc::milliseconds(500)); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, seventh_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, seventh_block_time), seventh_block_time.to_time_point() - fc::milliseconds(100)); // use previous deadline as now fc::mock_time_traits::set_now(seventh_block_time.to_time_point() - fc::milliseconds(100)); auto eighth_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 7); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, eighth_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, eighth_block_time), eighth_block_time.to_time_point() - fc::milliseconds(200)); // use previous deadline as now fc::mock_time_traits::set_now(eighth_block_time.to_time_point() - fc::milliseconds(200)); auto ninth_block_time = eosio::chain::block_timestamp_type(production_round_1st_block_slot + 8); - BOOST_CHECK_EQUAL(calculate_block_deadline(cpu_effort_us, eosio::pending_block_mode::producing, ninth_block_time), + BOOST_CHECK_EQUAL(calculate_producing_block_deadline(cpu_effort_us, ninth_block_time), ninth_block_time.to_time_point() - fc::milliseconds(300)); } } +BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { + using namespace eosio; + using namespace eosio::chain; + using namespace eosio::chain::literals; + using namespace eosio::block_timing_util; + + producer_watermarks empty_watermarks; + // use full cpu effort for most of these tests since calculate_producing_block_deadline is tested above + constexpr uint32_t full_cpu_effort = eosio::chain::config::block_interval_us; + + { // no producers + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, chain::block_timestamp_type{}, {}, {}, empty_watermarks), std::optional{}); + } + { // producers not in active_schedule + std::set producers{"p1"_n, "p2"_n}; + std::vector active_schedule{{"active1"_n}, {"active2"_n}}; + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, chain::block_timestamp_type{}, producers, active_schedule, empty_watermarks), std::optional{}); + } + { // Only one producer in active_schedule, we should produce every block + std::set producers{"p1"_n, "p2"_n}; + std::vector active_schedule{{"p1"_n}}; + const uint32_t prod_round_1st_block_slot = 100 * active_schedule.size() * eosio::chain::config::producer_repetitions - 1; + for (uint32_t i = 0; i < static_cast(config::producer_repetitions * active_schedule.size() * 3); ++i) { // 3 rounds to test boundaries + block_timestamp_type block_timestamp(prod_round_1st_block_slot + i); + auto block_time = block_timestamp.to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); + } + } + { // We have all producers in active_schedule configured, we should produce every block + std::set producers{"p1"_n, "p2"_n, "p3"_n}; + std::vector active_schedule{{"p1"_n}, {"p2"_n}}; + const uint32_t prod_round_1st_block_slot = 100 * active_schedule.size() * eosio::chain::config::producer_repetitions - 1; + for (uint32_t i = 0; i < static_cast(config::producer_repetitions * active_schedule.size() * 3); ++i) { // 3 rounds to test boundaries + block_timestamp_type block_timestamp(prod_round_1st_block_slot + i); + auto block_time = block_timestamp.to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); + } + } + { // We have all producers in active_schedule of 21 (plus a couple of extra producers configured), we should produce every block + std::set producers = { + "inita"_n, "initb"_n, "initc"_n, "initd"_n, "inite"_n, "initf"_n, "initg"_n, "p1"_n, + "inith"_n, "initi"_n, "initj"_n, "initk"_n, "initl"_n, "initm"_n, "initn"_n, + "inito"_n, "initp"_n, "initq"_n, "initr"_n, "inits"_n, "initt"_n, "initu"_n, "p2"_n + }; + std::vector active_schedule{ + {"inita"_n}, {"initb"_n}, {"initc"_n}, {"initd"_n}, {"inite"_n}, {"initf"_n}, {"initg"_n}, + {"inith"_n}, {"initi"_n}, {"initj"_n}, {"initk"_n}, {"initl"_n}, {"initm"_n}, {"initn"_n}, + {"inito"_n}, {"initp"_n}, {"initq"_n}, {"initr"_n}, {"inits"_n}, {"initt"_n}, {"initu"_n} + }; + const uint32_t prod_round_1st_block_slot = 100 * active_schedule.size() * eosio::chain::config::producer_repetitions - 1; + for (uint32_t i = 0; i < static_cast(config::producer_repetitions * active_schedule.size() * 3); ++i) { // 3 rounds to test boundaries + block_timestamp_type block_timestamp(prod_round_1st_block_slot + i); + auto block_time = block_timestamp.to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); + } + } + { // Tests for when we only have a subset of all active producers, we do not produce all blocks, only produce blocks for our round + std::vector active_schedule{ // 21 + {"inita"_n}, {"initb"_n}, {"initc"_n}, {"initd"_n}, {"inite"_n}, {"initf"_n}, {"initg"_n}, + {"inith"_n}, {"initi"_n}, {"initj"_n}, {"initk"_n}, {"initl"_n}, {"initm"_n}, {"initn"_n}, + {"inito"_n}, {"initp"_n}, {"initq"_n}, {"initr"_n}, {"inits"_n}, {"initt"_n}, {"initu"_n} + }; + const uint32_t prod_round_1st_block_slot = 100 * active_schedule.size() * eosio::chain::config::producer_repetitions - 1; + + // initb is second in the schedule, so it will produce config::producer_repetitions after start, verify its block times + std::set producers = { "initb"_n }; + block_timestamp_type block_timestamp(prod_round_1st_block_slot); + auto expected_block_time = block_timestamp_type(prod_round_1st_block_slot + config::producer_repetitions).to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot-1}, producers, active_schedule, empty_watermarks), expected_block_time); // same + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot+config::producer_repetitions-1}, producers, active_schedule, empty_watermarks), expected_block_time); // same + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot+config::producer_repetitions-2}, producers, active_schedule, empty_watermarks), expected_block_time); // same + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot+config::producer_repetitions-3}, producers, active_schedule, empty_watermarks), expected_block_time); // same + // current which gives same expected + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot+config::producer_repetitions}, producers, active_schedule, empty_watermarks), expected_block_time); + expected_block_time += fc::microseconds(config::block_interval_us); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot+config::producer_repetitions+1}, producers, active_schedule, empty_watermarks), expected_block_time); + + // inita is first in the schedule, prod_round_1st_block_slot is block time of the first block, so will return the next block time as that is when current should be produced + producers = std::set{ "inita"_n }; + block_timestamp = block_timestamp_type{prod_round_1st_block_slot}; + expected_block_time = block_timestamp.to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot-1}, producers, active_schedule, empty_watermarks), expected_block_time); // same + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot-2}, producers, active_schedule, empty_watermarks), expected_block_time); // same + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp_type{block_timestamp.slot-3}, producers, active_schedule, empty_watermarks), expected_block_time); // same + for (size_t i = 0; i < config::producer_repetitions; ++i) { + expected_block_time = block_timestamp_type(prod_round_1st_block_slot+i).to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + block_timestamp = block_timestamp.next(); + } + expected_block_time = block_timestamp.to_time_point(); + BOOST_CHECK_NE(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); // end of round, so not the next + + // initc is third in the schedule, verify its wake-up time is as expected + producers = std::set{ "initc"_n }; + block_timestamp = block_timestamp_type(prod_round_1st_block_slot); + // expect 2*producer_repetitions since we expect wake-up time to be after the first two rounds + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions).to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + + // inith, initk - configured for 2 of the 21 producers. inith is 8th in schedule, initk is 11th in schedule + producers = std::set{ "inith"_n, "initk"_n }; + block_timestamp = block_timestamp_type(prod_round_1st_block_slot); + // expect to produce after 7 rounds since inith is 8th + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 7*config::producer_repetitions).to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + // give it a time after inith otherwise would return inith time + block_timestamp = block_timestamp_type(prod_round_1st_block_slot + 8*config::producer_repetitions); // after inith round + // expect to produce after 10 rounds since inith is 11th + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 10*config::producer_repetitions).to_time_point(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + + // cpu_effort at 50%, initc + constexpr uint32_t half_cpu_effort = eosio::chain::config::block_interval_us / 2u; + producers = std::set{ "initc"_n }; + block_timestamp = block_timestamp_type(prod_round_1st_block_slot); + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions).to_time_point(); + // first in round is not affected by cpu effort + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(half_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + block_timestamp = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions + 1); + // second in round is 50% sooner + expected_block_time = block_timestamp.to_time_point(); + expected_block_time -= fc::microseconds(half_cpu_effort); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(half_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + // third in round is 2*50% sooner + block_timestamp = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions + 2); + // second in round is 50% sooner + expected_block_time = block_timestamp.to_time_point(); + expected_block_time -= fc::microseconds(2*half_cpu_effort); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(half_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + } + { // test watermark + std::vector active_schedule{ // 21 + {"inita"_n}, {"initb"_n}, {"initc"_n}, {"initd"_n}, {"inite"_n}, {"initf"_n}, {"initg"_n}, + {"inith"_n}, {"initi"_n}, {"initj"_n}, {"initk"_n}, {"initl"_n}, {"initm"_n}, {"initn"_n}, + {"inito"_n}, {"initp"_n}, {"initq"_n}, {"initr"_n}, {"inits"_n}, {"initt"_n}, {"initu"_n} + }; + const uint32_t prod_round_1st_block_slot = 100 * active_schedule.size() * eosio::chain::config::producer_repetitions - 1; + + producer_watermarks prod_watermarks; + std::set producers; + block_timestamp_type block_timestamp(prod_round_1st_block_slot); + // initc, with no watermarks + producers = std::set{ "initc"_n }; + auto expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions).to_time_point(); // without watermark + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + // add watermark at first block, first block should not be allowed, wake-up time should be after first block of initc + prod_watermarks.consider_new_watermark("initc"_n, 2, block_timestamp_type((prod_round_1st_block_slot + 2*config::producer_repetitions + 1))); // +1 since watermark is in block production time + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions + 1).to_time_point(); // with watermark, wait until next + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, prod_watermarks), expected_block_time); + // add watermark at first 2 blocks, first & second block should not be allowed, wake-up time should be after second block of initc + prod_watermarks.consider_new_watermark("initc"_n, 2, block_timestamp_type((prod_round_1st_block_slot + 2*config::producer_repetitions + 1 + 1))); + expected_block_time = block_timestamp_type(prod_round_1st_block_slot + 2*config::producer_repetitions + 2).to_time_point(); // with watermark, wait until next + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, prod_watermarks), expected_block_time); + } + +} + BOOST_AUTO_TEST_SUITE_END()