mirror of https://github.com/oxen-io/oxen-core.git
Switch to a circular buffer for recording checkpointing votes
This commit is contained in:
parent
9b9029d7c6
commit
bdf8829f6b
|
@ -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;
|
||||
}
|
||||
//-----------------------------------------------------------------------------------------------
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue