diff --git a/libraries/chain/block_state.cpp b/libraries/chain/block_state.cpp index 397cfc8889..7773dafc64 100644 --- a/libraries/chain/block_state.cpp +++ b/libraries/chain/block_state.cpp @@ -3,6 +3,8 @@ #include #include +#include + namespace eosio::chain { block_state::block_state(const block_header_state& prev, signed_block_ptr b, const protocol_feature_set& pfs, @@ -86,7 +88,73 @@ std::pair> block_state::aggregate_vote(const vote_ return {}; } } - + +// Called from net threads +void block_state::verify_qc(const valid_quorum_certificate& qc) const { + const auto& finalizers = active_finalizer_policy->finalizers; + auto num_finalizers = finalizers.size(); + + // utility to accumulate voted weights + auto weights = [&] ( const hs_bitset& votes_bitset ) -> uint64_t { + uint64_t sum = 0; + auto n = std::min(num_finalizers, votes_bitset.size()); + for (auto i = 0u; i < n; ++i) { + if( votes_bitset[i] ) { // ith finalizer voted + sum += finalizers[i].weight; + } + } + return sum; + }; + + // compute strong and weak accumulated weights + auto strong_weights = qc._strong_votes ? weights( *qc._strong_votes ) : 0; + auto weak_weights = qc._weak_votes ? weights( *qc._weak_votes ) : 0; + + // verfify quorum is met + if( qc.is_strong() ) { + EOS_ASSERT( strong_weights >= active_finalizer_policy->threshold, + block_validate_exception, + "strong quorum is not met, strong_weights: ${s}, threshold: ${t}", + ("s", strong_weights)("t", active_finalizer_policy->threshold) ); + } else { + EOS_ASSERT( strong_weights + weak_weights >= active_finalizer_policy->threshold, + block_validate_exception, + "weak quorum is not met, strong_weights: ${s}, weak_weights: ${w}, threshold: ${t}", + ("s", strong_weights)("w", weak_weights)("t", active_finalizer_policy->threshold) ); + } + + std::vector pubkeys; + std::vector> digests; + + // utility to aggregate public keys for verification + auto aggregate_pubkeys = [&](const auto& votes_bitset) -> bls_public_key { + const auto n = std::min(num_finalizers, votes_bitset.size()); + std::vector pubkeys_to_aggregate; + pubkeys_to_aggregate.reserve(n); + for(auto i = 0u; i < n; ++i) { + if (votes_bitset[i]) { // ith finalizer voted + pubkeys_to_aggregate.emplace_back(finalizers[i].public_key); + } + } + + return fc::crypto::blslib::aggregate(pubkeys_to_aggregate); + }; + + // aggregate public keys and digests for strong and weak votes + if( qc._strong_votes ) { + pubkeys.emplace_back(aggregate_pubkeys(*qc._strong_votes)); + digests.emplace_back(std::vector{strong_digest.data(), strong_digest.data() + strong_digest.data_size()}); + } + + if( qc._weak_votes ) { + pubkeys.emplace_back(aggregate_pubkeys(*qc._weak_votes)); + digests.emplace_back(std::vector{weak_digest.data(), weak_digest.data() + weak_digest.data_size()}); + } + + // validate aggregayed signature + EOS_ASSERT( fc::crypto::blslib::aggregate_verify( pubkeys, digests, qc._sig ), block_validate_exception, "signature validation failed" ); +} + std::optional block_state::get_best_qc() const { auto block_number = block_num(); diff --git a/libraries/chain/controller.cpp b/libraries/chain/controller.cpp index 747020a592..0858dec20b 100644 --- a/libraries/chain/controller.cpp +++ b/libraries/chain/controller.cpp @@ -3030,19 +3030,183 @@ struct controller_impl { } void integrate_received_qc_to_block(const block_id_type& id, const signed_block_ptr& b) { -#warning validate received QC before integrate it: https://github.com/AntelopeIO/leap/issues/2071 - // extract QC from block extensions + // extract QC from block extension + const auto& block_exts = b->validate_and_extract_extensions(); + if( block_exts.count(quorum_certificate_extension::extension_id()) == 0 ) { + return; + } + const auto& qc_ext = std::get(block_exts. lower_bound(quorum_certificate_extension::extension_id())->second); + const auto& new_qc = qc_ext.qc.qc; + + const auto bsp = fork_db_fetch_bsp_by_num( qc_ext.qc.block_height ); + if( !bsp ) { + return; + } + + // Don't save the QC from block extension if the claimed block has a better valid_qc. + if (bsp->valid_qc && (bsp->valid_qc->is_strong() || new_qc.is_weak())) { + return; + } + + // save the QC + bsp->valid_qc = new_qc; + + // advance LIB if QC is strong and final_on_strong_qc_block_num has value + if( new_qc.is_strong() && bsp->core.final_on_strong_qc_block_num ) { + // We evaluate a block extension qc and advance lib if strong. + // This is done before evaluating the block. It is possible the block + // will not be valid or forked out. This is safe because the block is + // just acting as a carrier of this info. It doesn't matter if the block + // is actually valid as it simply is used as a network message for this data. + set_if_irreversible_block_num(*bsp->core.final_on_strong_qc_block_num); + } + } + + // Verify QC claim made by instant_finality_extension in header extension + // and quorum_certificate_extension in block extension are valid. + // Called from net-threads. It is thread safe as signed_block is never modified + // after creation. + void verify_qc_claim( const signed_block_ptr& b, const block_header_state& prev ) { + // extract current block extension and previous header extension auto block_exts = b->validate_and_extract_extensions(); - if( block_exts.count(quorum_certificate_extension::extension_id()) > 0 ) { - const auto& qc_ext = std::get(block_exts. lower_bound(quorum_certificate_extension::extension_id())->second); - auto last_qc_block_bsp = fork_db_fetch_bsp_by_num( qc_ext.qc.block_height ); -#warning a mutex might be needed for updating valid_pc - last_qc_block_bsp->valid_qc = qc_ext.qc.qc; + std::optional prev_header_ext = prev.header.extract_header_extension(instant_finality_extension::extension_id()); + + std::optional header_ext = b->extract_header_extension(instant_finality_extension::extension_id()); + if( !header_ext ) { + EOS_ASSERT( block_exts.count(quorum_certificate_extension::extension_id()) == 0, + block_validate_exception, + "A block must have QC header extension if it provides QC block extension." ); + + // If the previous block has the QC header extension, + // then the current block must also have the header extension. + EOS_ASSERT( !prev_header_ext, + block_validate_exception, + "A block must have QC header extension because its previous block has the extension" ); + + // If header extension does not have instant_finality_extension, + // do not continue. + return; } + + const auto& if_ext = std::get(*header_ext); + if( !if_ext.qc_info ) { + EOS_ASSERT( block_exts.count(quorum_certificate_extension::extension_id()) == 0, + block_validate_exception, + "A block must have QC claim if it provides QC block extension" ); + + // If header extension does not have QC claim, + // do not continue. + return; + } + + // extract QC claim + qc_info_t qc_claim{ *if_ext.qc_info }; + + // A block should not be able to claim there was a QC on a block that + // is prior to the transition to IF. + EOS_ASSERT( prev_header_ext, + block_validate_exception, + "Previous header extension must include instant_finality_extension " ); + + auto prev_if_ext = std::get(*prev_header_ext); + auto prev_qc_info = prev_if_ext.qc_info; + + // validate QC claim against previous block QC info + if( prev_qc_info ) { + // new claimed QC block nubmber cannot be smaller than previous block's + EOS_ASSERT( qc_claim.last_qc_block_num >= prev_qc_info->last_qc_block_num, + block_validate_exception, + "claimed last_qc_block_num (${n1}) must be equal to or greater than previous block's last_qc_block_num (${n2})", + ("n1", qc_claim.last_qc_block_num)("n2", prev_qc_info->last_qc_block_num) ); + + if( qc_claim.last_qc_block_num == prev_qc_info->last_qc_block_num ) { + if( qc_claim.is_last_qc_strong == prev_qc_info->is_last_qc_strong ) { + // QC block extension is redundant + EOS_ASSERT( block_exts.count(quorum_certificate_extension::extension_id()) == 0, + block_validate_exception, + "A block should not provide QC block extension if QC claim is the same as previous block" ); + + // if previous block's header extension has the same claim, just return + // (previous block already validated the claim) + return; + } + + // new claimed QC must be stricter than previous if block number is the same + EOS_ASSERT( qc_claim.is_last_qc_strong || !prev_qc_info->is_last_qc_strong, + block_validate_exception, + "claimed QC (${s1}) must be stricter than previous block's (${s2}) if block number is the same", + ("s1", qc_claim.is_last_qc_strong)("s2", prev_qc_info->is_last_qc_strong) ); + } + } + + if( block_exts.count(quorum_certificate_extension::extension_id()) == 0 ) { + // If claim is a strong QC and there wasn't already an identical claim + // in the previous block (checked earlier), QC proof must be provided + EOS_ASSERT( !qc_claim.is_last_qc_strong, + block_validate_exception, + "QC block extension must be provided if the claimed QC block is strong" ); + + // Conditions: + // * the claim is that the last QC is a weak QC, + // * it wasn't already satisfied by the claim in the prior block, + // * and there is no block extension + // Actions: + // * if it claims a block number lower than that of the current + // last irreversible block, then the new block should be rejected; + // * if it claims a block number greater than that of the current last + // irreversible block, then the new block must have a corresponding + // QC in the extension that must be validated; + // * if it claims a block number exactly equal to that of the current + // last irreversible block number, then the claim of the QC being + // weak can be accepted without a block extension. + // Notes: + // This block wouldn't advance LIB as it has no QC. + // So the LIB from that branch's POV should be the same as the + // last_final_block_num in the core of the block state it is building. + // It is safer to use that rather than if_irreversible_block_num + // because if_irreversible_block_num changes in non-deterministic ways + // as other blocks are received and validated. + // + EOS_ASSERT( qc_claim.last_qc_block_num == prev.core.last_final_block_num, + block_validate_exception, + "QC block extension must be included if the claimed QC block is not current irreversible block" ); + + return; + } + + const auto& qc_ext = std::get(block_exts.lower_bound(quorum_certificate_extension::extension_id())->second); + const auto& qc_proof = qc_ext.qc; + + // Check QC information in header extension and block extension match + EOS_ASSERT( qc_proof.block_height == qc_claim.last_qc_block_num, + block_validate_exception, + "QC block number (${n1}) in block extension does not match last_qc_block_num (${n2}) in header extension", + ("n1", qc_proof.block_height)("n2", qc_claim.last_qc_block_num) ); + + // Verify claimed strictness is the same as in proof + EOS_ASSERT( qc_proof.qc.is_strong() == qc_claim.is_last_qc_strong, + block_validate_exception, + "QC is_strong (${s1}) in block extension does not match is_last_qc_strong (${22}) in header extension", + ("s1", qc_proof.qc.is_strong())("s2", qc_claim.is_last_qc_strong) ); + + // find the claimed block's block state + auto bsp = fork_db_fetch_bsp_by_num( qc_claim.last_qc_block_num ); + EOS_ASSERT( bsp, + block_validate_exception, + "Block state was not found in forkdb for block number ${b}", + ("b", qc_claim.last_qc_block_num) ); + + // verify the QC proof against the claimed block + bsp->verify_qc(qc_proof.qc); } // thread safe, expected to be called from thread other than the main thread block_handle create_block_state_i( const block_id_type& id, const signed_block_ptr& b, const block_header_state& prev ) { + // Verify claim made by instant_finality_extension in block header extension and + // quorum_certificate_extension in block extension are valid. + // This is the only place the evaluation is done. + verify_qc_claim(b, prev); + auto trx_mroot = calculate_trx_merkle( b->transactions, true ); EOS_ASSERT( b->transaction_mroot == trx_mroot, block_validate_exception, "invalid block transaction merkle root ${b} != ${c}", ("b", b->transaction_mroot)("c", trx_mroot) ); @@ -3589,6 +3753,12 @@ struct controller_impl { return is_trx_transient ? nullptr : deep_mind_logger; } + void set_if_irreversible_block_num(uint32_t block_num) { + if( block_num > if_irreversible_block_num ) { + if_irreversible_block_num = block_num; + } + } + uint32_t earliest_available_block_num() const { return (blog.first_block_num() != 0) ? blog.first_block_num() : fork_db_root_block_num(); } @@ -4040,7 +4210,7 @@ std::optional controller::pending_producer_block_id()const { void controller::set_if_irreversible_block_num(uint32_t block_num) { // needs to be set by qc_chain at startup and as irreversible changes assert(block_num > 0); - my->if_irreversible_block_num = block_num; + my->set_if_irreversible_block_num(block_num); } uint32_t controller::last_irreversible_block_num() const { @@ -4221,7 +4391,7 @@ bool controller::process_vote_message( const vote_message& vote ) { }; auto [valid, new_lib] = my->fork_db.apply_if>>(do_vote); if (new_lib) { - my->if_irreversible_block_num = *new_lib; + my->set_if_irreversible_block_num(*new_lib); } return valid; }; diff --git a/libraries/chain/include/eosio/chain/block_state.hpp b/libraries/chain/include/eosio/chain/block_state.hpp index d8fa699e7d..4548c2804f 100644 --- a/libraries/chain/include/eosio/chain/block_state.hpp +++ b/libraries/chain/include/eosio/chain/block_state.hpp @@ -38,7 +38,9 @@ struct block_state : public block_header_state { // block_header_state provi deque extract_trxs_metas(); void set_trxs_metas(deque&& trxs_metas, bool keys_recovered); const deque& trxs_metas() const { return cached_trxs; } + std::pair> aggregate_vote(const vote_message& vote); // aggregate vote into pending_qc + void verify_qc(const valid_quorum_certificate& qc) const; // verify given qc is valid with respect block_state using bhs_t = block_header_state; using bhsp_t = block_header_state_ptr; diff --git a/unittests/block_state_tests.cpp b/unittests/block_state_tests.cpp index 350b577aef..2e885a1cb0 100644 --- a/unittests/block_state_tests.cpp +++ b/unittests/block_state_tests.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -87,4 +88,209 @@ BOOST_AUTO_TEST_CASE(aggregate_vote_test) try { } } FC_LOG_AND_RETHROW(); +BOOST_AUTO_TEST_CASE(verify_qc_test) try { + using namespace eosio::chain; + using namespace fc::crypto::blslib; + + // prepare digests + digest_type strong_digest(fc::sha256("0000000000000000000000000000002")); + std::vector strong_digest_data(strong_digest.data(), strong_digest.data() + strong_digest.data_size()); + digest_type weak_digest(fc::sha256("0000000000000000000000000000003")); + std::vector weak_digest_data(weak_digest.data(), weak_digest.data() + weak_digest.data_size()); + + // initialize a set of private keys + std::vector private_key { + bls_private_key("PVT_BLS_r4ZpChd87ooyzl6MIkw23k7PRX8xptp7TczLJHCIIW88h/hS"), + bls_private_key("PVT_BLS_/l7xzXANaB+GrlTsbZEuTiSOiWTtpBoog+TZnirxUUSaAfCo"), + bls_private_key("PVT_BLS_3FoY73Q/gED3ejyg8cvnGqHrMmx4cLKwh/e0sbcsCxpCeqn3") + }; + auto num_finalizers = private_key.size(); + + // construct finalizers, with weight 1, 2, 3 respectively + std::vector public_key(num_finalizers); + std::vector finalizers(num_finalizers); + for (size_t i = 0; i < num_finalizers; ++i) { + public_key[i] = private_key[i].get_public_key(); + uint64_t weight = i + 1; + finalizers[i] = finalizer_authority{ "test", weight, public_key[i] }; + } + + // consturct a test bsp + block_state_ptr bsp = std::make_shared(); + constexpr uint32_t generation = 1; + constexpr uint64_t threshold = 4; // 2/3 of total weights of 6 + bsp->active_finalizer_policy = std::make_shared( generation, threshold, finalizers ); + bsp->strong_digest = strong_digest; + bsp->weak_digest = weak_digest; + + auto bitset_to_vector = [](const hs_bitset& bs) { + std::vector r; + r.resize(bs.num_blocks()); + boost::to_block_range(bs, r.begin()); + return r; + }; + + { // valid strong QC + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + strong_votes[2] = 1; // finalizer 2 voted with weight 3 + + bls_signature sig_0 = private_key[0].sign(strong_digest_data); + bls_signature sig_2 = private_key[2].sign(strong_digest_data); + bls_signature agg_sig; + agg_sig = fc::crypto::blslib::aggregate({agg_sig, sig_0}); + agg_sig = fc::crypto::blslib::aggregate({agg_sig, sig_2}); + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), {}, agg_sig); + + BOOST_REQUIRE_NO_THROW( bsp->verify_qc(qc) ); + } + + { // valid weak QC + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + bls_signature strong_sig = private_key[0].sign(strong_digest_data); + + hs_bitset weak_votes(num_finalizers); + weak_votes[2] = 1; // finalizer 2 voted with weight 3 + bls_signature weak_sig = private_key[2].sign(weak_digest_data); + + bls_signature agg_sig; + agg_sig = fc::crypto::blslib::aggregate({agg_sig, strong_sig}); + agg_sig = fc::crypto::blslib::aggregate({agg_sig, weak_sig}); + + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), bitset_to_vector(weak_votes), agg_sig); + BOOST_REQUIRE_NO_THROW( bsp->verify_qc(qc) ); + } + + { // valid strong QC signed by all finalizers + hs_bitset strong_votes(num_finalizers); + std::vector sigs(num_finalizers); + bls_signature agg_sig; + + for (auto i = 0u; i < num_finalizers; ++i) { + strong_votes[i] = 1; + sigs[i] = private_key[i].sign(strong_digest_data); + agg_sig = fc::crypto::blslib::aggregate({agg_sig, sigs[i]}); + } + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), {}, agg_sig); + + BOOST_REQUIRE_NO_THROW( bsp->verify_qc(qc) ); + } + + { // valid weak QC signed by all finalizers + hs_bitset weak_votes(num_finalizers); + std::vector sigs(num_finalizers); + bls_signature agg_sig; + + for (auto i = 0u; i < num_finalizers; ++i) { + weak_votes[i] = 1; + sigs[i] = private_key[i].sign(weak_digest_data); + agg_sig = fc::crypto::blslib::aggregate({agg_sig, sigs[i]}); + } + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, {}, bitset_to_vector(weak_votes), agg_sig); + + BOOST_REQUIRE_NO_THROW( bsp->verify_qc(qc) ); + } + + { // strong QC quorem not met + hs_bitset strong_votes(num_finalizers); + strong_votes[2] = 1; // finalizer 2 voted with weight 3 (threshold is 4) + + bls_signature sig_2 = private_key[2].sign(strong_digest_data); + bls_signature agg_sig = fc::crypto::blslib::aggregate({agg_sig, sig_2}); + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), {}, agg_sig); + + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_starts_with("strong quorum is not met") ); + } + + { // weak QC quorem not met + hs_bitset weak_votes(num_finalizers); + weak_votes[2] = 1; // finalizer 2 voted with weight 3 (threshold is 4) + + bls_signature sig_2 = private_key[2].sign(weak_digest_data); + bls_signature agg_sig = fc::crypto::blslib::aggregate({agg_sig, sig_2}); + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, {}, bitset_to_vector(weak_votes), agg_sig); + + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_starts_with("weak quorum is not met") ); + } + + { // strong QC with a wrong signing private key + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + strong_votes[2] = 1; // finalizer 2 voted with weight 3 + + bls_signature sig_0 = private_key[0].sign(strong_digest_data); + bls_signature sig_2 = private_key[1].sign(strong_digest_data); // signed by finalizer 1 which is not set in strong_votes + bls_signature sig; + sig = fc::crypto::blslib::aggregate({sig, sig_0}); + sig = fc::crypto::blslib::aggregate({sig, sig_2}); + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), {}, sig); + + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_is("signature validation failed") ); + } + + { // strong QC with a wrong digest + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + strong_votes[2] = 1; // finalizer 2 voted with weight 3 + + bls_signature sig_0 = private_key[0].sign(weak_digest_data); // should have used strong digest + bls_signature sig_2 = private_key[2].sign(strong_digest_data); + bls_signature sig; + sig = fc::crypto::blslib::aggregate({sig, sig_0}); + sig = fc::crypto::blslib::aggregate({sig, sig_2}); + + // create a valid_quorum_certificate + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), {}, sig); + + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_is("signature validation failed") ); + } + + { // weak QC with a wrong signing private key + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + bls_signature strong_sig = private_key[0].sign(strong_digest_data); + + hs_bitset weak_votes(num_finalizers); + weak_votes[2] = 1; // finalizer 2 voted with weight 3 + bls_signature weak_sig = private_key[1].sign(weak_digest_data); // wrong key + + bls_signature sig; + sig = fc::crypto::blslib::aggregate({sig, strong_sig}); + sig = fc::crypto::blslib::aggregate({sig, weak_sig}); + + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), bitset_to_vector(weak_votes), sig); + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_is("signature validation failed") ); + } + + { // weak QC with a wrong digest + hs_bitset strong_votes(num_finalizers); + strong_votes[0] = 1; // finalizer 0 voted with weight 1 + bls_signature strong_sig = private_key[0].sign(weak_digest_data); // wrong digest + + hs_bitset weak_votes(num_finalizers); + weak_votes[2] = 1; // finalizer 2 voted with weight 3 + bls_signature weak_sig = private_key[2].sign(weak_digest_data); + + bls_signature sig; + sig = fc::crypto::blslib::aggregate({sig, strong_sig}); + sig = fc::crypto::blslib::aggregate({sig, weak_sig}); + + valid_quorum_certificate qc({}, {}, bitset_to_vector(strong_votes), bitset_to_vector(weak_votes), sig); + BOOST_CHECK_EXCEPTION( bsp->verify_qc(qc), block_validate_exception, eosio::testing::fc_exception_message_is("signature validation failed") ); + } +} FC_LOG_AND_RETHROW(); + BOOST_AUTO_TEST_SUITE_END()