diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 3aabb50115..391085284c 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1131,8 +1131,19 @@ struct controller_impl { } bool fork_db_validated_block_exists( const block_id_type& id ) const { + return fork_db.apply([&](const auto& forkdb) { + auto bsp = forkdb.get_block(id); + return bsp ? bsp->is_valid() : false; + }); + } + + // precondition: claimed_id is either id, or an ancestor of id + // returns true if block `id`, or one of its ancestors not older than claimed_id, is found in fork_db + // and `is_valid()` + // ------------------------------------------------------------------------------------------------------ + bool fork_db_validated_block_exists( const block_id_type& id, const block_id_type& claimed_id ) const { return fork_db.apply([&](const auto& forkdb) { - return forkdb.validated_block_exists(id); + return forkdb.validated_block_exists(id, claimed_id); }); } @@ -4236,14 +4247,14 @@ struct controller_impl { if (use_thread_pool == use_thread_pool_t::yes && async_voting == async_t::yes) { boost::asio::post(thread_pool.get_executor(), [this, bsp=bsp]() { const auto& latest_qc_claim__block_ref = bsp->core.get_block_reference(bsp->core.latest_qc_claim().block_num); - if (fork_db_validated_block_exists(latest_qc_claim__block_ref.block_id)) { + if (fork_db_validated_block_exists(bsp->previous(), latest_qc_claim__block_ref.block_id)) { create_and_send_vote_msg(bsp); } }); } else { // bsp can be used directly instead of copy needed for post const auto& latest_qc_claim__block_ref = bsp->core.get_block_reference(bsp->core.latest_qc_claim().block_num); - if (fork_db_validated_block_exists(latest_qc_claim__block_ref.block_id)) { + if (fork_db_validated_block_exists(bsp->previous(), latest_qc_claim__block_ref.block_id)) { create_and_send_vote_msg(bsp); } } diff --git a/libraries/chain/fork_database.cpp b/libraries/chain/fork_database.cpp index 89d8db9ebe..e6f20d204f 100644 --- a/libraries/chain/fork_database.cpp +++ b/libraries/chain/fork_database.cpp @@ -99,7 +99,7 @@ namespace eosio::chain { bsp_t get_block_impl( const block_id_type& id, include_root_t include_root = include_root_t::no ) const; bool block_exists_impl( const block_id_type& id ) const; - bool validated_block_exists_impl( const block_id_type& id ) const; + bool validated_block_exists_impl( const block_id_type& id, const block_id_type& claimed_id ) const; void reset_root_impl( const bsp_t& root_bs ); void advance_root_impl( const block_id_type& id ); void remove_impl( const block_id_type& id ); @@ -603,15 +603,30 @@ namespace eosio::chain { } template - bool fork_database_t::validated_block_exists(const block_id_type& id) const { + bool fork_database_t::validated_block_exists(const block_id_type& id, const block_id_type& claimed_id) const { std::lock_guard g( my->mtx ); - return my->validated_block_exists_impl(id); + return my->validated_block_exists_impl(id, claimed_id); } + // precondition: claimed_id is either id, or an ancestor of id + // returns true if block `id`, or one of its ancestors not older than claimed_id, is found in fork_db + // and `is_valid()`. + // ------------------------------------------------------------------------------------------------------ template - bool fork_database_impl::validated_block_exists_impl(const block_id_type& id) const { - auto itr = index.find( id ); - return itr != index.end() && (*itr)->is_valid(); + bool fork_database_impl::validated_block_exists_impl(const block_id_type& id, const block_id_type& claimed_id) const { + bool id_present = false; + + for (auto i = index.find(id); i != index.end(); i = index.find((*i)->previous())) { + id_present = true; + if ((*i)->is_valid()) + return true; + if ((*i)->id() == claimed_id) + return false; + } + + // if we return `true`, let's validate the precondition and make sure claimed_id is not in another branch + assert(!id_present || block_header::num_from_id(claimed_id) <= block_header::num_from_id(root->id())); + return id_present || id == root->id(); } // ------------------ fork_database ------------------------- diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index d00f037bce..21e825bf1a 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -87,8 +87,6 @@ struct block_state : public block_header_state { // block_header_state provi std::optional base_digest; // For finality_data sent to SHiP, computed on demand in get_finality_data() // ------ private methods ----------------------------------------------------------- - void set_valid(bool v) { validated.store(v); } - bool is_valid() const { return validated.load(); } bool is_pub_keys_recovered() const { return pub_keys_recovered; } deque extract_trxs_metas(); void set_trxs_metas(deque&& trxs_metas, bool keys_recovered); @@ -97,9 +95,9 @@ struct block_state : public block_header_state { // block_header_state provi friend struct test_block_state_accessor; friend struct fc::reflector; friend struct controller_impl; - template friend struct fork_database_impl; friend struct completed_block; friend struct building_block; + public: // ------ functions ----------------------------------------------------------------- const block_id_type& id() const { return block_header_state::id(); } @@ -115,6 +113,9 @@ struct block_state : public block_header_state { // block_header_state provi uint32_t latest_qc_block_num() const { return core.latest_qc_claim().block_num; } block_timestamp_type latest_qc_block_timestamp() const { return core.latest_qc_block_timestamp(); } + void set_valid(bool v) { validated.store(v); } + bool is_valid() const { return validated.load(); } + std::optional get_best_qc() const { return aggregating_qc.get_best_qc(block_num()); } // thread safe bool received_qc_is_strong() const { return aggregating_qc.received_qc_is_strong(); } // thread safe // return true if better qc, thread safe diff --git a/libraries/chain/include/eosio/chain/block_state_legacy.hpp b/libraries/chain/include/eosio/chain/block_state_legacy.hpp index 72977f864a..4484db593e 100644 --- a/libraries/chain/include/eosio/chain/block_state_legacy.hpp +++ b/libraries/chain/include/eosio/chain/block_state_legacy.hpp @@ -51,20 +51,17 @@ namespace eosio::chain { const producer_authority_schedule* pending_schedule_auth() const { return &block_header_state_legacy::pending_schedule.schedule; } const deque& trxs_metas() const { return _cached_trxs; } - + void set_valid(bool v) { validated.store(v); } + bool is_valid() const { return validated.load(); } + using fork_db_block_state_accessor_t = block_state_legacy_accessor; + private: // internal use only, not thread safe - friend struct block_state_legacy_accessor; friend struct fc::reflector; friend struct controller_impl; - template friend struct fork_database_impl; friend struct completed_block; friend struct block_state; - // not thread safe, expected to only be called from main thread - void set_valid(bool v) { validated = v; } - bool is_valid() const { return validated; } - bool is_pub_keys_recovered()const { return _pub_keys_recovered; } deque extract_trxs_metas() { @@ -78,7 +75,7 @@ namespace eosio::chain { _cached_trxs = std::move( trxs_metas ); } - bool validated = false; + copyable_atomic validated{false}; bool _pub_keys_recovered = false; /// this data is redundant with the data stored in block, but facilitates diff --git a/libraries/chain/include/eosio/chain/fork_database.hpp b/libraries/chain/include/eosio/chain/fork_database.hpp index daac6f7bd3..6d9de2754a 100644 --- a/libraries/chain/include/eosio/chain/fork_database.hpp +++ b/libraries/chain/include/eosio/chain/fork_database.hpp @@ -51,7 +51,7 @@ namespace eosio::chain { bsp_t get_block( const block_id_type& id, include_root_t include_root = include_root_t::no ) const; bool block_exists( const block_id_type& id ) const; - bool validated_block_exists( const block_id_type& id ) const; + bool validated_block_exists( const block_id_type& id, const block_id_type& claimed_id ) const; /** * Purges any existing blocks from the fork database and resets the root block_header_state to the provided value. diff --git a/unittests/fork_db_tests.cpp b/unittests/fork_db_tests.cpp index 99851bd667..410fe3c63d 100644 --- a/unittests/fork_db_tests.cpp +++ b/unittests/fork_db_tests.cpp @@ -54,33 +54,8 @@ struct test_block_state_accessor { using namespace eosio::chain; -BOOST_AUTO_TEST_SUITE(fork_database_tests) - -BOOST_AUTO_TEST_CASE(add_remove_test) try { - fork_database_if_t forkdb; - - // Setup fork database with blocks based on a root of block 10 - // Add a number of forks in the fork database - auto root = test_block_state_accessor::make_genesis_block_state(); - auto bsp11a = test_block_state_accessor::make_unique_block_state(11, root); - auto bsp12a = test_block_state_accessor::make_unique_block_state(12, bsp11a); - auto bsp13a = test_block_state_accessor::make_unique_block_state(13, bsp12a); - auto bsp11b = test_block_state_accessor::make_unique_block_state(11, root); - auto bsp12b = test_block_state_accessor::make_unique_block_state(12, bsp11b); - auto bsp13b = test_block_state_accessor::make_unique_block_state(13, bsp12b); - auto bsp14b = test_block_state_accessor::make_unique_block_state(14, bsp13b); - auto bsp12bb = test_block_state_accessor::make_unique_block_state(12, bsp11b); - auto bsp13bb = test_block_state_accessor::make_unique_block_state(13, bsp12bb); - auto bsp13bbb = test_block_state_accessor::make_unique_block_state(13, bsp12bb); - auto bsp12bbb = test_block_state_accessor::make_unique_block_state(12, bsp11b); - auto bsp11c = test_block_state_accessor::make_unique_block_state(11, root); - auto bsp12c = test_block_state_accessor::make_unique_block_state(12, bsp11c); - auto bsp13c = test_block_state_accessor::make_unique_block_state(13, bsp12c); - - // keep track of all those added for easy verification - std::vector all { bsp11a, bsp12a, bsp13a, bsp11b, bsp12b, bsp12bb, bsp12bbb, bsp13b, bsp13bb, bsp13bbb, bsp14b, bsp11c, bsp12c, bsp13c }; - - auto reset = [&]() { +struct generate_forkdb_state { + generate_forkdb_state() { forkdb.reset_root(root); forkdb.add(bsp11a, ignore_duplicate_t::no); forkdb.add(bsp11b, ignore_duplicate_t::no); @@ -96,9 +71,37 @@ BOOST_AUTO_TEST_CASE(add_remove_test) try { forkdb.add(bsp13bbb, ignore_duplicate_t::no); forkdb.add(bsp14b, ignore_duplicate_t::no); forkdb.add(bsp13c, ignore_duplicate_t::no); - }; - reset(); + } + + fork_database_if_t forkdb; + + // Setup fork database with blocks based on a root of block 10 + // Add a number of forks in the fork database + block_state_ptr root = test_block_state_accessor::make_genesis_block_state(); + block_state_ptr bsp11a = test_block_state_accessor::make_unique_block_state(11, root); + block_state_ptr bsp12a = test_block_state_accessor::make_unique_block_state(12, bsp11a); + block_state_ptr bsp13a = test_block_state_accessor::make_unique_block_state(13, bsp12a); + block_state_ptr bsp11b = test_block_state_accessor::make_unique_block_state(11, root); + block_state_ptr bsp12b = test_block_state_accessor::make_unique_block_state(12, bsp11b); + block_state_ptr bsp13b = test_block_state_accessor::make_unique_block_state(13, bsp12b); + block_state_ptr bsp14b = test_block_state_accessor::make_unique_block_state(14, bsp13b); + block_state_ptr bsp12bb = test_block_state_accessor::make_unique_block_state(12, bsp11b); + block_state_ptr bsp13bb = test_block_state_accessor::make_unique_block_state(13, bsp12bb); + block_state_ptr bsp13bbb = test_block_state_accessor::make_unique_block_state(13, bsp12bb); + block_state_ptr bsp12bbb = test_block_state_accessor::make_unique_block_state(12, bsp11b); + block_state_ptr bsp11c = test_block_state_accessor::make_unique_block_state(11, root); + block_state_ptr bsp12c = test_block_state_accessor::make_unique_block_state(12, bsp11c); + block_state_ptr bsp13c = test_block_state_accessor::make_unique_block_state(13, bsp12c); + // keep track of all those added for easy verification + std::vector all{bsp11a, bsp12a, bsp13a, bsp11b, bsp12b, bsp12bb, bsp12bbb, + bsp13b, bsp13bb, bsp13bbb, bsp14b, bsp11c, bsp12c, bsp13c}; +}; + + +BOOST_AUTO_TEST_SUITE(fork_database_tests) + +BOOST_FIXTURE_TEST_CASE(add_remove_test, generate_forkdb_state) try { // test get_block for (auto& i : all) { BOOST_TEST(forkdb.get_block(i->id()) == i); @@ -137,4 +140,52 @@ BOOST_AUTO_TEST_CASE(add_remove_test) try { BOOST_TEST(branch[1] == bsp11a); } FC_LOG_AND_RETHROW(); + +// test `fork_database_t::validated_block_exists() const` member +// ------------------------------------------------------------- +BOOST_FIXTURE_TEST_CASE(validated_block_exists, generate_forkdb_state) try { + + // if a block is valid in fork_db, all its ancestors are necessarily valid. + root->set_valid(true); + bsp11b->set_valid(true); + bsp12b->set_valid(true); + bsp13b->set_valid(true); + bsp14b->set_valid(true); + + bsp13a->set_valid(false); + + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp14b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp13b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp12b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp11b->id())); + + bsp14b->set_valid(false); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp14b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp13b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp12b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp11b->id())); + + bsp13b->set_valid(false); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp14b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp13b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp12b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp11b->id())); + + bsp12b->set_valid(false); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp14b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp13b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp12b->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), bsp11b->id())); + + bsp11b->set_valid(false); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp14b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp13b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp12b->id())); + BOOST_REQUIRE_EQUAL(false, forkdb.validated_block_exists(bsp14b->id(), bsp11b->id())); + + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), root->id())); + BOOST_REQUIRE_EQUAL(true, forkdb.validated_block_exists(bsp14b->id(), block_id_type{})); + +} FC_LOG_AND_RETHROW(); + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/forked_tests_savanna.cpp b/unittests/forked_tests_savanna.cpp index d36a927ed9..9600517241 100644 --- a/unittests/forked_tests_savanna.cpp +++ b/unittests/forked_tests_savanna.cpp @@ -70,6 +70,7 @@ BOOST_AUTO_TEST_SUITE(forked_tests_savanna) // - produce blocks and verify that finality still advances. // --------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(fork_with_bad_block_savanna, savanna_cluster::cluster_t) try { + auto& C=_nodes[2]; auto& D=_nodes[3]; struct fork_tracker { vector blocks; }; @@ -86,8 +87,8 @@ BOOST_FIXTURE_TEST_CASE(fork_with_bad_block_savanna, savanna_cluster::cluster_t) // split the network. Finality will stop advancing as // votes and blocks are not propagated. - const std::vector partition {2, 3}; - set_partition(partition); // simulate 2 disconnected partitions: nodes {0, 1} and nodes {2, 3} + + set_partition( {&C, &D} ); // simulate 2 disconnected partitions: nodes {0, 1} and nodes {2, 3} // at this point, each node has a QC to include into // the next block it produces which will advance lib. @@ -204,6 +205,7 @@ BOOST_FIXTURE_TEST_CASE(fork_with_bad_block_savanna, savanna_cluster::cluster_t) // - unsplit the network, produce blocks on _nodes[0] and verify lib advances. // ----------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( forking_savanna, savanna_cluster::cluster_t ) try { + auto& C=_nodes[2]; auto& D=_nodes[3]; _nodes[0].produce_blocks(2); // produce two extra blocks at the beginning so that producer schedules align const vector producers { "dan"_n, "sam"_n, "pam"_n }; @@ -213,8 +215,7 @@ BOOST_FIXTURE_TEST_CASE( forking_savanna, savanna_cluster::cluster_t ) try { auto sb = _nodes[0].produce_block(); BOOST_REQUIRE_EQUAL(sb->producer, producers[prod]); // first block produced by producers[prod] - const std::vector partition {2, 3}; - set_partition(partition); // simulate 2 disconnected partitions: nodes {0, 1} and nodes {2, 3} + set_partition( {&C, &D} ); // simulate 2 disconnected partitions: nodes {0, 1} and nodes {2, 3} // at this point, each node has a QC to include into // the next block it produces which will advance lib. @@ -291,6 +292,7 @@ BOOST_FIXTURE_TEST_CASE( forking_savanna, savanna_cluster::cluster_t ) try { // - Unpartition the network, veerify lib advances. // ---------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( verify_savanna_fork_choice, savanna_cluster::cluster_t) try { + auto& A=_nodes[0]; const vector producers { "dan"_n, "sam"_n, "pam"_n }; _nodes[0].create_accounts(producers); auto prod = _nodes[0].set_producers(producers); // set new producers and produce blocks until the switch is pending @@ -300,8 +302,7 @@ BOOST_FIXTURE_TEST_CASE( verify_savanna_fork_choice, savanna_cluster::cluster_t) BOOST_REQUIRE_EQUAL(sb_common->producer, producers[prod]); // first block produced by producers[prod] - const std::vector partition {1, 2, 3}; - set_partition(partition); // simulate 2 disconnected partitions: + set_partition( {&A} ); // simulate 2 disconnected partitions: // P0 (node {0}) and P1 (nodes {1, 2, 3}). // At this point, each node has a QC to include into // the next block it produces which will advance lib by one) @@ -396,6 +397,7 @@ BOOST_FIXTURE_TEST_CASE( irreversible_mode_savanna_1, savanna_cluster::cluster_t // advances past the fork block. // --------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( irreversible_mode_savanna_2, savanna_cluster::cluster_t ) try { + auto& D=_nodes[3]; const vector producers {"producer1"_n, "producer2"_n}; _nodes[0].create_accounts(producers); _nodes[0].set_producers(producers); // set new producers and produce blocks until the switch is pending @@ -408,8 +410,7 @@ BOOST_FIXTURE_TEST_CASE( irreversible_mode_savanna_2, savanna_cluster::cluster_t // partition node3. lib will not advance on node3 anymore, but will advance on the other 3 nodes - const std::vector partition {3}; - set_partition(partition); // simulate 2 disconnected partitions: nodes {0, 1, 2} and node {3} + set_partition( {&D} ); // simulate 2 disconnected partitions: nodes {0, 1, 2} and node {3} // produce blocks on _nodes[3], creating account "bob"_n. Finality will not advance // -------------------------------------------------------------------------------- @@ -492,6 +493,7 @@ BOOST_FIXTURE_TEST_CASE( irreversible_mode_savanna_2, savanna_cluster::cluster_t // - and restart producing on P1, check that finality advances again // --------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( split_and_rejoin, savanna_cluster::cluster_t ) try { + auto& C=_nodes[2]; auto& D=_nodes[3]; const vector producers { "p1"_n, "p2"_n, "p3"_n }; _nodes[0].create_accounts(producers); _nodes[0].set_producers(producers); // set new producers and produce blocks until the switch is pending @@ -501,8 +503,7 @@ BOOST_FIXTURE_TEST_CASE( split_and_rejoin, savanna_cluster::cluster_t ) try { dlog("lib0 = ${lib0}", ("lib0", lib0)); // 45 // split the network - const std::vector partition {2, 3}; - set_partition(partition); // simulate 2 disconnected partitions: nodes {0, 1} and node {2, 3} + set_partition( {&C, &D} ); // simulate 2 disconnected partitions: nodes {0, 1} and node {2, 3} // produce 12 blocks on _nodes[0]'s partition _nodes[0].create_accounts( {"bob"_n} ); @@ -522,7 +523,7 @@ BOOST_FIXTURE_TEST_CASE( split_and_rejoin, savanna_cluster::cluster_t ) try { // update the network split so that {0, 1, 2} are in one partition, enough for finality to start // advancing again - set_partition(std::vector{3}); // simulate 2 disconnected partitions: nodes {0, 1, 2} and node {3} + set_partition( {&D} ); // simulate 2 disconnected partitions: nodes {0, 1, 2} and node {3} propagate_heads(); // otherwise we get unlinkable_block when newly produced blocks are pushed to node2 @@ -539,6 +540,7 @@ BOOST_FIXTURE_TEST_CASE( split_and_rejoin, savanna_cluster::cluster_t ) try { // Verify that a fork switch applies the blocks, and the included transactions in order. // ------------------------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE( push_block_returns_forked_transactions_savanna, savanna_cluster::cluster_t ) try { + auto& C=_nodes[2]; auto& D=_nodes[3]; const vector producers { "p1"_n, "p2"_n, "p3"_n }; _nodes[0].create_accounts(producers); _nodes[0].set_producers(producers); // set new producers and produce blocks until the switch is pending @@ -552,8 +554,7 @@ BOOST_FIXTURE_TEST_CASE( push_block_returns_forked_transactions_savanna, savanna signed_block_ptr cb; // split the network - const std::vector partition {2, 3}; - set_partition(partition); // simulate 2 disconnected partitions: nodes {0, 1} and node {2, 3} + set_partition( {&C, &D} ); // simulate 2 disconnected partitions: nodes {0, 1} and node {2, 3} cb = _nodes[0].produce_block(); _nodes[2].produce_block(); diff --git a/unittests/savanna_cluster.cpp b/unittests/savanna_cluster.cpp index f6a59afaea..ee85b96a28 100644 --- a/unittests/savanna_cluster.cpp +++ b/unittests/savanna_cluster.cpp @@ -21,6 +21,7 @@ node_t::node_t(size_t node_idx, cluster_t& cluster, setup_policy policy /* = set if (status == vote_result_t::success) { vote_message_ptr vote_msg = std::get<2>(v); _last_vote = vote_t(vote_msg->block_id, vote_msg->strong); + _votes[vote_msg->block_id] = vote_msg; if (_propagate_votes) { if (_vote_delay) @@ -28,10 +29,10 @@ node_t::node_t(size_t node_idx, cluster_t& cluster, setup_policy policy /* = set while (_delayed_votes.size() > _vote_delay) { vote_message_ptr vote = _delayed_votes.front(); _delayed_votes.erase(_delayed_votes.cbegin()); - cluster.dispatch_vote_to_peers(node_idx, skip_self_t::yes, vote); + _cluster.dispatch_vote_to_peers(node_idx, skip_self_t::yes, vote); } if (!_vote_delay) - cluster.dispatch_vote_to_peers(node_idx, skip_self_t::yes, vote_msg); + _cluster.dispatch_vote_to_peers(node_idx, skip_self_t::yes, vote_msg); } } }; @@ -41,7 +42,7 @@ node_t::node_t(size_t node_idx, cluster_t& cluster, setup_policy policy /* = set if (!_pushing_a_block) { // we want to propagate only blocks we produce, not the ones we receive from the network auto& b = std::get<0>(p); - cluster.push_block_to_peers(node_idx, skip_self_t::yes, b); + _cluster.push_block_to_peers(node_idx, skip_self_t::yes, b); } }; @@ -58,10 +59,13 @@ node_t::node_t(size_t node_idx, cluster_t& cluster, setup_policy policy /* = set node_t::~node_t() {} -void node_t::propagate_delayed_votes_to(const node_t& to) { +void node_t::propagate_delayed_votes_to(const node_t& n) { for (auto& vote : _delayed_votes) - if (to.is_open()) - to.control->process_vote_message(++_cluster._connection_id, vote); + _cluster.dispatch_vote_to(n, vote); +} + +void node_t::push_vote_to(const node_t& n, const block_id_type& block_id) { + _cluster.dispatch_vote_to(n, get_vote(block_id)); } } diff --git a/unittests/savanna_cluster.hpp b/unittests/savanna_cluster.hpp index 8c867cbe4c..feb4da5e7b 100644 --- a/unittests/savanna_cluster.hpp +++ b/unittests/savanna_cluster.hpp @@ -76,10 +76,13 @@ namespace savanna_cluster { struct qc_s { explicit qc_s(uint32_t block_num, bool strong) : block_num(block_num), strong(strong) {} - explicit qc_s(const std::optional& qc) : block_num(qc->block_num), strong(qc->is_strong()) {} + explicit qc_s(const std::optional& qc) : block_num(qc ? qc->block_num : uint32_t(-1)), strong(qc->is_strong()) {} friend std::ostream& operator<<(std::ostream& s, const qc_s& v) { - s << "qc_s(" << v.block_num << ", " << (v.strong ? "strong" : "weak") << ")"; + if (v.block_num == uint32_t(-1)) + s << "no_qc"; + else + s << "qc_s(" << v.block_num << ", " << (v.strong ? "strong" : "weak") << ")"; return s; } bool operator==(const qc_s&) const = default; @@ -104,6 +107,8 @@ namespace savanna_cluster { // ---------------------------------------------------------------------------- class node_t : public tester { private: + using votes_map_t = boost::unordered_flat_map; + size_t _node_idx; bool _pushing_a_block{false}; bool _propagate_votes{true}; // if false, votes are dropped @@ -113,6 +118,8 @@ namespace savanna_cluster { size_t _vote_delay{0}; // delay vote propagation by this much std::vector _delayed_votes; + votes_map_t _votes; // all votes from this node + std::function _accepted_block_cb; std::function _voted_block_cb; @@ -125,14 +132,18 @@ namespace savanna_cluster { node_t(node_t&&) = default; + size_t node_idx() const { return _node_idx; } + bool& propagate_votes() { return _propagate_votes; } size_t& vote_delay() { return _vote_delay; } - void propagate_delayed_votes_to(const node_t& to); + void propagate_delayed_votes_to(const node_t& n); const vote_t& last_vote() const { return _last_vote; } + vote_message_ptr get_vote(const block_id_type& block_id) const { return _votes.at(block_id); } + void set_node_finalizers(std::span names) { _node_finalizers = std::vector{ names.begin(), names.end() }; if (control) { @@ -223,14 +234,16 @@ namespace savanna_cluster { } template - void push_blocks_to(Node& to, uint32_t block_num_limit = std::numeric_limits::max()) const { + void push_blocks_to(Node& n, uint32_t block_num_limit = std::numeric_limits::max()) const { auto limit = std::min(fork_db_head().block_num(), block_num_limit); - while (to.fork_db_head().block_num() < limit) { - auto sb = control->fetch_block_by_number(to.fork_db_head().block_num() + 1); - to.push_block(sb); + while (n.fork_db_head().block_num() < limit) { + auto sb = control->fetch_block_by_number(n.fork_db_head().block_num() + 1); + n.push_block(sb); } } + void push_vote_to(const node_t& n, const block_id_type& block_id); + bool is_head_missing_finalizer_votes() { if (!control->get_testing_allow_voting_flag()) return false; @@ -356,6 +369,8 @@ namespace savanna_cluster { // -------------------------------------------------------------------------------------- class cluster_t { public: + using peers_t = boost::unordered_flat_map>; + explicit cluster_t(cluster_config cfg = {}) : _fin_keys(cfg.num_nodes * 2, cfg.num_nodes) // allow for some spare heys , _num_nodes(cfg.num_nodes) @@ -396,6 +411,23 @@ namespace savanna_cluster { _shutting_down = true; } + peers_t partitions(const std::initializer_list>& l) { + std::vector> indice_vec; + for (const auto& v : l) { + auto view = v | std::views::transform([](const node_t* n) { return n->node_idx(); }); + indice_vec.emplace_back(view.begin(), view.end()); + } + return compute_peers(indice_vec); + } + + peers_t partition(const std::vector& nodes) { + return partitions({nodes}); + } + + void set_partition(const std::vector& nodes) { + _peers = partition(nodes); + } + // `set_partitions` allows to configure logical network connections between nodes. // - an empty list will connect each node of the cluster to every other nodes. // - a non-empty list partitions the network into two or more partitions. @@ -403,32 +435,8 @@ namespace savanna_cluster { // for nodes (`complement`) form another partition // (within each partition, nodes are fully connected) // ----------------------------------------------------------------------------------------- - void set_partitions(std::initializer_list> l) { - auto inside = [&](size_t node_idx) { - return ranges::any_of(l, [node_idx](const auto& v) { - return ranges::any_of(v, [node_idx](auto i) { return i == node_idx; }); }); - }; - - std::vector complement; - for (size_t i = 0; i < _nodes.size(); ++i) - if (!inside(i)) - complement.push_back(i); - - auto set_peers = [&](const std::vector& v) { for (auto i : v) _peers[i] = v; }; - - _peers.clear(); - for (const auto& v : l) - set_peers(v); - set_peers(complement); - } - - // this is a convenience function for the most common case where we want to partition - // the nodes into two separate disconnected partitions. - // Simply provide a set of indices for nodes that need to be logically disconnected - // from the rest of the network. - // ---------------------------------------------------------------------------------- - void set_partition(const std::vector& indices) { - set_partitions({indices}); + void set_partitions(const std::initializer_list>& part_vec) { + _peers = partitions(part_vec); } // After creating forks on different nodes on a partitioned network, make sure that, @@ -545,6 +553,8 @@ namespace savanna_cluster { return {}; } + peers_t& peers() { return _peers; } + // debugging utilities // ------------------- void print(const char* name, const signed_block_ptr& b) const { @@ -563,8 +573,6 @@ namespace savanna_cluster { fc::milliseconds(eosio::chain::config::block_interval_ms); private: - using peers_t = boost::unordered_flat_map>; - peers_t _peers; size_t _num_nodes; bool _shutting_down {false}; @@ -572,10 +580,35 @@ namespace savanna_cluster { friend node_t; + peers_t compute_peers(const std::vector>& l) const { + auto inside = [&](size_t node_idx) { + return ranges::any_of(l, [node_idx](const auto& v) { + return ranges::any_of(v, [node_idx](auto i) { return i == node_idx; }); }); + }; + + std::vector complement; + for (size_t i = 0; i < _nodes.size(); ++i) + if (!inside(i)) + complement.push_back(i); + + peers_t peers; + + auto set_peers = [&](const std::vector& v) { for (auto i : v) peers[i] = v; }; + + for (const auto& v : l) + set_peers(v); + set_peers(complement); + return peers; + } + + void dispatch_vote_to(const node_t& n, const vote_message_ptr& msg) { + if (n.is_open()) + n.control->process_vote_message(++_connection_id, msg); + } + void dispatch_vote_to_peers(size_t node_idx, skip_self_t skip_self, const vote_message_ptr& msg) { for_each_peer(node_idx, skip_self, [&](node_t& n) { - if (n.is_open()) - n.control->process_vote_message(++_connection_id, msg); + dispatch_vote_to(n, msg); }); } diff --git a/unittests/savanna_cluster_tests.cpp b/unittests/savanna_cluster_tests.cpp index 698e276ec0..760ea57a10 100644 --- a/unittests/savanna_cluster_tests.cpp +++ b/unittests/savanna_cluster_tests.cpp @@ -9,6 +9,8 @@ BOOST_AUTO_TEST_SUITE(savanna_cluster_tests) // test set_finalizer host function serialization and tester set_finalizers BOOST_FIXTURE_TEST_CASE(simple_test, savanna_cluster::cluster_t) { try { + auto& C=_nodes[2]; auto& D=_nodes[3]; + auto node3_lib = _nodes[3].lib_num(); // Store initial lib (after Savanna transtion) BOOST_REQUIRE_EQUAL(num_nodes(), num_lib_advancing([&]() { // Check that lib advances on all nodes _nodes[0].produce_block(); // Blocks & votes are propagated to all connected peers. @@ -35,8 +37,7 @@ BOOST_FIXTURE_TEST_CASE(simple_test, savanna_cluster::cluster_t) { try { node3_lib = _nodes[3].lib_num(); BOOST_REQUIRE_EQUAL(node0_lib, node3_lib); - const std::vector partition {2, 3}; - set_partition(partition); // Simulate 2 disconnected partitions: + set_partition( {&C, &D} ); // Simulate 2 disconnected partitions: // nodes {0, 1} and nodes {2, 3} // at this point, each node has a QC to include into the next block it produces which will advance lib. diff --git a/unittests/savanna_disaster_recovery_tests.cpp b/unittests/savanna_disaster_recovery_tests.cpp index 1ce5dc56db..becc997e40 100644 --- a/unittests/savanna_disaster_recovery_tests.cpp +++ b/unittests/savanna_disaster_recovery_tests.cpp @@ -3,7 +3,7 @@ using namespace eosio::chain; using namespace eosio::testing; -BOOST_AUTO_TEST_SUITE(savanna_disaster_recovery) +BOOST_AUTO_TEST_SUITE(savanna_disaster_recovery_tests) // ------------------------------------------------------------------------------------------ // Check that a node can go down cleanly, restart from its existing state, and start voting @@ -213,8 +213,7 @@ BOOST_FIXTURE_TEST_CASE(all_nodes_shutdown_with_reversible_blocks_lost, savanna_ // split network { A, B } and { C, D } // A produces two more blocks, so A and B will vote strong but finality will not advance // ------------------------------------------------------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); BOOST_REQUIRE_EQUAL(1u, A.lib_advances_by([&]() { A.produce_blocks(2); })); // lib stalls with network partitioned, 1 QC in flight // remove network split @@ -306,8 +305,7 @@ BOOST_FIXTURE_TEST_CASE(restart_from_fork_db_with_only_root_block, savanna_clust BOOST_REQUIRE_EQUAL(2u, C.lib_advances_by([&]() { b1 = C.produce_block(); b2 = C.produce_block(); })); // Partition C by itself, so it doesn't receive b1 and b2 when opebed - const std::vector tmp_partition {2}; - set_partition(tmp_partition); + set_partition( {&C} ); C.close(); C.remove_state(); diff --git a/unittests/savanna_misc_tests.cpp b/unittests/savanna_misc_tests.cpp index 451aed5e81..7a8e9c78f4 100644 --- a/unittests/savanna_misc_tests.cpp +++ b/unittests/savanna_misc_tests.cpp @@ -74,8 +74,7 @@ BOOST_FIXTURE_TEST_CASE(weak_masking_issue, savanna_cluster::cluster_t) try { // partition D out. D will be used to produce blocks on an alternative fork. // We will have 3 finalizers voting which is enough to reach QCs // ------------------------------------------------------------------------- - const std::vector partition {3}; - set_partition(partition); + set_partition( {&D} ); auto b1 = A.produce_block(); // receives strong votes from 3 finalizers (D partitioned out) print("b1", b1); @@ -85,8 +84,7 @@ BOOST_FIXTURE_TEST_CASE(weak_masking_issue, savanna_cluster::cluster_t) try { BOOST_REQUIRE_GT(b2->timestamp.slot, b1->timestamp.slot); - const std::vector tmp_partition {0}; // we temporarily separate A (before pushing b2) - set_partitions({tmp_partition, partition}); // because we don't want A to see the block produced by D (b2) + set_partitions({ {&A}, {&D}}); // because we don't want A to see the block produced by D (b2) // otherwise it will switch forks and build its next block (b3) // on top of it @@ -98,7 +96,7 @@ BOOST_FIXTURE_TEST_CASE(weak_masking_issue, savanna_cluster::cluster_t) try { BOOST_REQUIRE_EQUAL(qc_s(qc(b2)), strong_qc(b0)); // b2 should include a strong qc on b0 - set_partition(partition); // restore our original partition {A, B, C} and {D} + set_partition( {&D} ); // restore our original partition {A, B, C} and {D} signed_block_ptr b3; { @@ -250,14 +248,13 @@ BOOST_FIXTURE_TEST_CASE(gh_534_liveness_issue, savanna_cluster::cluster_t) try { // partition D out. D will be used to produce blocks on an alternative fork. // We will have 3 finalizers voting which is enough to reach QCs // ------------------------------------------------------------------------- - const std::vector partition {3}; - set_partition(partition); + set_partition( {&D} ); auto b3 = D.produce_block(); // produce a block on D print("b3", b3); - const std::vector tmp_partition {0}; // we temporarily separate A (before pushing b3) - set_partition(tmp_partition); // because we don't want A to see the block produced by D (b3) + // we temporarily separate A (before pushing b3) + set_partition( {&A} ); // because we don't want A to see the block produced by D (b3) // otherwise it will switch forks and build its next block (b4) // on top of it @@ -271,7 +268,7 @@ BOOST_FIXTURE_TEST_CASE(gh_534_liveness_issue, savanna_cluster::cluster_t) try { // so it didn't see b3 and its enclosed QC. B.check_fsi({.last_vote = b3, .lock = b2, .other_branch_latest_time = {}}); - set_partition(partition); // restore our original partition {A, B, C} and {D} + set_partition( {&D} ); // restore our original partition {A, B, C} and {D} // from now on, to reproduce the scenario where votes are delayed, so the QC we receive don't // claim the parent block, but an ancestor, we need to artificially delay propagating the votes. @@ -357,8 +354,7 @@ BOOST_FIXTURE_TEST_CASE(validate_qc_after_restart_from_snapshot, savanna_cluster auto b1 = A.produce_block(); // receives strong votes from all finalizers print("b1", b1); - const std::vector partition {0}; // partition A so that B, C and D don't see b2 (yet) - set_partition(partition); + set_partition( {&A} ); // partition A so that B, C and D don't see b2 (yet) auto b2 = A.produce_block(); // receives just 1 strong vote fron A print("b2", b2); @@ -397,7 +393,7 @@ BOOST_FIXTURE_TEST_CASE(validate_qc_after_restart_from_snapshot, savanna_cluster A.remove_state(); A.remove_reversible_data_and_blocks_log(); - set_partition({0}); // partition A so it doesn't receive blocks on `open()` + set_partition( {&A} ); // partition A so it doesn't receive blocks on `open()` A.open_from_snapshot(b3_snapshot); // After starting up from the snapshot, their node receives block b4 from the P2P network. @@ -508,8 +504,8 @@ BOOST_FIXTURE_TEST_CASE(validate_qc_requiring_finalizer_policies, savanna_cluste auto p2 = pending->generation; // and its generation is higher than the active one BOOST_REQUIRE_EQUAL(p2, p1 + 1); // b3 has new pending finalizer policy p2 - const std::vector partition {0}; // partition A so that B, C and D don't see b4 (yet) - set_partition(partition); // and don't vote on it + // partition A so that B, C and D don't see b4 (yet) + set_partition( {&A} ); // and don't vote on it auto b4 = A.produce_block(); print("b4", b4); @@ -584,7 +580,7 @@ BOOST_FIXTURE_TEST_CASE(validate_qc_requiring_finalizer_policies, savanna_cluste A.remove_state(); A.remove_reversible_data_and_blocks_log(); - set_partition({0}); // partition A so it doesn't receive blocks on `open()` + set_partition({&A}); // partition A so it doesn't receive blocks on `open()` A.open_from_snapshot(b6_snapshot); A.push_block(b7); // when pushing b7, if we try to access any block state @@ -750,8 +746,8 @@ BOOST_FIXTURE_TEST_CASE(verify_block_compatibitity, savanna_cluster::cluster_t) auto p2 = pending->generation; // and its generation is higher than the active one BOOST_REQUIRE_EQUAL(p2, p1 + 1); // b3 has new pending finalizer policy p2 - const std::vector partition {0}; // partition A so that B, C and D don't see b4 (yet) - set_partition(partition); // and don't vote on it + // partition A so that B, C and D don't see b4 (yet) + set_partition({&A}); // and don't vote on it // push action so that the block is not empty A.push_action(config::system_account_name, updateauth::get_name(), tester_account, @@ -857,7 +853,7 @@ Vote: Strong Strong Strong Weak - because `B2 timestamp < B4 timestamp`. -> safety check fails because `B6` does not extend `B4`. --------------------------------------------------------------------------------------------------------*/ -BOOST_FIXTURE_TEST_CASE(finality_advancing_past_block_claimed_on_alternate_branch, savanna_cluster::cluster_t) try { +BOOST_FIXTURE_TEST_CASE(finalizers_locked_preventing_vote_on_alternate_branch, savanna_cluster::cluster_t) try { using namespace savanna_cluster; auto& A=_nodes[0]; auto& B=_nodes[1]; auto& C=_nodes[2]; auto& D=_nodes[3]; @@ -884,8 +880,8 @@ BOOST_FIXTURE_TEST_CASE(finality_advancing_past_block_claimed_on_alternate_branc B.propagate_delayed_votes_to(D); // propagate votes on b2 to D, so it can form a QC on b2 C.propagate_delayed_votes_to(D); // which will be included in b6 - const std::vector partition {3}; // partition D so that it doesn't see b3, b4 and b5 - set_partition(partition); // and don't vote on it + // partition D so that it doesn't see b3, b4 and b5 + set_partition({&D}); // and don't vote on it auto b3 = A.produce_block(); print("b3", b3); @@ -931,4 +927,183 @@ BOOST_FIXTURE_TEST_CASE(finality_advancing_past_block_claimed_on_alternate_branc } FC_LOG_AND_RETHROW() +/* ----------------------------------------------------------------------------------------------------- + Finality advancing past block claimed on alternate branch + ========================================================= +Producer: C C C C C D D D D +Timestamp: t1 t2 t3 t4 t5 t6 t7 t8 t9 +Blocks: + b0 <--- b1 <--- b2 <--- b3 <-|- b4 <--- b5 + | + \----------------- b6 <--- b7 <--- b8 <--- b9 +QC claim: + Strong Strong Strong Strong Strong Strong Strong Weak Strong + b0 b0 b1 b3 b4 b1 b2 b7 b8 + +Votes: + Node A: Strong‡ Strong‡ Strong‡ Strong Weak¹ Weak Strong Strong + Node B: Strong¹ Strong¹ Strong Strong Weak¹ Weak Strong Strong + Node C: Strong Strong Strong Strong Strong‡ Weak¹ Weak¹ Strong¹ Strong + Node D: Strong¹ Strong¹ Strong Strong Strong Strong Strong + + ^ + | + Validating the strong QC on b2 should + not fail for nodes which receive b4 and + b5 prior to b7 despite b5 advancing the + fork DB root to b3. + +Meaning of the superscripts and marks on the votes: +The vote on block b was delayed in reaching the node for the producer p scheduled +for the block at the next time slot t after block b by enough that a block produced on time by +producer p for time slot t could not possibly utilize the vote in any QC the block could claim. +Furthermore, the delay is such that the earliest time slot at which producer p could +produce a block that utilizes the delayed vote is the time slot (t + d) where ... +¹ ... d = 1. +‡ ... d is infinite meaning the vote may never be received by producer p. + +steps mentioned in comments below refer to issue https://github.com/AntelopeIO/spring/issues/751 + +Diagram below shows the timeline for nodes A, B, C and D receiving blocks b1 through b9. +(x) marks the producer of the block. + +step network partition A B C D +--------------------------------------------------------------- + b1 b1 b1(x) b1 +(3) A / B C D +(4) b2 b2(x) b2 +(9) b3 b3(x) b3 +(15) A / B C / D +(18) b4 b4(x) +(20) b6(x) +(22) A D / B C +(23) b2 +(25) b3 +(26) A B C / D +(28) b4 +(30) A B / C / D +(31) b5(x) +(33) b7(x) +(35) A B D / C +(36) b6 b6 +(38) A B C D +(39) b5 b5 +(40) b6 +(41,43) b7 b7 b7 +(44) b8 b8 b8 b8(x) +(51) b9 b9 b9 b9(x) + +--------------------------------------------------------------------------------------------------------*/ +BOOST_FIXTURE_TEST_CASE(finality_advancing_past_block_claimed_on_alternate_branch, savanna_cluster::cluster_t) try { + using namespace savanna_cluster; + auto& A=_nodes[0]; auto& B=_nodes[1]; auto& C=_nodes[2]; auto& D=_nodes[3]; + + //_debug_mode = true; + + auto b0 = A.produce_block(); + print("b0", b0); + + signed_block_ptr b1, b2, b3, b4, b5, b6, b7, b8, b9; + + set_partition({ &A }); + + { + fc::scoped_set_value tmp_B(B.vote_delay(), 1); // delay votes from B for 1 slot + fc::scoped_set_value tmp_D(D.vote_delay(), 1); // delay votes from D for 1 slot + + b1 = C.produce_block(); + print("b1", b1); + BOOST_REQUIRE_EQUAL(qc_s(qc(b1)), strong_qc(b0)); // b1 claims a strong QC on b0 + + b2 = C.produce_block(); + print("b2", b2); + BOOST_REQUIRE(!qc(b2)); // b2 should not include a QC (votes on b1 delayed) + BOOST_REQUIRE_EQUAL(qc_claim(b2), qc_claim(b1)); // C didn't form a QC on b1, so b2 should repeat b1's claim + + // D doesn't receive B's vote on b2 yet because it is delayed, or A's vote because it is partitioned out + } + + set_partitions({{ &A }, { &D }}); // both A and D are isolated by themselves (step 15) + + b3 = C.produce_block(); + print("b3", b3); + BOOST_REQUIRE_EQUAL(qc_s(qc(b3)), strong_qc(b1)); // b3 claims a strong QC on b1 (B and D votes delayed by 1) + + C.push_blocks_to(D); // we want D to receive b3 (so it can build b6 on it), but no votes + D.push_vote_to(C, b3->calculate_id()); // and we want C to get D's vote on b3 so it can form a QC + // this simulates D being isolated just after receiving b3 and voting + // on it, but before receiving B and C votes on b3. + + b4 = C.produce_block(); + print("b4", b4); + BOOST_REQUIRE_EQUAL(qc_s(qc(b4)), strong_qc(b3)); // b4 claims a strong QC on b3 (B and D votes not delayed anymore) + + b6 = D.produce_block(_block_interval_us * 2); // Node D produces and broadcasts b6 one second early (due + print("b6", b6); // to clock differences). + BOOST_REQUIRE_EQUAL(b6->previous, b3->calculate_id()); // b6 has b3 as its parent block + BOOST_REQUIRE(!qc(b6)); // b6 does not include a new qc (lacking votes on b2 and b3) + BOOST_REQUIRE_EQUAL(qc_claim(b6), qc_claim(b3)); // and repeats b3's strong QC claim on b1. + + C.push_blocks_to(A); // simulates A and D temporarily reconnecting, D sending the blocks + A.push_vote_to(D, b2->calculate_id()); // produced by C, A voting on them and D receiving these votes + + set_partition({ &D }); // B and C re-establish connection with A (step 26,27) + + C.push_blocks_to(A); // Now that A is reconnected to B and C, it can receive blocks and + A.push_vote_to(C, b4->calculate_id()); // vote on them + + set_partitions({ { &C }, { &D } }); // Node C is isolated from the other nodes (step 30) + // so A, B and C get b5 after b6 + + b5 = C.produce_block(); + print("b5", b5); + BOOST_REQUIRE_EQUAL(qc_s(qc(b5)), strong_qc(b4)); // b5 claims a strong QC on b4 + + b7 = D.produce_block(); // Node D produces b7 + print("b7", b7); + BOOST_REQUIRE_EQUAL(b7->previous, b6->calculate_id()); // b7 has b6 as its parent block + BOOST_REQUIRE_EQUAL(qc_s(qc(b7)), strong_qc(b2)); // b7 claims a strong QC on b2 + + set_partition( { &C } ); // step 35 + + A.push_block(b6); // don't use `push_blocks_to` because of fork + B.push_block(b6); // step 36 + + set_partition( {} ); // step 38 + + A.push_block(b5); // A receives b5 + BOOST_REQUIRE_EQUAL(A.lib_number, b3->block_num()); // which advances lib to b3 + + B.push_block(b5); // B receives b5 + BOOST_REQUIRE_EQUAL(B.lib_number, b3->block_num()); // which advances lib to b3 + + // Following requires issue #694 fix: + // Nodes A and B have received b5, which has advanced finality to b3. + // when we push b6 and b7 (produced by D) to these nodes, they will want to verify the QC included in b7 (strong QC on b2). + // If, in order to verify this QC, they attempt to lookup b2 in fork_db, this will fail because lib (and hence fork_db's root) + // has advanced to b3. + // --------------------------------------------------------------------------------------------------------------------------- + A.push_block(b7); // prior to PR #719 (fixing issue #694), we'd have an exception here + B.push_block(b7); // prior to PR #719 (fixing issue #694), we'd have an exception here + + C.push_block(b6); + C.push_block(b7); + + // with issue #694 fixed, A and B were able to successfully validate the received block b7 + // However, unless the separate issue #778 is fixed, A and B would still not vote on b7 (which is added to the fork database + // but does not become the new best head since b5 has a later `latest_qc_block_timestamp`). + // --------------------------------------------------------------------------------------------------------------------------- + b8 = D.produce_block(); // Node D produces b8 + print("b8", b8); + BOOST_REQUIRE_EQUAL(b8->previous, b7->calculate_id()); // b8 has b7 as its parent block + BOOST_REQUIRE_EQUAL(qc_s(qc(b8)), weak_qc(b7)); // b8 claims a weak QC on b7 (A, B and C voted weak since locked on b4) + // prior to PR #788 (fixing issue #778), we'd have an test failure here + + b9 = D.produce_block(); // Node D produces b9 + print("b9", b9); + BOOST_REQUIRE_EQUAL(qc_s(qc(b9)), strong_qc(b8)); // b9 claims a strong QC on b8 (all nodes were able to vote strong) + +} FC_LOG_AND_RETHROW() + + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/savanna_proposer_policy_tests.cpp b/unittests/savanna_proposer_policy_tests.cpp index 3720aad199..709b58057c 100644 --- a/unittests/savanna_proposer_policy_tests.cpp +++ b/unittests/savanna_proposer_policy_tests.cpp @@ -5,7 +5,7 @@ using namespace eosio::testing; static const uint32_t prod_rep = static_cast(config::producer_repetitions); -BOOST_AUTO_TEST_SUITE(savanna_proposer_policy) +BOOST_AUTO_TEST_SUITE(savanna_proposer_policy_tests) // --------------------------------------------------------------------------------------------------- // Proposer Policy change - check expected delay when policy change initiated on @@ -128,14 +128,13 @@ BOOST_FIXTURE_TEST_CASE(policy_change_last_block_delay_check, savanna_cluster::c // Verify that a proposer policy does not become active when finality has stalled // --------------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(no_proposer_policy_change_without_finality, savanna_cluster::cluster_t) try { - auto& A=_nodes[0]; + auto& A=_nodes[0]; auto& C=_nodes[2]; auto& D=_nodes[3]; // split network { A, B } and { C, D } // Regardless of how many blocks A produces, finality will not advance // by more than one (1 QC in flight) // ------------------------------------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); auto sb = A.produce_block(); // take care of the in-flight QC const vector producers { "pa"_n, "pb"_n }; @@ -183,14 +182,13 @@ BOOST_FIXTURE_TEST_CASE(no_proposer_policy_change_without_finality, savanna_clus // finality stalled long enough) // --------------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(no_proposer_policy_change_without_finality_2, savanna_cluster::cluster_t) try { - auto& A=_nodes[0]; + auto& A=_nodes[0]; auto& C=_nodes[2]; auto& D=_nodes[3]; // split network { A, B } and { C, D } // Regardless of how many blocks A produces, finality will not advance // by more than one (1 QC in flight) // ------------------------------------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); auto sb = A.produce_block(); // take care of the in-flight QC const vector producers { "pa"_n, "pb"_n }; @@ -233,7 +231,7 @@ BOOST_FIXTURE_TEST_CASE(no_proposer_policy_change_without_finality_2, savanna_cl // Verify that a proposer policy becomes active when finality has advanced enough to make it pending // --------------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(pending_proposer_policy_becomes_active_without_finality, savanna_cluster::cluster_t) try { - auto& A=_nodes[0]; + auto& A=_nodes[0]; auto& C=_nodes[2]; auto& D=_nodes[3]; static_assert(prod_rep >= 4); auto sb = A.produce_block(); @@ -254,8 +252,7 @@ BOOST_FIXTURE_TEST_CASE(pending_proposer_policy_becomes_active_without_finality, // Regardless of how many blocks A produces, finality will not advance // by more than one (1 QC in flight) // ------------------------------------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); sb = A.produce_block(); // produce one more block for lib final advance (in-flight QC) diff --git a/unittests/savanna_transition_tests.cpp b/unittests/savanna_transition_tests.cpp index f615a48089..54162c09ac 100644 --- a/unittests/savanna_transition_tests.cpp +++ b/unittests/savanna_transition_tests.cpp @@ -3,7 +3,7 @@ using namespace eosio::chain; using namespace eosio::testing; -BOOST_AUTO_TEST_SUITE(savanna_transition) +BOOST_AUTO_TEST_SUITE(savanna_transition_tests) // --------------------------------------------------------------------------------------------------- // Verify a straightforward transition, with all four nodes healthy and voting @@ -31,7 +31,7 @@ BOOST_FIXTURE_TEST_CASE(straightforward_transition, // --------------------------------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(transition_with_split_network_before_critical_block, savanna_cluster::pre_transition_cluster_t) try { - auto& A=_nodes[0]; + auto& A=_nodes[0]; auto& C=_nodes[2]; auto& D=_nodes[3]; // set two producers, so that we have at least one block between the genesis and critical block. // with one producer the critical block comes right after the genesis block. @@ -62,8 +62,7 @@ BOOST_FIXTURE_TEST_CASE(transition_with_split_network_before_critical_block, // partition network and produce blocks // ---------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); A.produce_blocks(20); // verify that lib has stalled @@ -131,8 +130,7 @@ BOOST_FIXTURE_TEST_CASE(restart_from_snapshot_at_beginning_of_transition_while_p // partition network and produce blocks // ---------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); A.produce_blocks(2); auto snapshot_C = C.snapshot(); @@ -230,8 +228,7 @@ BOOST_FIXTURE_TEST_CASE(restart_from_snapshot_at_end_of_transition_while_preserv // partition network and produce a block, which will be the first proper savanna block // ----------------------------------------------------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); signed_block_ptr b = A.produce_block(); BOOST_REQUIRE(b->is_proper_svnn_block()); @@ -308,8 +305,7 @@ BOOST_FIXTURE_TEST_CASE(restart_from_snapshot_at_beginning_of_transition_with_lo // partition network and produce blocks // ---------------------------------------- - const std::vector partition {2, 3}; - set_partition(partition); + set_partition( {&C, &D} ); A.produce_blocks(2); auto snapshot_C = C.snapshot();