oxen-core/src/cryptonote_core/service_node_quorum_cop.cpp

750 lines
35 KiB
C++

// Copyright (c) 2018, The Loki Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "service_node_quorum_cop.h"
#include "service_node_voting.h"
#include "service_node_list.h"
#include "uptime_proof.h"
#include "cryptonote_config.h"
#include "cryptonote_core.h"
#include "cryptonote_basic/hardfork.h"
#include "version.h"
#include "common/oxen.h"
#include "common/util.h"
#include "epee/net/local_ip.h"
#include <boost/endian/conversion.hpp>
#undef OXEN_DEFAULT_LOG_CATEGORY
#define OXEN_DEFAULT_LOG_CATEGORY "quorum_cop"
namespace service_nodes
{
std::optional<std::vector<std::string_view>> service_node_test_results::why() const
{
if (passed())
return std::nullopt;
std::vector<std::string_view> results{{"Service Node is currently failing the following tests:"sv}};
if (!uptime_proved) results.push_back("Uptime proof missing."sv);
if (!checkpoint_participation) results.push_back("Skipped voting in too many checkpoints."sv);
if (!pulse_participation) results.push_back("Skipped voting in too many pulse quorums."sv);
// These ones are not likely to be useful when we are reporting on ourself:
if (!timestamp_participation) results.push_back("Too many out-of-sync timesync replies."sv);
if (!timesync_status) results.push_back("Too many missed timesync replies."sv);
if (!storage_server_reachable) results.push_back("Storage server is not reachable."sv);
if (!lokinet_reachable) results.push_back("Lokinet router is not reachable."sv);
return results;
}
quorum_cop::quorum_cop(cryptonote::core& core)
: m_core(core), m_obligations_height(0), m_last_checkpointed_height(0)
{
}
void quorum_cop::init()
{
m_obligations_height = 0;
m_last_checkpointed_height = 0;
}
// Perform service node tests -- this returns true is the server node is in a good state, that is,
// has submitted uptime proofs, participated in required quorums, etc.
service_node_test_results quorum_cop::check_service_node(uint8_t hf_version, const crypto::public_key &pubkey, const service_node_info &info) const
{
const auto& netconf = m_core.get_net_config();
service_node_test_results result; // Defaults to true for individual tests
bool ss_reachable = true, lokinet_reachable = true;
uint64_t timestamp = 0;
decltype(std::declval<proof_info>().public_ips) ips{};
service_nodes::participation_history<service_nodes::participation_entry> checkpoint_participation{};
service_nodes::participation_history<service_nodes::participation_entry> pulse_participation{};
service_nodes::participation_history<service_nodes::timestamp_participation_entry> timestamp_participation{};
service_nodes::participation_history<service_nodes::timesync_entry> timesync_status{};
constexpr std::array<uint16_t, 3> MIN_TIMESTAMP_VERSION{9,1,0};
const auto unreachable_threshold = netconf.UPTIME_PROOF_VALIDITY - netconf.UPTIME_PROOF_FREQUENCY;
m_core.get_service_node_list().access_proof(pubkey, [&](const proof_info &proof) {
ss_reachable = !proof.ss_reachable.unreachable_for(unreachable_threshold);
lokinet_reachable = !proof.lokinet_reachable.unreachable_for(unreachable_threshold);
timestamp = std::max(proof.timestamp, proof.effective_timestamp);
ips = proof.public_ips;
checkpoint_participation = proof.checkpoint_participation;
pulse_participation = proof.pulse_participation;
timestamp_participation = proof.timestamp_participation;
timesync_status = proof.timesync_status;
});
std::chrono::seconds time_since_last_uptime_proof{std::time(nullptr) - timestamp};
bool check_uptime_obligation = true;
bool check_checkpoint_obligation = true;
if (check_uptime_obligation && time_since_last_uptime_proof > netconf.UPTIME_PROOF_VALIDITY)
{
LOG_PRINT_L1(
"Service Node: " << pubkey << ", failed uptime proof obligation check: the last uptime proof (" <<
tools::get_human_readable_timespan(time_since_last_uptime_proof) << ") was older than max validity (" <<
tools::get_human_readable_timespan(netconf.UPTIME_PROOF_VALIDITY) << ")");
result.uptime_proved = false;
}
if (!ss_reachable)
{
LOG_PRINT_L1("Service Node storage server is not reachable for node: " << pubkey);
result.storage_server_reachable = false;
}
// TODO: perhaps come back and make this activate on some "soft fork" height before HF19?
if (!lokinet_reachable && hf_version >= cryptonote::network_version_19)
{
LOG_PRINT_L1("Service Node lokinet is not reachable for node: " << pubkey);
result.lokinet_reachable = false;
}
// IP change checks
if (ips[0].first && ips[1].first) {
// Figure out when we last had a blockchain-level IP change penalty (or when we registered);
// we only consider IP changes starting two hours after the last IP penalty.
std::vector<cryptonote::block> blocks;
if (m_core.get_blocks(info.last_ip_change_height, 1, blocks)) {
uint64_t find_ips_used_since = std::max(
uint64_t(std::time(nullptr)) - std::chrono::seconds{IP_CHANGE_WINDOW}.count(),
uint64_t(blocks[0].timestamp) + std::chrono::seconds{IP_CHANGE_BUFFER}.count());
if (ips[0].second > find_ips_used_since && ips[1].second > find_ips_used_since)
result.single_ip = false;
}
}
if (!info.is_decommissioned())
{
if (check_checkpoint_obligation && !checkpoint_participation.check_participation(CHECKPOINT_MAX_MISSABLE_VOTES) )
{
LOG_PRINT_L1("Service Node: " << pubkey << ", failed checkpoint obligation check");
result.checkpoint_participation = false;
}
if (!pulse_participation.check_participation(PULSE_MAX_MISSABLE_VOTES) )
{
LOG_PRINT_L1("Service Node: " << pubkey << ", failed pulse obligation check");
result.pulse_participation = false;
}
if (!timestamp_participation.check_participation(TIMESTAMP_MAX_MISSABLE_VOTES) )
{
LOG_PRINT_L1("Service Node: " << pubkey << ", failed timestamp obligation check");
result.timestamp_participation = false;
}
if (!timesync_status.check_participation(TIMESYNC_MAX_UNSYNCED_VOTES) )
{
LOG_PRINT_L1("Service Node: " << pubkey << ", failed timesync obligation check");
result.timesync_status = false;
}
}
return result;
}
void quorum_cop::blockchain_detached(uint64_t height, bool by_pop_blocks)
{
uint8_t hf_version = get_network_version(m_core.get_nettype(), height);
uint64_t const REORG_SAFETY_BUFFER_BLOCKS = (hf_version >= cryptonote::network_version_12_checkpointing)
? REORG_SAFETY_BUFFER_BLOCKS_POST_HF12
: REORG_SAFETY_BUFFER_BLOCKS_PRE_HF12;
if (m_obligations_height >= height)
{
if (!by_pop_blocks)
{
LOG_ERROR("The blockchain was detached to height: " << height << ", but quorum cop has already processed votes for obligations up to " << m_obligations_height);
LOG_ERROR("This implies a reorg occured that was over " << REORG_SAFETY_BUFFER_BLOCKS << ". This should rarely happen! Please report this to the devs.");
}
m_obligations_height = height;
}
if (m_last_checkpointed_height >= height + REORG_SAFETY_BUFFER_BLOCKS)
{
if (!by_pop_blocks)
{
LOG_ERROR("The blockchain was detached to height: " << height << ", but quorum cop has already processed votes for checkpointing up to " << m_last_checkpointed_height);
LOG_ERROR("This implies a reorg occured that was over " << REORG_SAFETY_BUFFER_BLOCKS << ". This should rarely happen! Please report this to the devs.");
}
m_last_checkpointed_height = height - (height % CHECKPOINT_INTERVAL);
}
m_vote_pool.remove_expired_votes(height);
}
void quorum_cop::set_votes_relayed(std::vector<quorum_vote_t> const &relayed_votes)
{
m_vote_pool.set_relayed(relayed_votes);
}
std::vector<quorum_vote_t> quorum_cop::get_relayable_votes(uint64_t current_height, uint8_t hf_version, bool quorum_relay)
{
return m_vote_pool.get_relayable_votes(current_height, hf_version, quorum_relay);
}
int find_index_in_quorum_group(std::vector<crypto::public_key> const &group, crypto::public_key const &my_pubkey)
{
int result = -1;
auto it = std::find(group.begin(), group.end(), my_pubkey);
if (it == group.end()) return result;
result = std::distance(group.begin(), it);
return result;
}
void quorum_cop::process_quorums(cryptonote::block const &block)
{
uint8_t const hf_version = block.major_version;
if (hf_version < cryptonote::network_version_9_service_nodes)
return;
const auto& netconf = m_core.get_net_config();
uint64_t const REORG_SAFETY_BUFFER_BLOCKS = (hf_version >= cryptonote::network_version_12_checkpointing)
? REORG_SAFETY_BUFFER_BLOCKS_POST_HF12
: REORG_SAFETY_BUFFER_BLOCKS_PRE_HF12;
const auto& my_keys = m_core.get_service_keys();
bool voting_enabled = m_core.service_node() && m_core.is_service_node(my_keys.pub, /*require_active=*/true);
uint64_t const height = cryptonote::get_block_height(block);
uint64_t const latest_height = std::max(m_core.get_current_blockchain_height(), m_core.get_target_blockchain_height());
if (latest_height < VOTE_LIFETIME)
return;
uint64_t const start_voting_from_height = latest_height - VOTE_LIFETIME;
if (height < start_voting_from_height)
return;
service_nodes::quorum_type const max_quorum_type = service_nodes::max_quorum_type_for_hf(hf_version);
bool tested_myself_once_per_block = false;
time_t start_time = m_core.get_start_time();
std::chrono::seconds live_time{time(nullptr) - start_time};
for (int i = 0; i <= (int)max_quorum_type; i++)
{
quorum_type const type = static_cast<quorum_type>(i);
switch(type)
{
default:
{
assert("Unhandled quorum type " == 0);
LOG_ERROR("Unhandled quorum type with value: " << (int)type);
} break;
case quorum_type::obligations:
{
m_obligations_height = std::max(m_obligations_height, start_voting_from_height);
for (; m_obligations_height < (height - REORG_SAFETY_BUFFER_BLOCKS); m_obligations_height++)
{
uint8_t const obligations_height_hf_version = get_network_version(m_core.get_nettype(), m_obligations_height);
if (obligations_height_hf_version < cryptonote::network_version_9_service_nodes) continue;
// NOTE: Count checkpoints for other nodes, irrespective of being
// a service node or not for statistics. Also count checkpoints
// before the minimum lifetime for same purposes, note, we still
// don't vote for the first 2 hours so this is purely cosmetic
if (obligations_height_hf_version >= cryptonote::network_version_12_checkpointing)
{
service_nodes::service_node_list &node_list = m_core.get_service_node_list();
auto quorum = node_list.get_quorum(quorum_type::checkpointing, m_obligations_height);
std::vector<cryptonote::block> blocks;
if (quorum && m_core.get_blocks(m_obligations_height, 1, blocks))
{
cryptonote::block const &block = blocks[0];
if (start_time < static_cast<ptrdiff_t>(block.timestamp)) // NOTE: If we started up before receiving the block, we likely have the voting information, if not we probably don't.
{
uint64_t quorum_height = offset_testing_quorum_height(quorum_type::checkpointing, m_obligations_height);
for (size_t index_in_quorum = 0; index_in_quorum < quorum->validators.size(); index_in_quorum++)
{
crypto::public_key const &key = quorum->validators[index_in_quorum];
node_list.record_checkpoint_participation(
key,
quorum_height,
m_vote_pool.received_checkpoint_vote(m_obligations_height, index_in_quorum));
}
}
}
}
// NOTE: Wait at least 2 hours before we're allowed to vote so that we collect necessary
// voting information from people on the network
if (live_time < m_core.get_net_config().UPTIME_PROOF_VALIDITY)
continue;
if (!m_core.service_node())
continue;
auto quorum = m_core.get_quorum(quorum_type::obligations, m_obligations_height);
if (!quorum)
{
// TODO(oxen): Fatal error
LOG_ERROR("Obligations quorum for height: " << m_obligations_height << " was not cached in daemon!");
continue;
}
if (quorum->workers.empty()) continue;
int index_in_group = voting_enabled ? find_index_in_quorum_group(quorum->validators, my_keys.pub) : -1;
if (index_in_group >= 0)
{
//
// NOTE: I am in the quorum
//
auto worker_states = m_core.get_service_node_list_state(quorum->workers);
auto worker_it = worker_states.begin();
std::unique_lock lock{m_lock};
int good = 0, total = 0;
for (size_t node_index = 0; node_index < quorum->workers.size(); ++worker_it, ++node_index)
{
// If the SN no longer exists then it'll be omitted from the worker_states vector,
// so if the elements don't line up skip ahead.
while (worker_it->pubkey != quorum->workers[node_index] && node_index < quorum->workers.size())
node_index++;
if (node_index == quorum->workers.size())
break;
total++;
const auto &node_key = worker_it->pubkey;
const auto &info = *worker_it->info;
if (!info.can_be_voted_on(m_obligations_height))
continue;
auto test_results = check_service_node(obligations_height_hf_version, node_key, info);
bool passed = test_results.passed();
new_state vote_for_state;
uint16_t reason = 0;
if (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");
} else if (!test_results.single_ip) {
// Don't worry about this if the SN is getting recommissioned (above) -- it'll
// already reenter at the bottom.
vote_for_state = new_state::ip_change_penalty;
LOG_PRINT_L2("Service node " << quorum->workers[node_index] << " was observed with multiple IPs recently; voting to reset reward position");
} else {
good++;
continue;
}
}
else {
if (!test_results.uptime_proved) reason |= cryptonote::Decommission_Reason::missed_uptime_proof;
if (!test_results.checkpoint_participation) reason |= cryptonote::Decommission_Reason::missed_checkpoints;
if (!test_results.pulse_participation) reason |= cryptonote::Decommission_Reason::missed_pulse_participations;
if (!test_results.storage_server_reachable) reason |= cryptonote::Decommission_Reason::storage_server_unreachable;
if (!test_results.lokinet_reachable) reason |= cryptonote::Decommission_Reason::lokinet_unreachable;
if (!test_results.timestamp_participation) reason |= cryptonote::Decommission_Reason::timestamp_response_unreachable;
if (!test_results.timesync_status) reason |= cryptonote::Decommission_Reason::timesync_status_out_of_sync;
int64_t credit = calculate_decommission_credit(info, latest_height);
if (info.is_decommissioned()) {
if (credit >= 0) {
LOG_PRINT_L2("Decommissioned service node "
<< quorum->workers[node_index]
<< " is still not passing required checks, but has remaining credit (" << credit
<< " blocks); abstaining (to leave decommissioned)");
continue;
}
LOG_PRINT_L2("Decommissioned service node " << quorum->workers[node_index] << " has no remaining credit; voting to deregister");
vote_for_state = new_state::deregister; // Credit ran out!
} else {
if (credit >= DECOMMISSION_MINIMUM) {
vote_for_state = new_state::decommission;
LOG_PRINT_L2("Service node "
<< quorum->workers[node_index]
<< " has stopped passing required checks, but has sufficient earned credit (" << credit << " blocks) to avoid deregistration; voting to decommission");
} else {
vote_for_state = new_state::deregister;
LOG_PRINT_L2("Service node "
<< quorum->workers[node_index]
<< " has stopped passing required checks, but does not have sufficient earned credit ("
<< credit << " blocks, " << DECOMMISSION_MINIMUM
<< " required) to decommission; voting to deregister");
}
}
}
quorum_vote_t vote = service_nodes::make_state_change_vote(m_obligations_height, static_cast<uint16_t>(index_in_group), node_index, vote_for_state, reason, my_keys);
cryptonote::vote_verification_context vvc;
if (!handle_vote(vote, vvc))
LOG_ERROR("Failed to add state change vote; reason: " << print_vote_verification_context(vvc, &vote));
}
if (good > 0)
LOG_PRINT_L2(good << " of " << total << " service nodes are active and passing checks; no state change votes required");
}
else if (!tested_myself_once_per_block && (find_index_in_quorum_group(quorum->workers, my_keys.pub) >= 0))
{
// NOTE: Not in validating quorum , check if we're the ones
// being tested. If so, check if we would be decommissioned
// based on _our_ data and if so, report it to the user so they
// know about it.
const auto states_array = m_core.get_service_node_list_state({my_keys.pub});
if (states_array.size())
{
const auto &info = *states_array[0].info;
if (info.can_be_voted_on(m_obligations_height))
{
tested_myself_once_per_block = true;
auto my_test_results = check_service_node(obligations_height_hf_version, my_keys.pub, info);
const bool print_failings = info.is_decommissioned() ||
(info.is_active() && !my_test_results.passed() &&
// Don't warn uptime proofs if the daemon is just recently started and is candidate for testing (i.e. restarting the daemon)
(my_test_results.uptime_proved || live_time >= 1h));
if (print_failings)
{
LOG_PRINT_L0(
(info.is_decommissioned()
? "Service Node (yours) is currently decommissioned and being tested in quorum: "
: "Service Node (yours) is active but is not passing tests for quorum: ")
<< m_obligations_height);
if (auto why = my_test_results.why())
LOG_PRINT_L0(tools::join("\n", *why));
else
LOG_PRINT_L0("Service Node is passing all local tests");
LOG_PRINT_L0("(Note that some tests, such as storage server and lokinet reachability, can only assessed by remote service nodes)");
}
}
}
}
}
}
break;
case quorum_type::checkpointing:
{
if (voting_enabled)
{
uint64_t start_checkpointing_height = start_voting_from_height;
if ((start_checkpointing_height % CHECKPOINT_INTERVAL) > 0)
start_checkpointing_height += (CHECKPOINT_INTERVAL - (start_checkpointing_height % CHECKPOINT_INTERVAL));
m_last_checkpointed_height = std::max(start_checkpointing_height, m_last_checkpointed_height);
for (;
m_last_checkpointed_height <= height;
m_last_checkpointed_height += CHECKPOINT_INTERVAL)
{
uint8_t checkpointed_height_hf_version = get_network_version(m_core.get_nettype(), m_last_checkpointed_height);
if (checkpointed_height_hf_version <= cryptonote::network_version_11_infinite_staking)
continue;
if (m_last_checkpointed_height < REORG_SAFETY_BUFFER_BLOCKS)
continue;
auto quorum = m_core.get_quorum(quorum_type::checkpointing, m_last_checkpointed_height);
if (!quorum)
{
// TODO(oxen): Fatal error
LOG_ERROR("Checkpoint quorum for height: " << m_last_checkpointed_height << " was not cached in daemon!");
continue;
}
int index_in_group = find_index_in_quorum_group(quorum->validators, my_keys.pub);
if (index_in_group <= -1) continue;
//
// NOTE: I am in the quorum, handle checkpointing
//
crypto::hash block_hash = m_core.get_block_id_by_height(m_last_checkpointed_height);
quorum_vote_t vote = make_checkpointing_vote(checkpointed_height_hf_version, block_hash, m_last_checkpointed_height, static_cast<uint16_t>(index_in_group), my_keys);
cryptonote::vote_verification_context vvc = {};
if (!handle_vote(vote, vvc))
LOG_ERROR("Failed to add checkpoint vote; reason: " << print_vote_verification_context(vvc, &vote));
}
}
}
break;
case quorum_type::pulse:
case quorum_type::blink:
break;
}
}
}
bool quorum_cop::block_added(const cryptonote::block& block, const std::vector<cryptonote::transaction>& txs, cryptonote::checkpoint_t const * /*checkpoint*/)
{
process_quorums(block);
uint64_t const height = cryptonote::get_block_height(block) + 1; // chain height = new top block height + 1
m_vote_pool.remove_expired_votes(height);
m_vote_pool.remove_used_votes(txs, block.major_version);
// These feels out of place here because the hook system sucks: TODO replace it with
// std::function hooks instead.
m_core.update_omq_sns();
return true;
}
static bool handle_obligations_vote(cryptonote::core &core, const quorum_vote_t& vote, const std::vector<pool_vote_entry>& votes, const quorum& quorum)
{
if (votes.size() < STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE)
{
LOG_PRINT_L2("Don't have enough votes yet to submit a state change transaction: have " << votes.size() << " of " << STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE << " required");
return true;
}
auto net = core.get_blockchain_storage().get_network_version();
// NOTE: Verify state change is still valid or have we processed some other state change already that makes it invalid
{
crypto::public_key const &service_node_pubkey = quorum.workers[vote.state_change.worker_index];
auto service_node_infos = core.get_service_node_list_state({service_node_pubkey});
if (!service_node_infos.size() ||
!service_node_infos[0].info->can_transition_to_state(net, vote.block_height, vote.state_change.state))
// NOTE: Vote is valid but is invalidated because we cannot apply the change to a service node or it is not on the network anymore
// So don't bother generating a state change tx.
return true;
}
using version_t = cryptonote::tx_extra_service_node_state_change::version_t;
auto ver = net >= HF_VERSION_PROOF_BTENC ? version_t::v4_reasons : version_t::v0;
cryptonote::tx_extra_service_node_state_change state_change{
ver,
vote.state_change.state,
vote.block_height,
vote.state_change.worker_index,
vote.state_change.reason,
vote.state_change.reason,
{}};
state_change.votes.reserve(votes.size());
for (const auto &pool_vote : votes)
{
state_change.reason_consensus_any |= pool_vote.vote.state_change.reason;
state_change.reason_consensus_all &= pool_vote.vote.state_change.reason;
state_change.votes.emplace_back(pool_vote.vote.signature, pool_vote.vote.index_in_group);
}
cryptonote::transaction state_change_tx{};
if (cryptonote::add_service_node_state_change_to_tx_extra(state_change_tx.extra, state_change, net))
{
state_change_tx.version = cryptonote::transaction::get_max_version_for_hf(net);
state_change_tx.type = cryptonote::txtype::state_change;
cryptonote::tx_verification_context tvc{};
bool result = core.handle_incoming_tx(cryptonote::tx_to_blob(state_change_tx), tvc, cryptonote::tx_pool_options::new_tx());
if (!result || tvc.m_verifivation_failed)
{
LOG_PRINT_L1("A full state change tx for height: " << vote.block_height <<
" and service node: " << vote.state_change.worker_index <<
" could not be verified and was not added to the memory pool, reason: " <<
print_tx_verification_context(tvc, &state_change_tx));
return false;
}
}
else
LOG_PRINT_L1("Failed to add state change to tx extra for height: "
<< vote.block_height << " and service node: " << vote.state_change.worker_index);
return true;
}
static bool handle_checkpoint_vote(cryptonote::core& core, const quorum_vote_t& vote, const std::vector<pool_vote_entry>& votes, const quorum& quorum)
{
if (votes.size() < CHECKPOINT_MIN_VOTES)
{
LOG_PRINT_L2("Don't have enough votes yet to submit a checkpoint: have " << votes.size() << " of " << CHECKPOINT_MIN_VOTES << " required");
return true;
}
cryptonote::checkpoint_t checkpoint{};
cryptonote::Blockchain &blockchain = core.get_blockchain_storage();
// NOTE: Multiple network threads are going to try and update the
// checkpoint, blockchain.update_checkpoint does NOT do any
// validation- that is done here since we want to keep code for
// converting votes to data suitable for the DB in service node land.
// So then, multiple threads can race to update the checkpoint. One
// thread could retrieve an outdated checkpoint whilst another has
// already updated it. i.e. we could replace a checkpoint with lesser
// votes prematurely. The actual update in the DB is an atomic
// operation, but this check and validation step is NOT, taking the
// lock here makes it so.
std::unique_lock<cryptonote::Blockchain> lock{blockchain};
bool update_checkpoint;
if (blockchain.get_checkpoint(vote.block_height, checkpoint) &&
checkpoint.block_hash == vote.checkpoint.block_hash)
{
update_checkpoint = false;
if (checkpoint.signatures.size() != service_nodes::CHECKPOINT_QUORUM_SIZE)
{
checkpoint.signatures.reserve(service_nodes::CHECKPOINT_QUORUM_SIZE);
std::sort(checkpoint.signatures.begin(),
checkpoint.signatures.end(),
[](service_nodes::quorum_signature const &lhs, service_nodes::quorum_signature const &rhs) {
return lhs.voter_index < rhs.voter_index;
});
for (pool_vote_entry const &pool_vote : votes)
{
auto it = std::lower_bound(checkpoint.signatures.begin(),
checkpoint.signatures.end(),
pool_vote,
[](quorum_signature const &lhs, pool_vote_entry const &vote) {
return lhs.voter_index < vote.vote.index_in_group;
});
if (it == checkpoint.signatures.end() ||
pool_vote.vote.index_in_group != it->voter_index)
{
update_checkpoint = true;
checkpoint.signatures.insert(it, quorum_signature(pool_vote.vote.index_in_group, pool_vote.vote.signature));
}
}
}
}
else
{
update_checkpoint = true;
checkpoint = make_empty_service_node_checkpoint(vote.checkpoint.block_hash, vote.block_height);
checkpoint.signatures.reserve(votes.size());
for (pool_vote_entry const &pool_vote : votes)
checkpoint.signatures.push_back(quorum_signature(pool_vote.vote.index_in_group, pool_vote.vote.signature));
}
if (update_checkpoint)
blockchain.update_checkpoint(checkpoint);
return true;
}
bool quorum_cop::handle_vote(quorum_vote_t const &vote, cryptonote::vote_verification_context &vvc)
{
vvc = {};
if (!verify_vote_age(vote, m_core.get_current_blockchain_height(), vvc))
return false;
std::shared_ptr<const quorum> quorum = m_core.get_quorum(vote.type, vote.block_height);
if (!quorum)
{
vvc.m_invalid_block_height = true;
return false;
}
if (!verify_vote_signature(get_network_version(m_core.get_nettype(), vote.block_height), vote, vvc, *quorum))
return false;
std::vector<pool_vote_entry> votes = m_vote_pool.add_pool_vote_if_unique(vote, vvc);
if (!vvc.m_added_to_pool) // NOTE: Not unique vote
return true;
bool result = true;
switch(vote.type)
{
default:
{
LOG_PRINT_L1("Unhandled vote type with value: " << (int)vote.type);
assert("Unhandled vote type" == 0);
return false;
};
case quorum_type::obligations:
result &= handle_obligations_vote(m_core, vote, votes, *quorum);
break;
case quorum_type::checkpointing:
result &= handle_checkpoint_vote(m_core, vote, votes, *quorum);
break;
}
return result;
}
// Calculate the decommission credit for a service node. If the SN is current decommissioned this
// accumulated blocks.
int64_t quorum_cop::calculate_decommission_credit(const service_node_info &info, uint64_t current_height)
{
// If currently decommissioned, we need to know how long it was up before being decommissioned;
// otherwise we need to know how long since it last become active until now (or 0 if not staked
// yet).
int64_t blocks_up;
if (!info.is_fully_funded())
blocks_up = 0;
else if (info.is_decommissioned()) // decommissioned; the negative of active_since_height tells us when the period leading up to the current decommission started
blocks_up = int64_t(info.last_decommission_height) - (-info.active_since_height);
else
blocks_up = int64_t(current_height) - int64_t(info.active_since_height);
// Now we calculate the credit at last commission plus any credit earned from being up for `blocks_up` blocks since
int64_t credit = info.recommission_credit;
if (blocks_up > 0)
credit += blocks_up * DECOMMISSION_CREDIT_PER_DAY / BLOCKS_EXPECTED_IN_HOURS(24);
if (credit > DECOMMISSION_MAX_CREDIT)
credit = DECOMMISSION_MAX_CREDIT; // Cap the available decommission credit blocks if above the max
// If currently decommissioned, remove any used credits used for the current downtime
if (info.is_decommissioned())
credit -= int64_t(current_height) - int64_t(info.last_decommission_height);
return credit;
}
uint64_t quorum_checksum(const std::vector<crypto::public_key> &pubkeys, size_t offset) {
constexpr size_t KEY_BYTES = sizeof(crypto::public_key);
// Calculate a checksum by reading bytes 0-7 from the first pubkey as a little-endian uint64_t,
// then reading 1-8 from the second pubkey, 2-9 from the third, and so on, and adding all the
// uint64_t values together. If we get to 25 we wrap the read around the end and keep going.
uint64_t sum = 0;
alignas(uint64_t) std::array<char, sizeof(uint64_t)> local;
for (auto &pk : pubkeys) {
offset %= KEY_BYTES;
auto *pkdata = reinterpret_cast<const char *>(&pk);
if (offset <= KEY_BYTES - sizeof(uint64_t))
std::memcpy(local.data(), pkdata + offset, sizeof(uint64_t));
else {
size_t prewrap = KEY_BYTES - offset;
std::memcpy(local.data(), pkdata + offset, prewrap);
std::memcpy(local.data() + prewrap, pkdata, sizeof(uint64_t) - prewrap);
}
sum += boost::endian::little_to_native(*reinterpret_cast<uint64_t *>(local.data()));
++offset;
}
return sum;
}
}