From 1fccbf453570ff7254da15ee907cb7ea48e12269 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 16 Feb 2024 17:07:43 -0500 Subject: [PATCH 01/18] Make `finalizer` a template class so that it can easily be unit tested. --- libraries/chain/controller.cpp | 20 +-- libraries/chain/fork_database.cpp | 145 +++++++++--------- libraries/chain/hotstuff/finalizer.cpp | 112 +------------- .../include/eosio/chain/fork_database.hpp | 3 +- .../eosio/chain/hotstuff/finalizer.hpp | 99 ++++++------ .../eosio/chain/hotstuff/finalizer.ipp | 119 ++++++++++++++ unittests/finalizer_tests.cpp | 81 ++++++++-- 7 files changed, 329 insertions(+), 250 deletions(-) create mode 100644 libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 6b233f7d55..0d2ee20b59 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -697,7 +697,7 @@ struct building_block { qc_data ? qc_data->qc_claim : std::optional{} }; - assembled_block::assembled_block_if ab{std::move(bb.active_producer_authority), bb.parent.next(bhs_input), + assembled_block::assembled_block_if ab{bb.active_producer_authority, bb.parent.next(bhs_input), std::move(bb.pending_trx_metas), std::move(bb.pending_trx_receipts), qc_data ? std::move(qc_data->qc) : std::optional{}}; @@ -1584,9 +1584,9 @@ struct controller_impl { auto set_finalizer_defaults = [&](auto& forkdb) -> void { auto lib = forkdb.root(); my_finalizers.set_default_safety_information( - finalizer::safety_information{ .last_vote_range_start = block_timestamp_type(0), - .last_vote = {}, - .lock = finalizer::proposal_ref(lib) }); + finalizer_safety_information{ .last_vote_range_start = block_timestamp_type(0), + .last_vote = {}, + .lock = proposal_ref(lib) }); }; fork_db.apply_if(set_finalizer_defaults); } else { @@ -1594,9 +1594,9 @@ struct controller_impl { auto set_finalizer_defaults = [&](auto& forkdb) -> void { auto lib = forkdb.root(); my_finalizers.set_default_safety_information( - finalizer::safety_information{ .last_vote_range_start = block_timestamp_type(0), - .last_vote = {}, - .lock = finalizer::proposal_ref(lib) }); + finalizer_safety_information{ .last_vote_range_start = block_timestamp_type(0), + .last_vote = {}, + .lock = proposal_ref(lib) }); }; fork_db.apply_if(set_finalizer_defaults); } @@ -2787,9 +2787,9 @@ struct controller_impl { auto start_block = forkdb.chain_head; auto lib_block = forkdb.chain_head; my_finalizers.set_default_safety_information( - finalizer::safety_information{ .last_vote_range_start = block_timestamp_type(0), - .last_vote = finalizer::proposal_ref(start_block), - .lock = finalizer::proposal_ref(lib_block) }); + finalizer_safety_information{ .last_vote_range_start = block_timestamp_type(0), + .last_vote = proposal_ref(start_block->id(), start_block->timestamp()), + .lock = proposal_ref(lib_block->id(), lib_block->timestamp()) }); } log_irreversible(); diff --git a/libraries/chain/fork_database.cpp b/libraries/chain/fork_database.cpp index 1f35b71511..8b1205ad13 100644 --- a/libraries/chain/fork_database.cpp +++ b/libraries/chain/fork_database.cpp @@ -30,8 +30,9 @@ namespace eosio::chain { return std::pair(lhs.irreversible_blocknum(), lhs.block_num()) > std::pair(rhs.irreversible_blocknum(), rhs.block_num()); } - template // either [block_state_legacy_ptr, block_state_ptr], same with block_header_state_ptr + template // either [block_state_legacy_ptr, block_state_ptr], same with block_header_state_ptr struct fork_database_impl { + using bsp = BSP; using bs = bsp::element_type; using bhsp = bs::bhsp_t; using bhs = bhsp::element_type; @@ -80,20 +81,20 @@ namespace eosio::chain { }; - template - fork_database_t::fork_database_t(uint32_t magic_number) - :my( new fork_database_impl(magic_number) ) + template + fork_database_t::fork_database_t(uint32_t magic_number) + :my( new fork_database_impl(magic_number) ) {} - template - void fork_database_t::open( const std::filesystem::path& fork_db_file, validator_t& validator ) { + template + void fork_database_t::open( const std::filesystem::path& fork_db_file, validator_t& validator ) { std::lock_guard g( my->mtx ); my->open_impl( fork_db_file, validator ); } - template - void fork_database_impl::open_impl( const std::filesystem::path& fork_db_file, validator_t& validator ) { + template + void fork_database_impl::open_impl( const std::filesystem::path& fork_db_file, validator_t& validator ) { if( std::filesystem::exists( fork_db_file ) ) { try { string content; @@ -162,14 +163,14 @@ namespace eosio::chain { } } - template - void fork_database_t::close(const std::filesystem::path& fork_db_file) { + template + void fork_database_t::close(const std::filesystem::path& fork_db_file) { std::lock_guard g( my->mtx ); my->close_impl(fork_db_file); } - template - void fork_database_impl::close_impl(const std::filesystem::path& fork_db_file) { + template + void fork_database_impl::close_impl(const std::filesystem::path& fork_db_file) { if( !root ) { if( index.size() > 0 ) { elog( "fork_database is in a bad state when closing; not writing out '${filename}'", @@ -230,14 +231,14 @@ namespace eosio::chain { index.clear(); } - template - void fork_database_t::reset( const bhs& root_bhs ) { + template + void fork_database_t::reset( const bhs& root_bhs ) { std::lock_guard g( my->mtx ); my->reset_impl(root_bhs); } - template - void fork_database_impl::reset_impl( const bhs& root_bhs ) { + template + void fork_database_impl::reset_impl( const bhs& root_bhs ) { index.clear(); root = std::make_shared(); static_cast(*root) = root_bhs; @@ -245,14 +246,14 @@ namespace eosio::chain { head = root; } - template - void fork_database_t::rollback_head_to_root() { + template + void fork_database_t::rollback_head_to_root() { std::lock_guard g( my->mtx ); my->rollback_head_to_root_impl(); } - template - void fork_database_impl::rollback_head_to_root_impl() { + template + void fork_database_impl::rollback_head_to_root_impl() { auto& by_id_idx = index.template get(); auto itr = by_id_idx.begin(); while (itr != by_id_idx.end()) { @@ -264,14 +265,14 @@ namespace eosio::chain { head = root; } - template - void fork_database_t::advance_root( const block_id_type& id ) { + template + void fork_database_t::advance_root( const block_id_type& id ) { std::lock_guard g( my->mtx ); my->advance_root_impl( id ); } - template - void fork_database_impl::advance_root_impl( const block_id_type& id ) { + template + void fork_database_impl::advance_root_impl( const block_id_type& id ) { EOS_ASSERT( root, fork_database_exception, "root not yet set" ); auto new_root = get_block_impl( id ); @@ -305,14 +306,14 @@ namespace eosio::chain { root = new_root; } - template - fork_database_t::bhsp fork_database_t::get_block_header( const block_id_type& id ) const { + template + fork_database_t::bhsp fork_database_t::get_block_header( const block_id_type& id ) const { std::lock_guard g( my->mtx ); return my->get_block_header_impl( id ); } - template - fork_database_impl::bhsp fork_database_impl::get_block_header_impl( const block_id_type& id ) const { + template + fork_database_impl::bhsp fork_database_impl::get_block_header_impl( const block_id_type& id ) const { if( root->id() == id ) { return root; } @@ -324,8 +325,8 @@ namespace eosio::chain { return bhsp(); } - template - void fork_database_impl::add_impl(const bsp& n, bool ignore_duplicate, bool validate, validator_t& validator) { + template + void fork_database_impl::add_impl(const bsp& n, bool ignore_duplicate, bool validate, validator_t& validator) { EOS_ASSERT( root, fork_database_exception, "root not yet set" ); EOS_ASSERT( n, fork_database_exception, "attempt to add null block state" ); @@ -362,8 +363,8 @@ namespace eosio::chain { } } - template - void fork_database_t::add( const bsp& n, bool ignore_duplicate ) { + template + void fork_database_t::add( const bsp& n, bool ignore_duplicate ) { std::lock_guard g( my->mtx ); my->add_impl( n, ignore_duplicate, false, []( block_timestamp_type timestamp, @@ -373,20 +374,20 @@ namespace eosio::chain { ); } - template - bsp fork_database_t::root() const { + template + BSP fork_database_t::root() const { std::lock_guard g( my->mtx ); return my->root; } - template - bsp fork_database_t::head() const { + template + BSP fork_database_t::head() const { std::lock_guard g( my->mtx ); return my->head; } - template - bsp fork_database_t::pending_head() const { + template + BSP fork_database_t::pending_head() const { std::lock_guard g( my->mtx ); const auto& indx = my->index.template get(); @@ -399,16 +400,16 @@ namespace eosio::chain { return my->head; } - template - fork_database_t::branch_type - fork_database_t::fetch_branch(const block_id_type& h, uint32_t trim_after_block_num) const { + template + fork_database_t::branch_type + fork_database_t::fetch_branch(const block_id_type& h, uint32_t trim_after_block_num) const { std::lock_guard g(my->mtx); return my->fetch_branch_impl(h, trim_after_block_num); } - template - fork_database_t::branch_type - fork_database_impl::fetch_branch_impl(const block_id_type& h, uint32_t trim_after_block_num) const { + template + fork_database_t::branch_type + fork_database_impl::fetch_branch_impl(const block_id_type& h, uint32_t trim_after_block_num) const { branch_type result; result.reserve(index.size()); for (auto s = get_block_impl(h); s; s = get_block_impl(s->previous())) { @@ -419,16 +420,16 @@ namespace eosio::chain { return result; } - template - fork_database_t::full_branch_type - fork_database_t::fetch_full_branch(const block_id_type& h) const { + template + fork_database_t::full_branch_type + fork_database_t::fetch_full_branch(const block_id_type& h) const { std::lock_guard g(my->mtx); return my->fetch_full_branch_impl(h); } - template - fork_database_t::full_branch_type - fork_database_impl::fetch_full_branch_impl(const block_id_type& h) const { + template + fork_database_t::full_branch_type + fork_database_impl::fetch_full_branch_impl(const block_id_type& h) const { full_branch_type result; result.reserve(index.size()); for (auto s = get_block_impl(h); s; s = get_block_impl(s->previous())) { @@ -438,14 +439,14 @@ namespace eosio::chain { return result; } - template - bsp fork_database_t::search_on_branch( const block_id_type& h, uint32_t block_num ) const { + template + BSP fork_database_t::search_on_branch( const block_id_type& h, uint32_t block_num ) const { std::lock_guard g( my->mtx ); return my->search_on_branch_impl( h, block_num ); } - template - bsp fork_database_impl::search_on_branch_impl( const block_id_type& h, uint32_t block_num ) const { + template + BSP fork_database_impl::search_on_branch_impl( const block_id_type& h, uint32_t block_num ) const { for( auto s = get_block_impl(h); s; s = get_block_impl( s->previous() ) ) { if( s->block_num() == block_num ) return s; @@ -458,16 +459,16 @@ namespace eosio::chain { * Given two head blocks, return two branches of the fork graph that * end with a common ancestor (same prior block) */ - template - fork_database_t::branch_type_pair - fork_database_t::fetch_branch_from(const block_id_type& first, const block_id_type& second) const { + template + fork_database_t::branch_type_pair + fork_database_t::fetch_branch_from(const block_id_type& first, const block_id_type& second) const { std::lock_guard g(my->mtx); return my->fetch_branch_from_impl(first, second); } - template - fork_database_t::branch_type_pair - fork_database_impl::fetch_branch_from_impl(const block_id_type& first, const block_id_type& second) const { + template + fork_database_t::branch_type_pair + fork_database_impl::fetch_branch_from_impl(const block_id_type& first, const block_id_type& second) const { pair result; auto first_branch = (first == root->id()) ? root : get_block_impl(first); auto second_branch = (second == root->id()) ? root : get_block_impl(second); @@ -526,14 +527,14 @@ namespace eosio::chain { } /// fetch_branch_from_impl /// remove all of the invalid forks built off of this id including this id - template - void fork_database_t::remove( const block_id_type& id ) { + template + void fork_database_t::remove( const block_id_type& id ) { std::lock_guard g( my->mtx ); return my->remove_impl( id ); } - template - void fork_database_impl::remove_impl( const block_id_type& id ) { + template + void fork_database_impl::remove_impl( const block_id_type& id ) { deque remove_queue{id}; const auto& previdx = index.template get(); const auto& head_id = head->id(); @@ -554,14 +555,14 @@ namespace eosio::chain { } } - template - void fork_database_t::mark_valid( const bsp& h ) { + template + void fork_database_t::mark_valid( const bsp& h ) { std::lock_guard g( my->mtx ); my->mark_valid_impl( h ); } - template - void fork_database_impl::mark_valid_impl( const bsp& h ) { + template + void fork_database_impl::mark_valid_impl( const bsp& h ) { if( h->is_valid() ) return; auto& by_id_idx = index.template get(); @@ -581,14 +582,14 @@ namespace eosio::chain { } } - template - bsp fork_database_t::get_block(const block_id_type& id) const { + template + BSP fork_database_t::get_block(const block_id_type& id) const { std::lock_guard g( my->mtx ); return my->get_block_impl(id); } - template - bsp fork_database_impl::get_block_impl(const block_id_type& id) const { + template + BSP fork_database_impl::get_block_impl(const block_id_type& id) const { auto itr = index.find( id ); if( itr != index.end() ) return *itr; diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index d9b8d6e74a..b97f9bd74c 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -2,117 +2,13 @@ #include #include -namespace eosio::chain { - -// ---------------------------------------------------------------------------------------- -block_header_state_ptr get_block_by_num(const fork_database_if_t::full_branch_type& branch, std::optional block_num) { - if (!block_num || branch.empty()) - return block_state_ptr{}; - - // a branch always contains consecutive block numbers, starting with the highest - uint32_t first = branch[0]->block_num(); - uint32_t dist = first - *block_num; - return dist < branch.size() ? branch[dist] : block_state_ptr{}; -} - -// ---------------------------------------------------------------------------------------- -bool extends(const fork_database_if_t::full_branch_type& branch, const block_id_type& id) { - return !branch.empty() && - std::any_of(++branch.cbegin(), branch.cend(), [&](const auto& h) { return h->id() == id; }); -} +#include // implementation of finalizer methods -// ---------------------------------------------------------------------------------------- -finalizer::vote_decision finalizer::decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db) { - bool safety_check = false; - bool liveness_check = false; - - bool monotony_check = !fsi.last_vote || proposal->timestamp() > fsi.last_vote.timestamp; - // !fsi.last_vote means we have never voted on a proposal, so the protocol feature just activated and we can proceed - - if (!monotony_check) { - dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal->id())); - return vote_decision::no_vote; - } - - std::optional p_branch; // a branch that includes the root. - - if (!fsi.lock.empty()) { - // Liveness check : check if the height of this proposal's justification is higher - // than the height of the proposal I'm locked on. - // This allows restoration of liveness if a replica is locked on a stale proposal - // ------------------------------------------------------------------------------- - liveness_check = proposal->last_qc_block_timestamp() > fsi.lock.timestamp; - - if (!liveness_check) { - // Safety check : check if this proposal extends the proposal we're locked on - p_branch = fork_db.fetch_full_branch(proposal->id()); - safety_check = extends(*p_branch, fsi.lock.id); - } - } else { - // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. - // `fsi.lock` is initially set to `lib` when switching to IF or starting from a snapshot. - // ------------------------------------------------------------------------------------- - liveness_check = false; - safety_check = false; - } - - dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote = {can_vote}", - ("l",liveness_check)("s",safety_check)("m",monotony_check)("can_vote",(liveness_check || safety_check))); - - // Figure out if we can vote and wether our vote will be strong or weak - // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc - // ----------------------------------------------------------------------------------- - vote_decision decision = vote_decision::no_vote; - - if (liveness_check || safety_check) { - auto [p_start, p_end] = std::make_pair(proposal->last_qc_block_timestamp(), proposal->timestamp()); - - bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; - bool voting_strong = time_range_disjoint; - if (!voting_strong) { - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - voting_strong = extends(*p_branch, fsi.last_vote.id); - } - - fsi.last_vote = proposal_ref(proposal); - fsi.last_vote_range_start = p_start; - - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - auto bsp_final_on_strong_qc = get_block_by_num(*p_branch, proposal->final_on_strong_qc_block_num()); - if (voting_strong && bsp_final_on_strong_qc && bsp_final_on_strong_qc->timestamp() > fsi.lock.timestamp) - fsi.lock = proposal_ref(bsp_final_on_strong_qc); - - decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; - } else { - dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", - ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); - if (proposal->last_qc_block_num()) - dlog("last_qc_block_num=${lqc}", ("lqc", *proposal->last_qc_block_num())); - } - if (decision != vote_decision::no_vote) - dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); - return decision; -} +namespace eosio::chain { // ---------------------------------------------------------------------------------------- -std::optional finalizer::maybe_vote(const bls_public_key& pub_key, const block_state_ptr& p, - const digest_type& digest, const fork_database_if_t& fork_db) { - finalizer::vote_decision decision = decide_vote(p, fork_db); - if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { - bls_signature sig; - if (decision == vote_decision::weak_vote) { - // if voting weak, the digest to sign should be a hash of the concatenation of the finalizer_digest - // and the string "WEAK" - sig = priv_key.sign(create_weak_digest(digest)); - } else { - sig = priv_key.sign({(uint8_t*)digest.data(), (uint8_t*)digest.data() + digest.data_size()}); - } - return vote_message{ p->id(), decision == vote_decision::strong_vote, pub_key, sig }; - } - return {}; -} +// Explicit template instantiation +template struct finalizer_tpl; // ---------------------------------------------------------------------------------------- void my_finalizers_t::save_finalizer_safety_info() const { diff --git a/libraries/chain/include/eosio/chain/fork_database.hpp b/libraries/chain/include/eosio/chain/fork_database.hpp index abbaa0f878..6ecb426910 100644 --- a/libraries/chain/include/eosio/chain/fork_database.hpp +++ b/libraries/chain/include/eosio/chain/fork_database.hpp @@ -24,12 +24,13 @@ namespace eosio::chain { * fork_database should be used instead of fork_database_t directly as it manages * the different supported types. */ - template // either block_state_legacy_ptr or block_state_ptr + template // either block_state_legacy_ptr or block_state_ptr class fork_database_t { public: static constexpr uint32_t legacy_magic_number = 0x30510FDB; static constexpr uint32_t magic_number = 0x4242FDB; + using bsp = BSP; using bs = bsp::element_type; using bhsp = bs::bhsp_t; using bhs = bhsp::element_type; diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index 01fee6e411..70252767f6 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -31,72 +31,75 @@ // ------------------------------------------------------------------------------------------- namespace eosio::chain { - // ---------------------------------------------------------------------------------------- - struct finalizer { - enum class vote_decision { strong_vote, weak_vote, no_vote }; + struct proposal_ref { + block_id_type id; + block_timestamp_type timestamp; - struct proposal_ref { - block_id_type id; - block_timestamp_type timestamp; + proposal_ref() = default; - proposal_ref() = default; + proposal_ref(const block_id_type& id, block_timestamp_type t) : + id(id), timestamp(t) + {} - template - explicit proposal_ref(const BSP& p) : - id(p->id()), - timestamp(p->timestamp()) - {} + template + explicit proposal_ref(const BSP& p) : + id(p->id()), + timestamp(p->timestamp()) + {} - proposal_ref(const block_id_type& id, block_timestamp_type t) : - id(id), timestamp(t) - {} + void reset() { + id = block_id_type(); + timestamp = block_timestamp_type(); + } - void reset() { - id = block_id_type(); - timestamp = block_timestamp_type(); - } + bool empty() const { return id.empty(); } - bool empty() const { return id.empty(); } + explicit operator bool() const { return !id.empty(); } - explicit operator bool() const { return !id.empty(); } + bool operator==(const proposal_ref& o) const { + return id == o.id && timestamp == o.timestamp; + } + }; - auto operator==(const proposal_ref& o) const { - return id == o.id && timestamp == o.timestamp; - } - }; + struct finalizer_safety_information { + block_timestamp_type last_vote_range_start; + proposal_ref last_vote; + proposal_ref lock; - struct safety_information { - block_timestamp_type last_vote_range_start; - proposal_ref last_vote; - proposal_ref lock; + static constexpr uint64_t magic = 0x5AFE11115AFE1111ull; - static constexpr uint64_t magic = 0x5AFE11115AFE1111ull; + static finalizer_safety_information unset_fsi() { return {block_timestamp_type(), {}, {}}; } - static safety_information unset_fsi() { return {block_timestamp_type(), {}, {}}; } + bool operator==(const finalizer_safety_information& o) const { + return last_vote_range_start == o.last_vote_range_start && + last_vote == o.last_vote && + lock == o.lock; + } + }; - auto operator==(const safety_information& o) const { - return last_vote_range_start == o.last_vote_range_start && - last_vote == o.last_vote && - lock == o.lock; - } - }; + // ---------------------------------------------------------------------------------------- + template + struct finalizer_tpl { + enum class vote_decision { strong_vote, weak_vote, no_vote }; bls_private_key priv_key; - safety_information fsi; + finalizer_safety_information fsi; private: - using branch_type = fork_database_if_t::branch_type; - using full_branch_type = fork_database_if_t::full_branch_type; - vote_decision decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db); + using full_branch_type = FORK_DB::full_branch_type; public: - std::optional maybe_vote(const bls_public_key& pub_key, const block_state_ptr& bsp, - const digest_type& digest, const fork_database_if_t& fork_db); + vote_decision decide_vote(const FORK_DB::bsp& proposal, const FORK_DB& fork_db); + + std::optional maybe_vote(const bls_public_key& pub_key, const FORK_DB::bsp& bsp, + const digest_type& digest, const FORK_DB& fork_db); }; + using finalizer = finalizer_tpl; + // ---------------------------------------------------------------------------------------- struct my_finalizers_t { - using fsi_t = finalizer::safety_information; + using fsi_t = finalizer_safety_information; using fsi_map = std::map; const block_timestamp_type t_startup; // nodeos startup time, used for default safety_information @@ -148,16 +151,16 @@ namespace eosio::chain { } namespace std { - inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer::proposal_ref& r) { + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::proposal_ref& r) { os << "proposal_ref(id(" << r.id.str() << "), tstamp(" << r.timestamp.slot << "))"; return os; } - inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer::safety_information& fsi) { + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer_safety_information& fsi) { os << "fsi(" << fsi.last_vote_range_start.slot << ", " << fsi.last_vote << ", " << fsi.lock << ")"; return os; } } -FC_REFLECT(eosio::chain::finalizer::proposal_ref, (id)(timestamp)) -FC_REFLECT(eosio::chain::finalizer::safety_information, (last_vote_range_start)(last_vote)(lock)) \ No newline at end of file +FC_REFLECT(eosio::chain::proposal_ref, (id)(timestamp)) +FC_REFLECT(eosio::chain::finalizer_safety_information, (last_vote_range_start)(last_vote)(lock)) \ No newline at end of file diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp new file mode 100644 index 0000000000..76fd7442b8 --- /dev/null +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp @@ -0,0 +1,119 @@ +#pragma once + +namespace eosio::chain { +// ---------------------------------------------------------------------------------------- +template +typename FORK_DB::bhsp +get_block_by_num(const typename FORK_DB::full_branch_type& branch, std::optional block_num) { + if (!block_num || branch.empty()) + return block_state_ptr{}; + + // a branch always contains consecutive block numbers, starting with the highest + uint32_t first = branch[0]->block_num(); + uint32_t dist = first - *block_num; + return dist < branch.size() ? branch[dist] : block_state_ptr{}; +} + +// ---------------------------------------------------------------------------------------- +template +bool extends(const typename FORK_DB::full_branch_type& branch, const block_id_type& id) { + return !branch.empty() && + std::any_of(++branch.cbegin(), branch.cend(), [&](const auto& h) { return h->id() == id; }); +} + +// ---------------------------------------------------------------------------------------- +template +finalizer_tpl::vote_decision finalizer_tpl::decide_vote(const FORK_DB::bsp& proposal, const FORK_DB& fork_db) { + bool safety_check = false; + bool liveness_check = false; + + bool monotony_check = !fsi.last_vote || proposal->timestamp() > fsi.last_vote.timestamp; + // !fsi.last_vote means we have never voted on a proposal, so the protocol feature just activated and we can proceed + + if (!monotony_check) { + dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal->id())); + return vote_decision::no_vote; + } + + std::optional p_branch; // a branch that includes the root. + + if (!fsi.lock.empty()) { + // Liveness check : check if the height of this proposal's justification is higher + // than the height of the proposal I'm locked on. + // This allows restoration of liveness if a replica is locked on a stale proposal + // ------------------------------------------------------------------------------- + liveness_check = proposal->last_qc_block_timestamp() > fsi.lock.timestamp; + + if (!liveness_check) { + // Safety check : check if this proposal extends the proposal we're locked on + p_branch = fork_db.fetch_full_branch(proposal->id()); + safety_check = extends(*p_branch, fsi.lock.id); + } + } else { + // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. + // `fsi.lock` is initially set to `lib` when switching to IF or starting from a snapshot. + // ------------------------------------------------------------------------------------- + liveness_check = false; + safety_check = false; + } + + dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote = {can_vote}", + ("l",liveness_check)("s",safety_check)("m",monotony_check)("can_vote",(liveness_check || safety_check))); + + // Figure out if we can vote and wether our vote will be strong or weak + // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc + // ----------------------------------------------------------------------------------- + vote_decision decision = vote_decision::no_vote; + + if (liveness_check || safety_check) { + auto [p_start, p_end] = std::make_pair(proposal->last_qc_block_timestamp(), proposal->timestamp()); + + bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; + bool voting_strong = time_range_disjoint; + if (!voting_strong) { + if (!p_branch) + p_branch = fork_db.fetch_full_branch(proposal->id()); + voting_strong = extends(*p_branch, fsi.last_vote.id); + } + + fsi.last_vote = proposal_ref(proposal); + fsi.last_vote_range_start = p_start; + + if (!p_branch) + p_branch = fork_db.fetch_full_branch(proposal->id()); + auto bsp_final_on_strong_qc = get_block_by_num(*p_branch, proposal->final_on_strong_qc_block_num()); + if (voting_strong && bsp_final_on_strong_qc && bsp_final_on_strong_qc->timestamp() > fsi.lock.timestamp) + fsi.lock = proposal_ref(bsp_final_on_strong_qc); + + decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; + } else { + dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", + ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); + if (proposal->last_qc_block_num()) + dlog("last_qc_block_num=${lqc}", ("lqc", *proposal->last_qc_block_num())); + } + if (decision != vote_decision::no_vote) + dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); + return decision; +} + +// ---------------------------------------------------------------------------------------- +template +std::optional finalizer_tpl::maybe_vote(const bls_public_key& pub_key, const FORK_DB::bsp& p, + const digest_type& digest, const FORK_DB& fork_db) { + finalizer::vote_decision decision = decide_vote(p, fork_db); + if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { + bls_signature sig; + if (decision == vote_decision::weak_vote) { + // if voting weak, the digest to sign should be a hash of the concatenation of the finalizer_digest + // and the string "WEAK" + sig = priv_key.sign(create_weak_digest(digest)); + } else { + sig = priv_key.sign({(uint8_t*)digest.data(), (uint8_t*)digest.data() + digest.data_size()}); + } + return vote_message{ p->id(), decision == vote_decision::strong_vote, pub_key, sig }; + } + return {}; +} + +} \ No newline at end of file diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 27948c2e7d..715d8984d7 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -1,14 +1,15 @@ #include +#include // implementation of finalizer methods + #include #include #include + using namespace eosio; using namespace eosio::chain; using namespace eosio::testing; -using fsi_t = finalizer::safety_information; -using proposal_ref = finalizer::proposal_ref; using tstamp = block_timestamp_type; struct bls_keys_t { @@ -24,13 +25,14 @@ struct bls_keys_t { } }; -std::vector create_random_fsi(size_t count) { - std::vector res; +template +std::vector create_random_fsi(size_t count) { + std::vector res; res.reserve(count); for (size_t i=0; i& ke return res; } -template -void set_fsi(my_finalizers_t& fset, const std::vector& keys, const std::vector& fsi) { +template +void set_fsi(my_finalizers_t& fset, const std::vector& keys, const FSI_VEC& fsi) { ((fset.set_fsi(keys[I].pubkey, fsi[I])), ...); } @@ -68,6 +70,7 @@ BOOST_AUTO_TEST_CASE( basic_finalizer_safety_file_io ) try { fc::temp_directory tempdir; auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; + using fsi_t = finalizer_safety_information; fsi_t fsi { tstamp(0), proposal_ref{sha256::hash((const char *)"vote"), tstamp(7)}, proposal_ref{sha256::hash((const char *)"lock"), tstamp(3)} }; @@ -100,7 +103,8 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { fc::temp_directory tempdir; auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; - std::vector fsi = create_random_fsi(10); + using fsi_t = finalizer_safety_information; + std::vector fsi = create_random_fsi(10); std::vector keys = create_keys(10); { @@ -108,7 +112,7 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { bls_pub_priv_key_map_t local_finalizers = create_local_finalizers<1, 3, 5, 6>(keys); fset.set_keys(local_finalizers); - set_fsi<1, 3, 5, 6>(fset, keys, fsi); + set_fsi(fset, keys, fsi); fset.save_finalizer_safety_info(); // at this point we have saved the finalizer safety file, containing a specific fsi for finalizers <1, 3, 5, 6> @@ -154,6 +158,61 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() +// --------------------------------------------------------------------------------------- +// emulations of block_header_state and fork_database sufficient for instantiating a +// finalizer. +// --------------------------------------------------------------------------------------- +struct mock_bhs { + block_id_type block_id; + block_timestamp_type block_timestamp; + + const block_id_type& id() const { return block_id; } + block_timestamp_type timestamp() const { return block_timestamp; } +}; + +using mock_bhsp = std::shared_ptr; + +// --------------------------------------------------------------------------------------- +struct mock_bs : public mock_bhs {}; + +using mock_bsp = std::shared_ptr; + +// --------------------------------------------------------------------------------------- +struct mock_forkdb { + using bsp = mock_bsp; + using bhsp = mock_bhsp; + using full_branch_type = std::vector; + + bhsp root() const { return branch.back(); } + + full_branch_type fetch_full_branch(const block_id_type& id) const { + auto it = std::find_if(branch.cbegin(), branch.cend(), [&](const bhsp& p) { return p->id() == id; }); + assert(it != branch.cend()); + return full_branch_type(it, branch.cend()); + }; + + full_branch_type branch; +}; + +// real finalizer, using mock_forkdb and mock_bsp +using test_finalizer = finalizer_tpl; + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { + fc::temp_directory tempdir; + auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; + + using fsi_t = finalizer_safety_information; + fsi_t fsi { tstamp(0), + proposal_ref{sha256::hash((const char *)"vote"), tstamp(7)}, + proposal_ref{sha256::hash((const char *)"lock"), tstamp(3)} }; + + bls_keys_t k("alice"_n); + bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; + + test_finalizer finalizer{k.privkey, finalizer_safety_information{fsi}}; + +} FC_LOG_AND_RETHROW() From 273ebd35d1fc9eded609b649ed13e40a20650940 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sun, 18 Feb 2024 11:07:25 -0500 Subject: [PATCH 02/18] Add bhs_core.hpp --- .../eosio/chain/hotstuff/finalizer.ipp | 2 +- unittests/bhs_core.hpp | 352 ++++++++++++++++++ unittests/finalizer_tests.cpp | 75 +++- 3 files changed, 412 insertions(+), 17 deletions(-) create mode 100644 unittests/bhs_core.hpp diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp index 76fd7442b8..259504d690 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp @@ -100,7 +100,7 @@ finalizer_tpl::vote_decision finalizer_tpl::decide_vote(const // ---------------------------------------------------------------------------------------- template std::optional finalizer_tpl::maybe_vote(const bls_public_key& pub_key, const FORK_DB::bsp& p, - const digest_type& digest, const FORK_DB& fork_db) { + const digest_type& digest, const FORK_DB& fork_db) { finalizer::vote_decision decision = decide_vote(p, fork_db); if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { bls_signature sig; diff --git a/unittests/bhs_core.hpp b/unittests/bhs_core.hpp new file mode 100644 index 0000000000..3fa99d08fd --- /dev/null +++ b/unittests/bhs_core.hpp @@ -0,0 +1,352 @@ +#include + +namespace bhs_core { + +using eosio::chain::block_id_type; + +using block_num_type = uint32_t; +using block_time_type = eosio::chain::block_timestamp_type; + +struct block_ref +{ + block_id_type block_id; + block_time_type timestamp; + + block_num_type block_num() const; // Extract from block_id. +}; + +struct qc_link +{ + block_num_type source_block_num; + block_num_type target_block_num; // Must be less than or equal to source_block_num (only equal for genesis block). + bool is_link_strong; +}; + +struct qc_claim +{ + block_num_type block_num; + bool is_strong_qc; + + friend auto operator<=>(const qc_claim&, const qc_claim&) = default; +}; + +bool all_equal(auto ...ns) { + std::array a { ns... }; + for (int i=0; i<(int)a.size()-1; ++i) + if (a[i] != a[i+1]) + return false; + return true; +} + +struct core +{ + std::vector links; // Captures all relevant links sorted in order of ascending source_block_num. + std::vector refs; // Covers ancestor blocks with block numbers greater than or equal to last_final_block_num. + // Sorted in order of ascending block_num. + block_num_type final_on_strong_qc_block_num; + + // Invariants: + // 1. links.empty() == false + // 2. last_final_block_num() <= final_on_strong_qc_block_num <= latest_qc_claim().block_num + // 3. If refs.empty() == true, then (links.size() == 1) and + // (links.back().target_block_num == links.back().source_block_num == final_on_strong_qc_block_num == last_final_block_num()) + // 4. If refs.empty() == false, then refs.front().block_num() == links.front().target_block_num == last_final_block_num() + // 5. If refs.empty() == false, then refs.back().block_num() + 1 == links.back().source_block_num == current_block_num() + // 6. If refs.size() > 1, then: + // For i = 0 to refs.size() - 2: + // (refs[i].block_num() + 1 == refs[i+1].block_num()) and (refs[i].timestamp < refs[i+1].timestamp) + // 7. If links.size() > 1, then: + // For i = 0 to links.size() - 2: + // (links[i].source_block_num + 1 == links[i+1].source_block_num) and (links[i].target_block_num <= links[i+1].target_block_num) + // 8. current_block_num() - last_final_block_num() == refs.size() (always implied by invariants 3 to 6) + // 9. current_block_num() - links.front().source_block_num == links.size() - 1 (always implied by invariants 1 and 7) + + void check_invariants() { + assert(!links.empty()); // 1. + assert(last_final_block_num() <= final_on_strong_qc_block_num && // 2. + final_on_strong_qc_block_num <= latest_qc_claim().block_num); + if (refs.empty()) { // 3. + assert(links.size() == 1); + } else { + assert(all_equal(links.back().target_block_num, // 3. + links.back().source_block_num, + final_on_strong_qc_block_num, + last_final_block_num())); + assert(all_equal(refs.front().block_num(), // 4. + links.front().target_block_num, + last_final_block_num())); + assert(all_equal(refs.back().block_num() + 1, // 5. + links.back().source_block_num, + current_block_num())); + if (refs.size() > 1) { // 6. + for (size_t i=0; i 1) { // 7. + for (size_t i=0; icurrent_block_num() + * @pre If this->refs.empty() == false, then current_block is the block after the one referenced by this->refs.back() + * @pre this->latest_qc_claim().block_num <= most_recent_ancestor_with_qc.block_num <= this->current_block_num() + * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc + * + * @post returned core has current_block_num() == this->current_block_num() + 1 + * @post returned core has latest_qc_claim() == most_recent_ancestor_with_qc + * @post returned core has final_on_strong_qc_block_num >= this->final_on_strong_qc_block_num + * @post returned core has last_final_block_num() >= this->last_final_block_num() + */ + core next(const block_ref& current_block, const qc_claim& most_recent_ancestor_with_qc) const + { + assert(current_block.block_num() == current_block_num()); // Satisfied by precondition 1. + + assert(refs.empty() || (refs.back().timestamp < current_block.timestamp)); // Satisfied by precondition 2. + assert(refs.empty() || (refs.back().block_num() + 1 == current_block.block_num())); // Satisfied by precondition 2. + + assert(most_recent_ancestor_with_qc.block_num <= current_block_num()); // Satisfied by precondition 3. + + assert(latest_qc_claim() <= most_recent_ancestor_with_qc); // Satisfied by precondition 4. + + core next_core; + + auto new_block_nums = [&]() -> std::pair + { + assert(last_final_block_num() <= final_on_strong_qc_block_num); // Satisfied by invariant 2. + + if (!most_recent_ancestor_with_qc.is_strong_qc) { + return {last_final_block_num(), final_on_strong_qc_block_num}; + } + + if (most_recent_ancestor_with_qc.block_num < links.front().source_block_num) { + return {last_final_block_num(), final_on_strong_qc_block_num}; + } + + const auto& link1 = get_qc_link_from(most_recent_ancestor_with_qc.block_num); + + // TODO: Show the following hold true: + // final_on_strong_qc_block_num <= link1.target_block_num <= current_block_num(). + // link1.target_block_num == current_block_num() iff refs.empty() == true. + + // Since last_final_block_num() <= final_on_strong_qc_block_num + // and final_on_strong_qc_block_num <= link1.target_block_num, + // then last_final_block_num() <= link1.target_block_num. + + if (!link1.is_link_strong) { + return {last_final_block_num(), link1.target_block_num}; + } + + if (link1.target_block_num < links.front().source_block_num) { + return {last_final_block_num(), link1.target_block_num}; + } + + const auto& link2 = get_qc_link_from(link1.target_block_num); + + // TODO: Show the following hold true: + // last_final_block_num() <= link2.target_block_num + // link2.target_block_num <= link1.target_block_num + // link1.target_block_num <= most_recent_ancestor_with_qc.block_num + + return {link2.target_block_num, link1.target_block_num}; + }; + + const auto [new_last_final_block_num, new_final_on_strong_qc_block_num] = new_block_nums(); + + assert(new_last_final_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justification in new_block_nums. + assert(new_final_on_strong_qc_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by justification in new_block_nums. + + assert(final_on_strong_qc_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. + assert(last_final_block_num() <= new_last_final_block_num); // Satisfied by justifications in new_block_nums. + + next_core.final_on_strong_qc_block_num = new_final_on_strong_qc_block_num; + // Post-condition 3 is satisfied, assuming next_core will be returned without further modifications to next_core.final_on_strong_qc_block_num. + + // Post-condition 4 and invariant 2 will be satisfied when next_core.last_final_block_num() is updated to become new_last_final_block_num. + + // Setup next_core.links by garbage collecting unnecessary links and then adding the new QC link. + { + size_t links_index = 0; // Default to no garbage collection (if last_final_block_num does not change). + + if (last_final_block_num() < next_core.last_final_block_num()) { + // new_blocks_nums found the new_last_final_block_num from a link that had a source_block_num + // equal to new_final_on_strong_qc_block_num. + // The index within links was (new_final_on_strong_qc_block_num - last_final_block_num). + // All prior links can be garbage collected. + + links_index = new_final_on_strong_qc_block_num - last_final_block_num(); + + assert(links_index < links.size()); // Satisfied by justification in this->get_qc_link_from(next_core.final_on_strong_qc_block_num). + } + + next_core.links.reserve(links.size() - links_index + 1); + + // Garbage collect unnecessary links + std::copy(links.cbegin() + links_index, links.cend(), std::back_inserter(next_core.links)); + + assert(next_core.last_final_block_num() == new_last_final_block_num); // Satisfied by choice of links_index. + + // Also, by choice of links_index, at this point, next_core.links.back() == this->links.back(). + assert(next_core.links.back().source_block_num == current_block_num()); // Satisfied because last item in links has not yet changed. + assert(next_core.links.back().target_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied because of above and precondition 3. + + // Add new link + next_core.links.emplace_back( + qc_link{ + .source_block_num = current_block_num() + 1, + .target_block_num = most_recent_ancestor_with_qc.block_num, // Guaranteed to be less than current_block_num() + 1. + .is_link_strong = most_recent_ancestor_with_qc.is_strong_qc, + }); + + // Post-conditions 1, 2, and 4 are satisfied, assuming next_core will be returned without further modifications to next_core.links. + + // Invariants 1, 2, and 7 are satisfied for next_core.60 + } + + // Setup next_core.refs by garbage collecting unnecessary block references in the refs and then adding the new block reference. + { + const size_t refs_index = next_core.last_final_block_num() - last_final_block_num(); + + // Using the justifications in new_block_nums, 0 <= ref_index <= (current_block_num() - last_final_block_num). + // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num, and therefore ref_index == 0. + // Otherwise if refs.empty() == false, the justification in new_block_nums provides the stronger inequality + // 0 <= ref_index < (current_block_num() - last_final_block_num), which, using invariant 8, can be simplified to + // 0 <= ref_index < refs.size(). + + assert(!refs.empty() || (refs_index == 0)); // Satisfied by justification above. + assert(refs.empty() || (refs_index < refs.size())); // Satisfied by justification above. + + next_core.refs.reserve(refs.size() - refs_index + 1); + + // Garbage collect unnecessary block references + std::copy(refs.cbegin() + refs_index, refs.cend(), std::back_inserter(next_core.refs)); + + assert(refs.empty() || (refs.front().block_num() == new_last_final_block_num)); // Satisfied by choice of refs_index. + + // Add new block reference + next_core.refs.emplace_back(current_block); + + // Invariant 3 is trivially satisfied for next_core because next_core.refs.empty() == false. + + // Invariant 5 is clearly satisfied for next_core because next_core.refs.back().block_num() == this->current_block_num() + // and next_core.links.back().source_block_num == this->current_block_num() + 1. + + // Invariant 6 is also clearly satisfied for next_core because invariant 6 is satisfied for *this and the only + // additional requirements needed are the ones provided by precondition 2. + + // If this->refs.empty() == true, then new_last_final_block_num == last_final_block_num == current_block_num(), + // and next_core.refs.size() == 1 and next_core.front() == current_block. + // And so, next_core.front().block_num() == new_last_final_block_num. + // If this->refs.empty() == false, then adding the current_block to the end does not change the fact that + // refs.front().block_num() is still equal to new_last_final_block_num. + + assert(refs.front().block_num() == new_last_final_block_num); // Satisfied by justification above. + + // Because it was also already shown earlier that links.front().target_block_num == new_last_final_block_num, + // then the justification above satisfies the remaining equalities needed to satisfy invariant 4 for next_core. + + // So, invariants 3 to 6 are now satisfied for next_core in addition to the invariants 1, 2, and 7 that were shown to be satisfied + // earlier (and still remain satisfied since next_core.links and next_core.final_on_strong_qc_block_num have not changed). + } + + return next_core; + // Invariants 1 to 7 were verified to be satisfied for the current value of next_core at various points above. + // (And so, the remaining invariants for next_core are also automatically satisfied.) + } +}; + +} \ No newline at end of file diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 715d8984d7..6cdee3d3aa 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -10,7 +10,8 @@ using namespace eosio; using namespace eosio::chain; using namespace eosio::testing; -using tstamp = block_timestamp_type; +using tstamp = block_timestamp_type; +using fsi_t = finalizer_safety_information; struct bls_keys_t { bls_private_key privkey; @@ -39,6 +40,17 @@ std::vector create_random_fsi(size_t count) { return res; } +std::vector create_proposal_refs(size_t count) { + std::vector res; + res.reserve(count); + for (size_t i=0; i create_keys(size_t count) { std::vector res; res.reserve(count); @@ -69,11 +81,11 @@ BOOST_AUTO_TEST_SUITE(finalizer_tests) BOOST_AUTO_TEST_CASE( basic_finalizer_safety_file_io ) try { fc::temp_directory tempdir; auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; + auto proposals { create_proposal_refs(10) }; - using fsi_t = finalizer_safety_information; - fsi_t fsi { tstamp(0), - proposal_ref{sha256::hash((const char *)"vote"), tstamp(7)}, - proposal_ref{sha256::hash((const char *)"lock"), tstamp(3)} }; + fsi_t fsi { .last_vote_range_start = tstamp(0), + .last_vote = proposals[6], + .lock = proposals[2] }; bls_keys_t k("alice"_n); bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; @@ -103,7 +115,6 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { fc::temp_directory tempdir; auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; - using fsi_t = finalizer_safety_information; std::vector fsi = create_random_fsi(10); std::vector keys = create_keys(10); @@ -158,15 +169,19 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() +#include "bhs_core.hpp" + // --------------------------------------------------------------------------------------- // emulations of block_header_state and fork_database sufficient for instantiating a // finalizer. // --------------------------------------------------------------------------------------- struct mock_bhs { + uint32_t block_number; block_id_type block_id; block_timestamp_type block_timestamp; - const block_id_type& id() const { return block_id; } + uint32_t block_num() const { return block_number; } + const block_id_type& id() const { return block_id; } block_timestamp_type timestamp() const { return block_timestamp; } }; @@ -177,6 +192,22 @@ struct mock_bs : public mock_bhs {}; using mock_bsp = std::shared_ptr; +// --------------------------------------------------------------------------------------- +struct mock_proposal { + uint32_t block_number; + std::string proposer_name; + block_timestamp_type block_timestamp; + + uint32_t block_num() const { return block_number; } + const std::string& proposer() const { return proposer_name; } + block_timestamp_type timestamp() const { return block_timestamp; } + + mock_bhs to_bhs() const { + std::string id_str = proposer_name + std::to_string(block_number); + return mock_bhs{block_num(), sha256::hash(id_str.c_str()), timestamp() }; + } +}; + // --------------------------------------------------------------------------------------- struct mock_forkdb { using bsp = mock_bsp; @@ -199,22 +230,34 @@ using test_finalizer = finalizer_tpl; // --------------------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { - fc::temp_directory tempdir; - auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; + auto proposals { create_proposal_refs(10) }; + fsi_t fsi { .last_vote_range_start = tstamp(0), + .last_vote = proposals[6], + .lock = proposals[2] }; + + bls_keys_t k("alice"_n); + test_finalizer finalizer{k.privkey, fsi}; + + +} FC_LOG_AND_RETHROW() + + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( proposal_sim_1 ) try { + fsi_t fsi; // default uninitialized values, no previous lock or vote + + bls_keys_t k("alice"_n); + test_finalizer finalizer{k.privkey, fsi}; - using fsi_t = finalizer_safety_information; - fsi_t fsi { tstamp(0), - proposal_ref{sha256::hash((const char *)"vote"), tstamp(7)}, - proposal_ref{sha256::hash((const char *)"lock"), tstamp(3)} }; - bls_keys_t k("alice"_n); - bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; - test_finalizer finalizer{k.privkey, finalizer_safety_information{fsi}}; } FC_LOG_AND_RETHROW() + + + BOOST_AUTO_TEST_SUITE_END() From db5a2776f5825101bd95d2f8e651ea1b37b0a823 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Tue, 20 Feb 2024 14:16:11 -0500 Subject: [PATCH 03/18] make `core.last_qc_block_num` non optional. Missed `block_state.hpp`. --- libraries/chain/block_header_state.cpp | 4 +--- .../eosio/chain/block_header_state.hpp | 5 +++-- .../chain/include/eosio/chain/block_state.hpp | 6 +++--- unittests/block_header_state_tests.cpp | 19 +++++++++---------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/libraries/chain/block_header_state.cpp b/libraries/chain/block_header_state.cpp index f1a257a67a..042503c16b 100644 --- a/libraries/chain/block_header_state.cpp +++ b/libraries/chain/block_header_state.cpp @@ -48,9 +48,7 @@ block_header_state_core block_header_state_core::next(qc_claim_t incoming) const // next block which can become irreversible is the block with // old last_qc_block_num - if (old_last_qc_block_num.has_value()) { - result.final_on_strong_qc_block_num = *old_last_qc_block_num; - } + result.final_on_strong_qc_block_num = old_last_qc_block_num; } else { // new final_on_strong_qc_block_num should not be present result.final_on_strong_qc_block_num.reset(); diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index 5fb0d4ef45..f43977e0c4 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -32,7 +32,7 @@ struct block_header_state_input : public building_block_input { struct block_header_state_core { uint32_t last_final_block_num{0}; // last irreversible (final) block. std::optional final_on_strong_qc_block_num; // will become final if this header achives a strong QC. - std::optional last_qc_block_num; // The block number of the most recent ancestor block that has a QC justification + uint32_t last_qc_block_num; // The block number of the most recent ancestor block that has a QC justification block_timestamp_type last_qc_block_timestamp; // The block timestamp of the most recent ancestor block that has a QC justification uint32_t finalizer_policy_generation{0}; // @@ -70,6 +70,7 @@ struct block_header_state { const block_id_type& previous() const { return header.previous; } uint32_t block_num() const { return block_header::num_from_id(previous()) + 1; } block_timestamp_type last_qc_block_timestamp() const { return core.last_qc_block_timestamp; } + uint32_t last_qc_block_num() const { return core.last_qc_block_num; } const producer_authority_schedule& active_schedule_auth() const { return active_proposer_policy->proposer_schedule; } block_header_state next(block_header_state_input& data) const; @@ -78,7 +79,7 @@ struct block_header_state { // block descending from this need the provided qc in the block extension bool is_needed(const quorum_certificate& qc) const { - return !core.last_qc_block_num || qc.block_num > *core.last_qc_block_num; + return qc.block_num > core.last_qc_block_num; } flat_set get_activated_protocol_features() const { return activated_protocol_features->protocol_features; } diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index c628741fae..3d501dd9c3 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -41,9 +41,9 @@ struct block_state : public block_header_state { // block_header_state provi bool is_valid() const { return validated; } void set_valid(bool b) { validated = b; } uint32_t irreversible_blocknum() const { return core.last_final_block_num; } - std::optional get_best_qc() const; - std::optional last_qc_block_num() const { return core.last_qc_block_num; } - std::optional final_on_strong_qc_block_num() const { return core.final_on_strong_qc_block_num; } + uint32_t last_qc_block_num() const { return block_header_state::last_qc_block_num(); } + std::optional get_best_qc() const; + std::optional final_on_strong_qc_block_num() const { return core.final_on_strong_qc_block_num; } protocol_feature_activation_set_ptr get_activated_protocol_features() const { return block_header_state::activated_protocol_features; } bool is_pub_keys_recovered() const { return pub_keys_recovered; } diff --git a/unittests/block_header_state_tests.cpp b/unittests/block_header_state_tests.cpp index 2133a66802..312761acaf 100644 --- a/unittests/block_header_state_tests.cpp +++ b/unittests/block_header_state_tests.cpp @@ -14,13 +14,12 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_constructor_test) block_header_state_core bhs_core1(1, 2, 3); BOOST_REQUIRE_EQUAL(bhs_core1.last_final_block_num, 1u); BOOST_REQUIRE_EQUAL(*bhs_core1.final_on_strong_qc_block_num, 2u); - BOOST_REQUIRE_EQUAL(*bhs_core1.last_qc_block_num, 3u); + BOOST_REQUIRE_EQUAL(bhs_core1.last_qc_block_num, 3u); // verifies optional arguments work as expected block_header_state_core bhs_core2(10, std::nullopt, {}); BOOST_REQUIRE_EQUAL(bhs_core2.last_final_block_num, 10u); BOOST_REQUIRE(!bhs_core2.final_on_strong_qc_block_num.has_value()); - BOOST_REQUIRE(!bhs_core2.last_qc_block_num.has_value()); } // comprehensive state transition test @@ -38,7 +37,7 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_state_transition_test) auto new_bhs_core = old_bhs_core.next({old_last_qc_block_num, old_last_qc_block_timestamp, is_last_qc_strong}); BOOST_REQUIRE_EQUAL(new_bhs_core.last_final_block_num, old_bhs_core.last_final_block_num); BOOST_REQUIRE_EQUAL(*new_bhs_core.final_on_strong_qc_block_num, *old_bhs_core.final_on_strong_qc_block_num); - BOOST_REQUIRE_EQUAL(*new_bhs_core.last_qc_block_num, *old_bhs_core.last_qc_block_num); + BOOST_REQUIRE_EQUAL(new_bhs_core.last_qc_block_num, old_bhs_core.last_qc_block_num); BOOST_REQUIRE(new_bhs_core.last_qc_block_timestamp == old_bhs_core.last_qc_block_timestamp); } @@ -57,7 +56,7 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_state_transition_test) // old last_qc block became final_on_strong_qc block BOOST_REQUIRE_EQUAL(*new_bhs_core.final_on_strong_qc_block_num, old_last_qc_block_num); // new last_qc_block_num is the same as input - BOOST_REQUIRE_EQUAL(*new_bhs_core.last_qc_block_num, input_last_qc_block_num); + BOOST_REQUIRE_EQUAL(new_bhs_core.last_qc_block_num, input_last_qc_block_num); BOOST_REQUIRE(new_bhs_core.last_qc_block_timestamp == input_last_qc_block_timestamp); // verifies state transition works when is_last_qc_strong is false @@ -67,7 +66,7 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_state_transition_test) // new final_on_strong_qc_block_num should not be present BOOST_REQUIRE(!new_bhs_core.final_on_strong_qc_block_num.has_value()); // new last_qc_block_num is the same as input - BOOST_REQUIRE_EQUAL(*new_bhs_core.last_qc_block_num, input_last_qc_block_num); + BOOST_REQUIRE_EQUAL(new_bhs_core.last_qc_block_num, input_last_qc_block_num); BOOST_REQUIRE(new_bhs_core.last_qc_block_timestamp == input_last_qc_block_timestamp); } @@ -88,9 +87,9 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_3_chain_transition_test) // final_on_strong_qc_block_num should be same as old one BOOST_REQUIRE(!block3_bhs_core.final_on_strong_qc_block_num.has_value()); // new last_qc_block_num is the same as input - BOOST_REQUIRE_EQUAL(*block3_bhs_core.last_qc_block_num, block3_input_last_qc_block_num); + BOOST_REQUIRE_EQUAL(block3_bhs_core.last_qc_block_num, block3_input_last_qc_block_num); BOOST_REQUIRE(block3_bhs_core.last_qc_block_timestamp == block3_input_last_qc_block_timestamp); - auto block3_last_qc_block_num = *block3_bhs_core.last_qc_block_num; + auto block3_last_qc_block_num = block3_bhs_core.last_qc_block_num; // block3 --> block4 constexpr auto block4_input_last_qc_block_num = 3u; @@ -101,10 +100,10 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_3_chain_transition_test) // final_on_strong_qc_block_num should be block3's last_qc_block_num BOOST_REQUIRE_EQUAL(*block4_bhs_core.final_on_strong_qc_block_num, block3_last_qc_block_num); // new last_qc_block_num is the same as input - BOOST_REQUIRE_EQUAL(*block4_bhs_core.last_qc_block_num, block4_input_last_qc_block_num); + BOOST_REQUIRE_EQUAL(block4_bhs_core.last_qc_block_num, block4_input_last_qc_block_num); BOOST_REQUIRE(block4_bhs_core.last_qc_block_timestamp == block4_input_last_qc_block_timestamp); auto block4_final_on_strong_qc_block_num = *block4_bhs_core.final_on_strong_qc_block_num; - auto block4_last_qc_block_num = *block4_bhs_core.last_qc_block_num; + auto block4_last_qc_block_num = block4_bhs_core.last_qc_block_num; // block4 --> block5 constexpr auto block5_input_last_qc_block_num = 4u; @@ -115,7 +114,7 @@ BOOST_AUTO_TEST_CASE(block_header_state_core_3_chain_transition_test) // final_on_strong_qc_block_num should be block4's last_qc_block_num BOOST_REQUIRE_EQUAL(*block5_bhs_core.final_on_strong_qc_block_num, block4_last_qc_block_num); // new last_qc_block_num is the same as input - BOOST_REQUIRE_EQUAL(*block5_bhs_core.last_qc_block_num, block5_input_last_qc_block_num); + BOOST_REQUIRE_EQUAL(block5_bhs_core.last_qc_block_num, block5_input_last_qc_block_num); BOOST_REQUIRE(block5_bhs_core.last_qc_block_timestamp == block5_input_last_qc_block_timestamp); } From 908531fb6e1a7fae36e8b52ec1b4b20f39bf5eef Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Tue, 20 Feb 2024 14:45:30 -0500 Subject: [PATCH 04/18] Missed an instance of `*proposal->last_qc_block_num` and cleanup finalizer.ipp. --- libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp | 4 ++-- libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index 70252767f6..465307a9fc 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -82,8 +82,8 @@ namespace eosio::chain { struct finalizer_tpl { enum class vote_decision { strong_vote, weak_vote, no_vote }; - bls_private_key priv_key; - finalizer_safety_information fsi; + bls_private_key priv_key; + finalizer_safety_information fsi; private: using full_branch_type = FORK_DB::full_branch_type; diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp index 259504d690..08f00d0165 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp @@ -6,12 +6,12 @@ template typename FORK_DB::bhsp get_block_by_num(const typename FORK_DB::full_branch_type& branch, std::optional block_num) { if (!block_num || branch.empty()) - return block_state_ptr{}; + return {}; // a branch always contains consecutive block numbers, starting with the highest uint32_t first = branch[0]->block_num(); uint32_t dist = first - *block_num; - return dist < branch.size() ? branch[dist] : block_state_ptr{}; + return dist < branch.size() ? branch[dist] : typename FORK_DB::bhsp{}; } // ---------------------------------------------------------------------------------------- @@ -90,7 +90,7 @@ finalizer_tpl::vote_decision finalizer_tpl::decide_vote(const dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); if (proposal->last_qc_block_num()) - dlog("last_qc_block_num=${lqc}", ("lqc", *proposal->last_qc_block_num())); + dlog("last_qc_block_num=${lqc}", ("lqc", proposal->last_qc_block_num())); } if (decision != vote_decision::no_vote) dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); From cf5832d214d2184f0affc2f28a6a9264b22c6158 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 23 Feb 2024 09:19:37 -0500 Subject: [PATCH 05/18] wip --- libraries/chain/fork_database.cpp | 5 + .../include/eosio/chain/fork_database.hpp | 1 + unittests/bhs_core.hpp | 347 +++++++++++++++++- unittests/finalizer_tests.cpp | 132 ++++--- unittests/mock_utils.hpp | 175 +++++++++ 5 files changed, 589 insertions(+), 71 deletions(-) create mode 100644 unittests/mock_utils.hpp diff --git a/libraries/chain/fork_database.cpp b/libraries/chain/fork_database.cpp index 8b1205ad13..4075033c4f 100644 --- a/libraries/chain/fork_database.cpp +++ b/libraries/chain/fork_database.cpp @@ -163,6 +163,11 @@ namespace eosio::chain { } } + template + fork_database_t::~fork_database_t() { + // close is performed in fork_database::~fork_database() + } + template void fork_database_t::close(const std::filesystem::path& fork_db_file) { std::lock_guard g( my->mtx ); diff --git a/libraries/chain/include/eosio/chain/fork_database.hpp b/libraries/chain/include/eosio/chain/fork_database.hpp index 6ecb426910..708f8d5b8e 100644 --- a/libraries/chain/include/eosio/chain/fork_database.hpp +++ b/libraries/chain/include/eosio/chain/fork_database.hpp @@ -40,6 +40,7 @@ namespace eosio::chain { using branch_type_pair = pair; explicit fork_database_t(uint32_t magic_number = legacy_magic_number); + ~fork_database_t(); void open( const std::filesystem::path& fork_db_file, validator_t& validator ); void close( const std::filesystem::path& fork_db_file ); diff --git a/unittests/bhs_core.hpp b/unittests/bhs_core.hpp index 3fa99d08fd..7121649481 100644 --- a/unittests/bhs_core.hpp +++ b/unittests/bhs_core.hpp @@ -1,4 +1,6 @@ -#include +#pragma once +#include +#include namespace bhs_core { @@ -12,7 +14,7 @@ struct block_ref block_id_type block_id; block_time_type timestamp; - block_num_type block_num() const; // Extract from block_id. + block_num_type block_num() const { return eosio::chain::block_header::num_from_id(block_id); } }; struct qc_link @@ -43,7 +45,310 @@ struct core std::vector links; // Captures all relevant links sorted in order of ascending source_block_num. std::vector refs; // Covers ancestor blocks with block numbers greater than or equal to last_final_block_num. // Sorted in order of ascending block_num. - block_num_type final_on_strong_qc_block_num; + block_num_type final_on_strong_qc_block_num_; + + // greg + block_time_type last_qc_block_timestamp() const { + const block_ref& ref = get_block_reference(links.back().target_block_num); + return ref.timestamp; + } + + // greg + block_num_type last_qc_block_num() const { + return links.back().target_block_num; + } + + // greg + block_num_type final_on_strong_qc_block_num() const { + return final_on_strong_qc_block_num_; + } + + // Invariants: + // 1. links.empty() == false + // 2. last_final_block_num() <= links.front().source_block_num <= final_on_strong_qc_block_num <= latest_qc_claim().block_num + // 3. If refs.empty() == true, then (links.size() == 1) and + // (links.back().target_block_num == links.back().source_block_num == final_on_strong_qc_block_num == last_final_block_num()) + // 4. If refs.empty() == false, then refs.front().block_num() == links.front().target_block_num == last_final_block_num() + // 5. If refs.empty() == false, then refs.back().block_num() + 1 == links.back().source_block_num == current_block_num() + // 6. If refs.size() > 1, then: + // For i = 0 to refs.size() - 2: + // (refs[i].block_num() + 1 == refs[i+1].block_num()) and (refs[i].timestamp < refs[i+1].timestamp) + // 7. If links.size() > 1, then: + // For i = 0 to links.size() - 2: + // (links[i].source_block_num + 1 == links[i+1].source_block_num) and (links[i].target_block_num <= links[i+1].target_block_num) + // 8. current_block_num() - last_final_block_num() == refs.size() (always implied by invariants 3 to 6) + // 9. current_block_num() - links.front().source_block_num == links.size() - 1 (always implied by invariants 1 and 7) + + static core create_core_for_genesis_block(block_num_type block_num) + { + return core { + .links = { + qc_link{ + .source_block_num = block_num, + .target_block_num = block_num, + .is_link_strong = false, + }, + }, + .refs = {}, + .final_on_strong_qc_block_num_ = block_num, + }; + + // Invariants 1 to 7 can be easily verified to be satisfied for the returned core. + // (And so, remaining invariants are also automatically satisfied.) + } + + block_num_type current_block_num() const + { + assert(!links.empty()); // Satisfied by invariant 1. + + return links.back().source_block_num; + } + + block_num_type last_final_block_num() const + { + assert(!links.empty()); // Satisfied by invariant 1. + + return links.front().target_block_num; + } + + + qc_claim latest_qc_claim() const + { + assert(!links.empty()); // Satisfied by invariant 1. + + return qc_claim{.block_num = links.back().target_block_num, .is_strong_qc = links.back().is_link_strong}; + } + /** + * @pre last_final_block_num() <= block_num < current_block_num() + * + * @post returned block_ref has block_num() == block_num + */ + const block_ref& get_block_reference(block_num_type block_num) const + { + assert(last_final_block_num() <= block_num); // Satisfied by precondition. + assert(block_num < current_block_num()); // Satisfied by precondition. + + // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num(), + // and therefore it is impossible to satisfy the precondition. So going forward, it is safe to assume refs.empty() == false. + + const size_t ref_index = block_num - last_final_block_num(); + + // By the precondition, 0 <= ref_index < (current_block_num() - last_final_block_num()). + // Then, by invariant 8, 0 <= ref_index < refs.size(). + + assert(ref_index < refs.size()); // Satisfied by justification above. + + return refs[ref_index]; + // By invariants 4 and 6, tail[ref_index].block_num() == block_num, which satisfies the post-condition. + } + + /** + * @pre links.front().source_block_num <= block_num <= current_block_num() + * + * @post returned qc_link has source_block_num == block_num + */ + const qc_link& get_qc_link_from(block_num_type block_num) const + { + assert(!links.empty()); // Satisfied by invariant 1. + + assert(links.front().source_block_num <= block_num); // Satisfied by precondition. + assert(block_num <= current_block_num()); // Satisfied by precondition. + + const size_t link_index = block_num - links.front().source_block_num; + + // By the precondition, 0 <= link_index <= (current_block_num() - links.front().source_block_num). + // Then, by invariant 9, 0 <= link_index <= links.size() - 1 + + assert(link_index < links.size()); // Satisfied by justification above. + + return links[link_index]; + // By invariants 7, links[link_index].source_block_num == block_num, which satisfies the post-condition. + } + + /** + * @pre current_block.block_num() == this->current_block_num() + * @pre If this->refs.empty() == false, then current_block is the block after the one referenced by this->refs.back() + * @pre this->latest_qc_claim().block_num <= most_recent_ancestor_with_qc.block_num <= this->current_block_num() + * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc + * + * @post returned core has current_block_num() == this->current_block_num() + 1 + * @post returned core has latest_qc_claim() == most_recent_ancestor_with_qc + * @post returned core has final_on_strong_qc_block_num >= this->final_on_strong_qc_block_num + * @post returned core has last_final_block_num() >= this->last_final_block_num() + */ + core next(const block_ref& current_block, const qc_claim& most_recent_ancestor_with_qc) const + { + assert(current_block.block_num() == current_block_num()); // Satisfied by precondition 1. + + assert(refs.empty() || (refs.back().block_num() + 1 == current_block.block_num())); // Satisfied by precondition 2. + assert(refs.empty() || (refs.back().timestamp < current_block.timestamp)); // Satisfied by precondition 2. + + assert(most_recent_ancestor_with_qc.block_num <= current_block_num()); // Satisfied by precondition 3. + + assert(latest_qc_claim() <= most_recent_ancestor_with_qc); // Satisfied by precondition 4. + + core next_core; + + auto new_block_nums = [&]() -> std::tuple + { + // Invariant 2 guarantees that: + // last_final_block_num() <= links.front().source_block_num <= final_on_strong_qc_block_num <= latest_qc_claim().block_num + + assert(links.front().source_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by invariant 2 and precondition 4. + + // No changes on new claim of weak QC. + if (!most_recent_ancestor_with_qc.is_strong_qc) { + return {last_final_block_num(), links.front().source_block_num, final_on_strong_qc_block_num_}; + } + + const auto& link1 = get_qc_link_from(most_recent_ancestor_with_qc.block_num); + + // By the post-condition of get_qc_link_from, link1.source_block_num == most_recent_ancestor_with_qc.block_num. + // By the invariant on qc_link, link1.target_block_num <= link1.source_block_num. + // Therefore, link1.target_block_num <= most_recent_ancestor_with_qc.block_num. + // And also by precondition 3, link1.target_block_num <= current_block_num(). + + // If refs.empty() == true, then by invariant 3, link1 == links.front() == links.back() and so + // link1.target_block_num == current_block_num(). + + // Otherwise, if refs.empty() == false, consider two cases. + // Case 1: link1 != links.back() + // In this case, link1.target_block_num <= link1.source_block_num < links.back().source_block_num. + // The strict inequality is justified by invariant 7. + // Therefore, link1.target_block_num < current_block_num(). + // Case 2: link1 == links.back() + // In this case, link1.target_block_num < link1.source_block_num == links.back().source_block_num. + // The strict inequality is justified because the only the target_block_num and source_block_num of a qc_link + // can be equal is for genesis block. And link mapping genesis block number to genesis block number can only + // possibly exist for links.front(). + // Therefore, link1.target_block_num < current_block_num(). + + // So, link1.target_block_num == current_block_num() iff refs.empty() == true. + + assert(final_on_strong_qc_block_num_ <= link1.target_block_num); // TODO: Show that this is always true. + + // Finality does not advance if a better 3-chain is not found. + if (!link1.is_link_strong || (link1.target_block_num < links.front().source_block_num)) { + return {last_final_block_num(), links.front().source_block_num, link1.target_block_num}; + } + + const auto& link2 = get_qc_link_from(link1.target_block_num); + + // By the post-condition of get_qc_link_from, link2.source_block_num == link1.target_block_num. + // By the invariant on qc_link, link2.target_block_num <= link2.source_block_num. + // Therefore, link2.target_block_num <= link1.target_block_num. + + // Wherever link2 is found within links, it must be the case that links.front().target_block_num <= link2.target_block_num. + // This is justified by invariant 7. + // Therefore, last_final_block_num() <= link2.target_block_num. + + return {link2.target_block_num, link2.source_block_num, link1.target_block_num}; + }; + + const auto [new_last_final_block_num, new_links_front_source_block_num, new_final_on_strong_qc_block_num] = new_block_nums(); + + assert(new_last_final_block_num <= new_links_front_source_block_num); // Satisfied by justification in new_block_nums. + assert(new_links_front_source_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justification in new_block_nums. + assert(new_final_on_strong_qc_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by justification in new_block_nums. + + assert(last_final_block_num() <= new_last_final_block_num); // Satisfied by justifications in new_block_nums. + assert(links.front().source_block_num <= new_links_front_source_block_num); // Satisfied by justification in new_block_nums. + assert(final_on_strong_qc_block_num_ <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. + + next_core.final_on_strong_qc_block_num_ = new_final_on_strong_qc_block_num; + // Post-condition 3 is satisfied, assuming next_core will be returned without further modifications to next_core.final_on_strong_qc_block_num. + + // Post-condition 4 and invariant 2 will be satisfied when next_core.last_final_block_num() is updated to become new_last_final_block_num. + + // Setup next_core.links by garbage collecting unnecessary links and then adding the new QC link. + { + const size_t links_index = new_links_front_source_block_num - links.front().source_block_num; + + assert(links_index < links.size()); // Satisfied by justification in this->get_qc_link_from(new_links_front_source_block_num). + + next_core.links.reserve(links.size() - links_index + 1); + + // Garbage collect unnecessary links + std::copy(links.cbegin() + links_index, links.cend(), std::back_inserter(next_core.links)); + + assert(next_core.last_final_block_num() == new_last_final_block_num); // Satisfied by choice of links_index. + + // Also, by choice of links_index, at this point, next_core.links.back() == this->links.back(). + assert(next_core.links.back().source_block_num == current_block_num()); // Satisfied because last item in links has not yet changed. + assert(next_core.links.back().target_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied because of above and precondition 3. + + // Add new link + next_core.links.emplace_back( + qc_link{ + .source_block_num = current_block_num() + 1, + .target_block_num = most_recent_ancestor_with_qc.block_num, // Guaranteed to be less than current_block_num() + 1. + .is_link_strong = most_recent_ancestor_with_qc.is_strong_qc, + }); + + // Post-conditions 1, 2, and 4 are satisfied, assuming next_core will be returned without further modifications to next_core.links. + + // Invariants 1, 2, and 7 are satisfied for next_core. + } + + // Setup next_core.refs by garbage collecting unnecessary block references in the refs and then adding the new block reference. + { + const size_t refs_index = new_last_final_block_num - last_final_block_num(); + + // Using the justifications in new_block_nums, 0 <= ref_index <= (current_block_num() - last_final_block_num). + // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num, and therefore ref_index == 0. + // Otherwise if refs.empty() == false, the justification in new_block_nums provides the stronger inequality + // 0 <= ref_index < (current_block_num() - last_final_block_num), which, using invariant 8, can be simplified to + // 0 <= ref_index < refs.size(). + + assert(!refs.empty() || (refs_index == 0)); // Satisfied by justification above. + assert(refs.empty() || (refs_index < refs.size())); // Satisfied by justification above. + + next_core.refs.reserve(refs.size() - refs_index + 1); + + // Garbage collect unnecessary block references + std::copy(refs.cbegin() + refs_index, refs.cend(), std::back_inserter(next_core.refs)); + + assert(refs.empty() || (next_core.refs.front().block_num() == new_last_final_block_num)); // Satisfied by choice of refs_index. + + // Add new block reference + next_core.refs.emplace_back(current_block); + + // Invariant 3 is trivially satisfied for next_core because next_core.refs.empty() == false. + + // Invariant 5 is clearly satisfied for next_core because next_core.refs.back().block_num() == this->current_block_num() + // and next_core.links.back().source_block_num == this->current_block_num() + 1. + + // Invariant 6 is also clearly satisfied for next_core because invariant 6 is satisfied for *this and the only + // additional requirements needed are the ones provided by precondition 2. + + // If this->refs.empty() == true, then new_last_final_block_num == this->last_final_block_num() == this->current_block_num(), + // and next_core.refs.size() == 1 and next_core.refs.front() == current_block. + // And so, next_core.refs.front().block_num() == new_last_final_block_num. + // If this->refs.empty() == false, then adding the current_block to the end does not change the fact that + // next_core.refs.front().block_num() is still equal to new_last_final_block_num. + + assert(next_core.refs.front().block_num() == new_last_final_block_num); // Satisfied by justification above. + + // Because it was also already shown earlier that links.front().target_block_num == new_last_final_block_num, + // then the justification above satisfies the remaining equalities needed to satisfy invariant 4 for next_core. + + // So, invariants 3 to 6 are now satisfied for next_core in addition to the invariants 1, 2, and 7 that were shown to be satisfied + // earlier (and still remain satisfied since next_core.links and next_core.final_on_strong_qc_block_num have not changed). + } + + return next_core; + // Invariants 1 to 7 were verified to be satisfied for the current value of next_core at various points above. + // (And so, the remaining invariants for next_core are also automatically satisfied.) + } +}; + +#if 0 +struct core +{ + std::vector links; // Captures all relevant links sorted in order of ascending source_block_num. + std::vector refs; // Covers ancestor blocks with block numbers greater than or equal to last_final_block_num. + // Sorted in order of ascending block_num. + block_num_type final_on_strong_qc_block_num_; // Invariants: // 1. links.empty() == false @@ -63,14 +368,14 @@ struct core void check_invariants() { assert(!links.empty()); // 1. - assert(last_final_block_num() <= final_on_strong_qc_block_num && // 2. - final_on_strong_qc_block_num <= latest_qc_claim().block_num); + assert(last_final_block_num() <= final_on_strong_qc_block_num_ && // 2. + final_on_strong_qc_block_num_ <= latest_qc_claim().block_num); if (refs.empty()) { // 3. assert(links.size() == 1); } else { assert(all_equal(links.back().target_block_num, // 3. links.back().source_block_num, - final_on_strong_qc_block_num, + final_on_strong_qc_block_num_, last_final_block_num())); assert(all_equal(refs.front().block_num(), // 4. links.front().target_block_num, @@ -106,7 +411,7 @@ struct core }, }, .refs = {}, - .final_on_strong_qc_block_num = block_num, + .final_on_strong_qc_block_num_ = block_num, }; // Invariants 1 to 7 can be easily verified to be satisfied for the returned core. @@ -134,6 +439,23 @@ struct core return qc_claim{.block_num = links.back().target_block_num, .is_strong_qc = links.back().is_link_strong}; } + + // greg + block_time_type last_qc_block_timestamp() const { + const block_ref& ref = get_block_reference(links.back().target_block_num); + return ref.timestamp; + } + + // greg + block_num_type last_qc_block_num() const { + return links.back().target_block_num; + } + + // greg + block_num_type final_on_strong_qc_block_num() const { + return final_on_strong_qc_block_num_; + } + /** * @pre last_final_block_num() <= block_num < current_block_num() * @@ -207,14 +529,14 @@ struct core auto new_block_nums = [&]() -> std::pair { - assert(last_final_block_num() <= final_on_strong_qc_block_num); // Satisfied by invariant 2. + assert(last_final_block_num() <= final_on_strong_qc_block_num_); // Satisfied by invariant 2. if (!most_recent_ancestor_with_qc.is_strong_qc) { - return {last_final_block_num(), final_on_strong_qc_block_num}; + return {last_final_block_num(), final_on_strong_qc_block_num_}; } if (most_recent_ancestor_with_qc.block_num < links.front().source_block_num) { - return {last_final_block_num(), final_on_strong_qc_block_num}; + return {last_final_block_num(), final_on_strong_qc_block_num_}; } const auto& link1 = get_qc_link_from(most_recent_ancestor_with_qc.block_num); @@ -250,10 +572,10 @@ struct core assert(new_last_final_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justification in new_block_nums. assert(new_final_on_strong_qc_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by justification in new_block_nums. - assert(final_on_strong_qc_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. + assert(final_on_strong_qc_block_num_ <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. assert(last_final_block_num() <= new_last_final_block_num); // Satisfied by justifications in new_block_nums. - next_core.final_on_strong_qc_block_num = new_final_on_strong_qc_block_num; + next_core.final_on_strong_qc_block_num_ = new_final_on_strong_qc_block_num; // Post-condition 3 is satisfied, assuming next_core will be returned without further modifications to next_core.final_on_strong_qc_block_num. // Post-condition 4 and invariant 2 will be satisfied when next_core.last_final_block_num() is updated to become new_last_final_block_num. @@ -348,5 +670,6 @@ struct core // (And so, the remaining invariants for next_core are also automatically satisfied.) } }; +#endif } \ No newline at end of file diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 6cdee3d3aa..26d968282e 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include "mock_utils.hpp" using namespace eosio; using namespace eosio::chain; @@ -169,95 +170,108 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() -#include "bhs_core.hpp" +// real finalizer, using mock::forkdb and mock::bsp +// using test_finalizer_t = finalizer_tpl; + +block_state_ptr make_bsp(const mock_utils::proposal_t& p, const block_state_ptr& head, + std::optional claim = {}) { + block_header_state bhs; + auto id = p.calculate_id(); + // genesis block + block_header_state_core new_core; + if (p.block_num() > 0) + new_core = claim ? head->core.next(*claim) : head->core; + bhs = block_header_state{ .block_id = id, + .header = block_header(), + .activated_protocol_features = {}, + .core = new_core }; + block_state_ptr bsp = std::make_shared(block_state{bhs, {}, {}, {}}); + return bsp; +} // --------------------------------------------------------------------------------------- -// emulations of block_header_state and fork_database sufficient for instantiating a -// finalizer. -// --------------------------------------------------------------------------------------- -struct mock_bhs { - uint32_t block_number; - block_id_type block_id; - block_timestamp_type block_timestamp; - - uint32_t block_num() const { return block_number; } - const block_id_type& id() const { return block_id; } - block_timestamp_type timestamp() const { return block_timestamp; } -}; +template +struct simulator_t { + using finalizer_t = finalizer_tpl; + using bs = typename FORKDB::bs; + using bsp = typename FORKDB::bsp; -using mock_bhsp = std::shared_ptr; + bls_keys_t keys; + FORKDB forkdb; + finalizer_t finalizer; -// --------------------------------------------------------------------------------------- -struct mock_bs : public mock_bhs {}; + simulator_t() : + keys("alice"_n), + finalizer(keys.privkey) { -using mock_bsp = std::shared_ptr; + auto genesis = make_bsp(mock_utils::proposal_t{0, "n0", block_timestamp_type{0}}, bsp()); + forkdb.add(genesis); -// --------------------------------------------------------------------------------------- -struct mock_proposal { - uint32_t block_number; - std::string proposer_name; - block_timestamp_type block_timestamp; - - uint32_t block_num() const { return block_number; } - const std::string& proposer() const { return proposer_name; } - block_timestamp_type timestamp() const { return block_timestamp; } - - mock_bhs to_bhs() const { - std::string id_str = proposer_name + std::to_string(block_number); - return mock_bhs{block_num(), sha256::hash(id_str.c_str()), timestamp() }; + proposal_ref genesis_ref(genesis); + finalizer.fsi = fsi_t{block_timestamp_type(0), genesis_ref, {}}; } -}; -// --------------------------------------------------------------------------------------- -struct mock_forkdb { - using bsp = mock_bsp; - using bhsp = mock_bhsp; - using full_branch_type = std::vector; + std::optional vote(const bsp& p) { + auto decision = finalizer.decide_vote(p, forkdb); + switch(decision) { + case finalizer_t::vote_decision::strong_vote: return true; + case finalizer_t::vote_decision::weak_vote: return false; + default: break; + } + return {}; + } - bhsp root() const { return branch.back(); } + std::optional propose(const PROPOSAL& p) { + bsp h = make_bsp(p, forkdb.head()); + forkdb.add(h); + auto v = vote(h); + return v; + } - full_branch_type fetch_full_branch(const block_id_type& id) const { - auto it = std::find_if(branch.cbegin(), branch.cend(), [&](const bhsp& p) { return p->id() == id; }); - assert(it != branch.cend()); - return full_branch_type(it, branch.cend()); - }; + std::pair add(const PROPOSAL& p, std::optional _claim = {}) { + bsp h = forkdb.head(); + bhs_core::qc_claim old_claim = _claim ? *_claim : bhs_core::qc_claim{h->last_qc_block_num(), false}; + bsp new_bsp = make_bsp(p, h, _claim); + forkdb.add(new_bsp); - full_branch_type branch; + auto v = vote(new_bsp); + if (v) + return {forkdb.head(), new_bsp->latest_qc_claim()}; + return {forkdb.head(), old_claim}; + } }; -// real finalizer, using mock_forkdb and mock_bsp -using test_finalizer = finalizer_tpl; - +#if 0 // --------------------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { auto proposals { create_proposal_refs(10) }; fsi_t fsi { .last_vote_range_start = tstamp(0), .last_vote = proposals[6], .lock = proposals[2] }; + using namespace mock_utils; + simulator_t sim; - bls_keys_t k("alice"_n); - test_finalizer finalizer{k.privkey, fsi}; + auto vote = sim.propose(proposal_t{1, "n0", block_timestamp_type{1}}); + BOOST_CHECK(vote && *vote); + //bls_keys_t k("alice"_n); + //test_finalizer_t finalizer{k.privkey, fsi}; } FC_LOG_AND_RETHROW() - +#endif // --------------------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE( proposal_sim_1 ) try { - fsi_t fsi; // default uninitialized values, no previous lock or vote - - bls_keys_t k("alice"_n); - test_finalizer finalizer{k.privkey, fsi}; + using namespace mock_utils; + simulator_t sim; + auto [head1, claim1] = sim.add(proposal_t{1, "n0", block_timestamp_type{1}}, bhs_core::qc_claim{0, false}); + BOOST_CHECK_EQUAL(claim1.block_num, 1); + auto [head2, claim2] = sim.add(proposal_t{2, "n0", block_timestamp_type{2}}, claim1); + BOOST_CHECK_EQUAL(claim2.block_num, 2); } FC_LOG_AND_RETHROW() - - - - - - BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/mock_utils.hpp b/unittests/mock_utils.hpp new file mode 100644 index 0000000000..828c178d8d --- /dev/null +++ b/unittests/mock_utils.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include "bhs_core.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mock_utils { + +using namespace eosio; +using namespace eosio::chain; + +inline block_id_type calc_id(block_id_type id, uint32_t block_number) { + id._hash[0] &= 0xffffffff00000000; + id._hash[0] += fc::endian_reverse_u32(block_number); // store the block num in the ID, 160 bits is plenty for the hash + return id; +} + + +// --------------------------------------------------------------------------------------- +// emulations of block_header_state and fork_database sufficient for instantiating a +// finalizer. +// --------------------------------------------------------------------------------------- +struct bhs : bhs_core::core { + block_id_type block_id; + block_id_type previous_block; + block_timestamp_type block_timestamp; + + uint32_t block_num() const { return block_header::num_from_id(block_id); } + const block_id_type& id() const { return block_id; } + const block_id_type& previous() const { return previous_block; } + block_timestamp_type timestamp() const { return block_timestamp; } + bool is_valid() const { return true; } + uint32_t irreversible_blocknum() const { return last_final_block_num(); } + + static bhs genesis_bhs() { + return bhs{ bhs_core::core{{bhs_core::qc_link{0, 0, false}}, {}, 0}, + calc_id(fc::sha256::hash("genesis"), 0), + block_id_type{}, + block_timestamp_type{0} + }; + } +}; + +using bhsp = std::shared_ptr; + +// --------------------------------------------------------------------------------------- +struct bs : public bhs { + bs() : bhs(genesis_bhs()) {} + bs(const bhs& h) : bhs(h) {} + + uint32_t block_num() const { return bhs::block_num(); } + const block_id_type& id() const { return bhs::id(); } + const block_id_type& previous() const { return bhs::previous(); } + bool is_valid() const { return true; } + uint32_t irreversible_blocknum() const { return bhs::irreversible_blocknum(); } + + explicit operator bhs_core::block_ref() const { + return bhs_core::block_ref{id(), timestamp()}; + } +}; + +using bsp = std::shared_ptr; + +// --------------------------------------------------------------------------------------- +struct proposal_t { + uint32_t block_number; + std::string proposer_name; + block_timestamp_type block_timestamp; + + const std::string& proposer() const { return proposer_name; } + block_timestamp_type timestamp() const { return block_timestamp; } + uint32_t block_num() const { return block_number; } + + block_id_type calculate_id() const + { + std::string id_str = proposer_name + std::to_string(block_number); + return calc_id(fc::sha256::hash(id_str.c_str()), block_number); + } + + explicit operator bhs_core::block_ref() const { + return bhs_core::block_ref{calculate_id(), timestamp()}; + } +}; + +// --------------------------------------------------------------------------------------- +bsp make_bsp(const mock_utils::proposal_t& p, const bsp& previous, std::optional claim = {}) { + if (p.block_num() == 0) { + // genesis block + return std::make_shared(); + } + assert(claim); + bhs_core::block_ref ref(*previous); + return std::make_shared(bhs{previous->next(ref, *claim), ref.block_id, previous->id(), p.timestamp() }); +} + +// --------------------------------------------------------------------------------------- +struct forkdb_t { + using bsp = bsp; + using bs = bsp::element_type; + using bhsp = bhsp; + using bhs = bhsp::element_type; + using full_branch_type = std::vector; + + struct by_block_id; + struct by_lib_block_num; + struct by_prev; + + using fork_multi_index_type = boost::multi_index::multi_index_container< + bsp, + indexed_by, + BOOST_MULTI_INDEX_CONST_MEM_FUN(bs, const block_id_type&, id), + std::hash>, + ordered_non_unique, const_mem_fun>, + ordered_unique, + composite_key, + composite_key_compare, + std::greater, + std::greater, + sha256_less>>>>; + + fork_multi_index_type index; + bsp head_; + bsp root_; + + bsp root() const { return root_; } + bsp head() const { return head_; } + + void add(const bsp& n) { + auto inserted = index.insert(n); + if( !inserted.second ) + return; + if (index.size() == 1) + root_= n; + auto candidate = index.template get().begin(); + if( (*candidate)->is_valid() ) { + head_ = *candidate; + } + } + + bsp get_block_impl(const block_id_type& id) const { + auto itr = index.find( id ); + if( itr != index.end() ) + return *itr; + return bsp(); + } + + full_branch_type fetch_full_branch(const block_id_type& id) const { + full_branch_type result; + result.reserve(10); + for (auto s = get_block_impl(id); s; s = get_block_impl(s->previous())) { + result.push_back(s); + } + return result; + }; + +}; + + + + + + +} // namespace mock_utils From d3bec50436dec4bd6c18c65682276107bfee9bb8 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 23 Feb 2024 10:06:24 -0500 Subject: [PATCH 06/18] Remove templatized finalizer which won't be needed anymore. --- libraries/chain/hotstuff/finalizer.cpp | 111 +++++++++++++++- .../eosio/chain/block_header_state.hpp | 7 +- .../chain/include/eosio/chain/block_state.hpp | 2 +- .../eosio/chain/hotstuff/finalizer.hpp | 33 +++-- .../eosio/chain/hotstuff/finalizer.ipp | 118 ------------------ unittests/finalizer_tests.cpp | 1 - 6 files changed, 127 insertions(+), 145 deletions(-) delete mode 100644 libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index 71ac241a1c..9611b0f3c6 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -2,13 +2,116 @@ #include #include -#include // implementation of finalizer methods - namespace eosio::chain { // ---------------------------------------------------------------------------------------- -// Explicit template instantiation -template struct finalizer_tpl; +block_header_state_ptr get_block_by_num(const fork_database_if_t::full_branch_type& branch, std::optional block_num) { + if (!block_num || branch.empty()) + return {}; + + // a branch always contains consecutive block numbers, starting with the highest + uint32_t first = branch[0]->block_num(); + uint32_t dist = first - *block_num; + return dist < branch.size() ? branch[dist] : block_state_ptr{}; +} + +// ---------------------------------------------------------------------------------------- +bool extends(const fork_database_if_t::full_branch_type& branch, const block_id_type& id) { + return !branch.empty() && + std::any_of(++branch.cbegin(), branch.cend(), [&](const auto& h) { return h->id() == id; }); +} + +// ---------------------------------------------------------------------------------------- +finalizer::vote_decision finalizer::decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db) { + bool safety_check = false; + bool liveness_check = false; + + bool monotony_check = !fsi.last_vote || proposal->timestamp() > fsi.last_vote.timestamp; + // !fsi.last_vote means we have never voted on a proposal, so the protocol feature just activated and we can proceed + + if (!monotony_check) { + dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal->id())); + return vote_decision::no_vote; + } + + std::optional p_branch; // a branch that includes the root. + + if (!fsi.lock.empty()) { + // Liveness check : check if the height of this proposal's justification is higher + // than the height of the proposal I'm locked on. + // This allows restoration of liveness if a replica is locked on a stale proposal + // ------------------------------------------------------------------------------- + liveness_check = proposal->last_qc_block_timestamp() > fsi.lock.timestamp; + + if (!liveness_check) { + // Safety check : check if this proposal extends the proposal we're locked on + p_branch = fork_db.fetch_full_branch(proposal->id()); + safety_check = extends(*p_branch, fsi.lock.id); + } + } else { + // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. + // `fsi.lock` is initially set to `lib` when switching to IF or starting from a snapshot. + // ------------------------------------------------------------------------------------- + liveness_check = false; + safety_check = false; + } + + dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote = {can_vote}", + ("l",liveness_check)("s",safety_check)("m",monotony_check)("can_vote",(liveness_check || safety_check))); + + // Figure out if we can vote and wether our vote will be strong or weak + // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc + // ----------------------------------------------------------------------------------- + vote_decision decision = vote_decision::no_vote; + + if (liveness_check || safety_check) { + auto [p_start, p_end] = std::make_pair(proposal->last_qc_block_timestamp(), proposal->timestamp()); + + bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; + bool voting_strong = time_range_disjoint; + if (!voting_strong) { + if (!p_branch) + p_branch = fork_db.fetch_full_branch(proposal->id()); + voting_strong = extends(*p_branch, fsi.last_vote.id); + } + + fsi.last_vote = proposal_ref(proposal); + fsi.last_vote_range_start = p_start; + + if (!p_branch) + p_branch = fork_db.fetch_full_branch(proposal->id()); + auto bsp_final_on_strong_qc = get_block_by_num(*p_branch, proposal->final_on_strong_qc_block_num()); + if (voting_strong && bsp_final_on_strong_qc && bsp_final_on_strong_qc->timestamp() > fsi.lock.timestamp) + fsi.lock = proposal_ref(bsp_final_on_strong_qc); + + decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; + } else { + dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", + ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); + dlog("last_qc_block_num=${lqc}", ("lqc", proposal->last_qc_block_num())); + } + if (decision != vote_decision::no_vote) + dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); + return decision; +} + +// ---------------------------------------------------------------------------------------- +std::optional finalizer::maybe_vote(const bls_public_key& pub_key, const block_state_ptr& p, + const digest_type& digest, const fork_database_if_t& fork_db) { + finalizer::vote_decision decision = decide_vote(p, fork_db); + if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { + bls_signature sig; + if (decision == vote_decision::weak_vote) { + // if voting weak, the digest to sign should be a hash of the concatenation of the finalizer_digest + // and the string "WEAK" + sig = priv_key.sign(create_weak_digest(digest)); + } else { + sig = priv_key.sign({(uint8_t*)digest.data(), (uint8_t*)digest.data() + digest.data_size()}); + } + return vote_message{ p->id(), decision == vote_decision::strong_vote, pub_key, sig }; + } + return {}; +} // ---------------------------------------------------------------------------------------- void my_finalizers_t::save_finalizer_safety_info() const { diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index f89257e147..6686b96018 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -61,10 +61,9 @@ struct block_header_state { account_name producer() const { return header.producer; } const block_id_type& previous() const { return header.previous; } uint32_t block_num() const { return block_header::num_from_id(previous()) + 1; } - block_timestamp_type last_qc_block_timestamp() const { - auto last_qc_block_num = core.latest_qc_claim().block_num; - return core.get_block_reference(last_qc_block_num).timestamp; } - const producer_authority_schedule& active_schedule_auth() const { return active_proposer_policy->proposer_schedule; } + uint32_t last_qc_block_num() const { return core.latest_qc_claim().block_num; } + block_timestamp_type last_qc_block_timestamp() const { return core.get_block_reference(last_qc_block_num()).timestamp; } + const producer_authority_schedule& active_schedule_auth() const { return active_proposer_policy->proposer_schedule; } block_header_state next(block_header_state_input& data) const; block_header_state next(const signed_block_header& h, validator_t& validator) const; diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index 3515bf0622..9a5786be74 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -44,7 +44,7 @@ struct block_state : public block_header_state { // block_header_state provi void set_valid(bool b) { validated = b; } uint32_t irreversible_blocknum() const { return core.last_final_block_num(); } std::optional get_best_qc() const; - uint32_t last_qc_block_num() const { return core.latest_qc_claim().block_num; } + uint32_t last_qc_block_num() const { return block_header_state::last_qc_block_num(); } uint32_t final_on_strong_qc_block_num() const { return core.final_on_strong_qc_block_num; } protocol_feature_activation_set_ptr get_activated_protocol_features() const { return block_header_state::activated_protocol_features; } diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index 465307a9fc..ea53822b8d 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -31,22 +31,23 @@ // ------------------------------------------------------------------------------------------- namespace eosio::chain { + // ---------------------------------------------------------------------------------------- struct proposal_ref { block_id_type id; block_timestamp_type timestamp; proposal_ref() = default; - proposal_ref(const block_id_type& id, block_timestamp_type t) : - id(id), timestamp(t) - {} - template explicit proposal_ref(const BSP& p) : id(p->id()), timestamp(p->timestamp()) {} + proposal_ref(const block_id_type& id, block_timestamp_type t) : + id(id), timestamp(t) + {} + void reset() { id = block_id_type(); timestamp = block_timestamp_type(); @@ -56,11 +57,12 @@ namespace eosio::chain { explicit operator bool() const { return !id.empty(); } - bool operator==(const proposal_ref& o) const { + auto operator==(const proposal_ref& o) const { return id == o.id && timestamp == o.timestamp; } }; + // ---------------------------------------------------------------------------------------- struct finalizer_safety_information { block_timestamp_type last_vote_range_start; proposal_ref last_vote; @@ -70,7 +72,7 @@ namespace eosio::chain { static finalizer_safety_information unset_fsi() { return {block_timestamp_type(), {}, {}}; } - bool operator==(const finalizer_safety_information& o) const { + auto operator==(const finalizer_safety_information& o) const { return last_vote_range_start == o.last_vote_range_start && last_vote == o.last_vote && lock == o.lock; @@ -78,25 +80,22 @@ namespace eosio::chain { }; // ---------------------------------------------------------------------------------------- - template - struct finalizer_tpl { + struct finalizer { enum class vote_decision { strong_vote, weak_vote, no_vote }; - bls_private_key priv_key; - finalizer_safety_information fsi; + bls_private_key priv_key; + finalizer_safety_information fsi; private: - using full_branch_type = FORK_DB::full_branch_type; + using branch_type = fork_database_if_t::branch_type; + using full_branch_type = fork_database_if_t::full_branch_type; + vote_decision decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db); public: - vote_decision decide_vote(const FORK_DB::bsp& proposal, const FORK_DB& fork_db); - - std::optional maybe_vote(const bls_public_key& pub_key, const FORK_DB::bsp& bsp, - const digest_type& digest, const FORK_DB& fork_db); + std::optional maybe_vote(const bls_public_key& pub_key, const block_state_ptr& bsp, + const digest_type& digest, const fork_database_if_t& fork_db); }; - using finalizer = finalizer_tpl; - // ---------------------------------------------------------------------------------------- struct my_finalizers_t { using fsi_t = finalizer_safety_information; diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp deleted file mode 100644 index 2c9517d0ee..0000000000 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.ipp +++ /dev/null @@ -1,118 +0,0 @@ -#pragma once - -namespace eosio::chain { -// ---------------------------------------------------------------------------------------- -template -typename FORK_DB::bhsp -get_block_by_num(const typename FORK_DB::full_branch_type& branch, std::optional block_num) { - if (!block_num || branch.empty()) - return {}; - - // a branch always contains consecutive block numbers, starting with the highest - uint32_t first = branch[0]->block_num(); - uint32_t dist = first - *block_num; - return dist < branch.size() ? branch[dist] : typename FORK_DB::bhsp{}; -} - -// ---------------------------------------------------------------------------------------- -template -bool extends(const typename FORK_DB::full_branch_type& branch, const block_id_type& id) { - return !branch.empty() && - std::any_of(++branch.cbegin(), branch.cend(), [&](const auto& h) { return h->id() == id; }); -} - -// ---------------------------------------------------------------------------------------- -template -finalizer_tpl::vote_decision finalizer_tpl::decide_vote(const FORK_DB::bsp& proposal, const FORK_DB& fork_db) { - bool safety_check = false; - bool liveness_check = false; - - bool monotony_check = !fsi.last_vote || proposal->timestamp() > fsi.last_vote.timestamp; - // !fsi.last_vote means we have never voted on a proposal, so the protocol feature just activated and we can proceed - - if (!monotony_check) { - dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal->id())); - return vote_decision::no_vote; - } - - std::optional p_branch; // a branch that includes the root. - - if (!fsi.lock.empty()) { - // Liveness check : check if the height of this proposal's justification is higher - // than the height of the proposal I'm locked on. - // This allows restoration of liveness if a replica is locked on a stale proposal - // ------------------------------------------------------------------------------- - liveness_check = proposal->last_qc_block_timestamp() > fsi.lock.timestamp; - - if (!liveness_check) { - // Safety check : check if this proposal extends the proposal we're locked on - p_branch = fork_db.fetch_full_branch(proposal->id()); - safety_check = extends(*p_branch, fsi.lock.id); - } - } else { - // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. - // `fsi.lock` is initially set to `lib` when switching to IF or starting from a snapshot. - // ------------------------------------------------------------------------------------- - liveness_check = false; - safety_check = false; - } - - dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote = {can_vote}", - ("l",liveness_check)("s",safety_check)("m",monotony_check)("can_vote",(liveness_check || safety_check))); - - // Figure out if we can vote and wether our vote will be strong or weak - // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc - // ----------------------------------------------------------------------------------- - vote_decision decision = vote_decision::no_vote; - - if (liveness_check || safety_check) { - auto [p_start, p_end] = std::make_pair(proposal->last_qc_block_timestamp(), proposal->timestamp()); - - bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; - bool voting_strong = time_range_disjoint; - if (!voting_strong) { - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - voting_strong = extends(*p_branch, fsi.last_vote.id); - } - - fsi.last_vote = proposal_ref(proposal); - fsi.last_vote_range_start = p_start; - - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - auto bsp_final_on_strong_qc = get_block_by_num(*p_branch, proposal->final_on_strong_qc_block_num()); - if (voting_strong && bsp_final_on_strong_qc && bsp_final_on_strong_qc->timestamp() > fsi.lock.timestamp) - fsi.lock = proposal_ref(bsp_final_on_strong_qc); - - decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; - } else { - dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", - ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); - dlog("last_qc_block_num=${lqc}", ("lqc", proposal->last_qc_block_num())); - } - if (decision != vote_decision::no_vote) - dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); - return decision; -} - -// ---------------------------------------------------------------------------------------- -template -std::optional finalizer_tpl::maybe_vote(const bls_public_key& pub_key, const FORK_DB::bsp& p, - const digest_type& digest, const FORK_DB& fork_db) { - finalizer::vote_decision decision = decide_vote(p, fork_db); - if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { - bls_signature sig; - if (decision == vote_decision::weak_vote) { - // if voting weak, the digest to sign should be a hash of the concatenation of the finalizer_digest - // and the string "WEAK" - sig = priv_key.sign(create_weak_digest(digest)); - } else { - sig = priv_key.sign({(uint8_t*)digest.data(), (uint8_t*)digest.data() + digest.data_size()}); - } - return vote_message{ p->id(), decision == vote_decision::strong_vote, pub_key, sig }; - } - return {}; -} - -} \ No newline at end of file diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index d5e612039e..b6395850bb 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -1,5 +1,4 @@ #include -#include // implementation of finalizer methods #include #include From ba5b16c492989ac998dd015b93063a2a17f33b9f Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 23 Feb 2024 16:50:27 -0500 Subject: [PATCH 07/18] Update finalizer code to use new core and not depend on fork_database anymore. --- libraries/chain/controller.cpp | 10 +-- libraries/chain/finality_core.cpp | 23 +++++-- libraries/chain/hotstuff/finalizer.cpp | 64 ++++++------------- .../eosio/chain/block_header_state.hpp | 2 - .../chain/include/eosio/chain/block_state.hpp | 2 - .../include/eosio/chain/block_timestamp.hpp | 7 +- .../include/eosio/chain/finality_core.hpp | 15 +++++ .../eosio/chain/hotstuff/finalizer.hpp | 56 +++------------- 8 files changed, 73 insertions(+), 106 deletions(-) diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index c72d75d918..98cbde7483 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -1596,7 +1596,7 @@ struct controller_impl { my_finalizers.set_default_safety_information( finalizer_safety_information{ .last_vote_range_start = block_timestamp_type(0), .last_vote = {}, - .lock = proposal_ref(lib) }); + .lock = proposal_ref(lib->id(), lib->timestamp()) }); }; fork_db.apply_if(set_finalizer_defaults); } else { @@ -1606,7 +1606,7 @@ struct controller_impl { my_finalizers.set_default_safety_information( finalizer_safety_information{ .last_vote_range_start = block_timestamp_type(0), .last_vote = {}, - .lock = proposal_ref(lib) }); + .lock = proposal_ref(lib->id(), lib->timestamp()) }); }; fork_db.apply_if(set_finalizer_defaults); } @@ -2805,7 +2805,7 @@ struct controller_impl { log_irreversible(); } - fork_db.apply_if([&](auto& forkdb) { create_and_send_vote_msg(forkdb.chain_head, forkdb); }); + fork_db.apply_if([&](auto& forkdb) { create_and_send_vote_msg(forkdb.chain_head); }); // TODO: temp transition to instant-finality, happens immediately after block with new_finalizer_policy auto transition = [&](auto& forkdb) -> bool { @@ -3129,7 +3129,7 @@ struct controller_impl { return status; } - void create_and_send_vote_msg(const block_state_ptr& bsp, const fork_database_if_t& fork_db) { + void create_and_send_vote_msg(const block_state_ptr& bsp) { auto finalizer_digest = bsp->compute_finalizer_digest(); // Each finalizer configured on the node which is present in the active finalizer policy @@ -3139,7 +3139,7 @@ struct controller_impl { // off the main thread. net_plugin is fine for this to be emitted from any thread. // Just need to update the comment in net_plugin my_finalizers.maybe_vote( - *bsp->active_finalizer_policy, bsp, fork_db, finalizer_digest, [&](const vote_message& vote) { + *bsp->active_finalizer_policy, bsp, finalizer_digest, [&](const vote_message& vote) { // net plugin subscribed to this signal. it will broadcast the vote message // on receiving the signal emit(voted_block, vote); diff --git a/libraries/chain/finality_core.cpp b/libraries/chain/finality_core.cpp index 5a85bdda2a..8d10e3e3eb 100644 --- a/libraries/chain/finality_core.cpp +++ b/libraries/chain/finality_core.cpp @@ -78,6 +78,20 @@ qc_claim_t finality_core::latest_qc_claim() const return qc_claim_t{.block_num = links.back().target_block_num, .is_strong_qc = links.back().is_link_strong}; } +/** + * @pre all finality_core invariants + * @post same + * @returns boolean indicating whether `id` is an ancestor of this block + */ +bool finality_core::extends(const block_id_type& id) const { + uint32_t block_num = block_header::num_from_id(id); + if (block_num >= last_final_block_num() && block_num < current_block_num()) { + const block_ref& ref = get_block_reference(block_num); + return ref.block_id == id; + } + return false; +} + /** * @pre last_final_block_num() <= block_num < current_block_num() * @@ -260,10 +274,8 @@ finality_core finality_core::next(const block_ref& current_block, const qc_claim assert(links_index < links.size()); // Satisfied by justification in this->get_qc_link_from(new_links_front_source_block_num). - next_core.links.reserve(links.size() - links_index + 1); - // Garbage collect unnecessary links - std::copy(links.cbegin() + links_index, links.cend(), std::back_inserter(next_core.links)); + next_core.links = { links.cbegin() + links_index, links.cend() }; assert(next_core.last_final_block_num() == new_last_final_block_num); // Satisfied by choice of links_index. @@ -297,11 +309,8 @@ finality_core finality_core::next(const block_ref& current_block, const qc_claim assert(!refs.empty() || (refs_index == 0)); // Satisfied by justification above. assert(refs.empty() || (refs_index < refs.size())); // Satisfied by justification above. - next_core.refs.reserve(refs.size() - refs_index + 1); - // Garbage collect unnecessary block references - std::copy(refs.cbegin() + refs_index, refs.cend(), std::back_inserter(next_core.refs)); - + next_core.refs = {refs.cbegin() + refs_index, refs.cend()}; assert(refs.empty() || (next_core.refs.front().block_num() == new_last_final_block_num)); // Satisfied by choice of refs_index. // Add new block reference diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index 9611b0f3c6..5328144056 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -5,48 +5,30 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- -block_header_state_ptr get_block_by_num(const fork_database_if_t::full_branch_type& branch, std::optional block_num) { - if (!block_num || branch.empty()) - return {}; - - // a branch always contains consecutive block numbers, starting with the highest - uint32_t first = branch[0]->block_num(); - uint32_t dist = first - *block_num; - return dist < branch.size() ? branch[dist] : block_state_ptr{}; -} - -// ---------------------------------------------------------------------------------------- -bool extends(const fork_database_if_t::full_branch_type& branch, const block_id_type& id) { - return !branch.empty() && - std::any_of(++branch.cbegin(), branch.cend(), [&](const auto& h) { return h->id() == id; }); -} - -// ---------------------------------------------------------------------------------------- -finalizer::vote_decision finalizer::decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db) { +finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const block_id_type &proposal_id, + const block_timestamp_type proposal_timestamp) { bool safety_check = false; bool liveness_check = false; - bool monotony_check = !fsi.last_vote || proposal->timestamp() > fsi.last_vote.timestamp; - // !fsi.last_vote means we have never voted on a proposal, so the protocol feature just activated and we can proceed + bool monotony_check = fsi.last_vote.empty() || proposal_timestamp > fsi.last_vote.timestamp; + // fsi.last_vote.empty() means we have never voted on a proposal, so the protocol feature + // just activated and we can proceed if (!monotony_check) { - dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal->id())); + dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal_id)); return vote_decision::no_vote; } - std::optional p_branch; // a branch that includes the root. - if (!fsi.lock.empty()) { // Liveness check : check if the height of this proposal's justification is higher // than the height of the proposal I'm locked on. // This allows restoration of liveness if a replica is locked on a stale proposal // ------------------------------------------------------------------------------- - liveness_check = proposal->last_qc_block_timestamp() > fsi.lock.timestamp; + liveness_check = core.last_qc_block_timestamp() > fsi.lock.timestamp; if (!liveness_check) { // Safety check : check if this proposal extends the proposal we're locked on - p_branch = fork_db.fetch_full_branch(proposal->id()); - safety_check = extends(*p_branch, fsi.lock.id); + safety_check = core.extends(fsi.lock.block_id); } } else { // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. @@ -65,30 +47,25 @@ finalizer::vote_decision finalizer::decide_vote(const block_state_ptr& proposal, vote_decision decision = vote_decision::no_vote; if (liveness_check || safety_check) { - auto [p_start, p_end] = std::make_pair(proposal->last_qc_block_timestamp(), proposal->timestamp()); + auto [p_start, p_end] = std::make_pair(core.last_qc_block_timestamp(), proposal_timestamp); bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; bool voting_strong = time_range_disjoint; - if (!voting_strong) { - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - voting_strong = extends(*p_branch, fsi.last_vote.id); + if (!voting_strong && !fsi.last_vote.empty()) { + // we can vote strong if the proposal is a descendant of (i.e. extends) our last vote id + voting_strong = core.extends(fsi.last_vote.block_id); } - fsi.last_vote = proposal_ref(proposal); + fsi.last_vote = proposal_ref(proposal_id, proposal_timestamp); fsi.last_vote_range_start = p_start; - if (!p_branch) - p_branch = fork_db.fetch_full_branch(proposal->id()); - auto bsp_final_on_strong_qc = get_block_by_num(*p_branch, proposal->final_on_strong_qc_block_num()); - if (voting_strong && bsp_final_on_strong_qc && bsp_final_on_strong_qc->timestamp() > fsi.lock.timestamp) - fsi.lock = proposal_ref(bsp_final_on_strong_qc); + auto& final_on_strong_qc_block_ref = core.get_block_reference(core.final_on_strong_qc_block_num); + if (voting_strong && final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp) + fsi.lock = proposal_ref(final_on_strong_qc_block_ref.block_id, final_on_strong_qc_block_ref.timestamp); decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; } else { - dlog("last_qc_block_num=${lqc}, fork_db root block_num=${f}", - ("lqc",!!proposal->last_qc_block_num())("f",fork_db.root()->block_num())); - dlog("last_qc_block_num=${lqc}", ("lqc", proposal->last_qc_block_num())); + dlog("last_qc_block_num=${lqc}", ("lqc",core.last_qc_block_num())); } if (decision != vote_decision::no_vote) dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); @@ -96,9 +73,10 @@ finalizer::vote_decision finalizer::decide_vote(const block_state_ptr& proposal, } // ---------------------------------------------------------------------------------------- -std::optional finalizer::maybe_vote(const bls_public_key& pub_key, const block_state_ptr& p, - const digest_type& digest, const fork_database_if_t& fork_db) { - finalizer::vote_decision decision = decide_vote(p, fork_db); +std::optional finalizer::maybe_vote(const bls_public_key& pub_key, + const block_state_ptr& p, + const digest_type& digest) { + finalizer::vote_decision decision = decide_vote(p->core, p->id(), p->timestamp()); if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { bls_signature sig; if (decision == vote_decision::weak_vote) { diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index 6686b96018..4f90d4fa5e 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -61,8 +61,6 @@ struct block_header_state { account_name producer() const { return header.producer; } const block_id_type& previous() const { return header.previous; } uint32_t block_num() const { return block_header::num_from_id(previous()) + 1; } - uint32_t last_qc_block_num() const { return core.latest_qc_claim().block_num; } - block_timestamp_type last_qc_block_timestamp() const { return core.get_block_reference(last_qc_block_num()).timestamp; } const producer_authority_schedule& active_schedule_auth() const { return active_proposer_policy->proposer_schedule; } block_header_state next(block_header_state_input& data) const; diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index 9a5786be74..fc2275ae1a 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -44,8 +44,6 @@ struct block_state : public block_header_state { // block_header_state provi void set_valid(bool b) { validated = b; } uint32_t irreversible_blocknum() const { return core.last_final_block_num(); } std::optional get_best_qc() const; - uint32_t last_qc_block_num() const { return block_header_state::last_qc_block_num(); } - uint32_t final_on_strong_qc_block_num() const { return core.final_on_strong_qc_block_num; } protocol_feature_activation_set_ptr get_activated_protocol_features() const { return block_header_state::activated_protocol_features; } bool is_pub_keys_recovered() const { return pub_keys_recovered; } diff --git a/libraries/chain/include/eosio/chain/block_timestamp.hpp b/libraries/chain/include/eosio/chain/block_timestamp.hpp index a20f609ddc..58cda070f8 100644 --- a/libraries/chain/include/eosio/chain/block_timestamp.hpp +++ b/libraries/chain/include/eosio/chain/block_timestamp.hpp @@ -17,7 +17,10 @@ namespace eosio { namespace chain { template class block_timestamp { public: - explicit block_timestamp( uint32_t s=0 ) :slot(s){} + + block_timestamp() : slot(0) {} + + explicit block_timestamp( uint32_t s ) :slot(s){} block_timestamp(const fc::time_point& t) { set_time_point(t); @@ -51,6 +54,8 @@ namespace eosio { namespace chain { set_time_point(t); } + block_timestamp& operator=(const block_timestamp&) = default; + bool operator > ( const block_timestamp& t )const { return slot > t.slot; } bool operator >=( const block_timestamp& t )const { return slot >= t.slot; } bool operator < ( const block_timestamp& t )const { return slot < t.slot; } diff --git a/libraries/chain/include/eosio/chain/finality_core.hpp b/libraries/chain/include/eosio/chain/finality_core.hpp index 10d3268bdd..d0e0874a7d 100644 --- a/libraries/chain/include/eosio/chain/finality_core.hpp +++ b/libraries/chain/include/eosio/chain/finality_core.hpp @@ -13,7 +13,12 @@ struct block_ref block_id_type block_id; block_time_type timestamp; + bool empty() const { return block_id.empty(); } block_num_type block_num() const; // Extract from block_id. + + bool operator==(const block_ref& o) const { + return block_id == o.block_id && timestamp == o.timestamp; + } }; struct qc_link @@ -92,6 +97,16 @@ struct finality_core */ qc_claim_t latest_qc_claim() const; + block_num_type last_qc_block_num() const { return links.back().target_block_num; } + block_time_type last_qc_block_timestamp() const { return get_block_reference(last_qc_block_num()).timestamp; } + + /** + * @pre all finality_core invariants + * @post same + * @returns boolean indicating whether `id` is an ancestor of this block + */ + bool extends(const block_id_type& id) const; + /** * @pre last_final_block_num() <= block_num < current_block_num() * diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index ea53822b8d..985079f90f 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -1,13 +1,8 @@ #pragma once -#include -#include -#include +#include "eosio/chain/block_state.hpp" #include #include -#include -#include #include -#include // ------------------------------------------------------------------------------------------- // this file defines the classes: @@ -32,35 +27,7 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- - struct proposal_ref { - block_id_type id; - block_timestamp_type timestamp; - - proposal_ref() = default; - - template - explicit proposal_ref(const BSP& p) : - id(p->id()), - timestamp(p->timestamp()) - {} - - proposal_ref(const block_id_type& id, block_timestamp_type t) : - id(id), timestamp(t) - {} - - void reset() { - id = block_id_type(); - timestamp = block_timestamp_type(); - } - - bool empty() const { return id.empty(); } - - explicit operator bool() const { return !id.empty(); } - - auto operator==(const proposal_ref& o) const { - return id == o.id && timestamp == o.timestamp; - } - }; + using proposal_ref = block_ref; // ---------------------------------------------------------------------------------------- struct finalizer_safety_information { @@ -70,7 +37,7 @@ namespace eosio::chain { static constexpr uint64_t magic = 0x5AFE11115AFE1111ull; - static finalizer_safety_information unset_fsi() { return {block_timestamp_type(), {}, {}}; } + static finalizer_safety_information unset_fsi() { return {}; } auto operator==(const finalizer_safety_information& o) const { return last_vote_range_start == o.last_vote_range_start && @@ -83,17 +50,16 @@ namespace eosio::chain { struct finalizer { enum class vote_decision { strong_vote, weak_vote, no_vote }; - bls_private_key priv_key; - finalizer_safety_information fsi; + bls_private_key priv_key; + finalizer_safety_information fsi; private: - using branch_type = fork_database_if_t::branch_type; - using full_branch_type = fork_database_if_t::full_branch_type; - vote_decision decide_vote(const block_state_ptr& proposal, const fork_database_if_t& fork_db); + vote_decision decide_vote(const finality_core& core, const block_id_type &id, + const block_timestamp_type timestamp); public: std::optional maybe_vote(const bls_public_key& pub_key, const block_state_ptr& bsp, - const digest_type& digest, const fork_database_if_t& fork_db); + const digest_type& digest); }; // ---------------------------------------------------------------------------------------- @@ -112,7 +78,6 @@ namespace eosio::chain { template void maybe_vote(const finalizer_policy& fin_pol, const block_state_ptr& bsp, - const fork_database_if_t& fork_db, const digest_type& digest, F&& process_vote) { std::vector votes; @@ -121,7 +86,7 @@ namespace eosio::chain { // first accumulate all the votes for (const auto& f : fin_pol.finalizers) { if (auto it = finalizers.find(f.public_key); it != finalizers.end()) { - std::optional vote_msg = it->second.maybe_vote(it->first, bsp, digest, fork_db); + std::optional vote_msg = it->second.maybe_vote(it->first, bsp, digest); if (vote_msg) votes.push_back(std::move(*vote_msg)); } @@ -151,7 +116,7 @@ namespace eosio::chain { namespace std { inline std::ostream& operator<<(std::ostream& os, const eosio::chain::proposal_ref& r) { - os << "proposal_ref(id(" << r.id.str() << "), tstamp(" << r.timestamp.slot << "))"; + os << "proposal_ref(id(" << r.block_id.str() << "), tstamp(" << r.timestamp.slot << "))"; return os; } @@ -161,5 +126,4 @@ namespace std { } } -FC_REFLECT(eosio::chain::proposal_ref, (id)(timestamp)) FC_REFLECT(eosio::chain::finalizer_safety_information, (last_vote_range_start)(last_vote)(lock)) \ No newline at end of file From d115706d5f1cb1e0287de51412d25da0c03ac68c Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sat, 24 Feb 2024 15:02:34 -0500 Subject: [PATCH 08/18] Add spaceship operators to `sha256` and `block_timestamp` so we can use it is types containing these. --- .../chain/include/eosio/chain/block_timestamp.hpp | 9 +++------ .../chain/include/eosio/chain/finality_core.hpp | 4 +--- libraries/libfc/include/fc/crypto/elliptic.hpp | 9 ++------- libraries/libfc/include/fc/crypto/elliptic_r1.hpp | 9 ++------- libraries/libfc/include/fc/crypto/sha256.hpp | 9 ++++----- libraries/libfc/src/crypto/sha256.cpp | 13 ++----------- 6 files changed, 14 insertions(+), 39 deletions(-) diff --git a/libraries/chain/include/eosio/chain/block_timestamp.hpp b/libraries/chain/include/eosio/chain/block_timestamp.hpp index 58cda070f8..c79eb56c7e 100644 --- a/libraries/chain/include/eosio/chain/block_timestamp.hpp +++ b/libraries/chain/include/eosio/chain/block_timestamp.hpp @@ -54,14 +54,11 @@ namespace eosio { namespace chain { set_time_point(t); } + // needed, otherwise deleted because of above version of operator=() block_timestamp& operator=(const block_timestamp&) = default; - bool operator > ( const block_timestamp& t )const { return slot > t.slot; } - bool operator >=( const block_timestamp& t )const { return slot >= t.slot; } - bool operator < ( const block_timestamp& t )const { return slot < t.slot; } - bool operator <=( const block_timestamp& t )const { return slot <= t.slot; } - bool operator ==( const block_timestamp& t )const { return slot == t.slot; } - bool operator !=( const block_timestamp& t )const { return slot != t.slot; } + auto operator<=>(const block_timestamp&) const = default; + uint32_t slot; private: diff --git a/libraries/chain/include/eosio/chain/finality_core.hpp b/libraries/chain/include/eosio/chain/finality_core.hpp index d0e0874a7d..79c42abe5a 100644 --- a/libraries/chain/include/eosio/chain/finality_core.hpp +++ b/libraries/chain/include/eosio/chain/finality_core.hpp @@ -16,9 +16,7 @@ struct block_ref bool empty() const { return block_id.empty(); } block_num_type block_num() const; // Extract from block_id. - bool operator==(const block_ref& o) const { - return block_id == o.block_id && timestamp == o.timestamp; - } + auto operator<=>(const block_ref&) const = default; }; struct qc_link diff --git a/libraries/libfc/include/fc/crypto/elliptic.hpp b/libraries/libfc/include/fc/crypto/elliptic.hpp index c16e645a4a..cf1fef4305 100644 --- a/libraries/libfc/include/fc/crypto/elliptic.hpp +++ b/libraries/libfc/include/fc/crypto/elliptic.hpp @@ -127,13 +127,8 @@ namespace fc { { return a.get_secret() == b.get_secret(); } - inline friend bool operator!=( const private_key& a, const private_key& b ) - { - return a.get_secret() != b.get_secret(); - } - inline friend bool operator<( const private_key& a, const private_key& b ) - { - return a.get_secret() < b.get_secret(); + inline friend std::strong_ordering operator<=>( const private_key& a, const private_key& b ) { + return a.get_secret() <=> b.get_secret(); } unsigned int fingerprint() const { return get_public_key().fingerprint(); } diff --git a/libraries/libfc/include/fc/crypto/elliptic_r1.hpp b/libraries/libfc/include/fc/crypto/elliptic_r1.hpp index 1ada2a9d64..44a6cb9608 100644 --- a/libraries/libfc/include/fc/crypto/elliptic_r1.hpp +++ b/libraries/libfc/include/fc/crypto/elliptic_r1.hpp @@ -117,13 +117,8 @@ namespace fc { { return a.get_secret() == b.get_secret(); } - inline friend bool operator!=( const private_key& a, const private_key& b ) - { - return a.get_secret() != b.get_secret(); - } - inline friend bool operator<( const private_key& a, const private_key& b ) - { - return a.get_secret() < b.get_secret(); + inline friend std::strong_ordering operator<=>( const private_key& a, const private_key& b ) { + return a.get_secret() <=> b.get_secret(); } private: diff --git a/libraries/libfc/include/fc/crypto/sha256.hpp b/libraries/libfc/include/fc/crypto/sha256.hpp index 4e9c8615b3..f200ca6eb0 100644 --- a/libraries/libfc/include/fc/crypto/sha256.hpp +++ b/libraries/libfc/include/fc/crypto/sha256.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -72,12 +73,10 @@ class sha256 } friend sha256 operator << ( const sha256& h1, uint32_t i ); friend sha256 operator >> ( const sha256& h1, uint32_t i ); - friend bool operator == ( const sha256& h1, const sha256& h2 ); - friend bool operator != ( const sha256& h1, const sha256& h2 ); friend sha256 operator ^ ( const sha256& h1, const sha256& h2 ); - friend bool operator >= ( const sha256& h1, const sha256& h2 ); - friend bool operator > ( const sha256& h1, const sha256& h2 ); - friend bool operator < ( const sha256& h1, const sha256& h2 ); + + friend bool operator == ( const sha256& h1, const sha256& h2 ); + friend std::strong_ordering operator <=> ( const sha256& h1, const sha256& h2 ); uint32_t pop_count()const { diff --git a/libraries/libfc/src/crypto/sha256.cpp b/libraries/libfc/src/crypto/sha256.cpp index 5e6f226d8c..0a1f05a5f4 100644 --- a/libraries/libfc/src/crypto/sha256.cpp +++ b/libraries/libfc/src/crypto/sha256.cpp @@ -86,17 +86,8 @@ namespace fc { result._hash[3] = h1._hash[3] ^ h2._hash[3]; return result; } - bool operator >= ( const sha256& h1, const sha256& h2 ) { - return memcmp( h1._hash, h2._hash, sizeof(h1._hash) ) >= 0; - } - bool operator > ( const sha256& h1, const sha256& h2 ) { - return memcmp( h1._hash, h2._hash, sizeof(h1._hash) ) > 0; - } - bool operator < ( const sha256& h1, const sha256& h2 ) { - return memcmp( h1._hash, h2._hash, sizeof(h1._hash) ) < 0; - } - bool operator != ( const sha256& h1, const sha256& h2 ) { - return !(h1 == h2); + std::strong_ordering operator <=> ( const sha256& h1, const sha256& h2 ) { + return memcmp( h1._hash, h2._hash, sizeof(h1._hash) ) <=> 0; } bool operator == ( const sha256& h1, const sha256& h2 ) { // idea to not use memcmp, from: From d04abfc2c19906b995a856ea3c8e33bff773842c Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Sun, 25 Feb 2024 20:27:30 -0500 Subject: [PATCH 09/18] Cleanup new api added to finality_core. --- libraries/chain/finality_core.cpp | 9 +++++++++ libraries/chain/hotstuff/finalizer.cpp | 6 ++---- libraries/chain/include/eosio/chain/finality_core.hpp | 9 +++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/libraries/chain/finality_core.cpp b/libraries/chain/finality_core.cpp index 8d10e3e3eb..2568ea2037 100644 --- a/libraries/chain/finality_core.cpp +++ b/libraries/chain/finality_core.cpp @@ -78,6 +78,15 @@ qc_claim_t finality_core::latest_qc_claim() const return qc_claim_t{.block_num = links.back().target_block_num, .is_strong_qc = links.back().is_link_strong}; } +/** + * @pre all finality_core invariants + * @post same + * @returns timestamp of latest qc_claim made by the core + */ +block_time_type finality_core::latest_qc_block_timestamp() const { + return get_block_reference(links.back().target_block_num).timestamp; +} + /** * @pre all finality_core invariants * @post same diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index 5328144056..35a262ef6f 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -24,7 +24,7 @@ finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const // than the height of the proposal I'm locked on. // This allows restoration of liveness if a replica is locked on a stale proposal // ------------------------------------------------------------------------------- - liveness_check = core.last_qc_block_timestamp() > fsi.lock.timestamp; + liveness_check = core.latest_qc_block_timestamp() > fsi.lock.timestamp; if (!liveness_check) { // Safety check : check if this proposal extends the proposal we're locked on @@ -47,7 +47,7 @@ finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const vote_decision decision = vote_decision::no_vote; if (liveness_check || safety_check) { - auto [p_start, p_end] = std::make_pair(core.last_qc_block_timestamp(), proposal_timestamp); + auto [p_start, p_end] = std::make_pair(core.latest_qc_block_timestamp(), proposal_timestamp); bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; bool voting_strong = time_range_disjoint; @@ -64,8 +64,6 @@ finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const fsi.lock = proposal_ref(final_on_strong_qc_block_ref.block_id, final_on_strong_qc_block_ref.timestamp); decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; - } else { - dlog("last_qc_block_num=${lqc}", ("lqc",core.last_qc_block_num())); } if (decision != vote_decision::no_vote) dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); diff --git a/libraries/chain/include/eosio/chain/finality_core.hpp b/libraries/chain/include/eosio/chain/finality_core.hpp index 79c42abe5a..87f53bedfe 100644 --- a/libraries/chain/include/eosio/chain/finality_core.hpp +++ b/libraries/chain/include/eosio/chain/finality_core.hpp @@ -17,6 +17,7 @@ struct block_ref block_num_type block_num() const; // Extract from block_id. auto operator<=>(const block_ref&) const = default; + bool operator==(const block_ref& o) const = default; }; struct qc_link @@ -95,8 +96,12 @@ struct finality_core */ qc_claim_t latest_qc_claim() const; - block_num_type last_qc_block_num() const { return links.back().target_block_num; } - block_time_type last_qc_block_timestamp() const { return get_block_reference(last_qc_block_num()).timestamp; } + /** + * @pre all finality_core invariants + * @post same + * @returns timestamp of latest qc_claim made by the core + */ + block_time_type latest_qc_block_timestamp() const; /** * @pre all finality_core invariants From 9c1e7b250b7c6f6617f1eae6fed10511c7be8988 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Mon, 26 Feb 2024 11:09:59 -0500 Subject: [PATCH 10/18] fix build issue with gcc. --- libraries/chain/fork_database.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/chain/fork_database.cpp b/libraries/chain/fork_database.cpp index 4075033c4f..23e26e4365 100644 --- a/libraries/chain/fork_database.cpp +++ b/libraries/chain/fork_database.cpp @@ -164,7 +164,7 @@ namespace eosio::chain { } template - fork_database_t::~fork_database_t() { + fork_database_t::~fork_database_t() { // close is performed in fork_database::~fork_database() } From b547efc53279663ced5a6ffb5202757c8d3ed21a Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 10:31:51 -0500 Subject: [PATCH 11/18] Update tests and simulator. --- libraries/chain/hotstuff/finalizer.cpp | 42 +- .../include/eosio/chain/block_timestamp.hpp | 7 + .../include/eosio/chain/finality_core.hpp | 30 +- .../eosio/chain/hotstuff/finalizer.hpp | 34 +- libraries/libfc/include/fc/crypto/sha256.hpp | 5 + unittests/bhs_core.hpp | 675 ------------------ unittests/finalizer_tests.cpp | 327 ++++++++- unittests/mock_utils.hpp | 175 ----- 8 files changed, 393 insertions(+), 902 deletions(-) delete mode 100644 unittests/bhs_core.hpp delete mode 100644 unittests/mock_utils.hpp diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index 35a262ef6f..61182e9bbf 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -5,18 +5,17 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- -finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const block_id_type &proposal_id, - const block_timestamp_type proposal_timestamp) { - bool safety_check = false; - bool liveness_check = false; +finalizer::vote_result finalizer::decide_vote(const finality_core& core, const block_id_type &proposal_id, + const block_timestamp_type proposal_timestamp) { + vote_result res; - bool monotony_check = fsi.last_vote.empty() || proposal_timestamp > fsi.last_vote.timestamp; + res.monotony_check = fsi.last_vote.empty() || proposal_timestamp > fsi.last_vote.timestamp; // fsi.last_vote.empty() means we have never voted on a proposal, so the protocol feature // just activated and we can proceed - if (!monotony_check) { + if (!res.monotony_check) { dlog("monotony check failed for proposal ${p}, cannot vote", ("p", proposal_id)); - return vote_decision::no_vote; + return res; } if (!fsi.lock.empty()) { @@ -24,29 +23,30 @@ finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const // than the height of the proposal I'm locked on. // This allows restoration of liveness if a replica is locked on a stale proposal // ------------------------------------------------------------------------------- - liveness_check = core.latest_qc_block_timestamp() > fsi.lock.timestamp; + res.liveness_check = core.latest_qc_block_timestamp() > fsi.lock.timestamp; - if (!liveness_check) { + if (!res.liveness_check) { // Safety check : check if this proposal extends the proposal we're locked on - safety_check = core.extends(fsi.lock.block_id); + res.safety_check = core.extends(fsi.lock.block_id); } } else { // Safety and Liveness both fail if `fsi.lock` is empty. It should not happen. // `fsi.lock` is initially set to `lib` when switching to IF or starting from a snapshot. // ------------------------------------------------------------------------------------- - liveness_check = false; - safety_check = false; + res.liveness_check = false; + res.safety_check = false; } - dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote = {can_vote}", - ("l",liveness_check)("s",safety_check)("m",monotony_check)("can_vote",(liveness_check || safety_check))); + bool can_vote = res.liveness_check || res.safety_check; + dlog("liveness_check=${l}, safety_check=${s}, monotony_check=${m}, can vote=${can_vote}", + ("l",res.liveness_check)("s",res.safety_check)("m",res.monotony_check)("can_vote",can_vote)); // Figure out if we can vote and wether our vote will be strong or weak // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc // ----------------------------------------------------------------------------------- vote_decision decision = vote_decision::no_vote; - if (liveness_check || safety_check) { + if (can_vote) { auto [p_start, p_end] = std::make_pair(core.latest_qc_block_timestamp(), proposal_timestamp); bool time_range_disjoint = fsi.last_vote_range_start >= p_end || fsi.last_vote.timestamp <= p_start; @@ -63,18 +63,18 @@ finalizer::vote_decision finalizer::decide_vote(const finality_core& core, const if (voting_strong && final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp) fsi.lock = proposal_ref(final_on_strong_qc_block_ref.block_id, final_on_strong_qc_block_ref.timestamp); - decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; + res.decision = voting_strong ? vote_decision::strong_vote : vote_decision::weak_vote; } - if (decision != vote_decision::no_vote) - dlog("Voting ${s}", ("s", decision == vote_decision::strong_vote ? "strong" : "weak")); - return decision; + if (res.decision != vote_decision::no_vote) + dlog("Voting ${s}", ("s", res.decision == vote_decision::strong_vote ? "strong" : "weak")); + return res; } // ---------------------------------------------------------------------------------------- std::optional finalizer::maybe_vote(const bls_public_key& pub_key, - const block_state_ptr& p, + const block_header_state_ptr& p, const digest_type& digest) { - finalizer::vote_decision decision = decide_vote(p->core, p->id(), p->timestamp()); + finalizer::vote_decision decision = decide_vote(p->core, p->id(), p->timestamp()).decision; if (decision == vote_decision::strong_vote || decision == vote_decision::weak_vote) { bls_signature sig; if (decision == vote_decision::weak_vote) { diff --git a/libraries/chain/include/eosio/chain/block_timestamp.hpp b/libraries/chain/include/eosio/chain/block_timestamp.hpp index c79eb56c7e..0c5305ebb7 100644 --- a/libraries/chain/include/eosio/chain/block_timestamp.hpp +++ b/libraries/chain/include/eosio/chain/block_timestamp.hpp @@ -78,6 +78,13 @@ namespace eosio { namespace chain { } } /// eosio::chain +namespace std { + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::block_timestamp_type& t) { + os << "tstamp(" << t.slot << ")"; + return os; + } +} + #include FC_REFLECT(eosio::chain::block_timestamp_type, (slot)) diff --git a/libraries/chain/include/eosio/chain/finality_core.hpp b/libraries/chain/include/eosio/chain/finality_core.hpp index 87f53bedfe..f65d174f3f 100644 --- a/libraries/chain/include/eosio/chain/finality_core.hpp +++ b/libraries/chain/include/eosio/chain/finality_core.hpp @@ -139,7 +139,10 @@ struct finality_core * @pre current_block.block_num() == this->current_block_num() * @pre If this->refs.empty() == false, then current_block is the block after the one referenced by this->refs.back() * @pre this->latest_qc_claim().block_num <= most_recent_ancestor_with_qc.block_num <= this->current_block_num() - * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc ( (this->latest_qc_claim().block_num == most_recent_ancestor_with_qc.block_num) && most_recent_ancestor_with_qc.is_strong_qc ). When block_num is the same, most_recent_ancestor_with_qc must be stronger than latest_qc_claim() + * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc (i.e. + * this->latest_qc_claim().block_num == most_recent_ancestor_with_qc.block_num && + * most_recent_ancestor_with_qc.is_strong_qc ). + * When block_num is the same, most_recent_ancestor_with_qc must be stronger than latest_qc_claim() * * @post returned core has current_block_num() == this->current_block_num() + 1 * @post returned core has latest_qc_claim() == most_recent_ancestor_with_qc @@ -151,6 +154,31 @@ struct finality_core } /// eosio::chain +// ----------------------------------------------------------------------------- +namespace std { + // define std ostream output so we can use BOOST_CHECK_EQUAL in tests + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::block_ref& br) { + os << "block_ref(" << br.block_id << ", " << br.timestamp << ")"; + return os; + } + + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::qc_link& l) { + os << "qc_link(" << l.source_block_num << ", " << l.target_block_num << ", " << l.is_link_strong << ")"; + return os; + } + + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::qc_claim_t& c) { + os << "qc_claim_t(" << c.block_num << ", " << c.is_strong_qc << ")"; + return os; + } + + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::core_metadata& cm) { + os << "core_metadata(" << cm.last_final_block_num << ", " << cm.final_on_strong_qc_block_num << + ", " << cm.latest_qc_claim_block_num << ")"; + return os; + } +} + FC_REFLECT( eosio::chain::block_ref, (block_id)(timestamp) ) FC_REFLECT( eosio::chain::qc_link, (source_block_num)(target_block_num)(is_link_strong) ) FC_REFLECT( eosio::chain::qc_claim_t, (block_num)(is_strong_qc) ) diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index 985079f90f..fc5eac4b12 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -49,16 +49,20 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- struct finalizer { enum class vote_decision { strong_vote, weak_vote, no_vote }; + struct vote_result { + vote_decision decision {vote_decision::no_vote}; + bool safety_check {false}; + bool liveness_check {false}; + bool monotony_check {false}; + }; bls_private_key priv_key; finalizer_safety_information fsi; - private: - vote_decision decide_vote(const finality_core& core, const block_id_type &id, - const block_timestamp_type timestamp); + vote_result decide_vote(const finality_core& core, const block_id_type &id, + const block_timestamp_type timestamp); - public: - std::optional maybe_vote(const bls_public_key& pub_key, const block_state_ptr& bsp, + std::optional maybe_vote(const bls_public_key& pub_key, const block_header_state_ptr& bhsp, const digest_type& digest); }; @@ -77,7 +81,7 @@ namespace eosio::chain { template void maybe_vote(const finalizer_policy& fin_pol, - const block_state_ptr& bsp, + const block_header_state_ptr& bhsp, const digest_type& digest, F&& process_vote) { std::vector votes; @@ -86,7 +90,7 @@ namespace eosio::chain { // first accumulate all the votes for (const auto& f : fin_pol.finalizers) { if (auto it = finalizers.find(f.public_key); it != finalizers.end()) { - std::optional vote_msg = it->second.maybe_vote(it->first, bsp, digest); + std::optional vote_msg = it->second.maybe_vote(it->first, bhsp, digest); if (vote_msg) votes.push_back(std::move(*vote_msg)); } @@ -115,13 +119,21 @@ namespace eosio::chain { } namespace std { - inline std::ostream& operator<<(std::ostream& os, const eosio::chain::proposal_ref& r) { - os << "proposal_ref(id(" << r.block_id.str() << "), tstamp(" << r.timestamp.slot << "))"; + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer_safety_information& fsi) { + os << "fsi(" << fsi.last_vote_range_start.slot << ", " << fsi.last_vote << ", " << fsi.lock << ")"; return os; } - inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer_safety_information& fsi) { - os << "fsi(" << fsi.last_vote_range_start.slot << ", " << fsi.last_vote << ", " << fsi.lock << ")"; + inline std::ostream& operator<<(std::ostream& os, const eosio::chain::finalizer::vote_result& vr) { + os << "vote_result(\""; + using vote_decision = eosio::chain::finalizer::vote_decision; + switch(vr.decision) { + case vote_decision::strong_vote: os << "strong_vote"; break; + case vote_decision::weak_vote: os << "weak_vote"; break; + case vote_decision::no_vote: os << "no_vote"; break; + } + os << "\", monotony_check(" << vr.monotony_check << "), liveness_check(" << vr.liveness_check << + "), safety_check(" << vr.safety_check<< "))"; return os; } } diff --git a/libraries/libfc/include/fc/crypto/sha256.hpp b/libraries/libfc/include/fc/crypto/sha256.hpp index f200ca6eb0..39042a9cc3 100644 --- a/libraries/libfc/include/fc/crypto/sha256.hpp +++ b/libraries/libfc/include/fc/crypto/sha256.hpp @@ -129,6 +129,11 @@ namespace std } }; + inline std::ostream& operator<<(std::ostream& os, const fc::sha256& r) { + os << "sha256(" << r.str() << ")"; + return os; + } + } namespace boost diff --git a/unittests/bhs_core.hpp b/unittests/bhs_core.hpp deleted file mode 100644 index 7121649481..0000000000 --- a/unittests/bhs_core.hpp +++ /dev/null @@ -1,675 +0,0 @@ -#pragma once -#include -#include - -namespace bhs_core { - -using eosio::chain::block_id_type; - -using block_num_type = uint32_t; -using block_time_type = eosio::chain::block_timestamp_type; - -struct block_ref -{ - block_id_type block_id; - block_time_type timestamp; - - block_num_type block_num() const { return eosio::chain::block_header::num_from_id(block_id); } -}; - -struct qc_link -{ - block_num_type source_block_num; - block_num_type target_block_num; // Must be less than or equal to source_block_num (only equal for genesis block). - bool is_link_strong; -}; - -struct qc_claim -{ - block_num_type block_num; - bool is_strong_qc; - - friend auto operator<=>(const qc_claim&, const qc_claim&) = default; -}; - -bool all_equal(auto ...ns) { - std::array a { ns... }; - for (int i=0; i<(int)a.size()-1; ++i) - if (a[i] != a[i+1]) - return false; - return true; -} - -struct core -{ - std::vector links; // Captures all relevant links sorted in order of ascending source_block_num. - std::vector refs; // Covers ancestor blocks with block numbers greater than or equal to last_final_block_num. - // Sorted in order of ascending block_num. - block_num_type final_on_strong_qc_block_num_; - - // greg - block_time_type last_qc_block_timestamp() const { - const block_ref& ref = get_block_reference(links.back().target_block_num); - return ref.timestamp; - } - - // greg - block_num_type last_qc_block_num() const { - return links.back().target_block_num; - } - - // greg - block_num_type final_on_strong_qc_block_num() const { - return final_on_strong_qc_block_num_; - } - - // Invariants: - // 1. links.empty() == false - // 2. last_final_block_num() <= links.front().source_block_num <= final_on_strong_qc_block_num <= latest_qc_claim().block_num - // 3. If refs.empty() == true, then (links.size() == 1) and - // (links.back().target_block_num == links.back().source_block_num == final_on_strong_qc_block_num == last_final_block_num()) - // 4. If refs.empty() == false, then refs.front().block_num() == links.front().target_block_num == last_final_block_num() - // 5. If refs.empty() == false, then refs.back().block_num() + 1 == links.back().source_block_num == current_block_num() - // 6. If refs.size() > 1, then: - // For i = 0 to refs.size() - 2: - // (refs[i].block_num() + 1 == refs[i+1].block_num()) and (refs[i].timestamp < refs[i+1].timestamp) - // 7. If links.size() > 1, then: - // For i = 0 to links.size() - 2: - // (links[i].source_block_num + 1 == links[i+1].source_block_num) and (links[i].target_block_num <= links[i+1].target_block_num) - // 8. current_block_num() - last_final_block_num() == refs.size() (always implied by invariants 3 to 6) - // 9. current_block_num() - links.front().source_block_num == links.size() - 1 (always implied by invariants 1 and 7) - - static core create_core_for_genesis_block(block_num_type block_num) - { - return core { - .links = { - qc_link{ - .source_block_num = block_num, - .target_block_num = block_num, - .is_link_strong = false, - }, - }, - .refs = {}, - .final_on_strong_qc_block_num_ = block_num, - }; - - // Invariants 1 to 7 can be easily verified to be satisfied for the returned core. - // (And so, remaining invariants are also automatically satisfied.) - } - - block_num_type current_block_num() const - { - assert(!links.empty()); // Satisfied by invariant 1. - - return links.back().source_block_num; - } - - block_num_type last_final_block_num() const - { - assert(!links.empty()); // Satisfied by invariant 1. - - return links.front().target_block_num; - } - - - qc_claim latest_qc_claim() const - { - assert(!links.empty()); // Satisfied by invariant 1. - - return qc_claim{.block_num = links.back().target_block_num, .is_strong_qc = links.back().is_link_strong}; - } - /** - * @pre last_final_block_num() <= block_num < current_block_num() - * - * @post returned block_ref has block_num() == block_num - */ - const block_ref& get_block_reference(block_num_type block_num) const - { - assert(last_final_block_num() <= block_num); // Satisfied by precondition. - assert(block_num < current_block_num()); // Satisfied by precondition. - - // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num(), - // and therefore it is impossible to satisfy the precondition. So going forward, it is safe to assume refs.empty() == false. - - const size_t ref_index = block_num - last_final_block_num(); - - // By the precondition, 0 <= ref_index < (current_block_num() - last_final_block_num()). - // Then, by invariant 8, 0 <= ref_index < refs.size(). - - assert(ref_index < refs.size()); // Satisfied by justification above. - - return refs[ref_index]; - // By invariants 4 and 6, tail[ref_index].block_num() == block_num, which satisfies the post-condition. - } - - /** - * @pre links.front().source_block_num <= block_num <= current_block_num() - * - * @post returned qc_link has source_block_num == block_num - */ - const qc_link& get_qc_link_from(block_num_type block_num) const - { - assert(!links.empty()); // Satisfied by invariant 1. - - assert(links.front().source_block_num <= block_num); // Satisfied by precondition. - assert(block_num <= current_block_num()); // Satisfied by precondition. - - const size_t link_index = block_num - links.front().source_block_num; - - // By the precondition, 0 <= link_index <= (current_block_num() - links.front().source_block_num). - // Then, by invariant 9, 0 <= link_index <= links.size() - 1 - - assert(link_index < links.size()); // Satisfied by justification above. - - return links[link_index]; - // By invariants 7, links[link_index].source_block_num == block_num, which satisfies the post-condition. - } - - /** - * @pre current_block.block_num() == this->current_block_num() - * @pre If this->refs.empty() == false, then current_block is the block after the one referenced by this->refs.back() - * @pre this->latest_qc_claim().block_num <= most_recent_ancestor_with_qc.block_num <= this->current_block_num() - * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc - * - * @post returned core has current_block_num() == this->current_block_num() + 1 - * @post returned core has latest_qc_claim() == most_recent_ancestor_with_qc - * @post returned core has final_on_strong_qc_block_num >= this->final_on_strong_qc_block_num - * @post returned core has last_final_block_num() >= this->last_final_block_num() - */ - core next(const block_ref& current_block, const qc_claim& most_recent_ancestor_with_qc) const - { - assert(current_block.block_num() == current_block_num()); // Satisfied by precondition 1. - - assert(refs.empty() || (refs.back().block_num() + 1 == current_block.block_num())); // Satisfied by precondition 2. - assert(refs.empty() || (refs.back().timestamp < current_block.timestamp)); // Satisfied by precondition 2. - - assert(most_recent_ancestor_with_qc.block_num <= current_block_num()); // Satisfied by precondition 3. - - assert(latest_qc_claim() <= most_recent_ancestor_with_qc); // Satisfied by precondition 4. - - core next_core; - - auto new_block_nums = [&]() -> std::tuple - { - // Invariant 2 guarantees that: - // last_final_block_num() <= links.front().source_block_num <= final_on_strong_qc_block_num <= latest_qc_claim().block_num - - assert(links.front().source_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by invariant 2 and precondition 4. - - // No changes on new claim of weak QC. - if (!most_recent_ancestor_with_qc.is_strong_qc) { - return {last_final_block_num(), links.front().source_block_num, final_on_strong_qc_block_num_}; - } - - const auto& link1 = get_qc_link_from(most_recent_ancestor_with_qc.block_num); - - // By the post-condition of get_qc_link_from, link1.source_block_num == most_recent_ancestor_with_qc.block_num. - // By the invariant on qc_link, link1.target_block_num <= link1.source_block_num. - // Therefore, link1.target_block_num <= most_recent_ancestor_with_qc.block_num. - // And also by precondition 3, link1.target_block_num <= current_block_num(). - - // If refs.empty() == true, then by invariant 3, link1 == links.front() == links.back() and so - // link1.target_block_num == current_block_num(). - - // Otherwise, if refs.empty() == false, consider two cases. - // Case 1: link1 != links.back() - // In this case, link1.target_block_num <= link1.source_block_num < links.back().source_block_num. - // The strict inequality is justified by invariant 7. - // Therefore, link1.target_block_num < current_block_num(). - // Case 2: link1 == links.back() - // In this case, link1.target_block_num < link1.source_block_num == links.back().source_block_num. - // The strict inequality is justified because the only the target_block_num and source_block_num of a qc_link - // can be equal is for genesis block. And link mapping genesis block number to genesis block number can only - // possibly exist for links.front(). - // Therefore, link1.target_block_num < current_block_num(). - - // So, link1.target_block_num == current_block_num() iff refs.empty() == true. - - assert(final_on_strong_qc_block_num_ <= link1.target_block_num); // TODO: Show that this is always true. - - // Finality does not advance if a better 3-chain is not found. - if (!link1.is_link_strong || (link1.target_block_num < links.front().source_block_num)) { - return {last_final_block_num(), links.front().source_block_num, link1.target_block_num}; - } - - const auto& link2 = get_qc_link_from(link1.target_block_num); - - // By the post-condition of get_qc_link_from, link2.source_block_num == link1.target_block_num. - // By the invariant on qc_link, link2.target_block_num <= link2.source_block_num. - // Therefore, link2.target_block_num <= link1.target_block_num. - - // Wherever link2 is found within links, it must be the case that links.front().target_block_num <= link2.target_block_num. - // This is justified by invariant 7. - // Therefore, last_final_block_num() <= link2.target_block_num. - - return {link2.target_block_num, link2.source_block_num, link1.target_block_num}; - }; - - const auto [new_last_final_block_num, new_links_front_source_block_num, new_final_on_strong_qc_block_num] = new_block_nums(); - - assert(new_last_final_block_num <= new_links_front_source_block_num); // Satisfied by justification in new_block_nums. - assert(new_links_front_source_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justification in new_block_nums. - assert(new_final_on_strong_qc_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by justification in new_block_nums. - - assert(last_final_block_num() <= new_last_final_block_num); // Satisfied by justifications in new_block_nums. - assert(links.front().source_block_num <= new_links_front_source_block_num); // Satisfied by justification in new_block_nums. - assert(final_on_strong_qc_block_num_ <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. - - next_core.final_on_strong_qc_block_num_ = new_final_on_strong_qc_block_num; - // Post-condition 3 is satisfied, assuming next_core will be returned without further modifications to next_core.final_on_strong_qc_block_num. - - // Post-condition 4 and invariant 2 will be satisfied when next_core.last_final_block_num() is updated to become new_last_final_block_num. - - // Setup next_core.links by garbage collecting unnecessary links and then adding the new QC link. - { - const size_t links_index = new_links_front_source_block_num - links.front().source_block_num; - - assert(links_index < links.size()); // Satisfied by justification in this->get_qc_link_from(new_links_front_source_block_num). - - next_core.links.reserve(links.size() - links_index + 1); - - // Garbage collect unnecessary links - std::copy(links.cbegin() + links_index, links.cend(), std::back_inserter(next_core.links)); - - assert(next_core.last_final_block_num() == new_last_final_block_num); // Satisfied by choice of links_index. - - // Also, by choice of links_index, at this point, next_core.links.back() == this->links.back(). - assert(next_core.links.back().source_block_num == current_block_num()); // Satisfied because last item in links has not yet changed. - assert(next_core.links.back().target_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied because of above and precondition 3. - - // Add new link - next_core.links.emplace_back( - qc_link{ - .source_block_num = current_block_num() + 1, - .target_block_num = most_recent_ancestor_with_qc.block_num, // Guaranteed to be less than current_block_num() + 1. - .is_link_strong = most_recent_ancestor_with_qc.is_strong_qc, - }); - - // Post-conditions 1, 2, and 4 are satisfied, assuming next_core will be returned without further modifications to next_core.links. - - // Invariants 1, 2, and 7 are satisfied for next_core. - } - - // Setup next_core.refs by garbage collecting unnecessary block references in the refs and then adding the new block reference. - { - const size_t refs_index = new_last_final_block_num - last_final_block_num(); - - // Using the justifications in new_block_nums, 0 <= ref_index <= (current_block_num() - last_final_block_num). - // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num, and therefore ref_index == 0. - // Otherwise if refs.empty() == false, the justification in new_block_nums provides the stronger inequality - // 0 <= ref_index < (current_block_num() - last_final_block_num), which, using invariant 8, can be simplified to - // 0 <= ref_index < refs.size(). - - assert(!refs.empty() || (refs_index == 0)); // Satisfied by justification above. - assert(refs.empty() || (refs_index < refs.size())); // Satisfied by justification above. - - next_core.refs.reserve(refs.size() - refs_index + 1); - - // Garbage collect unnecessary block references - std::copy(refs.cbegin() + refs_index, refs.cend(), std::back_inserter(next_core.refs)); - - assert(refs.empty() || (next_core.refs.front().block_num() == new_last_final_block_num)); // Satisfied by choice of refs_index. - - // Add new block reference - next_core.refs.emplace_back(current_block); - - // Invariant 3 is trivially satisfied for next_core because next_core.refs.empty() == false. - - // Invariant 5 is clearly satisfied for next_core because next_core.refs.back().block_num() == this->current_block_num() - // and next_core.links.back().source_block_num == this->current_block_num() + 1. - - // Invariant 6 is also clearly satisfied for next_core because invariant 6 is satisfied for *this and the only - // additional requirements needed are the ones provided by precondition 2. - - // If this->refs.empty() == true, then new_last_final_block_num == this->last_final_block_num() == this->current_block_num(), - // and next_core.refs.size() == 1 and next_core.refs.front() == current_block. - // And so, next_core.refs.front().block_num() == new_last_final_block_num. - // If this->refs.empty() == false, then adding the current_block to the end does not change the fact that - // next_core.refs.front().block_num() is still equal to new_last_final_block_num. - - assert(next_core.refs.front().block_num() == new_last_final_block_num); // Satisfied by justification above. - - // Because it was also already shown earlier that links.front().target_block_num == new_last_final_block_num, - // then the justification above satisfies the remaining equalities needed to satisfy invariant 4 for next_core. - - // So, invariants 3 to 6 are now satisfied for next_core in addition to the invariants 1, 2, and 7 that were shown to be satisfied - // earlier (and still remain satisfied since next_core.links and next_core.final_on_strong_qc_block_num have not changed). - } - - return next_core; - // Invariants 1 to 7 were verified to be satisfied for the current value of next_core at various points above. - // (And so, the remaining invariants for next_core are also automatically satisfied.) - } -}; - -#if 0 -struct core -{ - std::vector links; // Captures all relevant links sorted in order of ascending source_block_num. - std::vector refs; // Covers ancestor blocks with block numbers greater than or equal to last_final_block_num. - // Sorted in order of ascending block_num. - block_num_type final_on_strong_qc_block_num_; - - // Invariants: - // 1. links.empty() == false - // 2. last_final_block_num() <= final_on_strong_qc_block_num <= latest_qc_claim().block_num - // 3. If refs.empty() == true, then (links.size() == 1) and - // (links.back().target_block_num == links.back().source_block_num == final_on_strong_qc_block_num == last_final_block_num()) - // 4. If refs.empty() == false, then refs.front().block_num() == links.front().target_block_num == last_final_block_num() - // 5. If refs.empty() == false, then refs.back().block_num() + 1 == links.back().source_block_num == current_block_num() - // 6. If refs.size() > 1, then: - // For i = 0 to refs.size() - 2: - // (refs[i].block_num() + 1 == refs[i+1].block_num()) and (refs[i].timestamp < refs[i+1].timestamp) - // 7. If links.size() > 1, then: - // For i = 0 to links.size() - 2: - // (links[i].source_block_num + 1 == links[i+1].source_block_num) and (links[i].target_block_num <= links[i+1].target_block_num) - // 8. current_block_num() - last_final_block_num() == refs.size() (always implied by invariants 3 to 6) - // 9. current_block_num() - links.front().source_block_num == links.size() - 1 (always implied by invariants 1 and 7) - - void check_invariants() { - assert(!links.empty()); // 1. - assert(last_final_block_num() <= final_on_strong_qc_block_num_ && // 2. - final_on_strong_qc_block_num_ <= latest_qc_claim().block_num); - if (refs.empty()) { // 3. - assert(links.size() == 1); - } else { - assert(all_equal(links.back().target_block_num, // 3. - links.back().source_block_num, - final_on_strong_qc_block_num_, - last_final_block_num())); - assert(all_equal(refs.front().block_num(), // 4. - links.front().target_block_num, - last_final_block_num())); - assert(all_equal(refs.back().block_num() + 1, // 5. - links.back().source_block_num, - current_block_num())); - if (refs.size() > 1) { // 6. - for (size_t i=0; i 1) { // 7. - for (size_t i=0; icurrent_block_num() - * @pre If this->refs.empty() == false, then current_block is the block after the one referenced by this->refs.back() - * @pre this->latest_qc_claim().block_num <= most_recent_ancestor_with_qc.block_num <= this->current_block_num() - * @pre this->latest_qc_claim() <= most_recent_ancestor_with_qc - * - * @post returned core has current_block_num() == this->current_block_num() + 1 - * @post returned core has latest_qc_claim() == most_recent_ancestor_with_qc - * @post returned core has final_on_strong_qc_block_num >= this->final_on_strong_qc_block_num - * @post returned core has last_final_block_num() >= this->last_final_block_num() - */ - core next(const block_ref& current_block, const qc_claim& most_recent_ancestor_with_qc) const - { - assert(current_block.block_num() == current_block_num()); // Satisfied by precondition 1. - - assert(refs.empty() || (refs.back().timestamp < current_block.timestamp)); // Satisfied by precondition 2. - assert(refs.empty() || (refs.back().block_num() + 1 == current_block.block_num())); // Satisfied by precondition 2. - - assert(most_recent_ancestor_with_qc.block_num <= current_block_num()); // Satisfied by precondition 3. - - assert(latest_qc_claim() <= most_recent_ancestor_with_qc); // Satisfied by precondition 4. - - core next_core; - - auto new_block_nums = [&]() -> std::pair - { - assert(last_final_block_num() <= final_on_strong_qc_block_num_); // Satisfied by invariant 2. - - if (!most_recent_ancestor_with_qc.is_strong_qc) { - return {last_final_block_num(), final_on_strong_qc_block_num_}; - } - - if (most_recent_ancestor_with_qc.block_num < links.front().source_block_num) { - return {last_final_block_num(), final_on_strong_qc_block_num_}; - } - - const auto& link1 = get_qc_link_from(most_recent_ancestor_with_qc.block_num); - - // TODO: Show the following hold true: - // final_on_strong_qc_block_num <= link1.target_block_num <= current_block_num(). - // link1.target_block_num == current_block_num() iff refs.empty() == true. - - // Since last_final_block_num() <= final_on_strong_qc_block_num - // and final_on_strong_qc_block_num <= link1.target_block_num, - // then last_final_block_num() <= link1.target_block_num. - - if (!link1.is_link_strong) { - return {last_final_block_num(), link1.target_block_num}; - } - - if (link1.target_block_num < links.front().source_block_num) { - return {last_final_block_num(), link1.target_block_num}; - } - - const auto& link2 = get_qc_link_from(link1.target_block_num); - - // TODO: Show the following hold true: - // last_final_block_num() <= link2.target_block_num - // link2.target_block_num <= link1.target_block_num - // link1.target_block_num <= most_recent_ancestor_with_qc.block_num - - return {link2.target_block_num, link1.target_block_num}; - }; - - const auto [new_last_final_block_num, new_final_on_strong_qc_block_num] = new_block_nums(); - - assert(new_last_final_block_num <= new_final_on_strong_qc_block_num); // Satisfied by justification in new_block_nums. - assert(new_final_on_strong_qc_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied by justification in new_block_nums. - - assert(final_on_strong_qc_block_num_ <= new_final_on_strong_qc_block_num); // Satisfied by justifications in new_block_nums. - assert(last_final_block_num() <= new_last_final_block_num); // Satisfied by justifications in new_block_nums. - - next_core.final_on_strong_qc_block_num_ = new_final_on_strong_qc_block_num; - // Post-condition 3 is satisfied, assuming next_core will be returned without further modifications to next_core.final_on_strong_qc_block_num. - - // Post-condition 4 and invariant 2 will be satisfied when next_core.last_final_block_num() is updated to become new_last_final_block_num. - - // Setup next_core.links by garbage collecting unnecessary links and then adding the new QC link. - { - size_t links_index = 0; // Default to no garbage collection (if last_final_block_num does not change). - - if (last_final_block_num() < next_core.last_final_block_num()) { - // new_blocks_nums found the new_last_final_block_num from a link that had a source_block_num - // equal to new_final_on_strong_qc_block_num. - // The index within links was (new_final_on_strong_qc_block_num - last_final_block_num). - // All prior links can be garbage collected. - - links_index = new_final_on_strong_qc_block_num - last_final_block_num(); - - assert(links_index < links.size()); // Satisfied by justification in this->get_qc_link_from(next_core.final_on_strong_qc_block_num). - } - - next_core.links.reserve(links.size() - links_index + 1); - - // Garbage collect unnecessary links - std::copy(links.cbegin() + links_index, links.cend(), std::back_inserter(next_core.links)); - - assert(next_core.last_final_block_num() == new_last_final_block_num); // Satisfied by choice of links_index. - - // Also, by choice of links_index, at this point, next_core.links.back() == this->links.back(). - assert(next_core.links.back().source_block_num == current_block_num()); // Satisfied because last item in links has not yet changed. - assert(next_core.links.back().target_block_num <= most_recent_ancestor_with_qc.block_num); // Satisfied because of above and precondition 3. - - // Add new link - next_core.links.emplace_back( - qc_link{ - .source_block_num = current_block_num() + 1, - .target_block_num = most_recent_ancestor_with_qc.block_num, // Guaranteed to be less than current_block_num() + 1. - .is_link_strong = most_recent_ancestor_with_qc.is_strong_qc, - }); - - // Post-conditions 1, 2, and 4 are satisfied, assuming next_core will be returned without further modifications to next_core.links. - - // Invariants 1, 2, and 7 are satisfied for next_core.60 - } - - // Setup next_core.refs by garbage collecting unnecessary block references in the refs and then adding the new block reference. - { - const size_t refs_index = next_core.last_final_block_num() - last_final_block_num(); - - // Using the justifications in new_block_nums, 0 <= ref_index <= (current_block_num() - last_final_block_num). - // If refs.empty() == true, then by invariant 3, current_block_num() == last_final_block_num, and therefore ref_index == 0. - // Otherwise if refs.empty() == false, the justification in new_block_nums provides the stronger inequality - // 0 <= ref_index < (current_block_num() - last_final_block_num), which, using invariant 8, can be simplified to - // 0 <= ref_index < refs.size(). - - assert(!refs.empty() || (refs_index == 0)); // Satisfied by justification above. - assert(refs.empty() || (refs_index < refs.size())); // Satisfied by justification above. - - next_core.refs.reserve(refs.size() - refs_index + 1); - - // Garbage collect unnecessary block references - std::copy(refs.cbegin() + refs_index, refs.cend(), std::back_inserter(next_core.refs)); - - assert(refs.empty() || (refs.front().block_num() == new_last_final_block_num)); // Satisfied by choice of refs_index. - - // Add new block reference - next_core.refs.emplace_back(current_block); - - // Invariant 3 is trivially satisfied for next_core because next_core.refs.empty() == false. - - // Invariant 5 is clearly satisfied for next_core because next_core.refs.back().block_num() == this->current_block_num() - // and next_core.links.back().source_block_num == this->current_block_num() + 1. - - // Invariant 6 is also clearly satisfied for next_core because invariant 6 is satisfied for *this and the only - // additional requirements needed are the ones provided by precondition 2. - - // If this->refs.empty() == true, then new_last_final_block_num == last_final_block_num == current_block_num(), - // and next_core.refs.size() == 1 and next_core.front() == current_block. - // And so, next_core.front().block_num() == new_last_final_block_num. - // If this->refs.empty() == false, then adding the current_block to the end does not change the fact that - // refs.front().block_num() is still equal to new_last_final_block_num. - - assert(refs.front().block_num() == new_last_final_block_num); // Satisfied by justification above. - - // Because it was also already shown earlier that links.front().target_block_num == new_last_final_block_num, - // then the justification above satisfies the remaining equalities needed to satisfy invariant 4 for next_core. - - // So, invariants 3 to 6 are now satisfied for next_core in addition to the invariants 1, 2, and 7 that were shown to be satisfied - // earlier (and still remain satisfied since next_core.links and next_core.final_on_strong_qc_block_num have not changed). - } - - return next_core; - // Invariants 1 to 7 were verified to be satisfied for the current value of next_core at various points above. - // (And so, the remaining invariants for next_core are also automatically satisfied.) - } -}; -#endif - -} \ No newline at end of file diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index b6395850bb..8cf1ee2170 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -1,10 +1,11 @@ +#include #include +#include #include #include #include - -#include "mock_utils.hpp" +#include using namespace eosio; using namespace eosio::chain; @@ -241,38 +242,326 @@ struct simulator_t { } }; -#if 0 // --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { +BOOST_AUTO_TEST_CASE( proposal_sim_1 ) try { + using namespace mock_utils; + + simulator_t sim; + + auto [head1, claim1] = sim.add(proposal_t{1, "n0", block_timestamp_type{1}}, bhs_core::qc_claim{0, false}); + BOOST_CHECK_EQUAL(claim1.block_num, 1); + + auto [head2, claim2] = sim.add(proposal_t{2, "n0", block_timestamp_type{2}}, claim1); + BOOST_CHECK_EQUAL(claim2.block_num, 2); + +} FC_LOG_AND_RETHROW() +#endif + +using eosio::chain::finality_core; +using eosio::chain::block_ref; +using bs = eosio::chain::block_state; +using bsp = eosio::chain::block_state_ptr; +using bhs = eosio::chain::block_header_state; +using bhsp = eosio::chain::block_header_state_ptr; +using vote_decision = finalizer::vote_decision; +using vote_result = finalizer::vote_result; + + +// --------------------------------------------------------------------------------------- +inline block_id_type calc_id(block_id_type id, uint32_t block_number) { + id._hash[0] &= 0xffffffff00000000; + id._hash[0] += fc::endian_reverse_u32(block_number); + return id; +} + +// --------------------------------------------------------------------------------------- +struct proposal_t { + uint32_t block_number; + std::string proposer_name; + block_timestamp_type block_timestamp; + + proposal_t(uint32_t block_number, const char* proposer, std::optional timestamp = {}) : + block_number(block_number), proposer_name(proposer), block_timestamp(timestamp ? *timestamp : block_number) + {} + + const std::string& proposer() const { return proposer_name; } + block_timestamp_type timestamp() const { return block_timestamp; } + uint32_t block_num() const { return block_number; } + + block_id_type calculate_id() const + { + std::string id_str = proposer_name + std::to_string(block_number); + return calc_id(fc::sha256::hash(id_str.c_str()), block_number); + } + + explicit operator block_ref() const { + return block_ref{calculate_id(), timestamp()}; + } +}; + +// --------------------------------------------------------------------------------------- +bsp make_bsp(const proposal_t& p, const bsp& previous, finalizer_policy_ptr finpol, + std::optional claim = {}) { + auto makeit = [](bhs &&h) { + bs new_bs; + dynamic_cast(new_bs) = std::move(h); + new_bs.validated = true; + return std::make_shared(std::move(new_bs)); + }; + + if (p.block_num() == 0) { + // special case of genesis block + block_ref ref{calc_id(fc::sha256::hash("genesis"), 0), block_timestamp_type{0}}; + bhs new_bhs { ref.block_id, block_header{ref.timestamp}, {}, + finality_core::create_core_for_genesis_block(0), {}, {}, std::move(finpol) }; + return makeit(std::move(new_bhs)); + } + + assert(claim); + block_ref ref{previous->id(), previous->timestamp()}; + bhs new_bhs { p.calculate_id(), block_header{p.block_timestamp, {}, {}, previous->id()}, {}, previous->core.next(ref, *claim), + {}, {}, std::move(finpol) }; + return makeit(std::move(new_bhs)); +} + +// --------------------------------------------------------------------------------------- +// simulates one finalizer voting on its own proposals "n0", and other proposals received +// from the network. +struct simulator_t { + using core = finality_core; + + bls_keys_t keys; + finalizer my_finalizer; + fork_database_if_t forkdb; + finalizer_policy_ptr finpol; + std::vector bsp_vec; + + struct result { + bsp new_bsp; + vote_result vote; + + qc_claim_t new_claim() const { + if (vote.decision == vote_decision::no_vote) + return new_bsp->core.latest_qc_claim(); + return { new_bsp->block_num(), vote.decision == vote_decision::strong_vote }; + } + }; + + simulator_t() : + keys("alice"_n), + my_finalizer(keys.privkey) { + + finalizer_policy fin_policy; + fin_policy.threshold = 0; + fin_policy.finalizers.push_back({"n0", 1, keys.pubkey}); + finpol = std::make_shared(fin_policy); + + auto genesis = make_bsp(proposal_t{0, "n0"}, bsp(), finpol); + bsp_vec.push_back(genesis); + forkdb.reset(*genesis); + + block_ref genesis_ref(genesis->id(), genesis->timestamp()); + my_finalizer.fsi = fsi_t{block_timestamp_type(0), genesis_ref, genesis_ref}; + } + + vote_result vote(const bhsp& p) { + auto vote_res = my_finalizer.decide_vote(p->core, p->id(), p->timestamp()); + return vote_res; + } + + vote_result propose(const proposal_t& p, std::optional _claim = {}) { + bsp h = forkdb.head(); + qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); + bsp new_bsp = make_bsp(p, h, finpol, old_claim); + bsp_vec.push_back(new_bsp); + auto v = vote(new_bsp); + return v; + } + + result add(const proposal_t& p, std::optional _claim = {}, const bsp& parent = {}) { + bsp h = parent ? parent : forkdb.head(); + qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); + bsp new_bsp = make_bsp(p, h, finpol, old_claim); + bsp_vec.push_back(new_bsp); + forkdb.add(new_bsp); + + auto v = vote(new_bsp); + return { new_bsp, v }; + } +}; + +#if 0 auto proposals { create_proposal_refs(10) }; fsi_t fsi { .last_vote_range_start = tstamp(0), .last_vote = proposals[6], .lock = proposals[2] }; - using namespace mock_utils; - simulator_t sim; - - auto vote = sim.propose(proposal_t{1, "n0", block_timestamp_type{1}}); - BOOST_CHECK(vote && *vote); - //bls_keys_t k("alice"_n); - //test_finalizer_t finalizer{k.privkey, fsi}; +#endif +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_basic ) try { + simulator_t sim; + // this proposal verifies all properties and extends genesis => expect strong vote + auto res = sim.add({1, "n0"}); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); +} FC_LOG_AND_RETHROW() +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_no_vote_if_finalizer_safety_lock_empty ) try { + simulator_t sim; + sim.my_finalizer.fsi.lock = {}; // force lock empty... finalizer should not vote + auto res = sim.add({1, "n0"}); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); } FC_LOG_AND_RETHROW() -#endif // --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( proposal_sim_1 ) try { - using namespace mock_utils; +BOOST_AUTO_TEST_CASE( decide_vote_normal_vote_sequence ) try { + simulator_t sim; + qc_claim_t new_claim { 0, true }; + for (uint32_t i=1; i<10; ++i) { + auto res = sim.add({i, "n0"}, new_claim); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); + new_claim = { res.new_bsp->block_num(), res.vote.decision == vote_decision::strong_vote }; + + auto lib { res.new_bsp->core.last_final_block_num() }; + BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); + + auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; + BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); + } +} FC_LOG_AND_RETHROW() - simulator_t sim; +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { + simulator_t sim; - auto [head1, claim1] = sim.add(proposal_t{1, "n0", block_timestamp_type{1}}, bhs_core::qc_claim{0, false}); - BOOST_CHECK_EQUAL(claim1.block_num, 1); + auto res = sim.add({1, "n0", 1}); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - auto [head2, claim2] = sim.add(proposal_t{2, "n0", block_timestamp_type{2}}, claim1); - BOOST_CHECK_EQUAL(claim2.block_num, 2); + auto res2 = sim.add({2, "n0", 1}); + BOOST_CHECK_EQUAL(res2.vote.monotony_check, false); + BOOST_CHECK(res2.vote.decision == vote_decision::no_vote); // use same timestamp as previous proposal => should not vote } FC_LOG_AND_RETHROW() + + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_liveness_check ) try { + simulator_t sim; + qc_claim_t new_claim { 0, true }; + for (uint32_t i=1; i<10; ++i) { + auto res = sim.add({i, "n0", i}, new_claim); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); + new_claim = res.new_claim(); + + auto lib { res.new_bsp->core.last_final_block_num() }; + BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); + + auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; + BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); + + if (i > 2) + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[i-2]->id()); + } + + // we just issued proposal #9. Verify we are locked on proposal #7 and our last_vote is #9 + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9); + + // proposal #6 from "n0" is final (although "n1" may not know it yet). + // proposal #7 would be final if it receives a strong QC + + // let's have "n1" build on proposal #6. Default will use timestamp(7) so we will fail the monotony check + auto res = sim.add({7, "n1"}, {}, sim.bsp_vec[6]); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + // les's vote for a couple more proposale, and finally when we'll reach timestamp 10 the + // monotony check will pass (both liveness and safety check should still fail) + // ------------------------------------------------------------------------------------ + res = sim.add({8, "n1"}, {}, res.new_bsp); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + res = sim.add({9, "n1"}, {}, res.new_bsp); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + res = sim.add({10, "n1"}, {}, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, false); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); + + // No matter how long we keep voting on this branch without a new qc claim, we will never achieve + // liveness or safety again + // ---------------------------------------------------------------------------------------------- + for (uint32_t i=11; i<20; ++i) { + res = sim.add({i, "n1"}, {}, res.new_bsp); + + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, false); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); + } + + // Now suppose we receive a qc in a block that was created in the "n0" branch, for example the qc from + // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim() + // liveness should be restored, because core.latest_qc_block_timestamp() > fsi.lock.timestamp + // --------------------------------------------------------------------------------------------------- + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9); + new_claim = sim.bsp_vec[9]->core.latest_qc_claim(); + res = sim.add({20, "n1"}, new_claim, res.new_bsp); + + + BOOST_CHECK(res.vote.decision == vote_decision::weak_vote); // because !time_range_disjoint and fsi.last_vote == 9 + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + + new_claim = res.new_claim(); + res = sim.add({21, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); // because core.extends(fsi.last_vote.block_id); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 21); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + + // this new proposal we just voted strong on was just building on proposal #6 and we had not advanced + // the core until the last proposal which provided a new qc_claim_t. + // as a result we now have a final_on_strong_qc = 5 (because the vote on 20 was weak) + // -------------------------------------------------------------------------------------------------- + auto final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 5); + + // Our finalizer should still be locked on the initial proposal 7 (we have not updated our lock because + // `(final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp)` is false + // ---------------------------------------------------------------------------------------------------- + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); + + new_claim = res.new_claim(); + res = sim.add({22, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 22); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 20); + + +#if 0 + std::cout << final_on_strong_qc << '\n'; + auto& ref { res.new_bsp->core.get_block_reference(final_on_strong_qc) }; + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.timestamp, ref.timestamp); + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, ref.block_id); + + + //res = sim.add({10, "n1", 12}, {}, res.new_bsp); + + std::cout << res.vote << '\n'; #endif +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/mock_utils.hpp b/unittests/mock_utils.hpp deleted file mode 100644 index 828c178d8d..0000000000 --- a/unittests/mock_utils.hpp +++ /dev/null @@ -1,175 +0,0 @@ -#pragma once - -#include "bhs_core.hpp" -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace mock_utils { - -using namespace eosio; -using namespace eosio::chain; - -inline block_id_type calc_id(block_id_type id, uint32_t block_number) { - id._hash[0] &= 0xffffffff00000000; - id._hash[0] += fc::endian_reverse_u32(block_number); // store the block num in the ID, 160 bits is plenty for the hash - return id; -} - - -// --------------------------------------------------------------------------------------- -// emulations of block_header_state and fork_database sufficient for instantiating a -// finalizer. -// --------------------------------------------------------------------------------------- -struct bhs : bhs_core::core { - block_id_type block_id; - block_id_type previous_block; - block_timestamp_type block_timestamp; - - uint32_t block_num() const { return block_header::num_from_id(block_id); } - const block_id_type& id() const { return block_id; } - const block_id_type& previous() const { return previous_block; } - block_timestamp_type timestamp() const { return block_timestamp; } - bool is_valid() const { return true; } - uint32_t irreversible_blocknum() const { return last_final_block_num(); } - - static bhs genesis_bhs() { - return bhs{ bhs_core::core{{bhs_core::qc_link{0, 0, false}}, {}, 0}, - calc_id(fc::sha256::hash("genesis"), 0), - block_id_type{}, - block_timestamp_type{0} - }; - } -}; - -using bhsp = std::shared_ptr; - -// --------------------------------------------------------------------------------------- -struct bs : public bhs { - bs() : bhs(genesis_bhs()) {} - bs(const bhs& h) : bhs(h) {} - - uint32_t block_num() const { return bhs::block_num(); } - const block_id_type& id() const { return bhs::id(); } - const block_id_type& previous() const { return bhs::previous(); } - bool is_valid() const { return true; } - uint32_t irreversible_blocknum() const { return bhs::irreversible_blocknum(); } - - explicit operator bhs_core::block_ref() const { - return bhs_core::block_ref{id(), timestamp()}; - } -}; - -using bsp = std::shared_ptr; - -// --------------------------------------------------------------------------------------- -struct proposal_t { - uint32_t block_number; - std::string proposer_name; - block_timestamp_type block_timestamp; - - const std::string& proposer() const { return proposer_name; } - block_timestamp_type timestamp() const { return block_timestamp; } - uint32_t block_num() const { return block_number; } - - block_id_type calculate_id() const - { - std::string id_str = proposer_name + std::to_string(block_number); - return calc_id(fc::sha256::hash(id_str.c_str()), block_number); - } - - explicit operator bhs_core::block_ref() const { - return bhs_core::block_ref{calculate_id(), timestamp()}; - } -}; - -// --------------------------------------------------------------------------------------- -bsp make_bsp(const mock_utils::proposal_t& p, const bsp& previous, std::optional claim = {}) { - if (p.block_num() == 0) { - // genesis block - return std::make_shared(); - } - assert(claim); - bhs_core::block_ref ref(*previous); - return std::make_shared(bhs{previous->next(ref, *claim), ref.block_id, previous->id(), p.timestamp() }); -} - -// --------------------------------------------------------------------------------------- -struct forkdb_t { - using bsp = bsp; - using bs = bsp::element_type; - using bhsp = bhsp; - using bhs = bhsp::element_type; - using full_branch_type = std::vector; - - struct by_block_id; - struct by_lib_block_num; - struct by_prev; - - using fork_multi_index_type = boost::multi_index::multi_index_container< - bsp, - indexed_by, - BOOST_MULTI_INDEX_CONST_MEM_FUN(bs, const block_id_type&, id), - std::hash>, - ordered_non_unique, const_mem_fun>, - ordered_unique, - composite_key, - composite_key_compare, - std::greater, - std::greater, - sha256_less>>>>; - - fork_multi_index_type index; - bsp head_; - bsp root_; - - bsp root() const { return root_; } - bsp head() const { return head_; } - - void add(const bsp& n) { - auto inserted = index.insert(n); - if( !inserted.second ) - return; - if (index.size() == 1) - root_= n; - auto candidate = index.template get().begin(); - if( (*candidate)->is_valid() ) { - head_ = *candidate; - } - } - - bsp get_block_impl(const block_id_type& id) const { - auto itr = index.find( id ); - if( itr != index.end() ) - return *itr; - return bsp(); - } - - full_branch_type fetch_full_branch(const block_id_type& id) const { - full_branch_type result; - result.reserve(10); - for (auto s = get_block_impl(id); s; s = get_block_impl(s->previous())) { - result.push_back(s); - } - return result; - }; - -}; - - - - - - -} // namespace mock_utils From fcf0e2dc0b84a242324678b9f67fa7caabd160af Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 13:53:08 -0500 Subject: [PATCH 12/18] cleanup before PR. --- unittests/finalizer_tests.cpp | 126 +++++----------------------------- 1 file changed, 18 insertions(+), 108 deletions(-) diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 8cf1ee2170..0bf6775c7b 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -170,93 +170,6 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() -#if 0 -// real finalizer, using mock::forkdb and mock::bsp -// using test_finalizer_t = finalizer_tpl; - -block_state_ptr make_bsp(const mock_utils::proposal_t& p, const block_state_ptr& head, - std::optional claim = {}) { - block_header_state bhs; - auto id = p.calculate_id(); - // genesis block - block_header_state_core new_core; - if (p.block_num() > 0) - new_core = claim ? head->core.next(*claim) : head->core; - bhs = block_header_state{ .block_id = id, - .header = block_header(), - .activated_protocol_features = {}, - .core = new_core }; - block_state_ptr bsp = std::make_shared(block_state{bhs, {}, {}, {}}); - return bsp; -} - -// --------------------------------------------------------------------------------------- -template -struct simulator_t { - using finalizer_t = finalizer_tpl; - using bs = typename FORKDB::bs; - using bsp = typename FORKDB::bsp; - - bls_keys_t keys; - FORKDB forkdb; - finalizer_t finalizer; - - simulator_t() : - keys("alice"_n), - finalizer(keys.privkey) { - - auto genesis = make_bsp(mock_utils::proposal_t{0, "n0", block_timestamp_type{0}}, bsp()); - forkdb.add(genesis); - - proposal_ref genesis_ref(genesis); - finalizer.fsi = fsi_t{block_timestamp_type(0), genesis_ref, {}}; - } - - std::optional vote(const bsp& p) { - auto decision = finalizer.decide_vote(p, forkdb); - switch(decision) { - case finalizer_t::vote_decision::strong_vote: return true; - case finalizer_t::vote_decision::weak_vote: return false; - default: break; - } - return {}; - } - - std::optional propose(const PROPOSAL& p) { - bsp h = make_bsp(p, forkdb.head()); - forkdb.add(h); - auto v = vote(h); - return v; - } - - std::pair add(const PROPOSAL& p, std::optional _claim = {}) { - bsp h = forkdb.head(); - bhs_core::qc_claim old_claim = _claim ? *_claim : bhs_core::qc_claim{h->last_qc_block_num(), false}; - bsp new_bsp = make_bsp(p, h, _claim); - forkdb.add(new_bsp); - - auto v = vote(new_bsp); - if (v) - return {forkdb.head(), new_bsp->latest_qc_claim()}; - return {forkdb.head(), old_claim}; - } -}; - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( proposal_sim_1 ) try { - using namespace mock_utils; - - simulator_t sim; - - auto [head1, claim1] = sim.add(proposal_t{1, "n0", block_timestamp_type{1}}, bhs_core::qc_claim{0, false}); - BOOST_CHECK_EQUAL(claim1.block_num, 1); - - auto [head2, claim2] = sim.add(proposal_t{2, "n0", block_timestamp_type{2}}, claim1); - BOOST_CHECK_EQUAL(claim2.block_num, 2); - -} FC_LOG_AND_RETHROW() -#endif - using eosio::chain::finality_core; using eosio::chain::block_ref; using bs = eosio::chain::block_state; @@ -266,7 +179,6 @@ using bhsp = eosio::chain::block_header_state_ptr; using vote_decision = finalizer::vote_decision; using vote_result = finalizer::vote_result; - // --------------------------------------------------------------------------------------- inline block_id_type calc_id(block_id_type id, uint32_t block_number) { id._hash[0] &= 0xffffffff00000000; @@ -390,13 +302,6 @@ struct simulator_t { } }; -#if 0 - auto proposals { create_proposal_refs(10) }; - fsi_t fsi { .last_vote_range_start = tstamp(0), - .last_vote = proposals[6], - .lock = proposals[2] }; -#endif - // --------------------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE( decide_vote_basic ) try { simulator_t sim; @@ -446,7 +351,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { // --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_liveness_check ) try { +BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { simulator_t sim; qc_claim_t new_claim { 0, true }; for (uint32_t i=1; i<10; ++i) { @@ -512,7 +417,6 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_check ) try { new_claim = sim.bsp_vec[9]->core.latest_qc_claim(); res = sim.add({20, "n1"}, new_claim, res.new_bsp); - BOOST_CHECK(res.vote.decision == vote_decision::weak_vote); // because !time_range_disjoint and fsi.last_vote == 9 BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20); BOOST_CHECK_EQUAL(res.vote.monotony_check, true); @@ -539,6 +443,10 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_check ) try { // ---------------------------------------------------------------------------------------------------- BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); + // this new strong vote will finally advance the final_on_strong_qc thanks to the chain + // weak 20 - strong 21 (meaning that if we get a strong QC on 22, 20 becomes final, so the core of + // 22 has a final_on_strong_qc = 20. + // ----------------------------------------------------------------------------------------------- new_claim = res.new_claim(); res = sim.add({22, "n1"}, new_claim, res.new_bsp); BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); @@ -548,19 +456,21 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_check ) try { BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; BOOST_CHECK_EQUAL(final_on_strong_qc, 20); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 4); + // OK, add one proposal + strong vote. This should finally move lib to 20 + // ---------------------------------------------------------------------- + new_claim = res.new_claim(); + res = sim.add({23, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 23); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 21); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 20); -#if 0 - std::cout << final_on_strong_qc << '\n'; - auto& ref { res.new_bsp->core.get_block_reference(final_on_strong_qc) }; - BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.timestamp, ref.timestamp); - BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, ref.block_id); - - - //res = sim.add({10, "n1", 12}, {}, res.new_bsp); - - std::cout << res.vote << '\n'; -#endif } FC_LOG_AND_RETHROW() From a978d98348a33d57df744a56c3bcd0eadd445fb9 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 14:05:13 -0500 Subject: [PATCH 13/18] Remove changes not needed anymore in fork_database. --- libraries/chain/fork_database.cpp | 149 +++++++++--------- .../include/eosio/chain/fork_database.hpp | 3 +- 2 files changed, 75 insertions(+), 77 deletions(-) diff --git a/libraries/chain/fork_database.cpp b/libraries/chain/fork_database.cpp index 23e26e4365..09c08aba5e 100644 --- a/libraries/chain/fork_database.cpp +++ b/libraries/chain/fork_database.cpp @@ -30,9 +30,8 @@ namespace eosio::chain { return std::pair(lhs.irreversible_blocknum(), lhs.block_num()) > std::pair(rhs.irreversible_blocknum(), rhs.block_num()); } - template // either [block_state_legacy_ptr, block_state_ptr], same with block_header_state_ptr + template // either [block_state_legacy_ptr, block_state_ptr], same with block_header_state_ptr struct fork_database_impl { - using bsp = BSP; using bs = bsp::element_type; using bhsp = bs::bhsp_t; using bhs = bhsp::element_type; @@ -81,20 +80,20 @@ namespace eosio::chain { }; - template - fork_database_t::fork_database_t(uint32_t magic_number) - :my( new fork_database_impl(magic_number) ) + template + fork_database_t::fork_database_t(uint32_t magic_number) + :my( new fork_database_impl(magic_number) ) {} - template - void fork_database_t::open( const std::filesystem::path& fork_db_file, validator_t& validator ) { + template + void fork_database_t::open( const std::filesystem::path& fork_db_file, validator_t& validator ) { std::lock_guard g( my->mtx ); my->open_impl( fork_db_file, validator ); } - template - void fork_database_impl::open_impl( const std::filesystem::path& fork_db_file, validator_t& validator ) { + template + void fork_database_impl::open_impl( const std::filesystem::path& fork_db_file, validator_t& validator ) { if( std::filesystem::exists( fork_db_file ) ) { try { string content; @@ -163,19 +162,19 @@ namespace eosio::chain { } } - template - fork_database_t::~fork_database_t() { + template + fork_database_t::~fork_database_t() { // close is performed in fork_database::~fork_database() } - template - void fork_database_t::close(const std::filesystem::path& fork_db_file) { + template + void fork_database_t::close(const std::filesystem::path& fork_db_file) { std::lock_guard g( my->mtx ); my->close_impl(fork_db_file); } - template - void fork_database_impl::close_impl(const std::filesystem::path& fork_db_file) { + template + void fork_database_impl::close_impl(const std::filesystem::path& fork_db_file) { if( !root ) { if( index.size() > 0 ) { elog( "fork_database is in a bad state when closing; not writing out '${filename}'", @@ -236,14 +235,14 @@ namespace eosio::chain { index.clear(); } - template - void fork_database_t::reset( const bhs& root_bhs ) { + template + void fork_database_t::reset( const bhs& root_bhs ) { std::lock_guard g( my->mtx ); my->reset_impl(root_bhs); } - template - void fork_database_impl::reset_impl( const bhs& root_bhs ) { + template + void fork_database_impl::reset_impl( const bhs& root_bhs ) { index.clear(); root = std::make_shared(); static_cast(*root) = root_bhs; @@ -251,14 +250,14 @@ namespace eosio::chain { head = root; } - template - void fork_database_t::rollback_head_to_root() { + template + void fork_database_t::rollback_head_to_root() { std::lock_guard g( my->mtx ); my->rollback_head_to_root_impl(); } - template - void fork_database_impl::rollback_head_to_root_impl() { + template + void fork_database_impl::rollback_head_to_root_impl() { auto& by_id_idx = index.template get(); auto itr = by_id_idx.begin(); while (itr != by_id_idx.end()) { @@ -270,14 +269,14 @@ namespace eosio::chain { head = root; } - template - void fork_database_t::advance_root( const block_id_type& id ) { + template + void fork_database_t::advance_root( const block_id_type& id ) { std::lock_guard g( my->mtx ); my->advance_root_impl( id ); } - template - void fork_database_impl::advance_root_impl( const block_id_type& id ) { + template + void fork_database_impl::advance_root_impl( const block_id_type& id ) { EOS_ASSERT( root, fork_database_exception, "root not yet set" ); auto new_root = get_block_impl( id ); @@ -311,14 +310,14 @@ namespace eosio::chain { root = new_root; } - template - fork_database_t::bhsp fork_database_t::get_block_header( const block_id_type& id ) const { + template + fork_database_t::bhsp fork_database_t::get_block_header( const block_id_type& id ) const { std::lock_guard g( my->mtx ); return my->get_block_header_impl( id ); } - template - fork_database_impl::bhsp fork_database_impl::get_block_header_impl( const block_id_type& id ) const { + template + fork_database_impl::bhsp fork_database_impl::get_block_header_impl( const block_id_type& id ) const { if( root->id() == id ) { return root; } @@ -330,8 +329,8 @@ namespace eosio::chain { return bhsp(); } - template - void fork_database_impl::add_impl(const bsp& n, bool ignore_duplicate, bool validate, validator_t& validator) { + template + void fork_database_impl::add_impl(const bsp& n, bool ignore_duplicate, bool validate, validator_t& validator) { EOS_ASSERT( root, fork_database_exception, "root not yet set" ); EOS_ASSERT( n, fork_database_exception, "attempt to add null block state" ); @@ -368,8 +367,8 @@ namespace eosio::chain { } } - template - void fork_database_t::add( const bsp& n, bool ignore_duplicate ) { + template + void fork_database_t::add( const bsp& n, bool ignore_duplicate ) { std::lock_guard g( my->mtx ); my->add_impl( n, ignore_duplicate, false, []( block_timestamp_type timestamp, @@ -379,20 +378,20 @@ namespace eosio::chain { ); } - template - BSP fork_database_t::root() const { + template + bsp fork_database_t::root() const { std::lock_guard g( my->mtx ); return my->root; } - template - BSP fork_database_t::head() const { + template + bsp fork_database_t::head() const { std::lock_guard g( my->mtx ); return my->head; } - template - BSP fork_database_t::pending_head() const { + template + bsp fork_database_t::pending_head() const { std::lock_guard g( my->mtx ); const auto& indx = my->index.template get(); @@ -405,16 +404,16 @@ namespace eosio::chain { return my->head; } - template - fork_database_t::branch_type - fork_database_t::fetch_branch(const block_id_type& h, uint32_t trim_after_block_num) const { + template + fork_database_t::branch_type + fork_database_t::fetch_branch(const block_id_type& h, uint32_t trim_after_block_num) const { std::lock_guard g(my->mtx); return my->fetch_branch_impl(h, trim_after_block_num); } - template - fork_database_t::branch_type - fork_database_impl::fetch_branch_impl(const block_id_type& h, uint32_t trim_after_block_num) const { + template + fork_database_t::branch_type + fork_database_impl::fetch_branch_impl(const block_id_type& h, uint32_t trim_after_block_num) const { branch_type result; result.reserve(index.size()); for (auto s = get_block_impl(h); s; s = get_block_impl(s->previous())) { @@ -425,16 +424,16 @@ namespace eosio::chain { return result; } - template - fork_database_t::full_branch_type - fork_database_t::fetch_full_branch(const block_id_type& h) const { + template + fork_database_t::full_branch_type + fork_database_t::fetch_full_branch(const block_id_type& h) const { std::lock_guard g(my->mtx); return my->fetch_full_branch_impl(h); } - template - fork_database_t::full_branch_type - fork_database_impl::fetch_full_branch_impl(const block_id_type& h) const { + template + fork_database_t::full_branch_type + fork_database_impl::fetch_full_branch_impl(const block_id_type& h) const { full_branch_type result; result.reserve(index.size()); for (auto s = get_block_impl(h); s; s = get_block_impl(s->previous())) { @@ -444,14 +443,14 @@ namespace eosio::chain { return result; } - template - BSP fork_database_t::search_on_branch( const block_id_type& h, uint32_t block_num ) const { + template + bsp fork_database_t::search_on_branch( const block_id_type& h, uint32_t block_num ) const { std::lock_guard g( my->mtx ); return my->search_on_branch_impl( h, block_num ); } - template - BSP fork_database_impl::search_on_branch_impl( const block_id_type& h, uint32_t block_num ) const { + template + bsp fork_database_impl::search_on_branch_impl( const block_id_type& h, uint32_t block_num ) const { for( auto s = get_block_impl(h); s; s = get_block_impl( s->previous() ) ) { if( s->block_num() == block_num ) return s; @@ -464,16 +463,16 @@ namespace eosio::chain { * Given two head blocks, return two branches of the fork graph that * end with a common ancestor (same prior block) */ - template - fork_database_t::branch_type_pair - fork_database_t::fetch_branch_from(const block_id_type& first, const block_id_type& second) const { + template + fork_database_t::branch_type_pair + fork_database_t::fetch_branch_from(const block_id_type& first, const block_id_type& second) const { std::lock_guard g(my->mtx); return my->fetch_branch_from_impl(first, second); } - template - fork_database_t::branch_type_pair - fork_database_impl::fetch_branch_from_impl(const block_id_type& first, const block_id_type& second) const { + template + fork_database_t::branch_type_pair + fork_database_impl::fetch_branch_from_impl(const block_id_type& first, const block_id_type& second) const { pair result; auto first_branch = (first == root->id()) ? root : get_block_impl(first); auto second_branch = (second == root->id()) ? root : get_block_impl(second); @@ -532,14 +531,14 @@ namespace eosio::chain { } /// fetch_branch_from_impl /// remove all of the invalid forks built off of this id including this id - template - void fork_database_t::remove( const block_id_type& id ) { + template + void fork_database_t::remove( const block_id_type& id ) { std::lock_guard g( my->mtx ); return my->remove_impl( id ); } - template - void fork_database_impl::remove_impl( const block_id_type& id ) { + template + void fork_database_impl::remove_impl( const block_id_type& id ) { deque remove_queue{id}; const auto& previdx = index.template get(); const auto& head_id = head->id(); @@ -560,14 +559,14 @@ namespace eosio::chain { } } - template - void fork_database_t::mark_valid( const bsp& h ) { + template + void fork_database_t::mark_valid( const bsp& h ) { std::lock_guard g( my->mtx ); my->mark_valid_impl( h ); } - template - void fork_database_impl::mark_valid_impl( const bsp& h ) { + template + void fork_database_impl::mark_valid_impl( const bsp& h ) { if( h->is_valid() ) return; auto& by_id_idx = index.template get(); @@ -587,14 +586,14 @@ namespace eosio::chain { } } - template - BSP fork_database_t::get_block(const block_id_type& id) const { + template + bsp fork_database_t::get_block(const block_id_type& id) const { std::lock_guard g( my->mtx ); return my->get_block_impl(id); } - template - BSP fork_database_impl::get_block_impl(const block_id_type& id) const { + template + bsp fork_database_impl::get_block_impl(const block_id_type& id) const { auto itr = index.find( id ); if( itr != index.end() ) return *itr; diff --git a/libraries/chain/include/eosio/chain/fork_database.hpp b/libraries/chain/include/eosio/chain/fork_database.hpp index 708f8d5b8e..97020d0a38 100644 --- a/libraries/chain/include/eosio/chain/fork_database.hpp +++ b/libraries/chain/include/eosio/chain/fork_database.hpp @@ -24,13 +24,12 @@ namespace eosio::chain { * fork_database should be used instead of fork_database_t directly as it manages * the different supported types. */ - template // either block_state_legacy_ptr or block_state_ptr + template // either block_state_legacy_ptr or block_state_ptr class fork_database_t { public: static constexpr uint32_t legacy_magic_number = 0x30510FDB; static constexpr uint32_t magic_number = 0x4242FDB; - using bsp = BSP; using bs = bsp::element_type; using bhsp = bs::bhsp_t; using bhs = bhsp::element_type; From c90f26d4334e7d5d4b0582b17538b8ca238e82f6 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 14:11:29 -0500 Subject: [PATCH 14/18] Remove unused variable. --- libraries/chain/hotstuff/finalizer.cpp | 2 -- libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index 61182e9bbf..fe2cafccd8 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -44,8 +44,6 @@ finalizer::vote_result finalizer::decide_vote(const finality_core& core, const b // Figure out if we can vote and wether our vote will be strong or weak // If we vote, update `fsi.last_vote` and also `fsi.lock` if we have a newer commit qc // ----------------------------------------------------------------------------------- - vote_decision decision = vote_decision::no_vote; - if (can_vote) { auto [p_start, p_end] = std::make_pair(core.latest_qc_block_timestamp(), proposal_timestamp); diff --git a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp index fc5eac4b12..112ccd8cdb 100644 --- a/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp +++ b/libraries/chain/include/eosio/chain/hotstuff/finalizer.hpp @@ -48,7 +48,7 @@ namespace eosio::chain { // ---------------------------------------------------------------------------------------- struct finalizer { - enum class vote_decision { strong_vote, weak_vote, no_vote }; + enum class vote_decision { no_vote, strong_vote, weak_vote }; struct vote_result { vote_decision decision {vote_decision::no_vote}; bool safety_check {false}; From 4865ef773e35901de602443619bbf1733f806f97 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 15:38:31 -0500 Subject: [PATCH 15/18] Remove unneeded includes, fix g++ compilation warnings. --- unittests/finalizer_tests.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 0bf6775c7b..c2ad81e737 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -1,6 +1,4 @@ -#include #include -#include #include #include @@ -372,7 +370,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { // we just issued proposal #9. Verify we are locked on proposal #7 and our last_vote is #9 BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); // proposal #6 from "n0" is final (although "n1" may not know it yet). // proposal #7 would be final if it receives a strong QC @@ -413,12 +411,12 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim() // liveness should be restored, because core.latest_qc_block_timestamp() > fsi.lock.timestamp // --------------------------------------------------------------------------------------------------- - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); new_claim = sim.bsp_vec[9]->core.latest_qc_claim(); res = sim.add({20, "n1"}, new_claim, res.new_bsp); BOOST_CHECK(res.vote.decision == vote_decision::weak_vote); // because !time_range_disjoint and fsi.last_vote == 9 - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20u); BOOST_CHECK_EQUAL(res.vote.monotony_check, true); BOOST_CHECK_EQUAL(res.vote.liveness_check, true); BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. @@ -426,7 +424,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { new_claim = res.new_claim(); res = sim.add({21, "n1"}, new_claim, res.new_bsp); BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); // because core.extends(fsi.last_vote.block_id); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 21); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 21u); BOOST_CHECK_EQUAL(res.vote.monotony_check, true); BOOST_CHECK_EQUAL(res.vote.liveness_check, true); BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. @@ -436,7 +434,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { // as a result we now have a final_on_strong_qc = 5 (because the vote on 20 was weak) // -------------------------------------------------------------------------------------------------- auto final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 5); + BOOST_CHECK_EQUAL(final_on_strong_qc, 5u); // Our finalizer should still be locked on the initial proposal 7 (we have not updated our lock because // `(final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp)` is false @@ -450,26 +448,26 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { new_claim = res.new_claim(); res = sim.add({22, "n1"}, new_claim, res.new_bsp); BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 22); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 22u); BOOST_CHECK_EQUAL(res.vote.monotony_check, true); BOOST_CHECK_EQUAL(res.vote.liveness_check, true); BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 20); - BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 4); + BOOST_CHECK_EQUAL(final_on_strong_qc, 20u); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 4u); // OK, add one proposal + strong vote. This should finally move lib to 20 // ---------------------------------------------------------------------- new_claim = res.new_claim(); res = sim.add({23, "n1"}, new_claim, res.new_bsp); BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 23); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 23u); BOOST_CHECK_EQUAL(res.vote.monotony_check, true); BOOST_CHECK_EQUAL(res.vote.liveness_check, true); BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 21); - BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 20); + BOOST_CHECK_EQUAL(final_on_strong_qc, 21u); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 20u); } FC_LOG_AND_RETHROW() From 8871942d4c64762af4dd02dbe73c1262ecb07e9d Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Thu, 29 Feb 2024 15:41:28 -0500 Subject: [PATCH 16/18] Fix typo. --- unittests/finalizer_tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index c2ad81e737..133f9a1dc7 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -380,7 +380,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { BOOST_CHECK(res.vote.decision == vote_decision::no_vote); BOOST_CHECK_EQUAL(res.vote.monotony_check, false); - // les's vote for a couple more proposale, and finally when we'll reach timestamp 10 the + // let's vote for a couple more proposals, and finally when we'll reach timestamp 10 the // monotony check will pass (both liveness and safety check should still fail) // ------------------------------------------------------------------------------------ res = sim.add({8, "n1"}, {}, res.new_bsp); @@ -408,7 +408,7 @@ BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { } // Now suppose we receive a qc in a block that was created in the "n0" branch, for example the qc from - // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim() + // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim(). // liveness should be restored, because core.latest_qc_block_timestamp() > fsi.lock.timestamp // --------------------------------------------------------------------------------------------------- BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); From b256eacf54293c3dc41933dff2ed194bf003bc37 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 1 Mar 2024 16:02:58 -0500 Subject: [PATCH 17/18] Move new voting tests to a separate file as suggested. --- unittests/finalizer_tests.cpp | 304 -------------------------- unittests/finalizer_vote_tests.cpp | 332 +++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 304 deletions(-) create mode 100644 unittests/finalizer_vote_tests.cpp diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 125af0c5dd..95a49c794f 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -1,10 +1,8 @@ -#include "eosio/chain/fork_database.hpp" #include #include #include #include -#include using namespace eosio; using namespace eosio::chain; @@ -169,307 +167,5 @@ BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() -using eosio::chain::finality_core; -using eosio::chain::block_ref; -using bs = eosio::chain::block_state; -using bsp = eosio::chain::block_state_ptr; -using bhs = eosio::chain::block_header_state; -using bhsp = eosio::chain::block_header_state_ptr; -using vote_decision = finalizer::vote_decision; -using vote_result = finalizer::vote_result; - -// --------------------------------------------------------------------------------------- -inline block_id_type calc_id(block_id_type id, uint32_t block_number) { - id._hash[0] &= 0xffffffff00000000; - id._hash[0] += fc::endian_reverse_u32(block_number); - return id; -} - -// --------------------------------------------------------------------------------------- -struct proposal_t { - uint32_t block_number; - std::string proposer_name; - block_timestamp_type block_timestamp; - - proposal_t(uint32_t block_number, const char* proposer, std::optional timestamp = {}) : - block_number(block_number), proposer_name(proposer), block_timestamp(timestamp ? *timestamp : block_number) - {} - - const std::string& proposer() const { return proposer_name; } - block_timestamp_type timestamp() const { return block_timestamp; } - uint32_t block_num() const { return block_number; } - - block_id_type calculate_id() const - { - std::string id_str = proposer_name + std::to_string(block_number); - return calc_id(fc::sha256::hash(id_str.c_str()), block_number); - } - - explicit operator block_ref() const { - return block_ref{calculate_id(), timestamp()}; - } -}; - -// --------------------------------------------------------------------------------------- -bsp make_bsp(const proposal_t& p, const bsp& previous, finalizer_policy_ptr finpol, - std::optional claim = {}) { - auto makeit = [](bhs &&h) { - bs new_bs; - dynamic_cast(new_bs) = std::move(h); - return std::make_shared(std::move(new_bs)); - }; - - if (p.block_num() == 0) { - // special case of genesis block - block_ref ref{calc_id(fc::sha256::hash("genesis"), 0), block_timestamp_type{0}}; - bhs new_bhs { ref.block_id, block_header{ref.timestamp}, {}, - finality_core::create_core_for_genesis_block(0), {}, {}, std::move(finpol) }; - return makeit(std::move(new_bhs)); - } - - assert(claim); - block_ref ref{previous->id(), previous->timestamp()}; - bhs new_bhs { p.calculate_id(), block_header{p.block_timestamp, {}, {}, previous->id()}, {}, previous->core.next(ref, *claim), - {}, {}, std::move(finpol) }; - return makeit(std::move(new_bhs)); -} - -// --------------------------------------------------------------------------------------- -// simulates one finalizer voting on its own proposals "n0", and other proposals received -// from the network. -struct simulator_t { - using core = finality_core; - - bls_keys_t keys; - finalizer my_finalizer; - fork_database_if_t forkdb; - finalizer_policy_ptr finpol; - std::vector bsp_vec; - - struct result { - bsp new_bsp; - vote_result vote; - - qc_claim_t new_claim() const { - if (vote.decision == vote_decision::no_vote) - return new_bsp->core.latest_qc_claim(); - return { new_bsp->block_num(), vote.decision == vote_decision::strong_vote }; - } - }; - - simulator_t() : - keys("alice"_n), - my_finalizer(keys.privkey) { - - finalizer_policy fin_policy; - fin_policy.threshold = 0; - fin_policy.finalizers.push_back({"n0", 1, keys.pubkey}); - finpol = std::make_shared(fin_policy); - - auto genesis = make_bsp(proposal_t{0, "n0"}, bsp(), finpol); - bsp_vec.push_back(genesis); - forkdb.reset_root(*genesis); - - block_ref genesis_ref(genesis->id(), genesis->timestamp()); - my_finalizer.fsi = fsi_t{block_timestamp_type(0), genesis_ref, genesis_ref}; - } - - vote_result vote(const bhsp& p) { - auto vote_res = my_finalizer.decide_vote(p->core, p->id(), p->timestamp()); - return vote_res; - } - - vote_result propose(const proposal_t& p, std::optional _claim = {}) { - bsp h = forkdb.head(); - qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); - bsp new_bsp = make_bsp(p, h, finpol, old_claim); - bsp_vec.push_back(new_bsp); - auto v = vote(new_bsp); - return v; - } - - result add(const proposal_t& p, std::optional _claim = {}, const bsp& parent = {}) { - bsp h = parent ? parent : forkdb.head(); - qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); - bsp new_bsp = make_bsp(p, h, finpol, old_claim); - bsp_vec.push_back(new_bsp); - forkdb.add(new_bsp, mark_valid_t::yes, ignore_duplicate_t::no); - - auto v = vote(new_bsp); - return { new_bsp, v }; - } -}; - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_basic ) try { - simulator_t sim; - // this proposal verifies all properties and extends genesis => expect strong vote - auto res = sim.add({1, "n0"}); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); -} FC_LOG_AND_RETHROW() - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_no_vote_if_finalizer_safety_lock_empty ) try { - simulator_t sim; - sim.my_finalizer.fsi.lock = {}; // force lock empty... finalizer should not vote - auto res = sim.add({1, "n0"}); - BOOST_CHECK(res.vote.decision == vote_decision::no_vote); -} FC_LOG_AND_RETHROW() - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_normal_vote_sequence ) try { - simulator_t sim; - qc_claim_t new_claim { 0, true }; - for (uint32_t i=1; i<10; ++i) { - auto res = sim.add({i, "n0"}, new_claim); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); - new_claim = { res.new_bsp->block_num(), res.vote.decision == vote_decision::strong_vote }; - - auto lib { res.new_bsp->core.last_final_block_num() }; - BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); - - auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; - BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); - } -} FC_LOG_AND_RETHROW() - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { - simulator_t sim; - - auto res = sim.add({1, "n0", 1}); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - - auto res2 = sim.add({2, "n0", 1}); - BOOST_CHECK_EQUAL(res2.vote.monotony_check, false); - BOOST_CHECK(res2.vote.decision == vote_decision::no_vote); // use same timestamp as previous proposal => should not vote - -} FC_LOG_AND_RETHROW() - - -// --------------------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { - simulator_t sim; - qc_claim_t new_claim { 0, true }; - for (uint32_t i=1; i<10; ++i) { - auto res = sim.add({i, "n0", i}, new_claim); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); - new_claim = res.new_claim(); - - auto lib { res.new_bsp->core.last_final_block_num() }; - BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); - - auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; - BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); - - if (i > 2) - BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[i-2]->id()); - } - - // we just issued proposal #9. Verify we are locked on proposal #7 and our last_vote is #9 - BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); - - // proposal #6 from "n0" is final (although "n1" may not know it yet). - // proposal #7 would be final if it receives a strong QC - - // let's have "n1" build on proposal #6. Default will use timestamp(7) so we will fail the monotony check - auto res = sim.add({7, "n1"}, {}, sim.bsp_vec[6]); - BOOST_CHECK(res.vote.decision == vote_decision::no_vote); - BOOST_CHECK_EQUAL(res.vote.monotony_check, false); - - // let's vote for a couple more proposals, and finally when we'll reach timestamp 10 the - // monotony check will pass (both liveness and safety check should still fail) - // ------------------------------------------------------------------------------------ - res = sim.add({8, "n1"}, {}, res.new_bsp); - BOOST_CHECK_EQUAL(res.vote.monotony_check, false); - - res = sim.add({9, "n1"}, {}, res.new_bsp); - BOOST_CHECK_EQUAL(res.vote.monotony_check, false); - - res = sim.add({10, "n1"}, {}, res.new_bsp); - BOOST_CHECK(res.vote.decision == vote_decision::no_vote); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, false); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); - - // No matter how long we keep voting on this branch without a new qc claim, we will never achieve - // liveness or safety again - // ---------------------------------------------------------------------------------------------- - for (uint32_t i=11; i<20; ++i) { - res = sim.add({i, "n1"}, {}, res.new_bsp); - - BOOST_CHECK(res.vote.decision == vote_decision::no_vote); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, false); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); - } - - // Now suppose we receive a qc in a block that was created in the "n0" branch, for example the qc from - // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim(). - // liveness should be restored, because core.latest_qc_block_timestamp() > fsi.lock.timestamp - // --------------------------------------------------------------------------------------------------- - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); - new_claim = sim.bsp_vec[9]->core.latest_qc_claim(); - res = sim.add({20, "n1"}, new_claim, res.new_bsp); - - BOOST_CHECK(res.vote.decision == vote_decision::weak_vote); // because !time_range_disjoint and fsi.last_vote == 9 - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20u); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, true); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. - - new_claim = res.new_claim(); - res = sim.add({21, "n1"}, new_claim, res.new_bsp); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); // because core.extends(fsi.last_vote.block_id); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 21u); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, true); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. - - // this new proposal we just voted strong on was just building on proposal #6 and we had not advanced - // the core until the last proposal which provided a new qc_claim_t. - // as a result we now have a final_on_strong_qc = 5 (because the vote on 20 was weak) - // -------------------------------------------------------------------------------------------------- - auto final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 5u); - - // Our finalizer should still be locked on the initial proposal 7 (we have not updated our lock because - // `(final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp)` is false - // ---------------------------------------------------------------------------------------------------- - BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); - - // this new strong vote will finally advance the final_on_strong_qc thanks to the chain - // weak 20 - strong 21 (meaning that if we get a strong QC on 22, 20 becomes final, so the core of - // 22 has a final_on_strong_qc = 20. - // ----------------------------------------------------------------------------------------------- - new_claim = res.new_claim(); - res = sim.add({22, "n1"}, new_claim, res.new_bsp); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 22u); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, true); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. - final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 20u); - BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 4u); - - // OK, add one proposal + strong vote. This should finally move lib to 20 - // ---------------------------------------------------------------------- - new_claim = res.new_claim(); - res = sim.add({23, "n1"}, new_claim, res.new_bsp); - BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); - BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 23u); - BOOST_CHECK_EQUAL(res.vote.monotony_check, true); - BOOST_CHECK_EQUAL(res.vote.liveness_check, true); - BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. - final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; - BOOST_CHECK_EQUAL(final_on_strong_qc, 21u); - BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 20u); - -} FC_LOG_AND_RETHROW() - BOOST_AUTO_TEST_SUITE_END() diff --git a/unittests/finalizer_vote_tests.cpp b/unittests/finalizer_vote_tests.cpp new file mode 100644 index 0000000000..999f4241cf --- /dev/null +++ b/unittests/finalizer_vote_tests.cpp @@ -0,0 +1,332 @@ +#include +#include + +#include +#include +#include +#include + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::testing; + +using bs = eosio::chain::block_state; +using bsp = eosio::chain::block_state_ptr; +using bhs = eosio::chain::block_header_state; +using bhsp = eosio::chain::block_header_state_ptr; +using vote_decision = finalizer::vote_decision; +using vote_result = finalizer::vote_result; +using tstamp = block_timestamp_type; +using fsi_t = finalizer_safety_information; + +// --------------------------------------------------------------------------------------- +struct bls_keys_t { + bls_private_key privkey; + bls_public_key pubkey; + std::string privkey_str; + std::string pubkey_str; + + bls_keys_t(name n) { + bls_signature pop; + std::tie(privkey, pubkey, pop) = eosio::testing::get_bls_key(n); + std::tie(privkey_str, pubkey_str) = std::pair{ privkey.to_string(), pubkey.to_string() }; + } +}; + +// --------------------------------------------------------------------------------------- +inline block_id_type calc_id(block_id_type id, uint32_t block_number) { + id._hash[0] &= 0xffffffff00000000; + id._hash[0] += fc::endian_reverse_u32(block_number); + return id; +} + +// --------------------------------------------------------------------------------------- +struct proposal_t { + uint32_t block_number; + std::string proposer_name; + block_timestamp_type block_timestamp; + + proposal_t(uint32_t block_number, const char* proposer, std::optional timestamp = {}) : + block_number(block_number), proposer_name(proposer), block_timestamp(timestamp ? *timestamp : block_number) + {} + + const std::string& proposer() const { return proposer_name; } + block_timestamp_type timestamp() const { return block_timestamp; } + uint32_t block_num() const { return block_number; } + + block_id_type calculate_id() const + { + std::string id_str = proposer_name + std::to_string(block_number); + return calc_id(fc::sha256::hash(id_str.c_str()), block_number); + } + + explicit operator block_ref() const { + return block_ref{calculate_id(), timestamp()}; + } +}; + +// --------------------------------------------------------------------------------------- +bsp make_bsp(const proposal_t& p, const bsp& previous, finalizer_policy_ptr finpol, + std::optional claim = {}) { + auto makeit = [](bhs &&h) { + bs new_bs; + dynamic_cast(new_bs) = std::move(h); + return std::make_shared(std::move(new_bs)); + }; + + if (p.block_num() == 0) { + // special case of genesis block + block_ref ref{calc_id(fc::sha256::hash("genesis"), 0), block_timestamp_type{0}}; + bhs new_bhs { ref.block_id, block_header{ref.timestamp}, {}, + finality_core::create_core_for_genesis_block(0), {}, {}, std::move(finpol) }; + return makeit(std::move(new_bhs)); + } + + assert(claim); + block_ref ref{previous->id(), previous->timestamp()}; + bhs new_bhs { p.calculate_id(), block_header{p.block_timestamp, {}, {}, previous->id()}, {}, previous->core.next(ref, *claim), + {}, {}, std::move(finpol) }; + return makeit(std::move(new_bhs)); +} + +// --------------------------------------------------------------------------------------- +// simulates one finalizer voting on its own proposals "n0", and other proposals received +// from the network. +struct simulator_t { + using core = finality_core; + + bls_keys_t keys; + finalizer my_finalizer; + fork_database_if_t forkdb; + finalizer_policy_ptr finpol; + std::vector bsp_vec; + + struct result { + bsp new_bsp; + vote_result vote; + + qc_claim_t new_claim() const { + if (vote.decision == vote_decision::no_vote) + return new_bsp->core.latest_qc_claim(); + return { new_bsp->block_num(), vote.decision == vote_decision::strong_vote }; + } + }; + + simulator_t() : + keys("alice"_n), + my_finalizer(keys.privkey) { + + finalizer_policy fin_policy; + fin_policy.threshold = 0; + fin_policy.finalizers.push_back({"n0", 1, keys.pubkey}); + finpol = std::make_shared(fin_policy); + + auto genesis = make_bsp(proposal_t{0, "n0"}, bsp(), finpol); + bsp_vec.push_back(genesis); + forkdb.reset_root(*genesis); + + block_ref genesis_ref(genesis->id(), genesis->timestamp()); + my_finalizer.fsi = fsi_t{block_timestamp_type(0), genesis_ref, genesis_ref}; + } + + vote_result vote(const bhsp& p) { + auto vote_res = my_finalizer.decide_vote(p->core, p->id(), p->timestamp()); + return vote_res; + } + + vote_result propose(const proposal_t& p, std::optional _claim = {}) { + bsp h = forkdb.head(); + qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); + bsp new_bsp = make_bsp(p, h, finpol, old_claim); + bsp_vec.push_back(new_bsp); + auto v = vote(new_bsp); + return v; + } + + result add(const proposal_t& p, std::optional _claim = {}, const bsp& parent = {}) { + bsp h = parent ? parent : forkdb.head(); + qc_claim_t old_claim = _claim ? *_claim : h->core.latest_qc_claim(); + bsp new_bsp = make_bsp(p, h, finpol, old_claim); + bsp_vec.push_back(new_bsp); + forkdb.add(new_bsp, mark_valid_t::yes, ignore_duplicate_t::no); + + auto v = vote(new_bsp); + return { new_bsp, v }; + } +}; + +BOOST_AUTO_TEST_SUITE(finalizer_vote_tests) + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_basic ) try { + simulator_t sim; + // this proposal verifies all properties and extends genesis => expect strong vote + auto res = sim.add({1, "n0"}); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); +} FC_LOG_AND_RETHROW() + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_no_vote_if_finalizer_safety_lock_empty ) try { + simulator_t sim; + sim.my_finalizer.fsi.lock = {}; // force lock empty... finalizer should not vote + auto res = sim.add({1, "n0"}); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); +} FC_LOG_AND_RETHROW() + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_normal_vote_sequence ) try { + simulator_t sim; + qc_claim_t new_claim { 0, true }; + for (uint32_t i=1; i<10; ++i) { + auto res = sim.add({i, "n0"}, new_claim); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); + new_claim = { res.new_bsp->block_num(), res.vote.decision == vote_decision::strong_vote }; + + auto lib { res.new_bsp->core.last_final_block_num() }; + BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); + + auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; + BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); + } +} FC_LOG_AND_RETHROW() + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_monotony_check ) try { + simulator_t sim; + + auto res = sim.add({1, "n0", 1}); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + + auto res2 = sim.add({2, "n0", 1}); + BOOST_CHECK_EQUAL(res2.vote.monotony_check, false); + BOOST_CHECK(res2.vote.decision == vote_decision::no_vote); // use same timestamp as previous proposal => should not vote + +} FC_LOG_AND_RETHROW() + + +// --------------------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE( decide_vote_liveness_and_safety_check ) try { + simulator_t sim; + qc_claim_t new_claim { 0, true }; + for (uint32_t i=1; i<10; ++i) { + auto res = sim.add({i, "n0", i}, new_claim); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(new_claim, res.new_bsp->core.latest_qc_claim()); + new_claim = res.new_claim(); + + auto lib { res.new_bsp->core.last_final_block_num() }; + BOOST_CHECK_EQUAL(lib, i <= 2 ? 0 : i - 3); + + auto final_on_strong_qc { res.new_bsp->core.final_on_strong_qc_block_num }; + BOOST_CHECK_EQUAL(final_on_strong_qc, i <= 1 ? 0 : i - 2); + + if (i > 2) + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[i-2]->id()); + } + + // we just issued proposal #9. Verify we are locked on proposal #7 and our last_vote is #9 + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); + + // proposal #6 from "n0" is final (although "n1" may not know it yet). + // proposal #7 would be final if it receives a strong QC + + // let's have "n1" build on proposal #6. Default will use timestamp(7) so we will fail the monotony check + auto res = sim.add({7, "n1"}, {}, sim.bsp_vec[6]); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + // let's vote for a couple more proposals, and finally when we'll reach timestamp 10 the + // monotony check will pass (both liveness and safety check should still fail) + // ------------------------------------------------------------------------------------ + res = sim.add({8, "n1"}, {}, res.new_bsp); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + res = sim.add({9, "n1"}, {}, res.new_bsp); + BOOST_CHECK_EQUAL(res.vote.monotony_check, false); + + res = sim.add({10, "n1"}, {}, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, false); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); + + // No matter how long we keep voting on this branch without a new qc claim, we will never achieve + // liveness or safety again + // ---------------------------------------------------------------------------------------------- + for (uint32_t i=11; i<20; ++i) { + res = sim.add({i, "n1"}, {}, res.new_bsp); + + BOOST_CHECK(res.vote.decision == vote_decision::no_vote); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, false); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); + } + + // Now suppose we receive a qc in a block that was created in the "n0" branch, for example the qc from + // proposal 8. We can get it from sim.bsp_vec[9]->core.latest_qc_claim(). + // liveness should be restored, because core.latest_qc_block_timestamp() > fsi.lock.timestamp + // --------------------------------------------------------------------------------------------------- + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 9u); + new_claim = sim.bsp_vec[9]->core.latest_qc_claim(); + res = sim.add({20, "n1"}, new_claim, res.new_bsp); + + BOOST_CHECK(res.vote.decision == vote_decision::weak_vote); // because !time_range_disjoint and fsi.last_vote == 9 + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 20u); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + + new_claim = res.new_claim(); + res = sim.add({21, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); // because core.extends(fsi.last_vote.block_id); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 21u); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + + // this new proposal we just voted strong on was just building on proposal #6 and we had not advanced + // the core until the last proposal which provided a new qc_claim_t. + // as a result we now have a final_on_strong_qc = 5 (because the vote on 20 was weak) + // -------------------------------------------------------------------------------------------------- + auto final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 5u); + + // Our finalizer should still be locked on the initial proposal 7 (we have not updated our lock because + // `(final_on_strong_qc_block_ref.timestamp > fsi.lock.timestamp)` is false + // ---------------------------------------------------------------------------------------------------- + BOOST_CHECK_EQUAL(sim.my_finalizer.fsi.lock.block_id, sim.bsp_vec[7]->id()); + + // this new strong vote will finally advance the final_on_strong_qc thanks to the chain + // weak 20 - strong 21 (meaning that if we get a strong QC on 22, 20 becomes final, so the core of + // 22 has a final_on_strong_qc = 20. + // ----------------------------------------------------------------------------------------------- + new_claim = res.new_claim(); + res = sim.add({22, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 22u); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 20u); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 4u); + + // OK, add one proposal + strong vote. This should finally move lib to 20 + // ---------------------------------------------------------------------- + new_claim = res.new_claim(); + res = sim.add({23, "n1"}, new_claim, res.new_bsp); + BOOST_CHECK(res.vote.decision == vote_decision::strong_vote); + BOOST_CHECK_EQUAL(block_header::num_from_id(sim.my_finalizer.fsi.last_vote.block_id), 23u); + BOOST_CHECK_EQUAL(res.vote.monotony_check, true); + BOOST_CHECK_EQUAL(res.vote.liveness_check, true); + BOOST_CHECK_EQUAL(res.vote.safety_check, false); // because liveness_check is true, safety is not checked. + final_on_strong_qc = res.new_bsp->core.final_on_strong_qc_block_num; + BOOST_CHECK_EQUAL(final_on_strong_qc, 21u); + BOOST_CHECK_EQUAL(res.new_bsp->core.last_final_block_num(), 20u); + +} FC_LOG_AND_RETHROW() + + +BOOST_AUTO_TEST_SUITE_END() From da8ef5e1207e59b4039702a631d2b219a5f79577 Mon Sep 17 00:00:00 2001 From: greg7mdp Date: Fri, 1 Mar 2024 16:17:36 -0500 Subject: [PATCH 18/18] Add test for corrupted safety file. --- unittests/finalizer_tests.cpp | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/unittests/finalizer_tests.cpp b/unittests/finalizer_tests.cpp index 95a49c794f..dfe8ce6580 100644 --- a/unittests/finalizer_tests.cpp +++ b/unittests/finalizer_tests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace eosio; using namespace eosio::chain; @@ -109,6 +110,47 @@ BOOST_AUTO_TEST_CASE( basic_finalizer_safety_file_io ) try { } FC_LOG_AND_RETHROW() +BOOST_AUTO_TEST_CASE( corrupt_finalizer_safety_file ) try { + fc::temp_directory tempdir; + auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat"; + auto proposals { create_proposal_refs(10) }; + + fsi_t fsi { .last_vote_range_start = tstamp(0), + .last_vote = proposals[6], + .lock = proposals[2] }; + + bls_keys_t k("alice"_n); + bls_pub_priv_key_map_t local_finalizers = { { k.pubkey_str, k.privkey_str } }; + + { + my_finalizers_t fset{.t_startup = block_timestamp_type{}, .persist_file_path = safety_file_path}; + fset.set_keys(local_finalizers); + + fset.set_fsi(k.pubkey, fsi); + fset.save_finalizer_safety_info(); + + // at this point we have saved the finalizer safety file + // corrupt it, so we can check that we throw an exception when reading it later. + + fc::datastream f; + f.set_file_path(safety_file_path); + f.open(fc::cfile::truncate_rw_mode); + size_t junk_data = 0xf0f0f0f0f0f0f0f0ull; + fc::raw::pack(f, junk_data); + } + + { + my_finalizers_t fset{.t_startup = block_timestamp_type{}, .persist_file_path = safety_file_path}; + BOOST_REQUIRE_THROW(fset.set_keys(local_finalizers), // that's when the finalizer safety file is read + finalizer_safety_exception); + + // make sure the safety info for our finalizer that we saved above is restored correctly + BOOST_CHECK_NE(fset.get_fsi(k.pubkey), fsi); + BOOST_CHECK_EQUAL(fset.get_fsi(k.pubkey), fsi_t()); + } + +} FC_LOG_AND_RETHROW() + BOOST_AUTO_TEST_CASE( finalizer_safety_file_io ) try { fc::temp_directory tempdir; auto safety_file_path = tempdir.path() / "finalizers" / "safety.dat";