Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/hotstuff_integration' into GH-2125
Browse files Browse the repository at this point in the history
…-forkdb
  • Loading branch information
heifner committed Feb 15, 2024
2 parents 6ab1146 + 2c77713 commit f8828ac
Show file tree
Hide file tree
Showing 7 changed files with 857 additions and 3 deletions.
8 changes: 8 additions & 0 deletions libraries/chain/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,10 @@ struct controller_impl {
}
}

uint32_t get_if_irreversible_block_num() const {
return if_irreversible_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 @@ -4324,6 +4328,10 @@ void controller::set_if_irreversible_block_num(uint32_t block_num) {
my->set_if_irreversible_block_num(block_num);
}

uint32_t controller::if_irreversible_block_num() const {
return my->get_if_irreversible_block_num();
}

uint32_t controller::last_irreversible_block_num() const {
return my->fork_db_root_block_num();
}
Expand Down
15 changes: 12 additions & 3 deletions libraries/chain/hotstuff/hotstuff.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ inline std::vector<uint32_t> bitset_to_vector(const hs_bitset& bs) {
vote_status pending_quorum_certificate::votes_t::add_vote(const std::vector<uint8_t>& proposal_digest, size_t index,
const bls_public_key& pubkey, const bls_signature& new_sig) {
if (_bitset[index]) {
dlog("duplicated vote");
return vote_status::duplicate; // shouldn't be already present
}
if (!fc::crypto::blslib::verify(pubkey, proposal_digest, new_sig)) {
Expand Down Expand Up @@ -55,15 +56,17 @@ pending_quorum_certificate::pending_quorum_certificate(size_t num_finalizers, ui

bool pending_quorum_certificate::is_quorum_met() const {
std::lock_guard g(*_mtx);
return _state == state_t::weak_achieved || _state == state_t::weak_final || _state == state_t::strong;
return is_quorum_met_no_lock();
}

// called by add_vote, already protected by mutex
vote_status pending_quorum_certificate::add_strong_vote(const std::vector<uint8_t>& proposal_digest, size_t index,
const bls_public_key& pubkey, const bls_signature& sig,
uint64_t weight) {
if (auto s = _strong_votes.add_vote(proposal_digest, index, pubkey, sig); s != vote_status::success)
if (auto s = _strong_votes.add_vote(proposal_digest, index, pubkey, sig); s != vote_status::success) {
dlog("add_strong_vote returned failure");
return s;
}
_strong_sum += weight;

switch (_state) {
Expand Down Expand Up @@ -131,6 +134,8 @@ pending_quorum_certificate::add_vote(bool strong, const std::vector<uint8_t>& pr
std::lock_guard g(*_mtx);
vote_status s = strong ? add_strong_vote(proposal_digest, index, pubkey, sig, weight)
: add_weak_vote(proposal_digest, index, pubkey, sig, weight);
dlog("status: ${s}, state: ${state}, quorum_met: ${q}",
("s", s ==vote_status::success ? "success":"failure")("state", _state==state_t::strong ? "strong":"weak")("q", is_quorum_met_no_lock() ? "yes":"no"));
return {s, _state};
}

Expand All @@ -143,7 +148,7 @@ valid_quorum_certificate pending_quorum_certificate::to_valid_quorum_certificate
if( _state == state_t::strong ) {
valid_qc._strong_votes = _strong_votes._bitset;
valid_qc._sig = _strong_votes._sig;
} else if (is_quorum_met()) {
} else if (is_quorum_met_no_lock()) {
valid_qc._strong_votes = _strong_votes._bitset;
valid_qc._weak_votes = _weak_votes._bitset;
valid_qc._sig = fc::crypto::blslib::aggregate({_strong_votes._sig, _weak_votes._sig});
Expand All @@ -153,6 +158,10 @@ valid_quorum_certificate pending_quorum_certificate::to_valid_quorum_certificate
return valid_qc;
}

bool pending_quorum_certificate::is_quorum_met_no_lock() const {
return _state == state_t::weak_achieved || _state == state_t::weak_final || _state == state_t::strong;
}

valid_quorum_certificate::valid_quorum_certificate(
const std::vector<uint32_t>& strong_votes, // bitset encoding, following canonical order
const std::vector<uint32_t>& weak_votes, // bitset encoding, following canonical order
Expand Down
1 change: 1 addition & 0 deletions libraries/chain/include/eosio/chain/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ namespace eosio::chain {
// Called by qc_chain to indicate the current irreversible block num
// After hotstuff is activated, this should be called on startup by qc_chain
void set_if_irreversible_block_num(uint32_t block_num);
uint32_t if_irreversible_block_num() const;

uint32_t last_irreversible_block_num() const;
block_id_type last_irreversible_block_id() const;
Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/eosio/chain/hotstuff/hotstuff.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ namespace eosio::chain {
const bls_public_key& pubkey,
const bls_signature& sig,
uint64_t weight);

bool is_quorum_met_no_lock() const;
};
} //eosio::chain

Expand Down
188 changes: 188 additions & 0 deletions unittests/finality_test_cluster.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#include "finality_test_cluster.hpp"

// Construct a test network and activate IF.
finality_test_cluster::finality_test_cluster() {
using namespace eosio::testing;

setup_node(node0, "node0"_n);
setup_node(node1, "node1"_n);
setup_node(node2, "node2"_n);

// collect node1's votes
node1.node.control->voted_block().connect( [&]( const eosio::chain::vote_message& vote ) {
node1.votes.emplace_back(vote);
});
// collect node2's votes
node2.node.control->voted_block().connect( [&]( const eosio::chain::vote_message& vote ) {
node2.votes.emplace_back(vote);
});

// form a 3-chain to make LIB advacing on node0
// node0's vote (internal voting) and node1's vote make the quorum
for (auto i = 0; i < 3; ++i) {
produce_and_push_block();
process_node1_vote();
}
FC_ASSERT(node0_lib_advancing(), "LIB has not advanced on node0");

// QC extension in the block sent to node1 and node2 makes them LIB advancing
produce_and_push_block();
process_node1_vote();
FC_ASSERT(node1_lib_advancing(), "LIB has not advanced on node1");
FC_ASSERT(node2_lib_advancing(), "LIB has not advanced on node2");

// clean up processed votes
for (auto& n : nodes) {
n.votes.clear();
n.prev_lib_num = n.node.control->if_irreversible_block_num();
}
}

// node0 produces a block and pushes it to node1 and node2
void finality_test_cluster::produce_and_push_block() {
auto b = node0.node.produce_block();
node1.node.push_block(b);
node2.node.push_block(b);
}

// send node1's vote identified by "vote_index" in the collected votes
eosio::chain::vote_status finality_test_cluster::process_node1_vote(uint32_t vote_index, vote_mode mode) {
return process_vote( node1, vote_index, mode );
}

// send node1's latest vote
eosio::chain::vote_status finality_test_cluster::process_node1_vote(vote_mode mode) {
return process_vote( node1, mode );
}

// send node2's vote identified by "vote_index" in the collected votes
eosio::chain::vote_status finality_test_cluster::process_node2_vote(uint32_t vote_index, vote_mode mode) {
return process_vote( node2, vote_index, mode );
}

// send node2's latest vote
eosio::chain::vote_status finality_test_cluster::process_node2_vote(vote_mode mode) {
return process_vote( node2, mode );
}

// returns true if node0's LIB has advanced
bool finality_test_cluster::node0_lib_advancing() {
return lib_advancing(node0);
}

// returns true if node1's LIB has advanced
bool finality_test_cluster::node1_lib_advancing() {
return lib_advancing(node1);
}

// returns true if node2's LIB has advanced
bool finality_test_cluster::node2_lib_advancing() {
return lib_advancing(node2);
}

// Produces a number of blocks and returns true if LIB is advancing.
// This function can be only used at the end of a test as it clears
// node1_votes and node2_votes when starting.
bool finality_test_cluster::produce_blocks_and_verify_lib_advancing() {
// start from fresh
node1.votes.clear();
node2.votes.clear();

for (auto i = 0; i < 3; ++i) {
produce_and_push_block();
process_node1_vote();
if (!node0_lib_advancing() || !node1_lib_advancing() || !node2_lib_advancing()) {
return false;
}
}

return true;
}

void finality_test_cluster::node1_corrupt_vote_proposal_id() {
node1_orig_vote = node1.votes[0];

if( node1.votes[0].proposal_id.data()[0] == 'a' ) {
node1.votes[0].proposal_id.data()[0] = 'b';
} else {
node1.votes[0].proposal_id.data()[0] = 'a';
}
}

void finality_test_cluster::node1_corrupt_vote_finalizer_key() {
node1_orig_vote = node1.votes[0];

// corrupt the finalizer_key
if( node1.votes[0].finalizer_key._pkey.x.d[0] == 1 ) {
node1.votes[0].finalizer_key._pkey.x.d[0] = 2;
} else {
node1.votes[0].finalizer_key._pkey.x.d[0] = 1;
}
}

void finality_test_cluster::node1_corrupt_vote_signature() {
node1_orig_vote = node1.votes[0];

// corrupt the signature
if( node1.votes[0].sig._sig.x.c0.d[0] == 1 ) {
node1.votes[0].sig._sig.x.c0.d[0] = 2;
} else {
node1.votes[0].sig._sig.x.c0.d[0] = 1;
}
}

void finality_test_cluster::node1_restore_to_original_vote() {
node1.votes[0] = node1_orig_vote;
}

bool finality_test_cluster::lib_advancing(node_info& node) {
auto curr_lib_num = node.node.control->if_irreversible_block_num();
auto advancing = curr_lib_num > node.prev_lib_num;
// update pre_lib_num for next time check
node.prev_lib_num = curr_lib_num;
return advancing;
}

// private methods follow
void finality_test_cluster::setup_node(node_info& node, eosio::chain::account_name local_finalizer) {
using namespace eosio::testing;

node.node.produce_block();
node.node.produce_block();

// activate hotstuff
eosio::testing::base_tester::finalizer_policy_input policy_input = {
.finalizers = { {.name = "node0"_n, .weight = 1},
{.name = "node1"_n, .weight = 1},
{.name = "node2"_n, .weight = 1}},
.threshold = 2,
.local_finalizers = {local_finalizer}
};
node.node.set_finalizers(policy_input);
auto block = node.node.produce_block();

// this block contains the header extension for the instant finality
std::optional<eosio::chain::block_header_extension> ext = block->extract_header_extension(eosio::chain::instant_finality_extension::extension_id());
BOOST_TEST(!!ext);
std::optional<eosio::chain::finalizer_policy> fin_policy = std::get<eosio::chain::instant_finality_extension>(*ext).new_finalizer_policy;
BOOST_TEST(!!fin_policy);
BOOST_TEST(fin_policy->finalizers.size() == 3);
BOOST_TEST(fin_policy->generation == 1);
}

// send a vote to node0
eosio::chain::vote_status finality_test_cluster::process_vote(node_info& node, size_t vote_index, vote_mode mode) {
FC_ASSERT( vote_index < node.votes.size(), "out of bound index in process_vote" );
auto& vote = node.votes[vote_index];
if( mode == vote_mode::strong ) {
vote.strong = true;
} else {
vote.strong = false;
}
return node0.node.control->process_vote_message( vote );
}

eosio::chain::vote_status finality_test_cluster::process_vote(node_info& node, vote_mode mode) {
auto vote_index = node.votes.size() - 1;
return process_vote( node, vote_index, mode );
}
101 changes: 101 additions & 0 deletions unittests/finality_test_cluster.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#pragma once

#include <eosio/chain/hotstuff/finalizer_authority.hpp>

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-compare"
#include <boost/test/unit_test.hpp>
#pragma GCC diagnostic pop
#include <eosio/testing/tester.hpp>

// Set up a test network which consists of 3 nodes:
// * node0 produces blocks and pushes them to node1 and node2;
// node0 votes the blocks it produces internally.
// * node1 votes on the proposal sent by node0
// * node2 votes on the proposal sent by node0
// Each node has one finalizer: node0 -- "node0"_n, node1 -- "node1"_n, node2 -- "node2"_n.
// Quorum is set to 2.
// After starup up, IF are activated on both nodes.
//
// APIs are provided to modify/delay/reoder/remove votes from node1 and node2 to node0.


class finality_test_cluster {
public:

enum class vote_mode {
strong,
weak,
};

// Construct a test network and activate IF.
finality_test_cluster();

// node0 produces a block and pushes it to node1 and node2
void produce_and_push_block();

// send node1's vote identified by "index" in the collected votes
eosio::chain::vote_status process_node1_vote(uint32_t vote_index, vote_mode mode = vote_mode::strong);

// send node1's latest vote
eosio::chain::vote_status process_node1_vote(vote_mode mode = vote_mode::strong);

// send node2's vote identified by "index" in the collected votes
eosio::chain::vote_status process_node2_vote(uint32_t vote_index, vote_mode mode = vote_mode::strong);

// send node2's latest vote
eosio::chain::vote_status process_node2_vote(vote_mode mode = vote_mode::strong);

// returns true if node0's LIB has advanced
bool node0_lib_advancing();

// returns true if node1's LIB has advanced
bool node1_lib_advancing();

// returns true if node2's LIB has advanced
bool node2_lib_advancing();

// Produces a number of blocks and returns true if LIB is advancing.
// This function can be only used at the end of a test as it clears
// node1_votes and node2_votes when starting.
bool produce_blocks_and_verify_lib_advancing();

// Intentionally corrupt node1's vote's proposal_id and save the original vote
void node1_corrupt_vote_proposal_id();

// Intentionally corrupt node1's vote's finalizer_key and save the original vote
void node1_corrupt_vote_finalizer_key();

// Intentionally corrupt node1's vote's signature and save the original vote
void node1_corrupt_vote_signature();

// Restore node1's original vote
void node1_restore_to_original_vote();

private:

struct node_info {
eosio::testing::tester node;
uint32_t prev_lib_num{0};
std::vector<eosio::chain::vote_message> votes;
};

std::array<node_info, 3> nodes;
node_info& node0 = nodes[0];
node_info& node1 = nodes[1];
node_info& node2 = nodes[2];

eosio::chain::vote_message node1_orig_vote;

// sets up "node_index" node
void setup_node(node_info& node, eosio::chain::account_name local_finalizer);

// returns true if LIB advances on "node_index" node
bool lib_advancing(node_info& node);

// send "vote_index" vote on node to node0
eosio::chain::vote_status process_vote(node_info& node, size_t vote_index, vote_mode mode);

// send the latest vote on "node_index" node to node0
eosio::chain::vote_status process_vote(node_info& node, vote_mode mode);
};
Loading

0 comments on commit f8828ac

Please sign in to comment.