diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 6eb60bf379..56f892e6cc 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -2956,12 +2956,12 @@ struct controller_impl { } FC_CAPTURE_AND_RETHROW((trace)) } /// push_transaction - void start_block( block_timestamp_type when, - uint16_t confirm_block_count, - const vector& new_protocol_feature_activations, - controller::block_status s, - const std::optional& producer_block_id, - const fc::time_point& deadline ) + transaction_trace_ptr start_block( block_timestamp_type when, + uint16_t confirm_block_count, + const vector& new_protocol_feature_activations, + controller::block_status s, + const std::optional& producer_block_id, + const fc::time_point& deadline ) { EOS_ASSERT( !pending, block_validate_exception, "pending block already exists" ); @@ -3000,6 +3000,8 @@ struct controller_impl { auto& bb = std::get(pending->_block_stage); + transaction_trace_ptr onblock_trace; + // block status is either ephemeral or incomplete. Modify state of speculative block only if we are building a // speculative incomplete block (otherwise we need clean state for head mode, ephemeral block) if ( pending->_block_status != controller::block_status::ephemeral ) { @@ -3106,10 +3108,11 @@ struct controller_impl { in_trx_requiring_checks = old_value; }); in_trx_requiring_checks = true; - auto trace = push_transaction( onbtrx, fc::time_point::maximum(), fc::microseconds::maximum(), - gpo.configuration.min_transaction_cpu_usage, true, 0 ); - if( trace->except ) { - wlog("onblock ${block_num} is REJECTING: ${entire_trace}",("block_num", chain_head.block_num() + 1)("entire_trace", trace)); + onblock_trace = push_transaction( onbtrx, fc::time_point::maximum(), fc::microseconds::maximum(), + gpo.configuration.min_transaction_cpu_usage, true, 0 ); + if( onblock_trace->except ) { + wlog("onblock ${block_num} is REJECTING: ${entire_trace}", + ("block_num", chain_head.block_num() + 1)("entire_trace", onblock_trace)); } } catch( const std::bad_alloc& e ) { elog( "on block transaction failed due to a std::bad_alloc" ); @@ -3132,6 +3135,7 @@ struct controller_impl { } guard_pending.cancel(); + return onblock_trace; } /// start_block void assemble_block(bool validating, std::optional validating_qc_data, const block_state_ptr& validating_bsp) @@ -4938,11 +4942,11 @@ void controller::validate_protocol_features( const vector& features features_to_activate ); } -void controller::start_block( block_timestamp_type when, - uint16_t confirm_block_count, - const vector& new_protocol_feature_activations, - block_status bs, - const fc::time_point& deadline ) +transaction_trace_ptr controller::start_block( block_timestamp_type when, + uint16_t confirm_block_count, + const vector& new_protocol_feature_activations, + block_status bs, + const fc::time_point& deadline ) { validate_db_available_size(); @@ -4952,8 +4956,8 @@ void controller::start_block( block_timestamp_type when, EOS_ASSERT( bs == block_status::incomplete || bs == block_status::ephemeral, block_validate_exception, "speculative block type required" ); - my->start_block( when, confirm_block_count, new_protocol_feature_activations, - bs, std::optional(), deadline ); + return my->start_block( when, confirm_block_count, new_protocol_feature_activations, + bs, std::optional(), deadline ); } void controller::assemble_and_complete_block( block_report& br, const signer_callback_type& signer_callback ) { diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index 1c8d73ae00..fe129b8e9a 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -141,12 +141,13 @@ namespace eosio::chain { /** * Starts a new pending block session upon which new transactions can be pushed. + * returns the trace for the on_block action */ - void start_block( block_timestamp_type time, - uint16_t confirm_block_count, - const vector& new_protocol_feature_activations, - block_status bs, - const fc::time_point& deadline = fc::time_point::maximum() ); + transaction_trace_ptr start_block( block_timestamp_type time, + uint16_t confirm_block_count, + const vector& new_protocol_feature_activations, + block_status bs, + const fc::time_point& deadline = fc::time_point::maximum() ); /** * @return transactions applied in aborted block diff --git a/libraries/chainbase b/libraries/chainbase index 0324e06d0b..386357f94f 160000 --- a/libraries/chainbase +++ b/libraries/chainbase @@ -1 +1 @@ -Subproject commit 0324e06d0b8f4e7e09b0d41c7f6a086fd1a7bac7 +Subproject commit 386357f94f79eb051f51a91ecc424edacbde2f55 diff --git a/libraries/libfc/include/fc/network/listener.hpp b/libraries/libfc/include/fc/network/listener.hpp index 50aec37cf1..a1f6287fa9 100644 --- a/libraries/libfc/include/fc/network/listener.hpp +++ b/libraries/libfc/include/fc/network/listener.hpp @@ -59,8 +59,8 @@ struct listener_base { /// detail for fc::create_listener(). /// ///////////////////////////////////////////////////////////////////////////////////////////// -template -struct listener : listener_base, std::enable_shared_from_this> { +template +struct listener : listener_base, std::enable_shared_from_this> { private: typename Protocol::acceptor acceptor_; boost::asio::deadline_timer accept_error_timer_; @@ -71,7 +71,7 @@ struct listener : listener_base, std::enable_shared_from_this(local_address), acceptor_(executor, endpoint), accept_error_timer_(executor), @@ -159,8 +159,8 @@ struct listener : listener_base, std::enable_shared_from_this -void create_listener(boost::asio::io_context& executor, logger& logger, boost::posix_time::time_duration accept_timeout, +template +void create_listener(Executor& executor, logger& logger, boost::posix_time::time_duration accept_timeout, const std::string& address, const std::string& extra_listening_log_info, const CreateSession& create_session) { using tcp = boost::asio::ip::tcp; @@ -186,7 +186,7 @@ void create_listener(boost::asio::io_context& executor, logger& logger, boost::p auto create_listener = [&](const auto& endpoint) { const auto& ip_addr = endpoint.address(); try { - auto listener = std::make_shared>( + auto listener = std::make_shared>( executor, logger, accept_timeout, address, endpoint, extra_listening_log_info, create_session); listener->log_listening(endpoint, address); listener->do_accept(); @@ -256,7 +256,7 @@ void create_listener(boost::asio::io_context& executor, logger& logger, boost::p fs::remove(sock_path); } - auto listener = std::make_shared>( + auto listener = std::make_shared>( executor, logger, accept_timeout, address, endpoint, extra_listening_log_info, create_session); listener->log_listening(endpoint, address); listener->do_accept(); diff --git a/libraries/state_history/include/eosio/state_history/log.hpp b/libraries/state_history/include/eosio/state_history/log.hpp index d9d09c3486..58d675bf28 100644 --- a/libraries/state_history/include/eosio/state_history/log.hpp +++ b/libraries/state_history/include/eosio/state_history/log.hpp @@ -78,6 +78,8 @@ static constexpr int state_history_log_header_serial_size = sizeof(state_history sizeof(state_history_log_header::payload_size); static_assert(sizeof(state_history_log_header) == state_history_log_header_serial_size); +static constexpr unsigned ship_log_iostreams_buffer_size = 64*1024; + namespace state_history { struct prune_config { uint32_t prune_blocks; //number of blocks to prune to when doing a prune @@ -108,16 +110,16 @@ struct locked_decompress_stream { template void init(StateHistoryLog&& log, fc::cfile& stream, uint64_t compressed_size) { auto istream = std::make_unique(); - istream->push(bio::zlib_decompressor()); - istream->push(bio::restrict(bio::file_source(stream.get_file_path().string()), stream.tellp(), compressed_size)); + istream->push(bio::zlib_decompressor(), ship_log_iostreams_buffer_size); + istream->push(bio::restrict(bio::file_source(stream.get_file_path().string()), stream.tellp(), compressed_size), ship_log_iostreams_buffer_size); buf = std::move(istream); } template void init(LogData&& log, fc::datastream& stream, uint64_t compressed_size) { auto istream = std::make_unique(); - istream->push(bio::zlib_decompressor()); - istream->push(bio::restrict(bio::file_source(log.filename), stream.pos() - log.data(), compressed_size)); + istream->push(bio::zlib_decompressor(), ship_log_iostreams_buffer_size); + istream->push(bio::restrict(bio::file_source(log.filename), stream.pos() - log.data(), compressed_size), ship_log_iostreams_buffer_size); buf = std::move(istream); } @@ -433,9 +435,9 @@ class state_history_log { detail::counter cnt; { bio::filtering_ostreambuf buf; - buf.push(boost::ref(cnt)); - buf.push(bio::zlib_compressor(boost::iostreams::zlib::no_compression)); - buf.push(bio::file_descriptor_sink(stream.fileno(), bio::never_close_handle)); + buf.push(boost::ref(cnt), ship_log_iostreams_buffer_size); + buf.push(bio::zlib_compressor(bio::zlib::no_compression, ship_log_iostreams_buffer_size)); + buf.push(bio::file_descriptor_sink(stream.fileno(), bio::never_close_handle), ship_log_iostreams_buffer_size); pack_to(buf); } diff --git a/libraries/testing/include/eosio/testing/tester.hpp b/libraries/testing/include/eosio/testing/tester.hpp index bbf086e9cc..6a26539cfb 100644 --- a/libraries/testing/include/eosio/testing/tester.hpp +++ b/libraries/testing/include/eosio/testing/tester.hpp @@ -9,6 +9,8 @@ #include #include +#include + #include #include @@ -57,7 +59,7 @@ namespace boost { namespace test_tools { namespace tt_detail { } } } -namespace eosio { namespace testing { +namespace eosio::testing { enum class setup_policy { none, old_bios_only, @@ -142,6 +144,12 @@ namespace eosio { namespace testing { }; } + struct produce_block_result_t { + signed_block_ptr block; + transaction_trace_ptr onblock_trace; + std::vector traces; // transaction traces + }; + /** * @class tester * @brief provides utility function to simplify the creation of unit tests @@ -154,8 +162,11 @@ namespace eosio { namespace testing { static const uint32_t DEFAULT_BILLED_CPU_TIME_US = 2000; static const fc::microseconds abi_serializer_max_time; + static constexpr fc::microseconds default_skip_time = fc::milliseconds(config::block_interval_ms); - virtual ~base_tester() {}; + virtual ~base_tester() { + lib_connection.disconnect(); + }; void init(const setup_policy policy = setup_policy::full, db_read_mode read_mode = db_read_mode::HEAD, std::optional genesis_max_inline_action_size = std::optional{}); void init(controller::config config, const snapshot_reader_ptr& snapshot); @@ -176,15 +187,22 @@ namespace eosio { namespace testing { void open( std::optional expected_chain_id = {} ); bool is_same_chain( base_tester& other ); - virtual signed_block_ptr produce_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) ) = 0; - virtual signed_block_ptr produce_empty_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) ) = 0; - virtual signed_block_ptr finish_block() = 0; + // `produce_block_ex` does the same thing as produce_block, but returns a struct including + // the transaction traces in addition to the `signed_block_ptr`. + virtual produce_block_result_t produce_block_ex( fc::microseconds skip_time = default_skip_time, + bool no_throw = false) = 0; + + virtual signed_block_ptr produce_block( fc::microseconds skip_time = default_skip_time, + bool no_throw = false) = 0; + virtual signed_block_ptr produce_empty_block( fc::microseconds skip_time = default_skip_time ) = 0; + virtual signed_block_ptr finish_block() = 0; + // produce one block and return traces for all applied transactions, both failed and executed - signed_block_ptr produce_block( std::vector& traces ); - void produce_blocks( uint32_t n = 1, bool empty = false ); + signed_block_ptr produce_blocks( uint32_t n = 1, bool empty = false ); void produce_blocks_until_end_of_round(); void produce_blocks_for_n_rounds(const uint32_t num_of_rounds = 1); - // Produce minimal number of blocks as possible to spend the given time without having any producer become inactive + // Produce minimal number of blocks as possible to spend the given time without having any + // producer become inactive void produce_min_num_of_blocks_to_spend_time_wo_inactive_prod(const fc::microseconds target_elapsed_time = fc::microseconds()); void push_block(signed_block_ptr b); @@ -467,14 +485,33 @@ namespace eosio { namespace testing { return {cfg, gen}; } + // checks that the active `finalizer_policy` for `block` matches the + // passed `generation` and `keys_span`. + // ----------------------------------------------------------------- + void check_head_finalizer_policy(uint32_t generation, + std::span keys_span) { + auto finpol = active_finalizer_policy(control->head_block_header().calculate_id()); + BOOST_REQUIRE(!!finpol); + BOOST_REQUIRE_EQUAL(finpol->generation, generation); + BOOST_REQUIRE_EQUAL(keys_span.size(), finpol->finalizers.size()); + std::vector keys {keys_span.begin(), keys_span.end() }; + std::sort(keys.begin(), keys.end()); + + std::vector active_keys; + for (const auto& auth : finpol->finalizers) + active_keys.push_back(auth.public_key); + std::sort(active_keys.begin(), active_keys.end()); + for (size_t i=0; i& traces ); + signed_block_ptr _produce_block( fc::microseconds skip_time, bool skip_pending_trxs ); + produce_block_result_t _produce_block( fc::microseconds skip_time, bool skip_pending_trxs, bool no_throw ); - void _start_block(fc::time_point block_time); - signed_block_ptr _finish_block(); - void _wait_for_vote_if_needed(controller& c); + transaction_trace_ptr _start_block(fc::time_point block_time); + signed_block_ptr _finish_block(); + void _wait_for_vote_if_needed(controller& c); // Fields: protected: @@ -491,9 +528,12 @@ namespace eosio { namespace testing { public: vector protocol_features_to_be_activated_wo_preactivation; + signed_block_ptr lib_block; // updated via irreversible_block signal + block_id_type lib_id; // updated via irreversible_block signal private: std::vector get_all_builtin_protocol_features(); + boost::signals2::connection lib_connection; }; class tester : public base_tester { @@ -545,11 +585,15 @@ namespace eosio { namespace testing { using base_tester::produce_block; - signed_block_ptr produce_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { - return _produce_block(skip_time, false); + produce_block_result_t produce_block_ex( fc::microseconds skip_time = default_skip_time, bool no_throw = false ) override { + return _produce_block(skip_time, false, no_throw); } - signed_block_ptr produce_empty_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { + signed_block_ptr produce_block( fc::microseconds skip_time = default_skip_time, bool no_throw = false ) override { + return _produce_block(skip_time, false, no_throw).block; + } + + signed_block_ptr produce_empty_block( fc::microseconds skip_time = default_skip_time ) override { unapplied_transactions.add_aborted( control->abort_block() ); return _produce_block(skip_time, true); } @@ -559,6 +603,7 @@ namespace eosio { namespace testing { } bool validate() { return true; } + }; class tester_no_disable_deferred_trx : public tester { @@ -619,8 +664,7 @@ namespace eosio { namespace testing { if (use_genesis) { init(def_conf.first, def_conf.second); - } - else { + } else { init(def_conf.first); } } @@ -635,20 +679,23 @@ namespace eosio { namespace testing { if (use_genesis) { init(def_conf.first, def_conf.second); - } - else { + } else { init(def_conf.first); } } - signed_block_ptr produce_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { - auto sb = _produce_block(skip_time, false); - validate_push_block(sb); - return sb; + produce_block_result_t produce_block_ex( fc::microseconds skip_time = default_skip_time, bool no_throw = false ) override { + auto produce_block_result = _produce_block(skip_time, false, no_throw); + validate_push_block(produce_block_result.block); + return produce_block_result; + } + + signed_block_ptr produce_block( fc::microseconds skip_time = default_skip_time, bool no_throw = false ) override { + return produce_block_ex(skip_time, no_throw).block; } - signed_block_ptr produce_block_no_validation( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) ) { - return _produce_block(skip_time, false); + signed_block_ptr produce_block_no_validation( fc::microseconds skip_time = default_skip_time ) { + return _produce_block(skip_time, false, false).block; } void validate_push_block(const signed_block_ptr& sb) { @@ -658,7 +705,7 @@ namespace eosio { namespace testing { _wait_for_vote_if_needed(*validating_node); } - signed_block_ptr produce_empty_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { + signed_block_ptr produce_empty_block( fc::microseconds skip_time = default_skip_time )override { unapplied_transactions.add_aborted( control->abort_block() ); auto sb = _produce_block(skip_time, true); validate_push_block(sb); @@ -670,8 +717,6 @@ namespace eosio { namespace testing { } bool validate() { - - const auto& hbh = control->head_block_header(); const auto& vn_hbh = validating_node->head_block_header(); bool ok = control->head_block_id() == validating_node->head_block_id() && @@ -689,9 +734,9 @@ namespace eosio { namespace testing { return ok; } - unique_ptr validating_node; - uint32_t num_blocks_to_producer_before_shutdown = 0; - bool skip_validate = false; + unique_ptr validating_node; + uint32_t num_blocks_to_producer_before_shutdown = 0; + bool skip_validate = false; }; class validating_tester_no_disable_deferred_trx : public validating_tester { @@ -700,6 +745,122 @@ namespace eosio { namespace testing { } }; + // ------------------------------------------------------------------------------------- + // creates and manages a set of `bls_public_key` used for finalizers voting and policies + // Supports initial transition to Savanna. + // ------------------------------------------------------------------------------------- + template + struct finalizer_keys { + explicit finalizer_keys(Tester& t, size_t num_keys = 0, size_t finalizer_policy_size = 0) : t(t) { + if (num_keys) + init_keys(num_keys, finalizer_policy_size); + } + + void init_keys( size_t num_keys = 50, size_t finalizer_policy_size = 21) { + fin_policy_size = finalizer_policy_size; + key_names.clear(); pubkeys.clear(); privkeys.clear(); + key_names.reserve(num_keys); + pubkeys.reserve(num_keys); + privkeys.reserve(num_keys); + for (size_t i=0; i set_finalizer_policy(size_t first_key) { + return t.set_active_finalizers({&key_names.at(first_key), fin_policy_size}); + } + + std::vector set_finalizer_policy(std::span indices) { + assert(indices.size() == fin_policy_size); + vector names; + names.reserve(fin_policy_size); + for (auto idx : indices) + names.push_back(key_names.at(idx)); + return t.set_active_finalizers({names.begin(), fin_policy_size}); + } + + // Produce blocks until the transition to Savanna is completed. + // This assumes `set_finalizer_policy` was called immediately + // before this. + // This should be done only once. + // ----------------------------------------------------------- + finalizer_policy transition_to_savanna(const std::function& block_callback = {}) { + auto produce_block = [&]() { + auto b = t.produce_block(); + if (block_callback) + block_callback(b); + return b; + }; + + // `genesis_block` is the first block where set_finalizers() was executed. + // It is the genesis block. + // It will include the first header extension for the instant finality. + // ----------------------------------------------------------------------- + auto genesis_block = produce_block(); + + // Do some sanity checks on the genesis block + // ------------------------------------------ + const auto& ext = genesis_block->template extract_header_extension(); + const auto& fin_policy = ext.new_finalizer_policy; + BOOST_TEST(!!fin_policy); + BOOST_TEST(fin_policy->finalizers.size() == fin_policy_size); + BOOST_TEST(fin_policy->generation == 1); + BOOST_TEST(fin_policy->threshold == (fin_policy_size * 2) / 3 + 1); + + // wait till the genesis_block becomes irreversible. + // The critical block is the block that makes the genesis_block irreversible + // ------------------------------------------------------------------------- + signed_block_ptr critical_block = nullptr; // last value of this var is the critical block + auto genesis_block_num = genesis_block->block_num(); + while(genesis_block_num > t.lib_block->block_num()) + critical_block = produce_block(); + + // Blocks after the critical block are proper IF blocks. + // ----------------------------------------------------- + auto first_proper_block = produce_block(); + BOOST_REQUIRE(first_proper_block->is_proper_svnn_block()); + + // wait till the first proper block becomes irreversible. Transition will be done then + // ----------------------------------------------------------------------------------- + signed_block_ptr pt_block = nullptr; // last value of this var is the first post-transition block + while(first_proper_block->block_num() > t.lib_block->block_num()) { + pt_block = produce_block(); + BOOST_REQUIRE(pt_block->is_proper_svnn_block()); + } + + // lib must advance after 3 blocks + // ------------------------------- + for (size_t i=0; i<3; ++i) + auto b = produce_block(); + + BOOST_REQUIRE_EQUAL(t.lib_block->block_num(), pt_block->block_num()); + return *fin_policy; + } + + Tester& t; + vector key_names; + vector pubkeys; + vector privkeys; + size_t fin_policy_size {0}; + }; + + /** * Utility predicate to check whether an fc::exception message is equivalent to a given string */ @@ -802,4 +963,4 @@ namespace eosio { namespace testing { string expected; }; -} } /// eosio::testing +} /// eosio::testing diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index 98729238c4..32f5cd8790 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -342,6 +342,12 @@ namespace eosio::testing { } } }); + + lib_connection = control->irreversible_block().connect([&](const block_signal_params& t) { + const auto& [ block, id ] = t; + lib_block = block; + lib_id = id; + }); } void base_tester::open( protocol_feature_set&& pfs, const snapshot_reader_ptr& snapshot ) { @@ -382,35 +388,39 @@ namespace eosio::testing { } signed_block_ptr base_tester::_produce_block( fc::microseconds skip_time, bool skip_pending_trxs ) { - std::vector traces; - return _produce_block( skip_time, skip_pending_trxs, false, traces ); + auto res = _produce_block( skip_time, skip_pending_trxs, false ); + return res.block; } - signed_block_ptr base_tester::_produce_block( fc::microseconds skip_time, bool skip_pending_trxs, - bool no_throw, std::vector& traces ) { + produce_block_result_t base_tester::_produce_block( fc::microseconds skip_time, bool skip_pending_trxs, bool no_throw ) { + produce_block_result_t res; + auto head_time = control->head_block_time(); auto next_time = head_time + skip_time; + static transaction_trace_ptr onblock_trace; if( !control->is_building_block() || control->pending_block_time() != next_time ) { - _start_block( next_time ); + res.onblock_trace = _start_block( next_time ); + } else { + res.onblock_trace = std::move(onblock_trace); // saved from _start_block call in last _produce_block } if( !skip_pending_trxs ) { for( auto itr = unapplied_transactions.begin(); itr != unapplied_transactions.end(); ) { auto trace = control->push_transaction( itr->trx_meta, fc::time_point::maximum(), fc::microseconds::maximum(), DEFAULT_BILLED_CPU_TIME_US, true, 0 ); - traces.emplace_back( trace ); if(!no_throw && trace->except) { // this always throws an fc::exception, since the original exception is copied into an fc::exception trace->except->dynamic_rethrow_exception(); } itr = unapplied_transactions.erase( itr ); + res.traces.emplace_back( std::move(trace) ); } vector scheduled_trxs; while ((scheduled_trxs = get_scheduled_transactions()).size() > 0 ) { for( const auto& trx : scheduled_trxs ) { auto trace = control->push_scheduled_transaction( trx, fc::time_point::maximum(), fc::microseconds::maximum(), DEFAULT_BILLED_CPU_TIME_US, true ); - traces.emplace_back( trace ); + res.traces.emplace_back( trace ); if( !no_throw && trace->except ) { // this always throws an fc::exception, since the original exception is copied into an fc::exception trace->except->dynamic_rethrow_exception(); @@ -419,13 +429,13 @@ namespace eosio::testing { } } - auto head_block = _finish_block(); + res.block = _finish_block(); - _start_block( next_time + fc::microseconds(config::block_interval_us)); - return head_block; + onblock_trace = _start_block( next_time + fc::microseconds(config::block_interval_us)); + return res; } - void base_tester::_start_block(fc::time_point block_time) { + transaction_trace_ptr base_tester::_start_block(fc::time_point block_time) { auto head_block_number = control->head_block_num(); auto producer = control->head_active_producers().get_scheduled_producer(block_time); @@ -452,11 +462,13 @@ namespace eosio::testing { preactivated_protocol_features.end() ); - control->start_block( block_time, head_block_number - last_produced_block_num, feature_to_be_activated, - controller::block_status::incomplete ); + auto onblock_trace = control->start_block( block_time, head_block_number - last_produced_block_num, + feature_to_be_activated, + controller::block_status::incomplete ); // Clear the list, if start block finishes successfuly, the protocol features should be assumed to be activated protocol_features_to_be_activated_wo_preactivation.clear(); + return onblock_trace; } signed_block_ptr base_tester::_finish_block() { @@ -505,18 +517,16 @@ namespace eosio::testing { } } - signed_block_ptr base_tester::produce_block( std::vector& traces ) { - return _produce_block( fc::milliseconds(config::block_interval_ms), false, true, traces ); - } - - void base_tester::produce_blocks( uint32_t n, bool empty ) { + signed_block_ptr base_tester::produce_blocks( uint32_t n, bool empty ) { + signed_block_ptr res; if( empty ) { for( uint32_t i = 0; i < n; ++i ) - produce_empty_block(); + res = produce_empty_block(); } else { for( uint32_t i = 0; i < n; ++i ) - produce_block(); + res = produce_block(); } + return res; } vector base_tester::get_scheduled_transactions() const { @@ -564,14 +574,14 @@ namespace eosio::testing { } - void base_tester::set_transaction_headers( transaction& trx, uint32_t expiration, uint32_t delay_sec ) const { - trx.expiration = fc::time_point_sec{control->head_block_time() + fc::seconds(expiration)}; - trx.set_reference_block( control->head_block_id() ); + void base_tester::set_transaction_headers( transaction& trx, uint32_t expiration, uint32_t delay_sec ) const { + trx.expiration = fc::time_point_sec{control->head_block_time() + fc::seconds(expiration)}; + trx.set_reference_block( control->head_block_id() ); - trx.max_net_usage_words = 0; // No limit - trx.max_cpu_usage_ms = 0; // No limit - trx.delay_sec = delay_sec; - } + trx.max_net_usage_words = 0; // No limit + trx.max_cpu_usage_ms = 0; // No limit + trx.delay_sec = delay_sec; + } transaction_trace_ptr base_tester::create_account( account_name a, account_name creator, bool multisig, bool include_code ) { @@ -1193,7 +1203,8 @@ namespace eosio::testing { } - std::pair> base_tester::set_finalizers(std::span finalizer_names) { + std::pair> + base_tester::set_finalizers(std::span finalizer_names) { auto num_finalizers = finalizer_names.size(); std::vector finalizers_info; finalizers_info.reserve(num_finalizers); @@ -1210,7 +1221,8 @@ namespace eosio::testing { return set_finalizers(policy_input); } - std::pair> base_tester::set_finalizers(const finalizer_policy_input& input) { + std::pair> + base_tester::set_finalizers(const finalizer_policy_input& input) { chain::bls_pub_priv_key_map_t local_finalizer_keys; fc::variants finalizer_auths; std::vector priv_keys; @@ -1264,7 +1276,8 @@ namespace eosio::testing { pubkeys.push_back(pubkey); input.finalizers.emplace_back(name, 1); } - input.threshold = names.size() * 2 / 3 + 1; + // same as reference-contracts/.../contracts/eosio.system/src/finalizer_key.cpp#L73 + input.threshold = (names.size() * 2) / 3 + 1; set_finalizers(input); return pubkeys; } diff --git a/plugins/state_history_plugin/tests/session_test.cpp b/plugins/state_history_plugin/tests/session_test.cpp index d064ab2cb4..7e7b0adbbb 100644 --- a/plugins/state_history_plugin/tests/session_test.cpp +++ b/plugins/state_history_plugin/tests/session_test.cpp @@ -205,7 +205,7 @@ struct test_server : mock_state_history_plugin { }; // Create and launch a listening port - auto server = std::make_shared>( + auto server = std::make_shared>( ship_ioc, logger, boost::posix_time::milliseconds(100), "", local_address, "", create_session); server->do_accept(); local_address = server->acceptor().local_endpoint(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c81a3a872c..e1376c6cb5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -51,6 +51,7 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version-label.sh ${CMAKE_CURRENT_BINA configure_file(${CMAKE_CURRENT_SOURCE_DIR}/full-version-label.sh ${CMAKE_CURRENT_BINARY_DIR}/full-version-label.sh COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/nodeos_producer_watermark_test.py ${CMAKE_CURRENT_BINARY_DIR}/nodeos_producer_watermark_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cli_test.py ${CMAKE_CURRENT_BINARY_DIR}/cli_test.py COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ship_reqs_across_svnn_test.py ${CMAKE_CURRENT_BINARY_DIR}/ship_reqs_across_svnn_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ship_test.py ${CMAKE_CURRENT_BINARY_DIR}/ship_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ship_streamer_test.py ${CMAKE_CURRENT_BINARY_DIR}/ship_streamer_test.py COPYONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bridge_for_fork_test_shape.json ${CMAKE_CURRENT_BINARY_DIR}/bridge_for_fork_test_shape.json COPYONLY) @@ -154,6 +155,8 @@ set_property(TEST disaster_recovery PROPERTY LABELS nonparallelizable_tests) add_test(NAME disaster_recovery_2 COMMAND tests/disaster_recovery_2.py -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST disaster_recovery_2 PROPERTY LABELS nonparallelizable_tests) +add_test(NAME ship_reqs_across_svnn_test COMMAND tests/ship_reqs_across_svnn_test.py -v ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +set_property(TEST ship_reqs_across_svnn_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME ship_test COMMAND tests/ship_test.py -v --num-clients 10 --num-requests 5000 ${UNSHARE} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) set_property(TEST ship_test PROPERTY LABELS nonparallelizable_tests) add_test(NAME ship_test_unix COMMAND tests/ship_test.py -v --num-clients 10 --num-requests 5000 ${UNSHARE} --unix-socket WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) diff --git a/tests/PerformanceHarness/NodeosPluginArgs/generate_nodeos_plugin_args_class_files.py b/tests/PerformanceHarness/NodeosPluginArgs/generate_nodeos_plugin_args_class_files.py index 40999e8ab2..2a1034372f 100755 --- a/tests/PerformanceHarness/NodeosPluginArgs/generate_nodeos_plugin_args_class_files.py +++ b/tests/PerformanceHarness/NodeosPluginArgs/generate_nodeos_plugin_args_class_files.py @@ -56,7 +56,7 @@ def main(): myStr = result.stdout myStr = myStr.rstrip("\n") - myStr = re.sub(":\n\s+-",':@@@\n -', string=myStr) + myStr = re.sub(":\n\\s+-",':@@@\n -', string=myStr) myStr = re.sub("\n\n",'\n@@@', string=myStr) myStr = re.sub("Application Options:\n",'', string=myStr) pluginSections = re.split("(@@@.*?@@@\n)", string=myStr) @@ -68,16 +68,16 @@ def pairwise(iterable): pluginOptsDict = {} for section, options in pairwise(pluginSections[1:]): - myOpts = re.sub("\s+", " ", options) + myOpts = re.sub("\\s+", " ", options) myOpts = re.sub("\n", " ", myOpts) myOpts = re.sub(" --", "\n--",string = myOpts) splitOpts=re.split("\n", myOpts) argDescDict = {} for opt in splitOpts[1:]: - secondSplit = re.split("(--[\w\-]+)", opt)[1:] + secondSplit = re.split("(--[\\w\\-]+)", opt)[1:] argument=secondSplit[0] - argDefaultDesc=secondSplit[1].lstrip("\s") + argDefaultDesc=secondSplit[1].lstrip("\\s") argDescDict[argument] = argDefaultDesc section=re.sub("@@@", "", section) section=re.sub("\n", "", section) @@ -122,7 +122,7 @@ def writeDataclass(plugin:str, dataFieldDict:dict, pluginOptsDict:dict): newKey="".join([x.capitalize() for x in key.split('-')]).replace('--','') newKey="".join([newKey[0].lower(), newKey[1:]]) value = chainPluginArgs[newKey] - match = re.search("\(=.*?\)", value) + match = re.search("\\(=.*?\\)", value) if match is not None: value = match.group(0)[2:-1] try: diff --git a/tests/ship_reqs_across_svnn_test.py b/tests/ship_reqs_across_svnn_test.py new file mode 100755 index 0000000000..c43b2c42d7 --- /dev/null +++ b/tests/ship_reqs_across_svnn_test.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import shutil +import signal + +from TestHarness import Cluster, TestHelper, Utils, WalletMgr + +############################################################### +# ship_reqs_across_savanna +# +# This test verifies SHiP get_blocks_result_v1 works across Legacy and Savanna boundary. +# 1. Start a producer Node and a SHiP node in Legacy mode +# 2. Transition to Savanna +# 3. Start a SHiP client, requesting blocks between block number 1 (pre Savanna) +# and a block whose block number greater than Savanna Genesis block (post Savanna) +# 4. Verify `finality_data` field in every block before Savanna Genesis block is NULL, +# and `finality_data` field in every block after Savanna Genesis block contains a value. +# +############################################################### + +Print=Utils.Print + +args = TestHelper.parse_args({"--dump-error-details","--keep-logs","-v","--leave-running","--unshared"}) + +Utils.Debug=args.v +cluster=Cluster(unshared=args.unshared, keepRunning=args.leave_running, keepLogs=args.keep_logs) +dumpErrorDetails=args.dump_error_details +walletPort=TestHelper.DEFAULT_WALLET_PORT + +totalProducerNodes=1 +totalNonProducerNodes=1 +totalNodes=totalProducerNodes+totalNonProducerNodes + +walletMgr=WalletMgr(True, port=walletPort) +testSuccessful=False + +shipTempDir=None + +try: + TestHelper.printSystemInfo("BEGIN") + + cluster.setWalletMgr(walletMgr) + Print("Stand up cluster") + + shipNodeNum = 1 + specificExtraNodeosArgs={} + specificExtraNodeosArgs[shipNodeNum]="--plugin eosio::state_history_plugin --trace-history --chain-state-history --state-history-stride 200 --plugin eosio::net_api_plugin --plugin eosio::producer_api_plugin --finality-data-history" + + if cluster.launch(topo="mesh", pnodes=totalProducerNodes, totalNodes=totalNodes, + activateIF=True, + specificExtraNodeosArgs=specificExtraNodeosArgs) is False: + Utils.cmdError("launcher") + Utils.errorExit("Failed to stand up cluster.") + + # Verify nodes are in sync and advancing + cluster.waitOnClusterSync(blockAdvancing=5) + Print("Cluster in Sync") + + Print("Shutdown unneeded bios node") + cluster.biosNode.kill(signal.SIGTERM) + + shipNode = cluster.getNode(shipNodeNum) + # Block with start_block_num is before Savanna and block with end_block_num is after Savanna + start_block_num = 1 + end_block_num = shipNode.getBlockNum() + + # Start a SHiP client and request blocks between start_block_num and end_block_num + shipClient = "tests/ship_streamer" + cmd = f"{shipClient} --start-block-num {start_block_num} --end-block-num {end_block_num} --fetch-block --fetch-traces --fetch-deltas --fetch-finality-data" + if Utils.Debug: Utils.Print(f"cmd: {cmd}") + shipTempDir = os.path.join(Utils.DataDir, "ship") + os.makedirs(shipTempDir, exist_ok = True) + shipClientFilePrefix = os.path.join(shipTempDir, "client") + clientOutFileName = f"{shipClientFilePrefix}.out" + clientOutFile = open(clientOutFileName, "w") + clientErrFile = open(f"{shipClientFilePrefix}.err", "w") + Print(f"Start client") + popen=Utils.delayedCheckOutput(cmd, stdout=clientOutFile, stderr=clientErrFile) + Print(f"Client started, Ship node head is: {shipNode.getBlockNum()}") + + Print("Wait for SHiP client to finish") + popen.wait() + Print("SHiP client stopped") + clientOutFile.close() + clientErrFile.close() + + Print("Shutdown SHiP node") + shipNode.kill(signal.SIGTERM) + + # Find the Savanna Genesis Block number + svnnGensisBlockNum = 0 + shipStderrFileName=Utils.getNodeDataDir(shipNodeNum, "stderr.txt") + with open(shipStderrFileName, 'r') as f: + line = f.readline() + while line: + match = re.search(r'Transitioning to savanna, IF Genesis Block (\d+)', line) + if match: + svnnGensisBlockNum = int(match.group(1)) + break + line = f.readline() + Print(f"Savanna Genesis Block number: {svnnGensisBlockNum}") + + # Make sure start_block_num is indeed pre Savanna and end_block_num is post Savanna + assert svnnGensisBlockNum > start_block_num, f'svnnGensisBlockNum {svnnGensisBlockNum} must be greater than start_block_num {start_block_num}' + assert svnnGensisBlockNum < end_block_num, f'svnnGensisBlockNum {svnnGensisBlockNum} must be less than end_block_num {end_block_num}' + + Print("Verify ship_client output is well formed") + blocks_result_v1 = None + # Verify SHiP client receives well formed results + with open(clientOutFileName, 'r') as f: + lines = f.readlines() + try: + blocks_result_v1 = json.loads(" ".join(lines)) + except json.decoder.JSONDecodeError as er: + Utils.errorExit(f"ship_client output was malformed. Exception: {er}") + + # Verify `finality_data` field in every block before Savanna Genesis block is Null, + # and `finality_data` field in every block after Savanna Genesis block has a value. + Print("Verify finality_data") + for result in blocks_result_v1: + res = result["get_blocks_result_v1"] + block_num = int(res["this_block"]["block_num"]) + finality_data = res["finality_data"] + + if block_num < svnnGensisBlockNum: + assert finality_data is None, f"finality_data is not Null for block {block_num} before Savanna Genesis Block {svnnGensisBlockNum}" + else: + assert finality_data is not None, f"finality_data is Null for block {block_num} after Savanna Genesis Block {svnnGensisBlockNum}" + + testSuccessful = True +finally: + TestHelper.shutdown(cluster, walletMgr, testSuccessful=testSuccessful, dumpErrorDetails=dumpErrorDetails) + if shipTempDir is not None: + if testSuccessful and not args.keep_logs: + shutil.rmtree(shipTempDir, ignore_errors=True) + +errorCode = 0 if testSuccessful else 1 +exit(errorCode) diff --git a/unittests/api_tests.cpp b/unittests/api_tests.cpp index 0796d4333f..136d81b99a 100644 --- a/unittests/api_tests.cpp +++ b/unittests/api_tests.cpp @@ -3863,28 +3863,16 @@ BOOST_AUTO_TEST_CASE(get_code_hash_tests) { try { BOOST_AUTO_TEST_CASE(initial_set_finalizer_test) { try { validating_tester t; - uint32_t lib = 0; - signed_block_ptr lib_block; - t.control->irreversible_block().connect([&](const block_signal_params& t) { - const auto& [ block, id ] = t; - lib = block->block_num(); - lib_block = block; - }); - - t.produce_block(); - - // Create finalizer accounts - vector finalizers = { - "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 - }; - - t.create_accounts(finalizers); - t.produce_block(); + // Create finalizer keys + constexpr size_t num_finalizers = 21; + finalizer_keys fin_keys(t, num_finalizers, num_finalizers); // activate savanna - t.set_finalizers(finalizers); + fin_keys.set_node_finalizers(0, num_finalizers); // activate `num_finalizers` keys for this node, + // starting at key index 0. + fin_keys.set_finalizer_policy(0); // sets the finalizer_policy using consecutive keys, + // starting at key index 0. + // this block contains the header extension for the instant finality, savanna activated when it is LIB auto block = t.produce_block(); @@ -3892,44 +3880,39 @@ BOOST_AUTO_TEST_CASE(initial_set_finalizer_test) { try { BOOST_TEST(!!ext); std::optional fin_policy_diff = std::get(*ext).new_finalizer_policy_diff; BOOST_TEST(!!fin_policy_diff); - BOOST_TEST(fin_policy_diff->finalizers_diff.insert_indexes.size() == finalizers.size()); + BOOST_TEST(fin_policy_diff->finalizers_diff.insert_indexes.size() == num_finalizers); BOOST_TEST(fin_policy_diff->generation == 1); - BOOST_TEST(fin_policy_diff->threshold == finalizers.size() / 3 * 2 + 1); + // same as reference-contracts/.../contracts/eosio.system/src/finalizer_key.cpp#L73 + BOOST_TEST(fin_policy_diff->threshold == (num_finalizers * 2) / 3 + 1); block_id_type if_genesis_block_id = block->calculate_id(); - for (block_num_type active_block_num = block->block_num(); active_block_num > lib; t.produce_block()) { + for (block_num_type active_block_num = block->block_num(); active_block_num > t.lib_block->block_num(); t.produce_block()) { (void)active_block_num; // avoid warning }; // lib_block is IF Genesis Block // block is IF Critical Block - auto fb = t.control->fetch_block_by_id(lib_block->calculate_id()); + auto fb = t.control->fetch_block_by_id(t.lib_id); BOOST_REQUIRE(!!fb); - BOOST_TEST(fb->calculate_id() == lib_block->calculate_id()); + BOOST_TEST(fb->calculate_id() == t.lib_id); ext = fb->extract_header_extension(instant_finality_extension::extension_id()); BOOST_REQUIRE(!!ext); BOOST_TEST(if_genesis_block_id == fb->calculate_id()); - auto lib_after_transition = lib; + auto lib_after_transition = t.lib_block->block_num(); // block after IF Critical Block is IF Proper Block block = t.produce_block(); // lib must advance after 3 blocks t.produce_blocks(3); - BOOST_CHECK_GT(lib, lib_after_transition); + BOOST_CHECK_GT(t.lib_block->block_num(), lib_after_transition); } FC_LOG_AND_RETHROW() } -void test_finality_transition(const vector& accounts, const base_tester::finalizer_policy_input& input, bool lib_advancing_expected) { +void test_finality_transition(const vector& accounts, + const base_tester::finalizer_policy_input& input, + bool lib_advancing_expected) { validating_tester t; - uint32_t lib = 0; - signed_block_ptr lib_block; - t.control->irreversible_block().connect([&](const block_signal_params& t) { - const auto& [ block, id ] = t; - lib = block->block_num(); - lib_block = block; - }); - t.produce_block(); // Create finalizer accounts @@ -3950,27 +3933,27 @@ void test_finality_transition(const vector& accounts, const base_t block_id_type if_genesis_block_id = block->calculate_id(); block_num_type active_block_num = block->block_num(); - while (active_block_num > lib) { + while (active_block_num > t.lib_block->block_num()) { block = t.produce_block(); } // lib_block is IF Genesis Block // block is IF Critical Block - auto fb = t.control->fetch_block_by_id(lib_block->calculate_id()); + auto fb = t.control->fetch_block_by_id(t.lib_id); BOOST_REQUIRE(!!fb); - BOOST_TEST(fb->calculate_id() == lib_block->calculate_id()); + BOOST_TEST(fb->calculate_id() == t.lib_id); ext = fb->extract_header_extension(instant_finality_extension::extension_id()); BOOST_REQUIRE(!!ext); BOOST_TEST(if_genesis_block_id == fb->calculate_id()); - auto lib_after_transition = lib; + auto lib_after_transition = t.lib_block->block_num(); // block after IF Critical Block is IF Proper Block block = t.produce_block(); t.produce_blocks(4); if( lib_advancing_expected ) { - BOOST_CHECK_GT(lib, lib_after_transition); + BOOST_CHECK_GT(t.lib_block->block_num(), lib_after_transition); } else { - BOOST_CHECK_EQUAL(lib, lib_after_transition); + BOOST_CHECK_EQUAL(t.lib_block->block_num(), lib_after_transition); } } diff --git a/unittests/finality_test_cluster.cpp b/unittests/finality_test_cluster.cpp index c1a9ce2064..4c1d1e9e47 100644 --- a/unittests/finality_test_cluster.cpp +++ b/unittests/finality_test_cluster.cpp @@ -1,254 +1,83 @@ #include "finality_test_cluster.hpp" -// Construct a test network and activate IF. -finality_test_cluster::finality_test_cluster() { - using namespace eosio::testing; - - setup_node(node0, "node0"_n); - setup_node(node1, "node1"_n); - setup_node(node2, "node2"_n); - - // node0's votes - node0.node.control->voted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { - last_vote_status = std::get<1>(v); - last_connection_vote = std::get<0>(v); - }); - // collect node1's votes - node1.node.control->voted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { - std::lock_guard g(node1.votes_mtx); - node1.votes.emplace_back(std::get<2>(v)); - }); - // collect node2's votes - node2.node.control->voted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { - std::lock_guard g(node2.votes_mtx); - node2.votes.emplace_back(std::get<2>(v)); - }); - -} - -void finality_test_cluster::initial_tests(){ - - auto block_1_n0 = node0.node.produce_block(); - auto block_1_n1 = node1.node.produce_block(); - auto block_1_n2 = node2.node.produce_block(); - - // this block contains the header exten/sion for the instant finality - std::optional ext = block_1_n0->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); - BOOST_TEST(!!ext); - std::optional fin_policy_diff = std::get(*ext).new_finalizer_policy_diff; - BOOST_TEST(!!fin_policy_diff); - BOOST_TEST(fin_policy_diff->finalizers_diff.insert_indexes.size() == 3); - BOOST_TEST(fin_policy_diff->generation == 1); - - produce_and_push_block(); // make setfinalizer irreversible - - // form a 3-chain to make LIB advacing on node0 - // node0's vote (internal voting) and node1's vote make the quorum - for (auto i = 0; i < 3; ++i) { - produce_and_push_block(); - process_node1_vote(); - } - FC_ASSERT(node0_lib_advancing(), "LIB has not advanced on node0"); - - // QC extension in the block sent to node1 and node2 makes them LIB advancing - produce_and_push_block(); - process_node1_vote(); - FC_ASSERT(node1_lib_advancing(), "LIB has not advanced on node1"); - FC_ASSERT(node2_lib_advancing(), "LIB has not advanced on node2"); - - // clean up processed votes - for (auto& n : nodes) { - std::lock_guard g(n.votes_mtx); - n.votes.clear(); - n.prev_lib_num = n.node.control->if_irreversible_block_num(); - } - -} - -eosio::chain::vote_status finality_test_cluster::wait_on_vote(uint32_t connection_id, bool duplicate) { - // wait for this node's vote to be processed - // duplicates are not signaled - size_t retrys = 200; - while ( (last_connection_vote != connection_id) && --retrys) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - if (!duplicate && last_connection_vote != connection_id) { - FC_ASSERT(false, "Never received vote"); - } else if (duplicate && last_connection_vote == connection_id) { - FC_ASSERT(false, "Duplicate should not have been signaled"); - } - return duplicate ? eosio::chain::vote_status::duplicate : last_vote_status.load(); -} - -// node0 produces a block and pushes it to node1 and node2 -eosio::chain::signed_block_ptr finality_test_cluster::produce_and_push_block() { - auto b = node0.node.produce_block(); - node1.node.push_block(b); - node2.node.push_block(b); - return b; -} - -// send node1's vote identified by "vote_index" in the collected votes -eosio::chain::vote_status finality_test_cluster::process_node1_vote(uint32_t vote_index, vote_mode mode, bool duplicate) { - return process_vote( node1, vote_index, mode, duplicate ); -} - -// send node1's latest vote -eosio::chain::vote_status finality_test_cluster::process_node1_vote(vote_mode mode) { - return process_vote( node1, mode ); -} - -// send node2's vote identified by "vote_index" in the collected votes -eosio::chain::vote_status finality_test_cluster::process_node2_vote(uint32_t vote_index, vote_mode mode) { - return process_vote( node2, vote_index, mode ); -} +using namespace eosio::chain; -// send node2's latest vote -eosio::chain::vote_status finality_test_cluster::process_node2_vote(vote_mode mode) { - return process_vote( node2, mode ); -} - -// returns true if node0's LIB has advanced -bool finality_test_cluster::node0_lib_advancing() { - return lib_advancing(node0); -} - -// returns true if node1's LIB has advanced -bool finality_test_cluster::node1_lib_advancing() { - return lib_advancing(node1); -} - -// returns true if node2's LIB has advanced -bool finality_test_cluster::node2_lib_advancing() { - return lib_advancing(node2); -} - -// Produces a number of blocks and returns true if LIB is advancing. -// This function can be only used at the end of a test as it clears -// node1_votes and node2_votes when starting. -bool finality_test_cluster::produce_blocks_and_verify_lib_advancing() { - // start from fresh - { - std::scoped_lock g(node1.votes_mtx, node2.votes_mtx); - node1.votes.clear(); - node2.votes.clear(); - } - - for (auto i = 0; i < 3; ++i) { - produce_and_push_block(); - process_node1_vote(); - produce_and_push_block(); - if (!node0_lib_advancing() || !node1_lib_advancing() || !node2_lib_advancing()) { - return false; - } - } - - return true; -} - -void finality_test_cluster::produce_blocks(uint32_t blocks_count) { - for (uint32_t i = 0; i < blocks_count; ++i) { - produce_and_push_block(); - process_node1_vote(); - } -} - -void finality_test_cluster::node1_corrupt_vote_block_id() { - std::lock_guard g(node1.votes_mtx); - node1_orig_vote = node1.votes[0]; +// Construct a test network and activate IF. - if( node1.votes[0]->block_id.data()[0] == 'a' ) { - node1.votes[0]->block_id.data()[0] = 'b'; - } else { - node1.votes[0]->block_id.data()[0] = 'a'; - } +void finality_node_t::corrupt_vote_block_id() { + std::lock_guard g(votes_mtx); + auto& last_vote = votes.back(); + orig_vote = std::make_shared(*last_vote); + last_vote->block_id.data()[0] ^= 1; // flip one bit } -void finality_test_cluster::node1_corrupt_vote_finalizer_key() { - std::lock_guard g(node1.votes_mtx); - node1_orig_vote = node1.votes[0]; +void finality_node_t::corrupt_vote_finalizer_key() { + std::lock_guard g(votes_mtx); + auto& last_vote = votes.back(); + orig_vote = std::make_shared(*last_vote); // corrupt the finalizer_key (manipulate so it is different) - auto g1 = node1.votes[0]->finalizer_key.jacobian_montgomery_le(); + auto g1 = last_vote->finalizer_key.jacobian_montgomery_le(); g1 = bls12_381::aggregate_public_keys(std::array{g1, g1}); auto affine = g1.toAffineBytesLE(bls12_381::from_mont::yes); - node1.votes[0]->finalizer_key = fc::crypto::blslib::bls_public_key(affine); + last_vote->finalizer_key = fc::crypto::blslib::bls_public_key(affine); } -void finality_test_cluster::node1_corrupt_vote_signature() { - std::lock_guard g(node1.votes_mtx); - node1_orig_vote = node1.votes[0]; +void finality_node_t::corrupt_vote_signature() { + std::lock_guard g(votes_mtx); + auto& last_vote = votes.back(); + orig_vote = std::make_shared(*last_vote); // corrupt the signature - auto g2 = node1.votes[0]->sig.jacobian_montgomery_le(); + auto g2 = last_vote->sig.jacobian_montgomery_le(); g2 = bls12_381::aggregate_signatures(std::array{g2, g2}); auto affine = g2.toAffineBytesLE(bls12_381::from_mont::yes); - node1.votes[0]->sig = fc::crypto::blslib::bls_signature(affine); + last_vote->sig = fc::crypto::blslib::bls_signature(affine); } -void finality_test_cluster::node1_restore_to_original_vote() { - std::lock_guard g(node1.votes_mtx); - node1.votes[0] = node1_orig_vote; -} - -bool finality_test_cluster::lib_advancing(node_info& node) { - auto curr_lib_num = node.node.control->if_irreversible_block_num(); - auto advancing = curr_lib_num > node.prev_lib_num; - // update pre_lib_num for next time check - node.prev_lib_num = curr_lib_num; - return advancing; +bool finality_node_t::lib_advancing() { + if (lib_num() > prev_lib_num) { + prev_lib_num = lib_num(); + return true; + } + assert(lib_num() == prev_lib_num); + return false; } // private methods follow -void finality_test_cluster::setup_node(node_info& node, eosio::chain::account_name local_finalizer) { +void finality_node_t::setup(size_t first_node_key, size_t num_node_keys) { using namespace eosio::testing; - node.node.produce_block(); - node.node.produce_block(); + cur_key = first_node_key; + finkeys.set_node_finalizers(first_node_key, num_node_keys); - // activate savanna - eosio::testing::base_tester::finalizer_policy_input policy_input = { - .finalizers = { {.name = "node0"_n, .weight = 1}, - {.name = "node1"_n, .weight = 1}, - {.name = "node2"_n, .weight = 1}}, - .threshold = 2, - .local_finalizers = {local_finalizer} - }; + control->voted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { + std::lock_guard g(votes_mtx); + votes.emplace_back(std::get<2>(v)); + }); +} - auto [trace_ptr, priv_keys] = node.node.set_finalizers(policy_input); - FC_ASSERT( priv_keys.size() == 1, "number of private keys should be 1" ); - node.priv_key = priv_keys[0]; // we only have one private key +// Update "vote_index" vote on node according to `mode` parameter +vote_message_ptr finality_node_t::get_vote(size_t vote_index, vote_mode mode) { + std::lock_guard g(votes_mtx); + if (votes.empty()) + return {}; -} + if (vote_index == (size_t)-1) + vote_index = votes.size() - 1; -// send a vote to node0 -eosio::chain::vote_status finality_test_cluster::process_vote(node_info& node, size_t vote_index, vote_mode mode, bool duplicate) { - std::unique_lock g(node.votes_mtx); - FC_ASSERT( vote_index < node.votes.size(), "out of bound index in process_vote" ); - auto& vote = node.votes[vote_index]; + assert(vote_index < votes.size()); + auto vote = votes[vote_index]; if( mode == vote_mode::strong ) { vote->strong = true; } else { vote->strong = false; // fetch the strong digest - auto strong_digest = node.node.control->get_strong_digest_by_id(vote->block_id); + auto strong_digest = control->get_strong_digest_by_id(vote->block_id); // convert the strong digest to weak and sign it - vote->sig = node.priv_key.sign(eosio::chain::create_weak_digest(strong_digest)); + vote->sig = finkeys.privkeys.at(cur_key).sign(eosio::chain::create_weak_digest(strong_digest)); } - g.unlock(); - static uint32_t connection_id = 0; - node0.node.control->process_vote_message( ++connection_id, vote ); - if (eosio::chain::block_header::num_from_id(vote->block_id) > node0.node.control->last_irreversible_block_num()) - return wait_on_vote(connection_id, duplicate); - return eosio::chain::vote_status::unknown_block; + return vote; } - -eosio::chain::vote_status finality_test_cluster::process_vote(node_info& node, vote_mode mode) { - size_t vote_index = -1; - std::unique_lock g(node.votes_mtx); - vote_index = node.votes.size() - 1; - g.unlock(); - return process_vote( node, vote_index, mode ); -} \ No newline at end of file diff --git a/unittests/finality_test_cluster.hpp b/unittests/finality_test_cluster.hpp index 74f561b085..0491189d75 100644 --- a/unittests/finality_test_cluster.hpp +++ b/unittests/finality_test_cluster.hpp @@ -1,116 +1,288 @@ #pragma once -#include #include #include #pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wsign-compare" -#include + #pragma GCC diagnostic ignored "-Wsign-compare" + #include #pragma GCC diagnostic pop -#include -// Set up a test network which consists of 3 nodes: -// * node0 produces blocks and pushes them to node1 and node2; -// node0 votes the blocks it produces internally. -// * node1 votes on the proposal sent by node0 -// * node2 votes on the proposal sent by node0 -// Each node has one finalizer: node0 -- "node0"_n, node1 -- "node1"_n, node2 -- "node2"_n. -// Quorum is set to 2. -// After starup up, IF are activated on both nodes. -// -// APIs are provided to modify/delay/reoder/remove votes from node1 and node2 to node0. +#include -class finality_test_cluster { -public: +// ---------------------------------------------------------------------------- +struct finality_node_t : public eosio::testing::tester { + using vote_message_ptr = eosio::chain::vote_message_ptr; + using vote_status = eosio::chain::vote_status; enum class vote_mode { strong, weak, }; - struct node_info { - eosio::testing::tester node; - uint32_t prev_lib_num{0}; - std::mutex votes_mtx; - std::vector votes; - fc::crypto::blslib::bls_private_key priv_key; - }; + uint32_t prev_lib_num{0}; + std::mutex votes_mtx; + std::vector votes; + eosio::chain::vote_message_ptr orig_vote; + eosio::testing::finalizer_keys finkeys; + size_t cur_key{0}; // index of key used in current policy + + finality_node_t() : finkeys(*this) {} + + size_t last_vote_index() const { + assert(!votes.empty()); + return votes.size() - 1; + } + + void setup(size_t first_node_key, size_t num_node_keys); + + // returns true if LIB advances on this node since we last checked + bool lib_advancing(); + + uint32_t lib_num() const { return lib_block->block_num(); } + + // Intentionally corrupt node's vote's block_id and save the original vote + void corrupt_vote_block_id(); + + // Intentionally corrupt node's vote's finalizer_key and save the original vote + void corrupt_vote_finalizer_key(); + + // Intentionally corrupt node's vote's signature and save the original vote + void corrupt_vote_signature(); + + // Restore node's original vote + void restore_to_original_vote(size_t idx) { + std::lock_guard g(votes_mtx); + assert(!votes.empty()); + + if (idx == (size_t)-1) + votes.back() = orig_vote; + else { + assert(idx < votes.size()); + votes[idx] = orig_vote; + } + } + + void clear_votes_and_reset_lib() { + std::lock_guard g(votes_mtx); + votes.clear(); + prev_lib_num = lib_num(); + } + + // Update "vote_index" vote on node according to `mode` parameter, and returns + // the updated vote. + vote_message_ptr get_vote(size_t vote_index = (size_t)-1, vote_mode mode = vote_mode::strong); + +}; + +struct finality_cluster_config_t { + bool transition_to_savanna; +}; + +// ------------------------------------------------------------------------------------ +// finality_test_cluster +// --------------------- +// +// Set up a test network which consists of NUM_NODES nodes (one producer, +// NUM_NODES finalizers) +// +// * node0 produces blocks and pushes them to [node1, node2, node3, ...]; +// node0 votes on the blocks it produces internally. +// +// * [node1, node2, node3, ...] vote on proposals sent by node0, votes are sent +// to node0 when `process_vote` is called +// +// Each node has one finalizer, quorum is computed using the same formula as in the +// system contracts. +// +// After startup up, IF is activated on node0. +// +// APIs are provided to modify/delay/alter/re-order/remove votes +// from [node1, node2, node3, ...] to node0. +// ------------------------------------------------------------------------------------ +template requires (NUM_NODES > 3) +class finality_test_cluster { +public: + using vote_message_ptr = eosio::chain::vote_message_ptr; + using vote_status = eosio::chain::vote_status; + using signed_block_ptr = eosio::chain::signed_block_ptr; + using tester = eosio::testing::tester; + using vote_mode = finality_node_t::vote_mode; + using bls_public_key = fc::crypto::blslib::bls_public_key; + + static constexpr size_t num_nodes = NUM_NODES; + static constexpr size_t keys_per_node = 10; + + // actual quorum - 1 since node0 processes its own votes + static constexpr size_t num_needed_for_quorum = (num_nodes * 2) / 3; + + static_assert(num_needed_for_quorum < num_nodes, + "this is needed for some tests (conflicting_votes_strong_first for example)"); // Construct a test network and activate IF. - finality_test_cluster(); + finality_test_cluster(finality_cluster_config_t config = {.transition_to_savanna = true}) { + using namespace eosio::testing; + size_t num_finalizers = nodes.size(); + + // ----------------------------------------------------------------------------------- + // each node gets an equal range of keys to be used as local finalizer. + // for example, the default parameters are `num_keys = 40` and `fin_policy_size = 4`, + // which means that for 4 nodes, we'll make each node capable on voting with 10 + // different keys (node0 will use keys 0 .. 9, node1 will use keys 10 .. 19, etc... + // (see set_node_finalizers() call). + // + // The first finalizer_policy (see set_finalizer_policy below) will activate + // keys 0, 10, 20 and 30 + // ----------------------------------------------------------------------------------- + size_t split = finality_test_cluster::keys_per_node; + + // set initial finalizer key for each node + // --------------------------------------- + for (size_t i=0; ivoted_block().connect( [&]( const eosio::chain::vote_signal_params& v ) { + last_vote_status = std::get<1>(v); + last_connection_vote = std::get<0>(v); + }); - // make setfinalizer final and test finality - void initial_tests(); - // send node1's vote identified by "index" in the collected votes - eosio::chain::vote_status process_node1_vote(uint32_t vote_index, vote_mode mode = vote_mode::strong, bool duplicate = false); + // set initial finalizer policy + // ---------------------------- + for (size_t i=0; i 0 && (num_voting_nodes + start_idx <= num_nodes)); + for (size_t i = start_idx; i nodes; - node_info& node0 = nodes[0]; - node_info& node1 = nodes[1]; - node_info& node2 = nodes[2]; + void clear_votes_and_reset_lib() { + for (auto& n : nodes) + n.clear_votes_and_reset_lib(); + } private: + std::atomic last_connection_vote{0}; + std::atomic last_vote_status{}; - std::atomic last_connection_vote{0}; - std::atomic last_vote_status{}; - - eosio::chain::vote_message_ptr node1_orig_vote; +public: + std::array nodes; - // sets up "node_index" node - void setup_node(node_info& node, eosio::chain::account_name local_finalizer); + // Used for transition to Savanna + std::optional fin_policy_0; // policy used to transition to Savanna + std::array fin_policy_indices_0; // set of key indices used for transition + std::vector fin_policy_pubkeys_0; // set of public keys used for transition - // returns true if LIB advances on "node_index" node - bool lib_advancing(node_info& node); + finality_node_t& node0 = nodes[0]; + finality_node_t& node1 = nodes[1]; - // send "vote_index" vote on node to node0 - eosio::chain::vote_status process_vote(node_info& node, size_t vote_index, vote_mode mode, bool duplicate = false); +private: + // sets up "node_index" node + void setup_node(finality_node_t& node, eosio::chain::account_name local_finalizer); - // send the latest vote on "node_index" node to node0 - eosio::chain::vote_status process_vote(node_info& node, vote_mode mode); + // send the vote message to node0 which is the producer (and Savanna leader), and wait till processed + vote_status process_vote(vote_message_ptr& vote, bool duplicate) { + static uint32_t connection_id = 0; + node0.control->process_vote_message( ++connection_id, vote ); + if (eosio::chain::block_header::num_from_id(vote->block_id) > node0.lib_num()) + return wait_on_vote(connection_id, duplicate); + return vote_status::unknown_block; + } - eosio::chain::vote_status wait_on_vote(uint32_t connection_id, bool duplicate); + vote_status wait_on_vote(uint32_t connection_id, bool duplicate) { + // wait for this node's vote to be processed + // duplicates are not signaled + size_t retrys = 200; + while ( (last_connection_vote != connection_id) && --retrys) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!duplicate && last_connection_vote != connection_id) { + FC_ASSERT(false, "Never received vote"); + } else if (duplicate && last_connection_vote == connection_id) { + FC_ASSERT(false, "Duplicate should not have been signaled"); + } + return duplicate ? vote_status::duplicate : last_vote_status.load(); + } }; diff --git a/unittests/finality_tests.cpp b/unittests/finality_tests.cpp index 763d3c2ce3..a7d6a1ef5f 100644 --- a/unittests/finality_tests.cpp +++ b/unittests/finality_tests.cpp @@ -1,583 +1,525 @@ #include "finality_test_cluster.hpp" +using namespace eosio::chain; + /* * register test suite `finality_tests` */ BOOST_AUTO_TEST_SUITE(finality_tests) -// verify LIB advances with 2 finalizers voting. -BOOST_AUTO_TEST_CASE(two_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// verify LIB advances with a quorum of finalizers voting. +// ------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(quorum_of_votes, finality_test_cluster<4>) { try { + produce_and_push_block(); for (auto i = 0; i < 3; ++i) { - // node0 produces a block and pushes to node1 and node2 - cluster.produce_and_push_block(); - // process node1's votes only - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - - // all nodes advance LIB - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + + // when a quorum of nodes vote, LIB should advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } } FC_LOG_AND_RETHROW() } // verify LIB does not advances with finalizers not voting. -BOOST_AUTO_TEST_CASE(no_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); - cluster.node0_lib_advancing(); // reset - cluster.node1_lib_advancing(); // reset - cluster.node2_lib_advancing(); // reset +// -------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(no_votes, finality_test_cluster<4>) { try { + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + produce_and_push_block(); + for (auto i = 0; i < 3; ++i) { + produce_and_push_block(); + // don't process votes + + // when only node0 votes, LIB shouldn't advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + } +} FC_LOG_AND_RETHROW() } + + +// verify LIB does not advances when one less than the quorum votes +// ---------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(quorum_minus_one, finality_test_cluster<4>) { try { + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + produce_and_push_block(); for (auto i = 0; i < 3; ++i) { - // node0 produces a block and pushes to node1 and node2 - cluster.produce_and_push_block(); - // process no votes - cluster.produce_and_push_block(); - - // all nodes don't advance LIB - BOOST_REQUIRE(!cluster.node0_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); + produce_and_push_block(); + process_votes(1, num_needed_for_quorum - 1); + + // when one less than required vote, LIB shouldn't advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); } } FC_LOG_AND_RETHROW() } -// verify LIB advances with all of the three finalizers voting -BOOST_AUTO_TEST_CASE(all_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); + +// verify LIB advances with all finalizers voting +// ---------------------------------------------- +BOOST_FIXTURE_TEST_CASE(all_votes, finality_test_cluster<4>) { try { + produce_and_push_block(); for (auto i = 0; i < 3; ++i) { - // process node1 and node2's votes - cluster.process_node1_vote(); - cluster.process_node2_vote(); - // node0 produces a block and pushes to node1 and node2 - cluster.produce_and_push_block(); - - // all nodes advance LIB - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + process_votes(1, num_nodes - 1); + produce_and_push_block(); + + // when all nodes vote, LIB should advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } } FC_LOG_AND_RETHROW() } // verify LIB advances when votes conflict (strong first and followed by weak) -BOOST_AUTO_TEST_CASE(conflicting_votes_strong_first) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); +// --------------------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(conflicting_votes_strong_first, finality_test_cluster<4>) { try { + produce_and_push_block(); for (auto i = 0; i < 3; ++i) { - cluster.process_node1_vote(); // strong - cluster.process_node2_vote(finality_test_cluster::vote_mode::weak); // weak - cluster.produce_and_push_block(); + auto next_idx = process_votes(1, num_needed_for_quorum); // first a quorum of strong votes + assert(next_idx < num_nodes); + process_vote(next_idx, -1, vote_mode::weak); // and one weak vote + produce_and_push_block(); - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + // when we have a quorum of strong votes, one weak vote should not prevent LIB from advancing + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } } FC_LOG_AND_RETHROW() } // verify LIB advances when votes conflict (weak first and followed by strong) -BOOST_AUTO_TEST_CASE(conflicting_votes_weak_first) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); +// really not significant difference with previous test, just position of weak +// vote in bitset changes. +// --------------------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(conflicting_votes_weak_first, finality_test_cluster<4>) { try { + produce_and_push_block(); for (auto i = 0; i < 3; ++i) { - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); // weak - cluster.process_node2_vote(); // strong - cluster.produce_and_push_block(); + process_vote(1, -1, vote_mode::weak); // a weak vote on node 1 + process_votes(2, num_needed_for_quorum); // and a quorum of strong votes + produce_and_push_block(); - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + // when we have a quorum of strong votes, one weak vote should not prevent LIB from advancing + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } } FC_LOG_AND_RETHROW() } // Verify a delayed vote works -BOOST_AUTO_TEST_CASE(one_delayed_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// --------------------------- +BOOST_FIXTURE_TEST_CASE(one_delayed_votes, finality_test_cluster<4>) { try { // hold the vote for the first block to simulate delay - cluster.produce_and_push_block(); - // LIB advanced on nodes because a new block was received - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + produce_and_push_block(); + produce_and_push_block(); - cluster.produce_and_push_block(); + // now node1 to nodeN each have a 2 vote vector // vote block 0 (index 0) to make it have a strong QC, - // prompting LIB advacing on node2 - cluster.process_node1_vote(0); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + // prompting LIB advancing on node2 + process_votes(1, num_needed_for_quorum, 0); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // block 1 (index 1) has the same QC claim as block 0. It cannot move LIB - cluster.process_node1_vote(1); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, 1); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // producing, pushing, and voting a new block makes LIB moving - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // Verify 3 consecutive delayed votes work -BOOST_AUTO_TEST_CASE(three_delayed_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// --------------------------------------- +BOOST_FIXTURE_TEST_CASE(three_delayed_votes, finality_test_cluster<4>) { try { // produce 4 blocks and hold the votes for the first 3 to simulate delayed votes - // The 4 blocks have the same QC claim as no QCs are created because missing one vote - for (auto i = 0; i < 4; ++i) { - cluster.produce_and_push_block(); - } - // LIB advanced on nodes because a new block was received - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + // The 4 blocks have the same QC claim as no QCs are created because quorum was + // not reached + for (auto i = 0; i < 4; ++i) + produce_and_push_block(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + // LIB did not advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // vote block 0 (index 0) to make it have a strong QC, // prompting LIB advacing on nodes - cluster.process_node1_vote(0); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, 0); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - // blocks 1 to 3 have the same QC claim as block 0. It cannot move LIB + // blocks 1 to 3 have the same QC claim as block 0. They cannot move LIB for (auto i=1; i < 4; ++i) { - cluster.process_node1_vote(i); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, i); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); } - // producing, pushing, and voting a new block makes LIB moving - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + // Now send votes for the last block that node0 produced (block 8). It will be + // able to incorporate these votes into a new QC, which will be attached to + // the next block it produces. + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(out_of_order_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// What happens when votes are processed out of order +// -------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(out_of_order_votes, finality_test_cluster<4>) { try { // produce 3 blocks and hold the votes to simulate delayed votes // The 3 blocks have the same QC claim as no QCs are created because missing votes - for (auto i = 0; i < 3; ++i) { - cluster.produce_and_push_block(); - } + for (auto i = 0; i < 3; ++i) + produce_and_push_block(); // vote out of the order: the newest to oldest // vote block 2 (index 2) to make it have a strong QC, // prompting LIB advacing - cluster.process_node1_vote(2); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, 2); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // block 1 (index 1) has the same QC claim as block 2. It will not move LIB - cluster.process_node1_vote(1); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node0_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, 1); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // block 0 (index 0) has the same QC claim as block 2. It will not move LIB - cluster.process_node1_vote(0); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node0_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, 0); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // producing, pushing, and voting a new block makes LIB moving - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // Verify a vote which was delayed by a large number of blocks does not cause any issues -BOOST_AUTO_TEST_CASE(long_delayed_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// ------------------------------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(long_delayed_votes, finality_test_cluster<4>) { try { // Produce and push a block, vote on it after a long delay. constexpr uint32_t delayed_vote_index = 0; - cluster.produce_and_push_block(); - // The strong QC extension for prior block makes LIB advance on nodes - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - // the vote makes a strong QC for the current block, prompting LIB advance on nodes - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + produce_and_push_block(); // this is the block we will vote on later + produce_and_push_block(); for (auto i = 2; i < 100; ++i) { - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node0_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } // Late vote does not cause any issues - BOOST_REQUIRE_NO_THROW(cluster.process_node1_vote(delayed_vote_index)); + BOOST_REQUIRE_NO_THROW(process_votes(1, num_needed_for_quorum, delayed_vote_index)); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(lost_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - +// Check that if we never vote on a block, it doesn't cause any problem +// -------------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(lost_votes, finality_test_cluster<4>) { try { // Produce and push a block, never vote on it to simulate lost. // The block contains a strong QC extension for prior block - cluster.produce_and_push_block(); + auto b1 = produce_and_push_block(); + process_votes(1, num_needed_for_quorum); + auto b2 = produce_and_push_block(); // this block contains a strong QC for the previous block + const auto& ext = b2->template extract_extension(); + BOOST_REQUIRE_EQUAL(ext.qc.block_num, b1->block_num()); // The strong QC extension for prior block makes LIB advance on nodes - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); + + // but don't propagate the votes on b2. Make sure they are lost + clear_votes_and_reset_lib(); - cluster.produce_and_push_block(); - // The block is not voted, so no strong QC is created and LIB does not advance on nodes - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); + produce_and_push_block(); // Produce another block + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // LIB doesn't advance - cluster.process_node1_vote(); - cluster.produce_and_push_block(); + process_votes(1, num_needed_for_quorum); // and propagate the votes for this new block to node0 + produce_and_push_block(); - // vote causes lib to advance - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // vote causes lib to advance - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(one_weak_vote) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - // Produce and push a block - cluster.produce_and_push_block(); - // Change the vote to a weak vote and process it - cluster.process_node1_vote(0, finality_test_cluster::vote_mode::weak); - // The strong QC extension for prior block makes LIB advance on node1 - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - - cluster.produce_and_push_block(); - // A weak QC is created and LIB does not advance on node2 - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - // no 2-chain was formed as prior block was not a strong block - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - // the vote makes a strong QC and a higher final_on_strong_qc, - // prompting LIB advance on nodes - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - - // now a 3 chain has formed. - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); +// One weak vote preventing a strong QC +// ------------------------------------ +BOOST_FIXTURE_TEST_CASE(one_weak_vote, finality_test_cluster<4>) { try { + produce_and_push_block(); + + auto next_idx = process_votes(1, num_needed_for_quorum -1); // one less strong vote than needed for quorum + process_vote(next_idx, -1, vote_mode::weak); // and one weak vote + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC (1 shy of strong) => LIB does not advance + + process_votes(1, num_needed_for_quorum); // now this provides enough strong votes for quorum + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(two_weak_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); +// A quorum-1 of weak votes and one strong vote +// -------------------------------------------- +BOOST_FIXTURE_TEST_CASE(quorum_minus_one_weak_vote, finality_test_cluster<4>) { try { + produce_and_push_block(); - // Produce and push a block - cluster.produce_and_push_block(); - // The strong QC extension for prior block makes LIB advance on nodes - BOOST_REQUIRE(cluster.node1_lib_advancing()); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - - // Change the vote to a weak vote and process it - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); - // A weak QC cannot advance LIB on nodes - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); - // A weak QC cannot advance LIB on node2 - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - // no 2-chain was formed as prior block was not a strong block - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // now a 3 chain has formed. - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC => LIB does not advance + + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -BOOST_AUTO_TEST_CASE(intertwined_weak_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); +// A sequence of "weak - strong - weak - strong" QCs +// ------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(weak_strong_weak_strong, finality_test_cluster<4>) { try { + produce_and_push_block(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC => LIB does not advance - // Weak vote - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance - // The strong QC extension for prior block makes LIB advance on nodes - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - // Strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // Weak vote - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); - // A weak QC cannot advance LIB on nodes - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - // Strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - // the vote makes a strong QC for the current block, prompting LIB advance on node0 - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // Strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC => LIB does not advance + + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -// Verify a combination of weak, delayed, lost votes still work -BOOST_AUTO_TEST_CASE(weak_delayed_lost_vote) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // A weak vote - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - // A delayed vote (index 1) +// A sequence of "weak - weak - strong - strong" QCs +// ------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(weak_weak_strong_strong, finality_test_cluster<4>) { try { + produce_and_push_block(); + + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC => LIB does not advance + + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // weak QC => LIB does not advance + + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance + + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // strong QC => LIB does advance + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); +} FC_LOG_AND_RETHROW() } + + +// Verify a combination of weak, delayed, lost votes still works +// ------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(weak_delayed_lost_vote, finality_test_cluster<4>) { try { + produce_and_push_block(); + + // quorum of weak votes + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + + // delay votes at index 1 constexpr uint32_t delayed_index = 1; - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); - // A strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + // quorum of strong votes + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // A lost vote - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); - // The delayed vote arrives, does not advance lib because it is weak - cluster.process_node1_vote(delayed_index); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); + // The delayed vote arrives, does not advance lib + process_votes(1, num_needed_for_quorum, delayed_index); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // strong vote advances lib - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // Verify a combination of delayed, weak, lost votes still work -BOOST_AUTO_TEST_CASE(delayed_strong_weak_lost_vote) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - // A delayed vote (index 0) - constexpr uint32_t delayed_index = 0; - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // A strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - // A weak vote - cluster.process_node1_vote(finality_test_cluster::vote_mode::weak); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - // A strong vote - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); +// ------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(delayed_strong_weak_lost_vote, finality_test_cluster<4>) { try { + produce_and_push_block(); + + // delay votes at index 1 + constexpr uint32_t delayed_index = 0; + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + + // quorum of strong votes + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); + + // quorum of weak votes + process_votes(1, num_needed_for_quorum, -1, vote_mode::weak); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + + // quorum of strong votes + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); // A lost vote - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - // The delayed vote arrives - cluster.process_node1_vote(delayed_index, finality_test_cluster::vote_mode::strong, true); - cluster.produce_and_push_block(); - BOOST_REQUIRE(!cluster.node2_lib_advancing()); - BOOST_REQUIRE(!cluster.node1_lib_advancing()); - - cluster.process_node1_vote(); - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); - - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + + // The delayed vote arrives, does not advance lib + process_votes(1, num_needed_for_quorum, delayed_index); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); + + // strong vote advances lib + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // verify duplicate votes do not affect LIB advancing -BOOST_AUTO_TEST_CASE(duplicate_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - cluster.produce_and_push_block(); +// -------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(duplicate_votes, finality_test_cluster<4>) { try { + produce_and_push_block(); + for (auto i = 0; i < 5; ++i) { - cluster.process_node1_vote(i, finality_test_cluster::vote_mode::strong); - // vote again to make it duplicate - BOOST_REQUIRE(cluster.process_node1_vote(i, finality_test_cluster::vote_mode::strong, true) == eosio::chain::vote_status::duplicate); - cluster.produce_and_push_block(); + process_votes(1, num_needed_for_quorum, i, vote_mode::strong); + + // vote again (with duplicate == true) to make it duplicate + process_votes(1, num_needed_for_quorum, i, vote_mode::strong, true); + produce_and_push_block(); // verify duplicate votes do not affect LIB advancing - BOOST_REQUIRE(cluster.node2_lib_advancing()); - BOOST_REQUIRE(cluster.node1_lib_advancing()); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); } + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // verify unknown_proposal votes are handled properly -BOOST_AUTO_TEST_CASE(unknown_proposal_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); +// -------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(unknown_proposal_votes, finality_test_cluster<4>) { try { + produce_and_push_block(); - // node0 produces a block and pushes to node1 - cluster.produce_and_push_block(); - // intentionally corrupt block_id in node1's vote - cluster.node1_corrupt_vote_block_id(); + // intentionally corrupt block_id in node1's vote (vote index 0) + node1.corrupt_vote_block_id(); // process the corrupted vote - BOOST_REQUIRE_THROW(cluster.process_node1_vote(0), fc::exception); // throws because it times out waiting on vote - cluster.produce_and_push_block(); - BOOST_REQUIRE(cluster.node2_lib_advancing()); + BOOST_REQUIRE_THROW(process_votes(1, 1), fc::exception); // throws as it times out waiting on vote (block id not found) + process_votes(2, num_needed_for_quorum - 1); - // restore to original vote - cluster.node1_restore_to_original_vote(); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); - // process the original vote. LIB should advance - cluster.produce_and_push_block(); - cluster.process_node1_vote(0, finality_test_cluster::vote_mode::strong, true); + node1.restore_to_original_vote(0); // restore node1's vote at index 0 to original vote + process_votes(1, 1, 0, vote_mode::strong); // send restored vote to node0 + produce_and_push_block(); // produce a block so the new QC can propagate + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } -// verify unknown finalizer_key votes are handled properly -BOOST_AUTO_TEST_CASE(unknown_finalizer_key_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); +// verify unknown finalizer_key votes are handled properly +// ------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(unknown_finalizer_key_votes, finality_test_cluster<4>) { try { // node0 produces a block and pushes to node1 - cluster.produce_and_push_block(); + produce_and_push_block(); // intentionally corrupt finalizer_key in node1's vote - cluster.node1_corrupt_vote_finalizer_key(); + node1.corrupt_vote_finalizer_key(); // process the corrupted vote. LIB should not advance - cluster.process_node1_vote(0); - BOOST_REQUIRE(cluster.process_node1_vote(0) == eosio::chain::vote_status::unknown_public_key); + process_vote(1, 0); + BOOST_REQUIRE(process_vote(1, 0) == eosio::chain::vote_status::unknown_public_key); // restore to original vote - cluster.node1_restore_to_original_vote(); + node1.restore_to_original_vote(0); // process the original vote. LIB should advance - cluster.process_node1_vote(0); + process_vote(1, 0); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } // verify corrupted signature votes are handled properly -BOOST_AUTO_TEST_CASE(corrupted_signature_votes) { try { - finality_test_cluster cluster; - cluster.initial_tests(); - - // node0 produces a block and pushes to node1 - cluster.produce_and_push_block(); +// ----------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(corrupted_signature_votes, finality_test_cluster<4>) { try { + produce_and_push_block(); - // intentionally corrupt signature in node1's vote - cluster.node1_corrupt_vote_signature(); + // intentionally corrupt signature in node1's vote (vote index 0) + node1.corrupt_vote_signature(); - // process the corrupted vote. LIB should not advance - BOOST_REQUIRE(cluster.process_node1_vote(0) == eosio::chain::vote_status::invalid_signature); + // process the corrupted vote + process_votes(1, 1); + process_votes(2, num_needed_for_quorum - 1); - // restore to original vote - cluster.node1_restore_to_original_vote(); + produce_and_push_block(); + BOOST_REQUIRE_EQUAL(num_lib_advancing(), 0); // because of the one corrupted vote, quorum is not reached - // process the original vote. LIB should advance - cluster.process_node1_vote(); + node1.restore_to_original_vote(0); // restore node1's vote at index 0 to original vote + process_votes(1, 1, 0, vote_mode::strong); // send restored vote to node0 + produce_and_push_block(); // produce a block so the new QC can propagate + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); + + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); +} FC_LOG_AND_RETHROW() } + +// verify LIB advances after second set_finalizers +// ----------------------------------------------- +BOOST_FIXTURE_TEST_CASE(second_set_finalizers, finality_test_cluster<4>) { try { + produce_and_push_block(); + process_votes(1, num_needed_for_quorum); + produce_and_push_block(); + + // when a quorum of nodes vote, LIB should advance + BOOST_REQUIRE_EQUAL(num_lib_advancing(), num_nodes); + BOOST_REQUIRE(produce_blocks_and_verify_lib_advancing()); + + // run a second set_finalizers + // --------------------------- + assert(fin_policy_0); // current finalizer policy from transition to Savanna + + auto indices1 = fin_policy_indices_0; // start from original set of indices + assert(indices1[0] == 0); // we used index 0 for node0 in original policy + indices1[0] = 1; // update key used for node0 in policy + auto pubkeys1 = node0.finkeys.set_finalizer_policy(indices1); + + // we need two 3-chains for the new finalizer policy to be activated + for (size_t i=0; i<6; ++i) { + produce_and_push_block(); + process_votes(1, num_nodes - 1); + node0.check_head_finalizer_policy(1, fin_policy_pubkeys_0); // original policy still active + } + + // we just completed the two 3-chains, so the next block we produce will have the new finalizer policy activated + produce_and_push_block(); + node0.check_head_finalizer_policy(2, pubkeys1); + node1.check_head_finalizer_policy(2, pubkeys1); - BOOST_REQUIRE(cluster.produce_blocks_and_verify_lib_advancing()); } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/finalizer_update_tests.cpp b/unittests/finalizer_update_tests.cpp index 6d52b4dbd8..d69cd6d2e3 100644 --- a/unittests/finalizer_update_tests.cpp +++ b/unittests/finalizer_update_tests.cpp @@ -9,101 +9,12 @@ using namespace eosio::chain::literals; using namespace eosio::testing; using namespace eosio::chain; -// --------------------------------------------------------------------- -// Given a newly created `validating_tester`, trigger the transition to -// Savanna, and produce blocks until the transition is completed. -// --------------------------------------------------------------------- -static std::pair, std::vector> -transition_to_Savanna(validating_tester& t, size_t num_local_finalizers, size_t finset_size) { - uint32_t lib = 0; - signed_block_ptr lib_block; - auto c = t.control->irreversible_block().connect([&](const block_signal_params& t) { - const auto& [ block, id ] = t; - lib = block->block_num(); - lib_block = block; - }); - - t.produce_block(); - - // Create finalizer accounts - vector finalizers; - finalizers.reserve(num_local_finalizers); - for (size_t i=0; iblock_num() > lib) - critical_block = t.produce_block(); - - // Blocks after the critical block are proper IF blocks. - // ----------------------------------------------------- - auto first_proper_block = t.produce_block(); - BOOST_REQUIRE(first_proper_block->is_proper_svnn_block()); - - // wait till the first proper block becomes irreversible. Transition will be done then - // ----------------------------------------------------------------------------------- - signed_block_ptr pt_block = nullptr; // last value of this var is the first post-transition block - while(first_proper_block->block_num() > lib) { - pt_block = t.produce_block(); - BOOST_REQUIRE(pt_block->is_proper_svnn_block()); - } - - // lib must advance after 3 blocks - // ------------------------------- - t.produce_blocks(3); - BOOST_REQUIRE_EQUAL(lib, pt_block->block_num()); - - c.disconnect(); - return { finalizers, pubkeys0 }; -} /* * register test suite `finalizer_update_tests` */ BOOST_AUTO_TEST_SUITE(finalizer_update_tests) -// --------------------------------------------------------------------- -// checks that the active finalizer_policy for `block` matches the -// passed `generation` and `keys_span`. -// --------------------------------------------------------------------- -static void check_finalizer_policy(validating_tester& t, - const signed_block_ptr& block, - uint32_t generation, - std::span keys_span) { - auto finpol = t.active_finalizer_policy(block->calculate_id()); - BOOST_REQUIRE(!!finpol); - BOOST_REQUIRE_EQUAL(finpol->generation, generation); // new policy should not be active - // until after two 3-chains - BOOST_REQUIRE_EQUAL(keys_span.size(), finpol->finalizers.size()); - std::vector keys {keys_span.begin(), keys_span.end() }; - std::sort(keys.begin(), keys.end()); - - std::vector active_keys; - for (const auto& auth : finpol->finalizers) - active_keys.push_back(auth.public_key); - std::sort(active_keys.begin(), active_keys.end()); - for (size_t i=0; i keys_span) { - auto b = t.produce_block(); - check_finalizer_policy(t, b, generation, keys_span); + t.produce_block(); + t.check_head_finalizer_policy(generation, keys_span); } // --------------------------------------------------------------------- @@ -121,28 +32,33 @@ static void ensure_next_block_finalizer_policy(validating_tester& t, // --------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(savanna_set_finalizer_single_test) { try { validating_tester t; - size_t num_local_finalizers = 50; - size_t finset_size = 21; + size_t num_keys = 22; + size_t finset_size = 21; + + // Create finalizer keys + finalizer_keys fin_keys(t, num_keys, finset_size); - auto [finalizers, pubkeys0] = transition_to_Savanna(t, num_local_finalizers, finset_size); - assert(finalizers.size() == num_local_finalizers && pubkeys0.size() == finset_size); + // set finalizers on current node + fin_keys.set_node_finalizers(0, num_keys); + + // run initial set_finalizer_policy() and waits until transition is complete + auto pubkeys0 = fin_keys.set_finalizer_policy(0); + fin_keys.transition_to_savanna(); // run set_finalizers(), verify it becomes active after exactly two 3-chains // ------------------------------------------------------------------------- - auto pubkeys1 = t.set_active_finalizers({&finalizers[1], finset_size}); - auto b0 = t.produce_block(); - check_finalizer_policy(t, b0, 1, pubkeys0); // new policy should only be active until after two 3-chains + auto pubkeys1 = fin_keys.set_finalizer_policy(1); + t.produce_block(); + t.check_head_finalizer_policy(1, pubkeys0); // new policy should only be active until after two 3-chains - t.produce_blocks(2); - auto b3 = t.produce_block(); - check_finalizer_policy(t, b3, 1, pubkeys0); // one 3-chain - new policy still should not be active + t.produce_blocks(3); + t.check_head_finalizer_policy(1, pubkeys0); // one 3-chain - new policy still should not be active - t.produce_blocks(1); - auto b5 = t.produce_block(); - check_finalizer_policy(t, b5, 1, pubkeys0); // one 3-chain + 2 blocks - new policy still should not be active + t.produce_blocks(2); + t.check_head_finalizer_policy(1, pubkeys0); // one 3-chain + 2 blocks - new policy still should not be active - auto b6 = t.produce_block(); - check_finalizer_policy(t, b6, 2, pubkeys1); // two 3-chain - new policy *should* be active + t.produce_block(); + t.check_head_finalizer_policy(2, pubkeys1); // two 3-chain - new policy *should* be active } FC_LOG_AND_RETHROW() } @@ -152,63 +68,70 @@ BOOST_AUTO_TEST_CASE(savanna_set_finalizer_single_test) { try { // --------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(savanna_set_finalizer_multiple_test) { try { validating_tester t; - size_t num_local_finalizers = 50; - size_t finset_size = 21; + size_t num_keys = 50; + size_t finset_size = 21; + + // Create finalizer keys + finalizer_keys fin_keys(t, num_keys, finset_size); + + // set finalizers on current node + fin_keys.set_node_finalizers(0, num_keys); - auto [finalizers, pubkeys0] = transition_to_Savanna(t, num_local_finalizers, finset_size); + // run initial set_finalizer_policy() and waits until transition is complete + auto pubkeys0 = fin_keys.set_finalizer_policy(0); + fin_keys.transition_to_savanna(); // run set_finalizers() twice in same block, verify only latest one becomes active // ------------------------------------------------------------------------------- - auto pubkeys1 = t.set_active_finalizers({&finalizers[1], finset_size}); - auto pubkeys2 = t.set_active_finalizers({&finalizers[2], finset_size}); - auto b0 = t.produce_block(); - check_finalizer_policy(t, b0, 1, pubkeys0); // new policy should only be active until after two 3-chains - t.produce_blocks(4); - auto b5 = t.produce_block(); - check_finalizer_policy(t, b5, 1, pubkeys0); // new policy should only be active until after two 3-chains - auto b6 = t.produce_block(); - check_finalizer_policy(t, b6, 2, pubkeys2); // two 3-chain - new policy pubkeys2 *should* be active + (void)fin_keys.set_finalizer_policy(1); + auto pubkeys2 = fin_keys.set_finalizer_policy(2); + t.produce_block(); + t.check_head_finalizer_policy(1, pubkeys0); // new policy should only be active until after two 3-chains + t.produce_blocks(5); + t.check_head_finalizer_policy(1, pubkeys0); // new policy should only be active until after two 3-chains + t.produce_block(); + t.check_head_finalizer_policy(2, pubkeys2); // two 3-chain - new policy pubkeys2 *should* be active // run a test with multiple set_finlizers in-flight during the two 3-chains they // take to become active // ----------------------------------------------------------------------------- - auto pubkeys3 = t.set_active_finalizers({&finalizers[3], finset_size}); - b0 = t.produce_block(); - auto pubkeys4 = t.set_active_finalizers({&finalizers[4], finset_size}); - auto b1 = t.produce_block(); - auto b2 = t.produce_block(); - auto pubkeys5 = t.set_active_finalizers({&finalizers[5], finset_size}); + auto pubkeys3 = fin_keys.set_finalizer_policy(3); + t.produce_block(); + auto pubkeys4 = fin_keys.set_finalizer_policy(4); + t.produce_block(); + t.produce_block(); + auto pubkeys5 = fin_keys.set_finalizer_policy(5); t.produce_blocks(2); - auto pubkeys6 = t.set_active_finalizers({&finalizers[6], finset_size}); - b5 = t.produce_block(); - auto pubkeys7 = t.set_active_finalizers({&finalizers[7], finset_size}); - check_finalizer_policy(t, b5, 2, pubkeys2); // 5 blocks after pubkeys3 (b5 - b0), pubkeys2 should still be active - b6 = t.produce_block(); - auto pubkeys8 = t.set_active_finalizers({&finalizers[8], finset_size}); - check_finalizer_policy(t, b6, 3, pubkeys3); // 6 blocks after pubkeys3 (b6 - b0), pubkeys3 should be active - auto b7 = t.produce_block(); - auto pubkeys9 = t.set_active_finalizers({&finalizers[9], finset_size}); - check_finalizer_policy(t, b7, 4, pubkeys4); // 6 blocks after pubkeys4 (b7 - b1), pubkeys4 should be active - - auto b8 = t.produce_block(); - auto pubkeys10 = t.set_active_finalizers({&finalizers[10], finset_size}); - check_finalizer_policy(t, b8, 4, pubkeys4); // 7 blocks after pubkeys4, pubkeys4 should still be active - auto b9 = t.produce_block(); - auto pubkeys11 = t.set_active_finalizers({&finalizers[11], finset_size}); - check_finalizer_policy(t, b9, 5, pubkeys5); // 6 blocks after pubkeys5 (b9 - b3), pubkeys5 should be active - auto b10 = t.produce_block(); - auto b11 = t.produce_block(); // two blocks between 5 & 6 proposals - check_finalizer_policy(t, b11, 6, pubkeys6); // the rest are all one block apart, tests pending with propsed + auto pubkeys6 = fin_keys.set_finalizer_policy(6); + t.produce_block(); + auto pubkeys7 = fin_keys.set_finalizer_policy(7); + t.check_head_finalizer_policy(2, pubkeys2); // 5 blocks after pubkeys3 (b5 - b0), pubkeys2 should still be active + t.produce_block(); + auto pubkeys8 = fin_keys.set_finalizer_policy(8); + t.check_head_finalizer_policy(3, pubkeys3); // 6 blocks after pubkeys3 (b6 - b0), pubkeys3 should be active + t.produce_block(); + auto pubkeys9 = fin_keys.set_finalizer_policy(9); + t.check_head_finalizer_policy(4, pubkeys4); // 6 blocks after pubkeys4 (b7 - b1), pubkeys4 should be active + + t.produce_block(); + auto pubkeys10 = fin_keys.set_finalizer_policy(10); + t.check_head_finalizer_policy(4, pubkeys4); // 7 blocks after pubkeys4, pubkeys4 should still be active + t.produce_block(); + auto pubkeys11 = fin_keys.set_finalizer_policy(11); + t.check_head_finalizer_policy(5, pubkeys5); // 6 blocks after pubkeys5 (b9 - b3), pubkeys5 should be active + t.produce_block(); + t.produce_block(); // two blocks between 5 & 6 proposals + t.check_head_finalizer_policy(6, pubkeys6); // the rest are all one block apart, tests pending with propsed auto b12 = t.produce_block(); - check_finalizer_policy(t, b12, 7, pubkeys7); + t.check_head_finalizer_policy(7, pubkeys7); auto b13 = t.produce_block(); - check_finalizer_policy(t, b13, 8, pubkeys8); + t.check_head_finalizer_policy(8, pubkeys8); auto b14 = t.produce_block(); - check_finalizer_policy(t, b14, 9, pubkeys9); + t.check_head_finalizer_policy(9, pubkeys9); auto b15 = t.produce_block(); - check_finalizer_policy(t, b15, 10, pubkeys10); + t.check_head_finalizer_policy(10, pubkeys10); auto b16 = t.produce_block(); - check_finalizer_policy(t, b16, 11, pubkeys11); + t.check_head_finalizer_policy(11, pubkeys11); // and no further change ensure_next_block_finalizer_policy(t, 11, pubkeys11); diff --git a/unittests/forked_tests.cpp b/unittests/forked_tests.cpp index 487677a36c..846f84ec93 100644 --- a/unittests/forked_tests.cpp +++ b/unittests/forked_tests.cpp @@ -171,8 +171,7 @@ BOOST_AUTO_TEST_CASE( forking ) try { wlog( "c1 blocks:" ); c.produce_blocks(3); - signed_block_ptr b; - b = c.produce_block(); + signed_block_ptr b = c.produce_block(); account_name expected_producer = "dan"_n; BOOST_REQUIRE_EQUAL( b->producer.to_string(), expected_producer.to_string() ); @@ -751,8 +750,8 @@ BOOST_AUTO_TEST_CASE( push_block_returns_forked_transactions ) try { }) ; // produce block which will apply the unapplied transactions - std::vector traces; - c.produce_block( traces ); + produce_block_result_t produce_block_result = c.produce_block_ex(fc::milliseconds(config::block_interval_ms), true); + std::vector& traces = produce_block_result.traces; BOOST_REQUIRE_EQUAL( 4u, traces.size() ); BOOST_CHECK_EQUAL( trace1->id, traces.at(0)->id ); diff --git a/unittests/producer_schedule_if_tests.cpp b/unittests/producer_schedule_if_tests.cpp index 59e3b8667c..6f83e19992 100644 --- a/unittests/producer_schedule_if_tests.cpp +++ b/unittests/producer_schedule_if_tests.cpp @@ -48,12 +48,6 @@ BOOST_FIXTURE_TEST_CASE( verify_producer_schedule_after_instant_finality_activat BOOST_TEST(scheduled_changed_to_new); }; - uint32_t lib = 0; - control->irreversible_block().connect([&](const block_signal_params& t) { - const auto& [ block, id ] = t; - lib = block->block_num(); - }); - // Create producer accounts vector producers = { "inita"_n, "initb"_n, "initc"_n, "initd"_n, "inite"_n, "initf"_n, "initg"_n, @@ -66,7 +60,7 @@ BOOST_FIXTURE_TEST_CASE( verify_producer_schedule_after_instant_finality_activat set_finalizers(producers); auto setfin_block = produce_block(); // this block contains the header extension of the finalizer set - for (block_num_type active_block_num = setfin_block->block_num(); active_block_num > lib; produce_block()) { + for (block_num_type active_block_num = setfin_block->block_num(); active_block_num > lib_block->block_num(); produce_block()) { set_producers({"initc"_n, "inite"_n}); // should be ignored since in transition (void)active_block_num; // avoid warning }; diff --git a/unittests/protocol_feature_tests.cpp b/unittests/protocol_feature_tests.cpp index 3893e46477..dc9cf3b433 100644 --- a/unittests/protocol_feature_tests.cpp +++ b/unittests/protocol_feature_tests.cpp @@ -1080,12 +1080,6 @@ BOOST_AUTO_TEST_CASE( protocol_activatation_works_after_transition_to_savanna ) c.set_bios_contract(); c.produce_block(); - uint32_t lib = 0; - c.control->irreversible_block().connect([&](const block_signal_params& t) { - const auto& [ block, id ] = t; - lib = block->block_num(); - }); - c.produce_block(); vector accounts = { @@ -1121,10 +1115,10 @@ BOOST_AUTO_TEST_CASE( protocol_activatation_works_after_transition_to_savanna ) ext = fb->extract_header_extension(instant_finality_extension::extension_id()); BOOST_REQUIRE(ext); - auto lib_after_transition = lib; + auto lib_after_transition = c.lib_block->block_num(); c.produce_blocks(4); - BOOST_CHECK_GT(lib, lib_after_transition); + BOOST_CHECK_GT(c.lib_block->block_num(), lib_after_transition); // verify protocol feature activation works under savanna diff --git a/unittests/restart_chain_tests.cpp b/unittests/restart_chain_tests.cpp index d60fe80ef8..fbd845a164 100644 --- a/unittests/restart_chain_tests.cpp +++ b/unittests/restart_chain_tests.cpp @@ -66,12 +66,15 @@ class replay_tester : public base_tester { } using base_tester::produce_block; - signed_block_ptr produce_block(fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms)) override { - return _produce_block(skip_time, false); + produce_block_result_t produce_block_ex(fc::microseconds skip_time = default_skip_time, bool no_throw = false) override { + return _produce_block(skip_time, false, no_throw); } - signed_block_ptr - produce_empty_block(fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms)) override { + signed_block_ptr produce_block(fc::microseconds skip_time = default_skip_time, bool no_throw = false) override { + return produce_block_ex(skip_time, no_throw).block; + } + + signed_block_ptr produce_empty_block(fc::microseconds skip_time = default_skip_time) override { unapplied_transactions.add_aborted(control->abort_block()); return _produce_block(skip_time, true); } @@ -92,7 +95,7 @@ BOOST_AUTO_TEST_CASE(test_existing_state_without_block_log) { blocks.push_back(chain.produce_block()); tester other; - for (auto new_block : blocks) { + for (const auto& new_block : blocks) { other.push_block(new_block); } blocks.clear(); @@ -108,7 +111,7 @@ BOOST_AUTO_TEST_CASE(test_existing_state_without_block_log) { blocks.push_back(chain.produce_block()); chain.control->abort_block(); - for (auto new_block : blocks) { + for (const auto& new_block : blocks) { other.push_block(new_block); } } @@ -122,7 +125,7 @@ BOOST_AUTO_TEST_CASE(test_restart_with_different_chain_id) { blocks.push_back(chain.produce_block()); tester other; - for (auto new_block : blocks) { + for (const auto& new_block : blocks) { other.push_block(new_block); } blocks.clear(); diff --git a/unittests/snapshot_tests.cpp b/unittests/snapshot_tests.cpp index ac2654b851..6bdb10f675 100644 --- a/unittests/snapshot_tests.cpp +++ b/unittests/snapshot_tests.cpp @@ -53,7 +53,7 @@ controller::config copy_config_and_files(const controller::config& config, int o class snapshotted_tester : public base_tester { public: enum config_file_handling { dont_copy_config_files, copy_config_files }; - snapshotted_tester(controller::config config, const snapshot_reader_ptr& snapshot, int ordinal, + snapshotted_tester(const controller::config& config, const snapshot_reader_ptr& snapshot, int ordinal, config_file_handling copy_files_from_config = config_file_handling::dont_copy_config_files) { FC_ASSERT(config.blocks_dir.filename().generic_string() != "." && config.state_dir.filename().generic_string() != ".", "invalid path names in controller::config"); @@ -64,11 +64,15 @@ class snapshotted_tester : public base_tester { init(copied_config, snapshot); } - signed_block_ptr produce_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { - return _produce_block(skip_time, false); + produce_block_result_t produce_block_ex( fc::microseconds skip_time = default_skip_time, bool no_throw = false )override { + return _produce_block(skip_time, false, no_throw); } - signed_block_ptr produce_empty_block( fc::microseconds skip_time = fc::milliseconds(config::block_interval_ms) )override { + signed_block_ptr produce_block( fc::microseconds skip_time = default_skip_time, bool no_throw = false )override { + return produce_block_ex(skip_time, no_throw).block; + } + + signed_block_ptr produce_empty_block( fc::microseconds skip_time = default_skip_time )override { control->abort_block(); return _produce_block(skip_time, true); } diff --git a/unittests/svnn_ibc_tests.cpp b/unittests/svnn_ibc_tests.cpp index d879e88a06..56d8c95c87 100644 --- a/unittests/svnn_ibc_tests.cpp +++ b/unittests/svnn_ibc_tests.cpp @@ -18,11 +18,14 @@ using namespace eosio::testing; using mvo = mutable_variant_object; +static digest_type hash_pair(const digest_type& a, const digest_type& b) { + return fc::sha256::hash(std::pair(a, b)); +} + BOOST_AUTO_TEST_SUITE(svnn_ibc) //extract instant finality data from block header extension, as well as qc data from block extension qc_data_t extract_qc_data(const signed_block_ptr& b) { - std::optional qc_data; auto hexts = b->validate_and_extract_header_extensions(); if (auto if_entry = hexts.lower_bound(instant_finality_extension::extension_id()); if_entry != hexts.end()) { auto& if_ext = std::get(if_entry->second); @@ -40,7 +43,6 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) //generate a proof of inclusion for a node at index from a list of leaves std::vector generate_proof_of_inclusion(const std::vector leaves, const size_t index) { - auto _leaves = leaves; auto _index = index; @@ -61,8 +63,7 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) _index = i / 2; // Update index for next level } - } - else { + } else { // Odd number of leaves at this level, and we're at the end new_level.push_back(left); // Promote the left (which is also the right in this case) if (_index == i) _index = i / 2; // Update index for next level, no sibling to add @@ -76,45 +77,47 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) BOOST_AUTO_TEST_CASE(ibc_test) { try { - // cluster is set up with the head about to produce IF Genesis - finality_test_cluster cluster; + // cluster is set up with the head about to produce IF Genesis + finality_test_cluster<4> cluster { finality_cluster_config_t{.transition_to_savanna = false} }; // produce IF Genesis block auto genesis_block = cluster.produce_and_push_block(); - // ensure out of scope setup and initial cluster wiring is consistent - BOOST_CHECK(genesis_block->block_num() == 6); + // ensure out of scope setup and initial cluster wiring is consistent + BOOST_CHECK_EQUAL(genesis_block->block_num(), 4); // check if IF Genesis block contains an IF extension - std::optional maybe_genesis_if_ext = genesis_block->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); + std::optional maybe_genesis_if_ext = + genesis_block->extract_header_extension(eosio::chain::instant_finality_extension::extension_id()); BOOST_CHECK(maybe_genesis_if_ext.has_value()); eosio::chain::block_header_extension genesis_if_ext = maybe_genesis_if_ext.value(); - + // check that the header extension is of the correct type BOOST_CHECK(std::holds_alternative(genesis_if_ext)); // and that it has the expected initial finalizer_policy - std::optional maybe_active_finalizer_policy_diff = std::get(genesis_if_ext).new_finalizer_policy_diff; + std::optional maybe_active_finalizer_policy_diff = + std::get(genesis_if_ext).new_finalizer_policy_diff; BOOST_CHECK(maybe_active_finalizer_policy_diff.has_value()); /** TODO diff or full active policy ? eosio::chain::finalizer_policy active_finalizer_policy = maybe_active_finalizer_policy.value(); - BOOST_CHECK(active_finalizer_policy.finalizers.size() == 3); - BOOST_CHECK(active_finalizer_policy.generation == 1); + BOOST_CHECK_EQUAL(active_finalizer_policy.finalizers.size(), cluster.num_nodes); + BOOST_CHECK_EQUAL(active_finalizer_policy.generation, 1); // compute the digest of the finalizer policy auto active_finalizer_policy_digest = fc::sha256::hash(active_finalizer_policy); - auto genesis_block_fd = cluster.node0.node.control->head_finality_data(); + auto genesis_block_fd = cluster.node0.control->head_finality_data(); // verify we have finality data for the IF genesis block BOOST_CHECK(genesis_block_fd.has_value()); // compute IF finality leaf auto genesis_base_digest = genesis_block_fd.value().base_digest; - auto genesis_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, genesis_base_digest)); + auto genesis_afp_base_digest = hash_pair(active_finalizer_policy_digest, genesis_base_digest); auto genesis_block_finality_digest = fc::sha256::hash(eosio::chain::finality_digest_data_v1{ .active_finalizer_policy_generation = active_finalizer_policy.generation, @@ -133,11 +136,11 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) }); // create the ibc account and deploy the ibc contract to it - cluster.node0.node.create_account( "ibc"_n ); - cluster.node0.node.set_code( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_wasm()); - cluster.node0.node.set_abi( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_abi()); + cluster.node0.create_account( "ibc"_n ); + cluster.node0.set_code( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_wasm()); + cluster.node0.set_abi( "ibc"_n, eosio::testing::test_contracts::svnn_ibc_abi()); - cluster.node0.node.push_action( "ibc"_n, "setfpolicy"_n, "ibc"_n, mvo() + cluster.node0.push_action( "ibc"_n, "setfpolicy"_n, "ibc"_n, mvo() ("from_block_num", 1) ("policy", mvo() ("generation", active_finalizer_policy.generation) @@ -149,23 +152,25 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // Transition block. Finalizers are not expected to vote on this block. auto block_1 = cluster.produce_and_push_block(); - auto block_1_fd = cluster.node0.node.control->head_finality_data(); + auto block_1_fd = cluster.node0.control->head_finality_data(); auto block_1_action_mroot = block_1_fd.value().action_mroot; - auto block_1_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_1->calculate_id()); + auto block_1_finality_digest = cluster.node0.control->get_strong_digest_by_id(block_1->calculate_id()); auto block_1_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ .block_num = block_1->block_num(), .finality_digest = block_1_finality_digest, .action_mroot = block_1_action_mroot }); - // Proper IF Block. From now on, finalizers must vote. Moving forward, the header action_mroot field is reconverted to provide the finality_mroot. The action_mroot is instead provided via the finality data + // Proper IF Block. From now on, finalizers must vote. Moving forward, the header action_mroot + // field is reconverted to provide the finality_mroot. + // The action_mroot is instead provided via the finality data auto block_2 = cluster.produce_and_push_block(); - cluster.process_node1_vote(); //enough to reach quorum threshold - auto block_2_fd = cluster.node0.node.control->head_finality_data(); + cluster.process_votes(1, cluster.num_needed_for_quorum); //enough to reach quorum threshold + auto block_2_fd = cluster.node0.control->head_finality_data(); auto block_2_action_mroot = block_2_fd.value().action_mroot; auto block_2_base_digest = block_2_fd.value().base_digest; - auto block_2_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_2->calculate_id()); - auto block_2_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_2_base_digest)); + auto block_2_finality_digest = cluster.node0.control->get_strong_digest_by_id(block_2->calculate_id()); + auto block_2_afp_base_digest = hash_pair(active_finalizer_policy_digest, block_2_base_digest); auto block_2_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ .block_num = block_2->block_num(), .finality_digest = block_2_finality_digest, @@ -175,10 +180,10 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // block_3 contains a QC over block_2 auto block_3 = cluster.produce_and_push_block(); - cluster.process_node1_vote(); - auto block_3_fd = cluster.node0.node.control->head_finality_data(); + cluster.process_votes(1, cluster.num_needed_for_quorum ); + auto block_3_fd = cluster.node0.control->head_finality_data(); auto block_3_action_mroot = block_3_fd.value().action_mroot; - auto block_3_finality_digest = cluster.node0.node.control->get_strong_digest_by_id(block_3->calculate_id()); + auto block_3_finality_digest = cluster.node0.control->get_strong_digest_by_id(block_3->calculate_id()); auto block_3_leaf = fc::sha256::hash(valid_t::finality_leaf_node_t{ .block_num = block_3->block_num(), .finality_digest = block_3_finality_digest, @@ -187,19 +192,23 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // block_4 contains a QC over block_3 auto block_4 = cluster.produce_and_push_block(); - cluster.process_node1_vote(); - auto block_4_fd = cluster.node0.node.control->head_finality_data(); + cluster.process_votes(1, cluster.num_needed_for_quorum); + auto block_4_fd = cluster.node0.control->head_finality_data(); auto block_4_base_digest = block_4_fd.value().base_digest; - auto block_4_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_4_base_digest)); + auto block_4_afp_base_digest = hash_pair(active_finalizer_policy_digest, block_4_base_digest); + + auto block_4_finality_root = block_4->action_mroot; + qc_data_t qc_b_4 = extract_qc_data(block_4); - auto block_4_finality_root = block_4->action_mroot; + BOOST_TEST(qc_b_4.qc.has_value()); - // block_5 contains a QC over block_4, which completes the 3-chain for block_2 and serves as a proof of finality for it + // block_5 contains a QC over block_4, which completes the 3-chain for block_2 and + // serves as a proof of finality for it auto block_5 = cluster.produce_and_push_block(); - cluster.process_node1_vote(); - auto block_5_fd = cluster.node0.node.control->head_finality_data(); + cluster.process_votes(1, cluster.num_needed_for_quorum); + auto block_5_fd = cluster.node0.control->head_finality_data(); auto block_5_base_digest = block_5_fd.value().base_digest; - auto block_5_afp_base_digest = fc::sha256::hash(std::pair(active_finalizer_policy_digest, block_5_base_digest)); + auto block_5_afp_base_digest = hash_pair(active_finalizer_policy_digest, block_5_base_digest); auto block_5_finality_root = block_5->action_mroot; // retrieve the QC over block_4 that is contained in block_5 @@ -207,16 +216,22 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) BOOST_TEST(qc_b_5.qc.has_value()); - // block_5 contains a QC over block_4, which completes the 3-chain for block_2 and serves as a proof of finality for it + // block_5 contains a QC over block_4, which completes the 3-chain for block_2 + // and serves as a proof of finality for it auto block_6 = cluster.produce_and_push_block(); - cluster.process_node1_vote(); + cluster.process_votes(1, cluster.num_needed_for_quorum); // retrieve the QC over block_5 that is contained in block_6 qc_data_t qc_b_6 = extract_qc_data(block_6); BOOST_TEST(qc_b_6.qc.has_value()); - - std::string raw_bitset("03"); //node0 ande node1 signed + + std::stringstream sstream; + sstream << std::hex << (1 << (cluster.num_needed_for_quorum + 1)) - 1; // we expect a quorum of finalizers to vote + // +1 because num_needed_for_quorum excludes node0 + std::string raw_bitset = sstream.str(); + if (raw_bitset.size() % 2) + raw_bitset.insert(0, "0"); // create a few proofs we'll use to perform tests @@ -320,31 +335,38 @@ BOOST_AUTO_TEST_SUITE(svnn_ibc) // verify first heavy proof - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_1); + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_1); - // now that we stored the proven root, we should be able to verify the same proof without the finality data (aka light proof) - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + // now that we stored the proven root, we should be able to verify the same proof without + // the finality data (aka light proof) + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); - // verify a second proof where the target block is different from the finality block. This also saves a second finality root to the contract, marking the beginning of the cache timer for the older finality root. - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_2); + // verify a second proof where the target block is different from the finality block. + // This also saves a second finality root to the contract, marking the beginning of the cache + // timer for the older finality root. + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, heavy_proof_2); cluster.produce_blocks(1); //advance 1 block to avoid duplicate transaction - // we should still be able to verify a proof of finality for block #2 without finality proof, since the previous root is still cached - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + // we should still be able to verify a proof of finality for block #2 without finality proof, + // since the previous root is still cached + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); cluster.produce_blocks(1200); //advance 10 minutes - // the root is still cached when performing this action, so the action succeeds. However, it also triggers garbage collection,removing the old proven root for block #2, so subsequent call with the same action data will fail - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + // the root is still cached when performing this action, so the action succeeds. + // However, it also triggers garbage collection,removing the old proven root for block #2, + // so subsequent calls with the same action data will fail + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); cluster.produce_blocks(1); //advance 1 block to avoid duplicate transaction bool failed = false; - // Since garbage collection was previously triggered for the merkle root of block #2 which this proof attempts to link to, action will now fail + // Since garbage collection was previously triggered for the merkle root of block #2 which this + // proof attempts to link to, action will now fail try { - cluster.node0.node.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); + cluster.node0.push_action("ibc"_n, "checkproof"_n, "ibc"_n, light_proof_1); } catch(const eosio_assert_message_exception& e){ failed = true;