diff --git a/libraries/chain/block_header_state.cpp b/libraries/chain/block_header_state.cpp index 7097e567f8..89d8e878f5 100644 --- a/libraries/chain/block_header_state.cpp +++ b/libraries/chain/block_header_state.cpp @@ -29,14 +29,17 @@ digest_type block_header_state::compute_base_digest() const { for (const auto& fp_pair : finalizer_policies) { fc::raw::pack( enc, fp_pair.first ); - assert(fp_pair.second); - fc::raw::pack( enc, *fp_pair.second ); + const finalizer_policy_tracker& tracker = fp_pair.second; + fc::raw::pack( enc, tracker.state ); + assert(tracker.policy); + fc::raw::pack( enc, *tracker.policy ); } assert(active_proposer_policy); fc::raw::pack( enc, *active_proposer_policy ); for (const auto& pp_pair : proposer_policies) { + fc::raw::pack( enc, pp_pair.first ); assert(pp_pair.second); fc::raw::pack( enc, *pp_pair.second ); } @@ -79,6 +82,7 @@ void finish_next(const block_header_state& prev, block_header_state& next_header_state, vector new_protocol_feature_activations, std::shared_ptr new_proposer_policy, + std::optional new_finalizer_policy, qc_claim_t qc_claim) { // activated protocol features @@ -110,10 +114,6 @@ void finish_next(const block_header_state& prev, next_header_state.proposer_policies[new_proposer_policy->active_time] = std::move(new_proposer_policy); } - // finalizer policy - // ---------------- - next_header_state.active_finalizer_policy = prev.active_finalizer_policy; - // finality_core // ------------- block_ref parent_block { @@ -122,6 +122,49 @@ void finish_next(const block_header_state& prev, }; next_header_state.core = prev.core.next(parent_block, qc_claim); + // finalizer policy + // ---------------- + next_header_state.active_finalizer_policy = prev.active_finalizer_policy; + + if(!prev.finalizer_policies.empty()) { + auto lib = next_header_state.core.last_final_block_num(); + auto it = prev.finalizer_policies.begin(); + if (it->first > lib) { + // we have at least one `finalizer_policy` in our map, but none of these is + // due to become active of this block because lib has not advanced enough, so + // we just copy the multimap and keep using the same `active_finalizer_policy` + next_header_state.finalizer_policies = prev.finalizer_policies; + } else { + while (it != prev.finalizer_policies.end() && it->first <= lib) { + const finalizer_policy_tracker& tracker = it->second; + if (tracker.state == finalizer_policy_tracker::state_t::pending) { + // new finalizer_policy becones active + next_header_state.active_finalizer_policy.reset(new finalizer_policy(*tracker.policy)); + next_header_state.active_finalizer_policy->generation = prev.active_finalizer_policy->generation + 1; + } else { + assert(tracker.state == finalizer_policy_tracker::state_t::proposed); + // block where finalizer_policy was proposed became final. The finalizer policy will + // become active when next block becomes final. + finalizer_policy_tracker t { finalizer_policy_tracker::state_t::pending, tracker.policy }; + next_header_state.finalizer_policies.emplace(next_header_state.block_num(), std::move(t)); + } + ++it; + } + if (it != prev.finalizer_policies.end()) { + // copy remainder of pending finalizer_policy changes + next_header_state.finalizer_policies.insert(boost::container::ordered_unique_range_t(), + it, prev.finalizer_policies.end()); + } + } + } + + if (new_finalizer_policy) { + next_header_state.finalizer_policies.emplace( + next_header_state.block_num(), + finalizer_policy_tracker{finalizer_policy_tracker::state_t::proposed, + std::make_shared(std::move(*new_finalizer_policy))}); + } + // Finally update block id from header // ----------------------------------- next_header_state.block_id = next_header_state.header.calculate_id(); @@ -145,7 +188,7 @@ block_header_state block_header_state::next(block_header_state_input& input) con // finality extension // ------------------ instant_finality_extension new_if_ext {input.most_recent_ancestor_with_qc, - std::move(input.new_finalizer_policy), + input.new_finalizer_policy, input.new_proposer_policy}; uint16_t if_ext_id = instant_finality_extension::extension_id(); @@ -162,7 +205,9 @@ block_header_state block_header_state::next(block_header_state_input& input) con next_header_state.header_exts.emplace(ext_id, std::move(pfa_ext)); } - finish_next(*this, next_header_state, std::move(input.new_protocol_feature_activations), std::move(input.new_proposer_policy), input.most_recent_ancestor_with_qc); + finish_next(*this, next_header_state, std::move(input.new_protocol_feature_activations), + std::move(input.new_proposer_policy), std::move(input.new_finalizer_policy), + input.most_recent_ancestor_with_qc); return next_header_state; } @@ -176,14 +221,16 @@ block_header_state block_header_state::next(block_header_state_input& input) con block_header_state block_header_state::next(const signed_block_header& h, validator_t& validator) const { auto producer = detail::get_scheduled_producer(active_proposer_policy->proposer_schedule.producers, h.timestamp).producer_name; - EOS_ASSERT( h.previous == block_id, unlinkable_block_exception, "previous mismatch ${p} != ${id}", ("p", h.previous)("id", block_id) ); + EOS_ASSERT( h.previous == block_id, unlinkable_block_exception, + "previous mismatch ${p} != ${id}", ("p", h.previous)("id", block_id) ); EOS_ASSERT( h.producer == producer, wrong_producer, "wrong producer specified" ); - EOS_ASSERT( !h.new_producers, producer_schedule_exception, "Block header contains legacy producer schedule outdated by activation of WTMsig Block Signatures" ); + EOS_ASSERT( !h.new_producers, producer_schedule_exception, + "Block header contains legacy producer schedule outdated by activation of WTMsig Block Signatures" ); block_header_state next_header_state; next_header_state.header = static_cast(h); next_header_state.header_exts = h.validate_and_extract_header_extensions(); - auto& exts = next_header_state.header_exts; + const auto& exts = next_header_state.header_exts; // retrieve protocol_feature_activation from incoming block header extension // ------------------------------------------------------------------------- @@ -199,8 +246,8 @@ block_header_state block_header_state::next(const signed_block_header& h, valida // -------------------------------------------------------------------- EOS_ASSERT(exts.count(instant_finality_extension::extension_id()) > 0, invalid_block_header_extension, "Instant Finality Extension is expected to be present in all block headers after switch to IF"); - auto if_entry = exts.lower_bound(instant_finality_extension::extension_id()); - auto& if_ext = std::get(if_entry->second); + auto if_entry = exts.lower_bound(instant_finality_extension::extension_id()); + const auto& if_ext = std::get(if_entry->second); if (h.is_proper_svnn_block()) { // if there is no Finality Tree Root associated with the block, @@ -211,14 +258,18 @@ block_header_state block_header_state::next(const signed_block_header& h, valida EOS_ASSERT(no_finality_tree_associated == h.action_mroot.empty(), block_validate_exception, "No Finality Tree Root associated with the block, does not match with empty action_mroot: " "(${n}), action_mroot empty (${e}), final_on_strong_qc_block_num (${f})", - ("n", no_finality_tree_associated)("e", h.action_mroot.empty())("f", next_core_metadata.final_on_strong_qc_block_num)); + ("n", no_finality_tree_associated)("e", h.action_mroot.empty()) + ("f", next_core_metadata.final_on_strong_qc_block_num)); }; - finish_next(*this, next_header_state, std::move(new_protocol_feature_activations), if_ext.new_proposer_policy, if_ext.qc_claim); + finish_next(*this, next_header_state, std::move(new_protocol_feature_activations), if_ext.new_proposer_policy, + if_ext.new_finalizer_policy, if_ext.qc_claim); return next_header_state; } } // namespace eosio::chain -FC_REFLECT( eosio::chain::finality_digest_data_v1, (major_version)(minor_version)(active_finalizer_policy_generation)(finality_tree_digest)(active_finalizer_policy_and_base_digest) ) +FC_REFLECT( eosio::chain::finality_digest_data_v1, + (major_version)(minor_version)(active_finalizer_policy_generation) + (finality_tree_digest)(active_finalizer_policy_and_base_digest) ) diff --git a/libraries/chain/block_state.cpp b/libraries/chain/block_state.cpp index c1263a91b5..c062646175 100644 --- a/libraries/chain/block_state.cpp +++ b/libraries/chain/block_state.cpp @@ -15,7 +15,9 @@ block_state::block_state(const block_header_state& prev, signed_block_ptr b, con , block(std::move(b)) , strong_digest(compute_finality_digest()) , weak_digest(create_weak_digest(strong_digest)) - , pending_qc(prev.active_finalizer_policy->finalizers.size(), prev.active_finalizer_policy->threshold, prev.active_finalizer_policy->max_weak_sum_before_weak_final()) + , pending_qc(active_finalizer_policy->finalizers.size(), + active_finalizer_policy->threshold, + active_finalizer_policy->max_weak_sum_before_weak_final()) { // ASSUMPTION FROM controller_impl::apply_block = all untrusted blocks will have their signatures pre-validated here if( !skip_validate_signee ) { @@ -37,7 +39,9 @@ block_state::block_state(const block_header_state& bhs, , block(std::make_shared(signed_block_header{bhs.header})) , strong_digest(compute_finality_digest()) , weak_digest(create_weak_digest(strong_digest)) - , pending_qc(bhs.active_finalizer_policy->finalizers.size(), bhs.active_finalizer_policy->threshold, bhs.active_finalizer_policy->max_weak_sum_before_weak_final()) + , pending_qc(active_finalizer_policy->finalizers.size(), + active_finalizer_policy->threshold, + active_finalizer_policy->max_weak_sum_before_weak_final()) , valid(valid) , pub_keys_recovered(true) // called by produce_block so signature recovery of trxs must have been done , cached_trxs(std::move(trx_metas)) @@ -84,7 +88,9 @@ block_state_ptr block_state::create_if_genesis_block(const block_state_legacy& b // TODO: https://github.com/AntelopeIO/leap/issues/2057 // TODO: Do not aggregate votes on blocks created from block_state_legacy. This can be removed when #2057 complete. - result.pending_qc = pending_quorum_certificate{result.active_finalizer_policy->finalizers.size(), result.active_finalizer_policy->threshold, result.active_finalizer_policy->max_weak_sum_before_weak_final()}; + result.pending_qc = pending_quorum_certificate{result.active_finalizer_policy->finalizers.size(), + result.active_finalizer_policy->threshold, + result.active_finalizer_policy->max_weak_sum_before_weak_final()}; // build leaf_node and validation_tree valid_t::finality_leaf_node_t leaf_node { diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 2540e65318..a753eb818a 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -525,8 +525,8 @@ struct building_block { bb.new_finalizer_policy = std::move(fin_pol); }, [&](building_block_if& bb) { - fin_pol.generation = bb.parent.active_finalizer_policy->generation + 1; - bb.new_finalizer_policy = std::move(fin_pol); + bb.new_finalizer_policy = std::move(fin_pol); + // generation will be updated when activated } }, v); } @@ -3580,6 +3580,10 @@ struct controller_impl { auto bsp = forkdb.get_block(id); if (bsp) { return my_finalizers.all_of_public_keys([&bsp](const auto& k) { + const finalizer_policy_ptr& fp { bsp->active_finalizer_policy }; + assert(fp); + if (!std::ranges::any_of(fp->finalizers, [&](const auto& auth) { return auth.public_key == k; })) + return true; // we only care about keys from the active finalizer_policy return bsp->has_voted(k); }); } @@ -3589,6 +3593,15 @@ struct controller_impl { return !voted || *voted; } + std::optional active_finalizer_policy(const block_id_type& id) const { + return fork_db.apply_s>([&](auto& forkdb) -> std::optional { + auto bsp = forkdb.get_block(id); + if (bsp) + return *bsp->active_finalizer_policy; + return {}; + }); + } + // thread safe void create_and_send_vote_msg(const block_state_ptr& bsp) { if (!bsp->block->is_proper_svnn_block()) @@ -5263,6 +5276,10 @@ bool controller::node_has_voted_if_finalizer(const block_id_type& id) const { return my->node_has_voted_if_finalizer(id); } +std::optional controller::active_finalizer_policy(const block_id_type& id) const { + return my->active_finalizer_policy(id); +} + const producer_authority_schedule& controller::active_producers()const { return my->active_producers(); } diff --git a/libraries/chain/hotstuff/finalizer.cpp b/libraries/chain/hotstuff/finalizer.cpp index af0d3028c2..aa34f01f2c 100644 --- a/libraries/chain/hotstuff/finalizer.cpp +++ b/libraries/chain/hotstuff/finalizer.cpp @@ -201,10 +201,10 @@ my_finalizers_t::fsi_map my_finalizers_t::load_finalizer_safety_info() { // ---------------------------------------------------------------------------------------- void my_finalizers_t::set_keys(const std::map& finalizer_keys) { - assert(finalizers.empty()); // set_keys should be called only once at startup if (finalizer_keys.empty()) return; + assert(finalizers.empty()); // set_keys should be called only once at startup fsi_map safety_info = load_finalizer_safety_info(); for (const auto& [pub_key_str, priv_key_str] : finalizer_keys) { auto public_key {bls_public_key{pub_key_str}}; diff --git a/libraries/chain/include/eosio/chain/block_header_state.hpp b/libraries/chain/include/eosio/chain/block_header_state.hpp index 02291c3547..a864f1b79a 100644 --- a/libraries/chain/include/eosio/chain/block_header_state.hpp +++ b/libraries/chain/include/eosio/chain/block_header_state.hpp @@ -20,6 +20,26 @@ namespace detail { struct schedule_info; }; constexpr uint32_t light_header_protocol_version_major = 1; constexpr uint32_t light_header_protocol_version_minor = 0; +// ------------------------------------------------------------------------------------------ +// this is used for tracking in-flight `finalizer_policy` changes, which have been requested, +// but are not activated yet. This struct is associated to a block_number in the +// `finalizer_policies` flat_multimap: `block_num => state, finalizer_policy` +// +// When state == proposed, the block_num identifies the block in which the new policy was +// proposed via set_finalizers. +// +// When that block becomes final, according to the block_header_state's finality_core, +// 1. the policy becomes pending +// 2. its key `block_num,` in the proposer_policies multimap, is the current block +// +// When this current block itself becomes final, the policy becomes active. +// ------------------------------------------------------------------------------------------ +struct finalizer_policy_tracker { + enum class state_t { proposed = 0, pending }; + state_t state; + finalizer_policy_ptr policy; +}; + struct building_block_input { block_id_type parent_id; block_timestamp_type parent_timestamp; @@ -49,8 +69,12 @@ struct block_header_state { proposer_policy_ptr active_proposer_policy; // producer authority schedule, supports `digest()` // block time when proposer_policy will become active - flat_map proposer_policies; - flat_map finalizer_policies; + flat_map proposer_policies; + + // track in-flight finalizer policies. This is a `multimap` because the same block number + // can hold a `proposed` and a `pending` finalizer_policy. When that block becomes final, the + // `pending` becomes active, and the `proposed` becomes `pending` (for a different block number). + flat_multimap finalizer_policies; // ------ data members caching information available elsewhere ---------------------- @@ -93,6 +117,10 @@ using block_header_state_ptr = std::shared_ptr; } +FC_REFLECT_ENUM( eosio::chain::finalizer_policy_tracker::state_t, (proposed)(pending)) + +FC_REFLECT( eosio::chain::finalizer_policy_tracker, (state)(policy)) + FC_REFLECT( eosio::chain::block_header_state, (block_id)(header) (activated_protocol_features)(core)(active_finalizer_policy) (active_proposer_policy)(proposer_policies)(finalizer_policies)(header_exts)) diff --git a/libraries/chain/include/eosio/chain/controller.hpp b/libraries/chain/include/eosio/chain/controller.hpp index d2ac81dc32..c92799825e 100644 --- a/libraries/chain/include/eosio/chain/controller.hpp +++ b/libraries/chain/include/eosio/chain/controller.hpp @@ -330,6 +330,9 @@ namespace eosio::chain { // thread safe, for testing bool node_has_voted_if_finalizer(const block_id_type& id) const; + // thread safe, for testing + std::optional active_finalizer_policy(const block_id_type& id) const; + bool light_validation_allowed() const; bool skip_auth_check()const; bool skip_trx_checks()const; diff --git a/libraries/chain/include/eosio/chain/snapshot_detail.hpp b/libraries/chain/include/eosio/chain/snapshot_detail.hpp index 6bf0a8b7a9..98abde8a65 100644 --- a/libraries/chain/include/eosio/chain/snapshot_detail.hpp +++ b/libraries/chain/include/eosio/chain/snapshot_detail.hpp @@ -117,7 +117,7 @@ namespace eosio::chain::snapshot_detail { finalizer_policy_ptr active_finalizer_policy; proposer_policy_ptr active_proposer_policy; flat_map proposer_policies; - flat_map finalizer_policies; + flat_multimap finalizer_policies; // from block_state std::optional valid; diff --git a/libraries/libfc/include/fc/crypto/bls_public_key.hpp b/libraries/libfc/include/fc/crypto/bls_public_key.hpp index 81a6538fe1..70e031fdd7 100644 --- a/libraries/libfc/include/fc/crypto/bls_public_key.hpp +++ b/libraries/libfc/include/fc/crypto/bls_public_key.hpp @@ -61,6 +61,14 @@ namespace fc::crypto::blslib { return ds; } + friend std::ostream& operator<<(std::ostream& os, const bls_public_key& k) { + os << "bls_public_key(0x" << std::hex; + for (auto c : k.affine_non_montgomery_le()) + os << std::setfill('0') << std::setw(2) << (int)c; + os << std::dec << ")"; + return os; + } + template friend T& operator>>(T& ds, bls_public_key& sig) { // Serialization as variable length array when it is stored as a fixed length array. This makes for easier deserialization by external tools diff --git a/libraries/testing/include/eosio/testing/tester.hpp b/libraries/testing/include/eosio/testing/tester.hpp index 6ca6dbf922..537e9c06b6 100644 --- a/libraries/testing/include/eosio/testing/tester.hpp +++ b/libraries/testing/include/eosio/testing/tester.hpp @@ -233,7 +233,7 @@ namespace eosio { namespace testing { uint32_t expiration = DEFAULT_EXPIRATION_DELTA, uint32_t delay_sec = 0 )const; - vector create_accounts( vector names, + vector create_accounts( const vector& names, bool multisig = false, bool include_code = true ) @@ -255,7 +255,17 @@ namespace eosio { namespace testing { // libtester uses 1 as weight of each of the finalizer, sets (2/3 finalizers + 1) // as threshold, and makes all finalizers vote QC - std::pair> set_finalizers(const vector& finalizer_names); + std::pair> + set_finalizers(std::span finalizer_names); + + std::pair> + set_finalizers(const std::vector& names) { + return set_finalizers(std::span{names.begin(), names.end()}); + } + + void set_node_finalizers(std::span finalizer_names); + + std::vector set_active_finalizers(std::span finalizer_names); // Finalizer policy input to set up a test: weights, threshold and local finalizers // which participate voting. @@ -271,6 +281,10 @@ namespace eosio { namespace testing { }; std::pair> set_finalizers(const finalizer_policy_input& input); + std::optional active_finalizer_policy(const block_id_type& id) const { + return control->active_finalizer_policy(id); + } + void link_authority( account_name account, account_name code, permission_name req, action_name type = {} ); void unlink_authority( account_name account, account_name code, action_name type = {} ); void set_authority( account_name account, permission_name perm, authority auth, diff --git a/libraries/testing/tester.cpp b/libraries/testing/tester.cpp index 719851a767..8afed28e64 100644 --- a/libraries/testing/tester.cpp +++ b/libraries/testing/tester.cpp @@ -20,7 +20,10 @@ eosio::chain::asset core_from_string(const std::string& s) { return eosio::chain::asset::from_string(s + " " CORE_SYMBOL_NAME); } -namespace eosio { namespace testing { +using bls_private_key = fc::crypto::blslib::bls_private_key; +using bls_public_key = fc::crypto::blslib::bls_public_key; + +namespace eosio::testing { fc::logger test_logger = fc::logger::get(); @@ -1189,7 +1192,7 @@ namespace eosio { namespace testing { } - std::pair> base_tester::set_finalizers(const vector& finalizer_names) { + std::pair> base_tester::set_finalizers(std::span finalizer_names) { auto num_finalizers = finalizer_names.size(); std::vector finalizers_info; finalizers_info.reserve(num_finalizers); @@ -1200,22 +1203,23 @@ namespace eosio { namespace testing { finalizer_policy_input policy_input = { .finalizers = finalizers_info, .threshold = num_finalizers * 2 / 3 + 1, - .local_finalizers = finalizer_names + .local_finalizers = std::vector{finalizer_names.begin(), finalizer_names.end()} }; return set_finalizers(policy_input); } - std::pair> base_tester::set_finalizers(const finalizer_policy_input& input) { + std::pair> base_tester::set_finalizers(const finalizer_policy_input& input) { chain::bls_pub_priv_key_map_t local_finalizer_keys; fc::variants finalizer_auths; - std::vector priv_keys; + std::vector priv_keys; for (const auto& f: input.finalizers) { auto [privkey, pubkey, pop] = get_bls_key( f.name ); // if it is a local finalizer, set up public to private key mapping for voting - if( auto it = std::ranges::find_if(input.local_finalizers, [&](const auto& name) { return name == f.name; }); it != input.local_finalizers.end()) { + if( auto it = std::ranges::find_if(input.local_finalizers, [&](const auto& name) { return name == f.name; }); + it != input.local_finalizers.end()) { local_finalizer_keys[pubkey.to_string()] = privkey.to_string(); priv_keys.emplace_back(privkey); }; @@ -1239,6 +1243,31 @@ namespace eosio { namespace testing { priv_keys }; } + void base_tester::set_node_finalizers(std::span names) { + + chain::bls_pub_priv_key_map_t local_finalizer_keys; + for (auto name: names) { + auto [privkey, pubkey, pop] = get_bls_key(name); + local_finalizer_keys[pubkey.to_string()] = privkey.to_string(); + } + control->set_node_finalizer_keys(local_finalizer_keys); + } + + std::vector base_tester::set_active_finalizers(std::span names) { + std::vector pubkeys; + pubkeys.reserve(names.size()); + finalizer_policy_input input; + input.finalizers.reserve(names.size()); + for (auto name : names) { + auto [privkey, pubkey, pop] = get_bls_key(name); + pubkeys.push_back(pubkey); + input.finalizers.emplace_back(name, 1); + } + input.threshold = names.size() * 2 / 3 + 1; + set_finalizers(input); + return pubkeys; + } + const table_id_object* base_tester::find_table( name code, name scope, name table ) { auto tid = control->db().find(boost::make_tuple(code, scope, table)); return tid; @@ -1460,7 +1489,7 @@ namespace eosio { namespace testing { const std::string mock::webauthn_private_key::_origin = "mock.webauthn.invalid"; const sha256 mock::webauthn_private_key::_origin_hash = fc::sha256::hash(mock::webauthn_private_key::_origin); -} } /// eosio::testing +} /// eosio::testing std::ostream& operator<<( std::ostream& osm, const fc::variant& v ) { //fc::json::to_stream( osm, v ); diff --git a/unittests/api_tests.cpp b/unittests/api_tests.cpp index f96fe14ec3..78438cf339 100644 --- a/unittests/api_tests.cpp +++ b/unittests/api_tests.cpp @@ -3860,7 +3860,7 @@ BOOST_AUTO_TEST_CASE(get_code_hash_tests) { try { } FC_LOG_AND_RETHROW() } // test set_finalizer host function serialization and tester set_finalizers -BOOST_AUTO_TEST_CASE(set_finalizer_test) { try { +BOOST_AUTO_TEST_CASE(initial_set_finalizer_test) { try { validating_tester t; uint32_t lib = 0; diff --git a/unittests/finalizer_update_tests.cpp b/unittests/finalizer_update_tests.cpp new file mode 100644 index 0000000000..17cdd1666c --- /dev/null +++ b/unittests/finalizer_update_tests.cpp @@ -0,0 +1,202 @@ +#pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wsign-compare" + #include +#pragma GCC diagnostic pop + +#include + +using namespace eosio::chain::literals; +using namespace eosio::testing; +using namespace eosio::chain; + +// --------------------------------------------------------------------- +// Given a newly created `validating_tester`, trigger the transition to +// Savanna, and produce blocks until the transition is completed. +// --------------------------------------------------------------------- +static std::pair, std::vector> +transition_to_Savanna(validating_tester& t, size_t num_local_finalizers, size_t finset_size) { + uint32_t lib = 0; + signed_block_ptr lib_block; + auto c = t.control->irreversible_block().connect([&](const block_signal_params& t) { + const auto& [ block, id ] = t; + lib = block->block_num(); + lib_block = block; + }); + + t.produce_block(); + + // Create finalizer accounts + vector finalizers; + finalizers.reserve(num_local_finalizers); + for (size_t i=0; iblock_num() > lib) + critical_block = t.produce_block(); + + // Blocks after the critical block are proper IF blocks. + // ----------------------------------------------------- + auto first_proper_block = t.produce_block(); + BOOST_REQUIRE(first_proper_block->is_proper_svnn_block()); + + // wait till the first proper block becomes irreversible. Transition will be done then + // ----------------------------------------------------------------------------------- + signed_block_ptr pt_block = nullptr; // last value of this var is the first post-transition block + while(first_proper_block->block_num() > lib) { + pt_block = t.produce_block(); + BOOST_REQUIRE(pt_block->is_proper_svnn_block()); + } + + // lib must advance after 3 blocks + // ------------------------------- + t.produce_blocks(3); + BOOST_REQUIRE_EQUAL(lib, pt_block->block_num()); + + c.disconnect(); + return { finalizers, pubkeys0 }; +} + +/* + * register test suite `finalizer_update_tests` + */ +BOOST_AUTO_TEST_SUITE(finalizer_update_tests) + +// --------------------------------------------------------------------- +// checks that the active finalizer_policy for `block` matches the +// passed `generation` and `keys_span`. +// --------------------------------------------------------------------- +static void check_finalizer_policy(validating_tester& t, + const signed_block_ptr& block, + uint32_t generation, + std::span keys_span) { + auto finpol = t.active_finalizer_policy(block->calculate_id()); + BOOST_REQUIRE(!!finpol); + BOOST_REQUIRE_EQUAL(finpol->generation, generation); // new policy should not be active + // until after two 3-chains + BOOST_REQUIRE_EQUAL(keys_span.size(), finpol->finalizers.size()); + std::vector keys {keys_span.begin(), keys_span.end() }; + std::sort(keys.begin(), keys.end()); + + std::vector active_keys; + for (const auto& auth : finpol->finalizers) + active_keys.push_back(auth.public_key); + std::sort(active_keys.begin(), active_keys.end()); + for (size_t i=0; i keys_span) { + auto b = t.produce_block(); + check_finalizer_policy(t, b, generation, keys_span); +} + +// --------------------------------------------------------------------- +// verify that finalizer policy change via set_finalizer take 2 3-chains +// to take effect. +// --------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE(savanna_set_finalizer_single_test) { try { + validating_tester t; + size_t num_local_finalizers = 50; + size_t finset_size = 21; + + auto [finalizers, pubkeys0] = transition_to_Savanna(t, num_local_finalizers, finset_size); + assert(finalizers.size() == num_local_finalizers && pubkeys0.size() == finset_size); + + // run set_finalizers(), verify it becomes active after exactly two 3-chains + // ------------------------------------------------------------------------- + auto pubkeys1 = t.set_active_finalizers({&finalizers[1], finset_size}); + auto b0 = t.produce_block(); + check_finalizer_policy(t, b0, 1, pubkeys0); // new policy should only be active until after two 3-chains + + t.produce_blocks(2); + auto b3 = t.produce_block(); + check_finalizer_policy(t, b3, 1, pubkeys0); // one 3-chain - new policy still should not be active + + t.produce_blocks(1); + auto b5 = t.produce_block(); + check_finalizer_policy(t, b5, 1, pubkeys0); // one 3-chain + 2 blocks - new policy still should not be active + + auto b6 = t.produce_block(); + check_finalizer_policy(t, b6, 2, pubkeys1); // two 3-chain - new policy *should* be active + +} FC_LOG_AND_RETHROW() } + +// --------------------------------------------------------------------------- +// Test correct behavior when multiple finalizer policy changes are in-flight +// at the same time. +// --------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE(savanna_set_finalizer_multiple_test) { try { + validating_tester t; + size_t num_local_finalizers = 50; + size_t finset_size = 21; + + auto [finalizers, pubkeys0] = transition_to_Savanna(t, num_local_finalizers, finset_size); + + // run set_finalizers() twice in same block, verify only latest one becomes active + // ------------------------------------------------------------------------------- + auto pubkeys1 = t.set_active_finalizers({&finalizers[1], finset_size}); + auto pubkeys2 = t.set_active_finalizers({&finalizers[2], finset_size}); + auto b0 = t.produce_block(); + check_finalizer_policy(t, b0, 1, pubkeys0); // new policy should only be active until after two 3-chains + t.produce_blocks(4); + auto b5 = t.produce_block(); + check_finalizer_policy(t, b5, 1, pubkeys0); // new policy should only be active until after two 3-chains + auto b6 = t.produce_block(); + check_finalizer_policy(t, b6, 2, pubkeys2); // two 3-chain - new policy pubkeys2 *should* be active + + // run a test with multiple set_finlizers in-flight during the two 3-chains they + // take to become active + // ----------------------------------------------------------------------------- + auto pubkeys3 = t.set_active_finalizers({&finalizers[3], finset_size}); + b0 = t.produce_block(); + auto pubkeys4 = t.set_active_finalizers({&finalizers[4], finset_size}); + auto b1 = t.produce_block(); + auto b2 = t.produce_block(); + auto pubkeys5 = t.set_active_finalizers({&finalizers[5], finset_size}); + t.produce_blocks(2); + b5 = t.produce_block(); + check_finalizer_policy(t, b5, 2, pubkeys2); // 5 blocks after pubkeys3 (b5 - b0), pubkeys2 should still be active + b6 = t.produce_block(); + check_finalizer_policy(t, b6, 3, pubkeys3); // 6 blocks after pubkeys3 (b6 - b0), pubkeys3 should be active + auto b7 = t.produce_block(); + check_finalizer_policy(t, b7, 4, pubkeys4); // 6 blocks after pubkeys4 (b7 - b1), pubkeys4 should be active + + auto b8 = t.produce_block(); + check_finalizer_policy(t, b8, 4, pubkeys4); // 7 blocks after pubkeys4, pubkeys4 should still be active + auto b9 = t.produce_block(); + check_finalizer_policy(t, b9, 5, pubkeys5); // 6 blocks after pubkeys5 (b9 - b3), pubkeys5 should be active + + // and no further change + ensure_next_block_finalizer_policy(t, 5, pubkeys5); + ensure_next_block_finalizer_policy(t, 5, pubkeys5); + ensure_next_block_finalizer_policy(t, 5, pubkeys5); + ensure_next_block_finalizer_policy(t, 5, pubkeys5); + ensure_next_block_finalizer_policy(t, 5, pubkeys5); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file