From dc4adfaed0f9bcfde852a6f95822127e3cddf4cd Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Sat, 5 Aug 2023 13:18:18 -0500 Subject: [PATCH 01/12] GH-1432 Refactor producer_watermarks, calculate_producer_wake_up_time, calculate_next_block_slot into block_timing_util --- .../producer_plugin/block_timing_util.hpp | 128 ++++++++++++++++-- plugins/producer_plugin/producer_plugin.cpp | 128 +++--------------- 2 files changed, 133 insertions(+), 123 deletions(-) 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..602032cc4e 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,32 @@ enum class pending_block_mode { producing, speculating }; namespace block_timing_util { + // Store watermarks + 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, @@ -25,22 +52,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; - } 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)); + 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)); + } + } + + inline uint32_t calculate_next_block_slot(const chain::account_name& producer_name, uint32_t current_block_slot, uint32_t block_num, + const std::vector& active_schedule, const producer_watermarks& prod_watermarks) { + // 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 = 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 { - return hard_deadline; + // 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; } } -}; + + // 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) { + auto next_producer_block_slot = calculate_next_block_slot(p, ref_block_slot, block_num, active_schedule, 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..0056ace507 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) { @@ -2668,8 +2586,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 +2607,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 +2621,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 +2654,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 +2667,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"); } } From 2bb1e0d0b55b6749caba0e5cb6d083fcf89363f6 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Sat, 5 Aug 2023 13:19:06 -0500 Subject: [PATCH 02/12] GH-1432 Add some initial tests for calculate_producer_wake_up_time --- .../test/test_block_timing_util.cpp | 133 +++++++++++++++--- 1 file changed, 114 insertions(+), 19 deletions(-) diff --git a/plugins/producer_plugin/test/test_block_timing_util.cpp b/plugins/producer_plugin/test/test_block_timing_util.cpp index c2d1e43e78..a6d0f2216d 100644 --- a/plugins/producer_plugin/test/test_block_timing_util.cpp +++ b/plugins/producer_plugin/test/test_block_timing_util.cpp @@ -4,6 +4,7 @@ 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 +35,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 +42,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 +74,128 @@ 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 these tests since calculate_producing_block_deadline is tested above + constexpr uint32_t full_cpu_effort = eosio::chain::config::block_interval_us; + +// 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); + + // 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 producer in active_schedule + 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); + } + } + { // Only producers in active_schedule + 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); + } + } + { // Only producers in active_schedule 21 + 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); + } + } + { // One of many producers + 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; // block production time + + // initb is second in the schedule, so it will produce config::producer_repetitions after + 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); + + producers = std::set{ "inita"_n }; + // 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 + 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 + 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(); + BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + } + +} + BOOST_AUTO_TEST_SUITE_END() From 7a3801ada8a56d9672d3b75c913995ceab2d1918 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Sat, 5 Aug 2023 13:20:12 -0500 Subject: [PATCH 03/12] GH-1432 Set _pending_block_deadline according to if we are producing or can produce. --- plugins/producer_plugin/producer_plugin.cpp | 33 ++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index 0056ace507..ff0be4ec6f 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -1884,19 +1884,32 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { return start_block_result::waiting_for_block; } - _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; + 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()) { + // head_block_time because we need to wake up not to produce a block but to start block production + 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); + + _pending_block_deadline = wake_time ? *wake_time - fc::microseconds(config::block_interval_us) : fc::time_point::maximum(); + } else { + _pending_block_deadline = fc::time_point::maximum(); } + 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 { From e2247daa537d267a00c60facaca03c80510ddcf1 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 7 Aug 2023 11:11:50 -0500 Subject: [PATCH 04/12] GH-1432 Add additional tests including watermark --- .../test/test_block_timing_util.cpp | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/plugins/producer_plugin/test/test_block_timing_util.cpp b/plugins/producer_plugin/test/test_block_timing_util.cpp index a6d0f2216d..66a7f273f8 100644 --- a/plugins/producer_plugin/test/test_block_timing_util.cpp +++ b/plugins/producer_plugin/test/test_block_timing_util.cpp @@ -100,15 +100,9 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { using namespace eosio::block_timing_util; producer_watermarks empty_watermarks; - // use full cpu effort for these tests since calculate_producing_block_deadline is tested above + // 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; -// 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); - // 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 @@ -160,7 +154,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { {"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; // block production time + 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 std::set producers = { "initb"_n }; @@ -175,8 +169,8 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_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); - producers = std::set{ "inita"_n }; // 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 @@ -189,11 +183,64 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { } 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 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(); BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), expected_block_time); + + // inith, initk + producers = std::set{ "inith"_n, "initk"_n }; + block_timestamp = block_timestamp_type(prod_round_1st_block_slot); + 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); + block_timestamp = block_timestamp_type(prod_round_1st_block_slot + 8*config::producer_repetitions); + 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; // block production time + + producer_watermarks prod_watermarks; + std::set producers; + block_timestamp_type block_timestamp(prod_round_1st_block_slot); + // initc + 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); + // watermark at first block + 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); + // watermark at first 2 blocks + 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); } } From 6dcb96ebd354f3e57245cff431686ec0b0339e32 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 7 Aug 2023 11:12:36 -0500 Subject: [PATCH 05/12] GH-1432 Wake up time is start block time already. --- plugins/producer_plugin/producer_plugin.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index ff0be4ec6f..9167d30bda 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -1898,12 +1898,11 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { _pending_block_deadline = block_timing_util::calculate_producing_block_deadline(_cpu_effort_us, block_time); } else if (!_producers.empty()) { - // head_block_time because we need to wake up not to produce a block but to start block production + // 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); - - _pending_block_deadline = wake_time ? *wake_time - fc::microseconds(config::block_interval_us) : fc::time_point::maximum(); + _pending_block_deadline = wake_time ? *wake_time : fc::time_point::maximum(); } else { _pending_block_deadline = fc::time_point::maximum(); } From 3510074827528853689b11d9902fbe5040151e37 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 11 Aug 2023 10:05:42 -0500 Subject: [PATCH 06/12] GH-1432 Refactor calculate_next_block_slot to not require active_schedule --- .../producer_plugin/block_timing_util.hpp | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) 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 602032cc4e..e39cf3d9ec 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 @@ -65,58 +65,51 @@ namespace block_timing_util { } } - inline uint32_t calculate_next_block_slot(const chain::account_name& producer_name, uint32_t current_block_slot, uint32_t block_num, - const std::vector& active_schedule, const producer_watermarks& prod_watermarks) { - // 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 = 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); + 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 { - // 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(); + // 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 { + // 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; } - - // 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; } } @@ -132,7 +125,14 @@ namespace block_timing_util { // 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, block_num, active_schedule, prod_watermarks); + // 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) { From c34ae35882fcf3b888e92defd28f9cc5185b53d9 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 11 Aug 2023 10:26:29 -0500 Subject: [PATCH 07/12] GH-1432 Set speculative deadline of 5secs instead of maximum to correspond to existing limit of 5secs on speculative execution on stale state. --- plugins/producer_plugin/producer_plugin.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index 9167d30bda..e0d158c231 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -1904,7 +1904,8 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { _producer_watermarks); _pending_block_deadline = wake_time ? *wake_time : fc::time_point::maximum(); } else { - _pending_block_deadline = fc::time_point::maximum(); + // set a deadline of 5 seconds to avoid speculatively executing trx on too old of state + _pending_block_deadline = chain.head_block_time() + fc::seconds(5); } const auto& preprocess_deadline = _pending_block_deadline; From c7bca0e33f8e2e4ceb25506d504e29ce55affa17 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Fri, 11 Aug 2023 12:20:02 -0500 Subject: [PATCH 08/12] GH-1432 Add additional comments --- .../producer_plugin/block_timing_util.hpp | 2 ++ .../test/test_block_timing_util.cpp | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) 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 e39cf3d9ec..f046d9449c 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 @@ -10,6 +10,8 @@ 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) { diff --git a/plugins/producer_plugin/test/test_block_timing_util.cpp b/plugins/producer_plugin/test/test_block_timing_util.cpp index 66a7f273f8..652ed626af 100644 --- a/plugins/producer_plugin/test/test_block_timing_util.cpp +++ b/plugins/producer_plugin/test/test_block_timing_util.cpp @@ -103,14 +103,15 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { // 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{}); + { // 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 producer in active_schedule + { // 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; @@ -120,7 +121,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); } } - { // Only producers in active_schedule + { // 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; @@ -130,7 +131,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); } } - { // Only producers in active_schedule 21 + { // We have all producers in active_schedule 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, @@ -148,7 +149,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { BOOST_CHECK_EQUAL(calculate_producer_wake_up_time(full_cpu_effort, 2, block_timestamp, producers, active_schedule, empty_watermarks), block_time); } } - { // One of many producers + { // 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}, @@ -156,7 +157,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { }; 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 + // 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(); @@ -184,18 +185,22 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { 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 + // 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 + // 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); - block_timestamp = block_timestamp_type(prod_round_1st_block_slot + 8*config::producer_repetitions); + // 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); @@ -224,20 +229,20 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { {"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; // block production time + 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 + // 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); - // watermark at first block + // 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); - // watermark at first 2 blocks + // 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); From fffbbf38b0b77e9704f79f690d7242f16b4167df Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 14 Aug 2023 10:31:01 -0500 Subject: [PATCH 09/12] GH-1432 Allow speculative blocks unless we are syncing --- plugins/producer_plugin/producer_plugin.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index e0d158c231..9b62af4c12 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -1879,9 +1879,14 @@ 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; } if (in_producing_mode()) { @@ -1902,10 +1907,10 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { 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); - _pending_block_deadline = wake_time ? *wake_time : fc::time_point::maximum(); + _pending_block_deadline = wake_time ? *wake_time : now + fc::microseconds(config::block_interval_us); } else { - // set a deadline of 5 seconds to avoid speculatively executing trx on too old of state - _pending_block_deadline = chain.head_block_time() + fc::seconds(5); + // set a deadline for the next block time, so we consistently create blocks with "current" block time + _pending_block_deadline = now + fc::microseconds(config::block_interval_us); } const auto& preprocess_deadline = _pending_block_deadline; From bf0231911d95b465acda7b6179254621f9e50f3c Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 14 Aug 2023 10:53:01 -0500 Subject: [PATCH 10/12] GH-1432 Also restart speculative blocks every block interval when configured with producers --- plugins/producer_plugin/producer_plugin.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/producer_plugin/producer_plugin.cpp b/plugins/producer_plugin/producer_plugin.cpp index 9b62af4c12..a97557ee0e 100644 --- a/plugins/producer_plugin/producer_plugin.cpp +++ b/plugins/producer_plugin/producer_plugin.cpp @@ -1889,6 +1889,8 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { last_start_block_time = now; } + // 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) { @@ -1907,10 +1909,8 @@ producer_plugin_impl::start_block_result producer_plugin_impl::start_block() { 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); - _pending_block_deadline = wake_time ? *wake_time : now + fc::microseconds(config::block_interval_us); - } else { - // set a deadline for the next block time, so we consistently create blocks with "current" block time - _pending_block_deadline = now + fc::microseconds(config::block_interval_us); + if (wake_time) + _pending_block_deadline = std::min(*wake_time, _pending_block_deadline); } const auto& preprocess_deadline = _pending_block_deadline; From 1eb4fcce1380a6ae24151fdbde011ead5c07e0ec Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 14 Aug 2023 11:38:24 -0500 Subject: [PATCH 11/12] GH-1432 Fix comment --- .../include/eosio/producer_plugin/block_timing_util.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f046d9449c..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 @@ -44,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 = From e3adab4f6301dce9c4b7e5b40b50808a5cac4f26 Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 14 Aug 2023 16:56:34 -0500 Subject: [PATCH 12/12] GH-1432 Fix comment --- plugins/producer_plugin/test/test_block_timing_util.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/producer_plugin/test/test_block_timing_util.cpp b/plugins/producer_plugin/test/test_block_timing_util.cpp index 652ed626af..efb045b477 100644 --- a/plugins/producer_plugin/test/test_block_timing_util.cpp +++ b/plugins/producer_plugin/test/test_block_timing_util.cpp @@ -1,3 +1,4 @@ + #include #include #include @@ -131,7 +132,7 @@ BOOST_AUTO_TEST_CASE(test_calculate_producer_wake_up_time) { 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 in active_schedule of 21 (plus a couple of extra producers configured), we should produce every block + { // 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,