Switch to a circular buffer for recording checkpointing votes

This commit is contained in:
Doyle 2019-07-04 15:31:35 +10:00
parent 9b9029d7c6
commit bdf8829f6b
8 changed files with 73 additions and 75 deletions

View File

@ -2207,8 +2207,6 @@ namespace cryptonote
bool core::add_service_node_vote(const service_nodes::quorum_vote_t& vote, vote_verification_context &vvc)
{
bool result = m_quorum_cop.handle_vote(vote, vvc);
if (vvc.m_added_to_pool) // NOTE: Is unique vote
m_service_node_list.handle_checkpoint_vote(vote);
return result;
}
//-----------------------------------------------------------------------------------------------

View File

@ -881,9 +881,9 @@ namespace cryptonote
bool relay_service_node_votes();
/**
* @brief Increment the number of expected checkpoints that the service node should have voted on by 1
* @brief Record if the service node has checkpointed at this point in time
*/
void expect_checkpoint_vote_from(crypto::public_key const &pubkey) { m_service_node_list.expect_checkpoint_vote_from(pubkey); }
void record_checkpoint_vote(crypto::public_key const &pubkey, bool voted) { m_service_node_list.record_checkpoint_vote(pubkey, voted); }
/// Time point at which the storage server last pinged us
std::atomic<time_t> m_last_storage_server_ping;

View File

@ -404,6 +404,7 @@ namespace service_nodes
info.active_since_height = -info.active_since_height;
info.last_decommission_height = block_height;
info.decommission_count++;
info.proof.votes.fill(true);
return true;
case new_state::recommission:
@ -429,7 +430,6 @@ namespace service_nodes
// Move the SN at the back of the list as if it had just registered (or just won)
info.last_reward_block_height = block_height;
info.last_reward_transaction_index = std::numeric_limits<uint32_t>::max();
return true;
case new_state::ip_change_penalty:
@ -1097,26 +1097,6 @@ namespace service_nodes
const size_t cache_state_from_height = (block_height < QUORUM_LIFETIME) ? 0 : block_height - QUORUM_LIFETIME;
while (!m_transient_state.quorum_states.empty() && m_transient_state.quorum_states.begin()->first < cache_state_from_height)
m_transient_state.quorum_states.erase(m_transient_state.quorum_states.begin());
//
// Reset checkpoint vote count
// i.e. Nodes are allowed to miss a certain number of checkpoint votes every N blocks.
//
for (auto it : m_transient_state.service_nodes_infos)
{
proof_info &proof = it.second.proof;
if (proof.num_checkpoint_votes_expected > CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK)
{
// NOTE: We can receive more votes than expected because you may receive votes from the latest quorums but don't
// check yet, because we deregister based on quorums on a sliding window that is offset from the latest block on
// the blockchain, i.e. check obligation quorums up to (latest height - N).
// So don't naiively set the num votes received to 0.
proof.num_checkpoint_votes_received = std::max(proof.num_checkpoint_votes_received - CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK, 0);
proof.num_checkpoint_votes_expected = std::max(proof.num_checkpoint_votes_expected - CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK, 0);
}
}
}
void service_node_list::blockchain_detached(uint64_t height)
@ -1408,6 +1388,18 @@ namespace service_nodes
boost::endian::little_to_native_inplace(seed);
seed += static_cast<uint64_t>(type);
// Shuffle 2
// |=================================|
// | |
// Shuffle 1 |
// |==============| |
// | | | |
// |sublist_size | |
// | | sublist_up_to |
// 0 N Y Z
// [.......................................]
// If we have a list [0,Z) but we need a shuffled sublist of the first N values that only
// includes values from [0,Y) then we do this using two shuffles: first of the [0,Y) sublist,
// then of the [N,Z) sublist (which is already partially shuffled, but that doesn't matter). We
@ -1452,9 +1444,9 @@ namespace service_nodes
auto quorum = std::make_shared<testing_quorum>();
std::vector<size_t> pub_keys_indexes;
size_t total_nodes = active_snode_list.size() + decomm_snode_list.size();
if (type == quorum_type::obligations)
{
size_t total_nodes = active_snode_list.size() + decomm_snode_list.size();
num_validators = std::min(active_snode_list.size(), STATE_CHANGE_QUORUM_SIZE);
pub_keys_indexes = generate_shuffled_service_node_index_list(total_nodes, block_hash, type, num_validators, active_snode_list.size());
manager.obligations = quorum;
@ -1463,6 +1455,12 @@ namespace service_nodes
}
else if (type == quorum_type::checkpointing)
{
size_t total_nodes = active_snode_list.size();
// TODO(loki): Soft fork, remove when testnet gets reset
if (m_blockchain.nettype() == cryptonote::TESTNET && height < 83429)
total_nodes = active_snode_list.size() + decomm_snode_list.size();
pub_keys_indexes = generate_shuffled_service_node_index_list(total_nodes, block_hash, type);
manager.checkpointing = quorum;
num_workers = std::min(pub_keys_indexes.size(), CHECKPOINT_QUORUM_SIZE);
@ -1785,36 +1783,16 @@ namespace service_nodes
return true;
}
void service_node_list::handle_checkpoint_vote(quorum_vote_t const &vote)
{
if (vote.type != quorum_type::checkpointing)
return;
crypto::public_key pubkey;
if (!get_quorum_pubkey(quorum_type::checkpointing, quorum_group::worker, vote.block_height, vote.index_in_group, pubkey))
{
MERROR("Unexpected missing quorum for checkpointing vote at height: " << vote.block_height << ", current height: " << m_transient_state.height);
return;
}
std::lock_guard<boost::recursive_mutex> lock(m_sn_mutex);
auto it = m_transient_state.service_nodes_infos.find(pubkey);
if (it == m_transient_state.service_nodes_infos.end())
return;
service_node_info &info = it->second;
info.proof.num_checkpoint_votes_received++;
}
void service_node_list::expect_checkpoint_vote_from(crypto::public_key const &pubkey)
void service_node_list::record_checkpoint_vote(crypto::public_key const &pubkey, bool voted)
{
std::lock_guard<boost::recursive_mutex> lock(m_sn_mutex);
auto it = m_transient_state.service_nodes_infos.find(pubkey);
if (it == m_transient_state.service_nodes_infos.end())
return;
service_node_info &info = it->second;
info.proof.num_checkpoint_votes_expected++;
proof_info &info = it->second.proof;
info.votes[info.vote_index] = voted;
info.vote_index = (info.vote_index + 1) % info.votes.size();
}
bool service_node_list::load()

View File

@ -43,9 +43,10 @@ namespace service_nodes
{
uint64_t timestamp = 0;
uint16_t version_major = 0, version_minor = 0, version_patch = 0;
int16_t num_checkpoint_votes_expected = 0;
int16_t num_checkpoint_votes_received = 0;
std::array<bool, CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK> votes;
uint8_t vote_index = 0;
std::array<std::pair<uint32_t, uint64_t>, 2> public_ips = {}; // (not serialized)
proof_info() { votes.fill(true); }
};
struct service_node_info // registration information
@ -220,10 +221,7 @@ namespace service_nodes
/// Record public ip and storage port and add them to the service node list
cryptonote::NOTIFY_UPTIME_PROOF::request generate_uptime_proof(crypto::public_key const &pubkey, crypto::secret_key const &key, uint32_t public_ip, uint16_t storage_port) const;
bool handle_uptime_proof (cryptonote::NOTIFY_UPTIME_PROOF::request const &proof);
// TODO(doyle): This is a really awkward API
void handle_checkpoint_vote (quorum_vote_t const &vote);
void expect_checkpoint_vote_from(crypto::public_key const &pubkey);
void record_checkpoint_vote (crypto::public_key const &pubkey, bool voted);
struct rollback_event
{

View File

@ -74,10 +74,10 @@ namespace service_nodes
if (check_uptime_obligation && time_since_last_uptime_proof > UPTIME_PROOF_MAX_TIME_IN_SECONDS)
{
LOG_PRINT_L1("Submitting deregister vote for: "
<< pubkey << ", due to uptime proof being older than: " << UPTIME_PROOF_MAX_TIME_IN_SECONDS
<< ", time since last uptime proof: "
<< tools::get_human_readable_timespan(std::chrono::seconds(time_since_last_uptime_proof)));
LOG_PRINT_L1(
"Service Node: " << pubkey << ", failed uptime proof obligation check: the last uptime proof was older than: "
<< UPTIME_PROOF_MAX_TIME_IN_SECONDS << "s. Time since last uptime proof was: "
<< tools::get_human_readable_timespan(std::chrono::seconds(time_since_last_uptime_proof)));
result.uptime_proved = false;
}
@ -96,18 +96,20 @@ namespace service_nodes
}
}
if (check_checkpoint_obligation && info.proof.num_checkpoint_votes_expected >= CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK)
if (check_checkpoint_obligation)
{
proof_info const &proof = info.proof;
// NOTE: We can receive more votes than expected, say, the nodes are in the quorums newer than the obligation check height
if (proof.num_checkpoint_votes_received < proof.num_checkpoint_votes_expected)
int num_votes = 0;
for (bool voted : proof.votes)
num_votes += voted;
if (num_votes <= CHECKPOINT_MAX_MISSABLE_VOTES)
{
int16_t missing_votes = info.proof.num_checkpoint_votes_expected - info.proof.num_checkpoint_votes_received;
if (missing_votes > CHECKPOINT_MAX_MISSABLE_VOTES)
{
LOG_PRINT_L1("Submitting deregister vote for: " << pubkey << ", due to missing more than: " << CHECKPOINT_MAX_MISSABLE_VOTES << " checkpoint votes");
result.voted_in_checkpoints = false;
}
LOG_PRINT_L1("Service Node: " << pubkey << ", failed checkpoint obligation check: missed the last: "
<< CHECKPOINT_MAX_MISSABLE_VOTES << " checkpoint votes from: "
<< CHECKPOINT_MIN_QUORUMS_NODE_MUST_VOTE_IN_BEFORE_DEREGISTER_CHECK
<< " quorums that they were required to participate in.");
result.voted_in_checkpoints = false;
}
}
@ -205,12 +207,13 @@ namespace service_nodes
// service nodes have fulfilled their checkpointing work
if (m_core.get_hard_fork_version(m_obligations_height) >= cryptonote::network_version_12_checkpointing)
{
std::shared_ptr<const testing_quorum> quorum =
m_core.get_testing_quorum(quorum_type::checkpointing, m_obligations_height);
if (quorum)
if (std::shared_ptr<const testing_quorum> quorum = m_core.get_testing_quorum(quorum_type::checkpointing, m_obligations_height))
{
for (crypto::public_key const &key : quorum->workers)
m_core.expect_checkpoint_vote_from(key);
for (size_t index_in_quorum = 0; index_in_quorum < quorum->workers.size(); index_in_quorum++)
{
crypto::public_key const &key = quorum->workers[index_in_quorum];
m_core.record_checkpoint_vote(key, m_vote_pool.received_checkpoint_vote(height, index_in_quorum));
}
}
}
@ -250,7 +253,7 @@ namespace service_nodes
auto test_results = check_service_node(node_key, info);
new_state vote_for_state;
if (test_results.uptime_proved) {
if (test_results.passed()) {
if (info.is_decommissioned()) {
vote_for_state = new_state::recommission;
LOG_PRINT_L2("Decommissioned service node " << quorum->workers[node_index] << " is now passing required checks; voting to recommission");

View File

@ -66,6 +66,8 @@ namespace service_nodes
bool uptime_proved = true;
bool single_ip = true;
bool voted_in_checkpoints = true;
bool passed() const { return uptime_proved && voted_in_checkpoints; }
};
class quorum_cop

View File

@ -580,5 +580,23 @@ namespace service_nodes
cull_votes(m_obligations_pool, min_height, height);
cull_votes(m_checkpoint_pool, min_height, height);
}
bool voting_pool::received_checkpoint_vote(uint64_t height, size_t index_in_quorum) const
{
auto pool_it = std::find_if(m_checkpoint_pool.begin(),
m_checkpoint_pool.end(),
[height](checkpoint_pool_entry const &entry) { return entry.height == height; });
if (pool_it == m_checkpoint_pool.end())
return false;
for (auto it = pool_it->votes.begin(); it != pool_it->votes.end(); it++)
{
if (it->vote.index_in_group == index_in_quorum)
return true;
}
return false;
}
}; // namespace service_nodes

View File

@ -147,6 +147,7 @@ namespace service_nodes
void remove_expired_votes(uint64_t height);
void remove_used_votes (std::vector<cryptonote::transaction> const &txs, uint8_t hard_fork_version);
std::vector<quorum_vote_t> get_relayable_votes (uint64_t height) const;
bool received_checkpoint_vote(uint64_t height, size_t index_in_quorum) const;
private:
std::vector<pool_vote_entry> *find_vote_pool(const quorum_vote_t &vote, bool create_if_not_found = false);