Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IF: Reject blocks with unsupported claims of last QC in block header extension #2120

Merged
merged 10 commits into from
Jan 24, 2024
66 changes: 65 additions & 1 deletion libraries/chain/block_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <eosio/chain/block_state_legacy.hpp>
#include <eosio/chain/exceptions.hpp>

#include <fc/crypto/bls_utils.hpp>

namespace eosio::chain {

block_state::block_state(const block_header_state& prev, signed_block_ptr b, const protocol_feature_set& pfs,
Expand Down Expand Up @@ -86,7 +88,69 @@ std::pair<bool, std::optional<uint32_t>> 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<bls_public_key> pubkeys;
std::vector<std::vector<uint8_t>> digests;

// utility to aggregate public keys and digests for verification
auto prepare_pubkeys_digests = [&] ( const auto& votes_bitset, const auto& digest ) {
auto n = std::min(num_finalizers, votes_bitset.size());
heifner marked this conversation as resolved.
Show resolved Hide resolved
pubkeys.reserve(n);
digests.reserve(n);
for (auto i = 0u; i < n; ++i) {
if( votes_bitset[i] ) { // ith finalizer voted the digest
pubkeys.emplace_back(finalizers[i].public_key);
digests.emplace_back(std::vector<uint8_t>{digest.data(), digest.data() + digest.data_size()});
}
}
};
arhag marked this conversation as resolved.
Show resolved Hide resolved

// aggregate public keys and digests for strong and weak votes
if( qc._strong_votes ) {
prepare_pubkeys_digests(*qc._strong_votes, strong_digest);
}
if( qc._weak_votes ) {
prepare_pubkeys_digests(*qc._weak_votes, weak_digest);
}
arhag marked this conversation as resolved.
Show resolved Hide resolved

// validate aggregayed signature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling aggregated

EOS_ASSERT( fc::crypto::blslib::aggregate_verify( pubkeys, digests, qc._sig ), block_validate_exception, "signature validation failed" );
}

std::optional<quorum_certificate> block_state::get_best_qc() const {
auto block_number = block_num();

Expand Down
95 changes: 89 additions & 6 deletions libraries/chain/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3030,19 +3030,96 @@ struct controller_impl {
}

void integrate_received_qc_to_block(const block_id_type& id, const signed_block_ptr& b) {
heifner marked this conversation as resolved.
Show resolved Hide resolved
#warning validate received QC before integrate it: https://github.com/AntelopeIO/leap/issues/2071
// extract QC from block extensions
auto block_exts = b->validate_and_extract_extensions();
if( block_exts.count(quorum_certificate_extension::extension_id()) > 0 ) {
const auto& qc_ext = std::get<quorum_certificate_extension>(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;
auto bsp = fork_db_fetch_bsp_by_num( qc_ext.qc.block_height );
heifner marked this conversation as resolved.
Show resolved Hide resolved
// Only save the QC from block extension if the claimed block does not
// have valid_qc or the new QC is better than valid_qc.
if( bsp && !bsp->valid_qc || (qc_ext.qc.qc.is_strong() && bsp->valid_qc->is_weak()) ) {
arhag marked this conversation as resolved.
Show resolved Hide resolved
bsp->valid_qc = qc_ext.qc.qc;

if( bsp->valid_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 block 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 ) {
heifner marked this conversation as resolved.
Show resolved Hide resolved
// If block header extension does not have instant_finality_extension,
// just return
std::optional<block_header_extension> header_ext = b->extract_header_extension(instant_finality_extension::extension_id());
if( !header_ext ) {
return;
}

// If qc_info is not included, just return
const auto& if_ext = std::get<instant_finality_extension>(*header_ext);
if( !if_ext.qc_info ) {
return;
}

// extract received QC information
qc_info_t received_qc_info{ *if_ext.qc_info };

// if previous block's header extension has the same claim, just return
// (previous block had already validated the claim)
std::optional<block_header_extension> prev_header_ext = prev.header.extract_header_extension(instant_finality_extension::extension_id());
if( prev_header_ext ) {
auto prev_if_ext = std::get<instant_finality_extension>(*prev_header_ext);
if( prev_if_ext.qc_info && prev_if_ext.qc_info->last_qc_block_num == received_qc_info.last_qc_block_num && prev_if_ext.qc_info->is_last_qc_strong == received_qc_info.is_last_qc_strong ) {
return;
arhag marked this conversation as resolved.
Show resolved Hide resolved
}
}
arhag marked this conversation as resolved.
Show resolved Hide resolved

// extract QC from block extensions
auto block_exts = b->validate_and_extract_extensions();
EOS_ASSERT( block_exts.count(quorum_certificate_extension::extension_id()) > 0,
block_validate_exception,
"received block has finality header extension but does not have QC block extension" );
arhag marked this conversation as resolved.
Show resolved Hide resolved
const auto& qc_ext = std::get<quorum_certificate_extension>(block_exts. lower_bound(quorum_certificate_extension::extension_id())->second);
const auto& received_qc = qc_ext.qc;
const auto& valid_qc = qc_ext.qc.qc;

// Check QC information in header extension and block extension match
EOS_ASSERT( received_qc.block_height == received_qc_info.last_qc_block_num,
arhag marked this conversation as resolved.
Show resolved Hide resolved
block_validate_exception,
"QC block number (${n1}) in block extension does not match last_qc_block_num (${n2}) in header extension",
("n1", received_qc.block_height)("n2", received_qc_info.last_qc_block_num) );
EOS_ASSERT( valid_qc.is_strong() == received_qc_info.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", valid_qc.is_strong())("s2", received_qc_info.is_last_qc_strong) );

// find the claimed block's block state
auto claimed_block_bsp = fork_db_fetch_bsp_by_num( received_qc_info.last_qc_block_num );
heifner marked this conversation as resolved.
Show resolved Hide resolved
EOS_ASSERT( claimed_block_bsp,
block_validate_exception,
"Block state was not found in forkdb for block number ${b}",
("b", valid_qc.is_strong()) );

// verify the claims
claimed_block_bsp->verify_qc(valid_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) );
Expand Down Expand Up @@ -3589,6 +3666,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();
}
Expand Down Expand Up @@ -4040,7 +4123,7 @@ std::optional<block_id_type> 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 {
Expand Down Expand Up @@ -4221,7 +4304,7 @@ bool controller::process_vote_message( const vote_message& vote ) {
};
auto [valid, new_lib] = my->fork_db.apply_if<std::pair<bool, std::optional<uint32_t>>>(do_vote);
if (new_lib) {
my->if_irreversible_block_num = *new_lib;
my->set_if_irreversible_block_num(*new_lib);
}
return valid;
};
Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/eosio/chain/block_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ struct block_state : public block_header_state { // block_header_state provi
deque<transaction_metadata_ptr> extract_trxs_metas();
void set_trxs_metas(deque<transaction_metadata_ptr>&& trxs_metas, bool keys_recovered);
const deque<transaction_metadata_ptr>& trxs_metas() const { return cached_trxs; }

std::pair<bool, std::optional<uint32_t>> 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;
Expand Down
Loading