Relax deregistration rules

The replaces the deregistration mechanism with a new state change
mechanism (beginning at the v12 fork) which can change a service node's
network status via three potential values (and is extensible in the
future to handle more):

- deregistered -- this is the same as the existing deregistration; the
SN is instantly removed from the SN list.
- decommissioned -- this is a sort of temporary deregistration: your SN
remains in the service node list, but is removed from the rewards list
and from any network duties.
- recommissioned -- this tx is sent by a quorum if they observe a
decommissioned SN sending uptime proofs again.  Upon reception, the SN
is reactivated and put on the end of the reward list.

Since this is broadening the quorum use, this also renames the relevant
quorum to a "obligations" quorum (since it validates SN obligations),
while the transactions are "state_change" transactions (since they
change the state of a registered SN).

The new parameters added to service_node_rules.h control how this works:

    // Service node decommissioning: as service nodes stay up they earn "credits" (measured in blocks)
    // towards a future outage.  A new service node starts out with INITIAL_CREDIT, and then builds up
    // CREDIT_PER_DAY for each day the service node remains active up to a maximum of
    // DECOMMISSION_MAX_CREDIT.
    //
    // If a service node stops sending uptime proofs, a quorum will consider whether the service node
    // has built up enough credits (at least MINIMUM): if so, instead of submitting a deregistration,
    // it instead submits a decommission.  This removes the service node from the list of active
    // service nodes both for rewards and for any active network duties.  If the service node comes
    // back online (i.e. starts sending the required performance proofs again) before the credits run
    // out then a quorum will reinstate the service node using a recommission transaction, which adds
    // the service node back to the bottom of the service node reward list, and resets its accumulated
    // credits to 0.  If it does not come back online within the required number of blocks (i.e. the
    // accumulated credit at the point of decommissioning) then a quorum will send a permanent
    // deregistration transaction to the network, starting a 30-day deregistration count down.

This commit currently includes values (which are not necessarily
finalized):
- 8 hours (240 blocks) of credit required for activation of a
decommission (rather than a deregister)
- 0 initial credits at registration
- a maximum of 24 hours (720 blocks) of credits
- credits accumulate at a rate that you hit 24 hours of credits after 30
days of operation.

Miscellaneous other details of this PR:

- a new TX extra tag is used for the state change (including
deregistrations).  The old extra tag has no version or type tag, so
couldn't be reused.  The data in the new tag is slightly more
efficiently packed than the old deregistration transaction, so it gets
used for deregistrations (starting at the v12 fork) as well.

- Correct validator/worker selection required generalizing the shuffle
function to be able to shuffle just part of a vector.  This lets us
stick any down service nodes at the end of the potential list, then
select validators by only shuffling the part of the index vector that
contains active service indices.  Once the validators are selected, the
remainder of the list (this time including decommissioned SN indices) is
shuffled to select quorum workers to check, thus allowing decommisioned
nodes to be randomly included in the nodes to check without being
selected as a validator.

- Swarm recalculation was not quite right: swarms were recalculated on
SN registrations, even if those registrations were include shared node
registrations, but *not* recalculated on stakes.  Starting with the
upgrade this behaviour is fixed (swarms aren't actually used currently
and aren't consensus-relevant so recalculating early won't hurt
anything).

- Details on decomm/dereg are added to RPC info and print_sn/print_sn_status

- Slightly improves the % of reward output in the print_sn output by
rounding it to two digits, and reserves space in the output string to
avoid excessive reallocations.

- Adds various debugging at higher debug levels to quorum voting (into
all of voting itself, vote transmission, and vote reception).

- Reset service node list internal data structure version to 0.  The SN
list has to be rescanned anyway at upgrade (its size has changed), so we
might as well reset the version and remove the version-dependent
serialization code.  (Note that the affected code here is for SN states
in lmdb storage, not for SN-to-SN communication serialization).
This commit is contained in:
Jason Rhinelander 2019-06-18 18:57:02 -03:00
parent 6e0a02d85f
commit 6d6541670e
24 changed files with 942 additions and 594 deletions

View file

@ -166,7 +166,7 @@ namespace cryptonote
};
enum class txtype : uint16_t {
standard,
deregister,
state_change,
key_image_unlock,
_count
};
@ -202,9 +202,9 @@ namespace cryptonote
{
FIELD(output_unlock_times)
if (version == txversion::v3_per_output_unlock_times) {
bool is_deregister = type == txtype::deregister;
FIELD(is_deregister)
type = is_deregister ? txtype::deregister : txtype::standard;
bool is_state_change = type == txtype::state_change;
FIELD(is_state_change)
type = is_state_change ? txtype::state_change : txtype::standard;
}
}
VARINT_FIELD(unlock_time)
@ -585,7 +585,7 @@ namespace cryptonote
switch(type)
{
case txtype::standard: return "standard";
case txtype::deregister: return "deregister";
case txtype::state_change: return "state_change";
case txtype::key_image_unlock: return "key_image_unlock";
default: assert(false); return "xx_unhandled_type";
}

View file

@ -175,9 +175,9 @@ namespace boost
{
a & x.output_unlock_times;
if (x.version == cryptonote::txversion::v3_per_output_unlock_times) {
bool is_deregister = x.type == cryptonote::txtype::deregister;
bool is_deregister = x.type == cryptonote::txtype::state_change;
a & is_deregister;
x.type = is_deregister ? cryptonote::txtype::deregister : cryptonote::txtype::standard;
x.type = is_deregister ? cryptonote::txtype::state_change : cryptonote::txtype::standard;
}
}
a & x.unlock_time;

View file

@ -552,18 +552,19 @@ namespace cryptonote
if (!pick<tx_extra_additional_pub_keys>(nar, tx_extra_fields, TX_EXTRA_TAG_ADDITIONAL_PUBKEYS)) return false;
if (!pick<tx_extra_nonce>(nar, tx_extra_fields, TX_EXTRA_NONCE)) return false;
if (!pick<tx_extra_service_node_register> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_REGISTER)) return false;
if (!pick<tx_extra_service_node_deregister> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_DEREGISTER)) return false;
if (!pick<tx_extra_service_node_winner> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_WINNER)) return false;
if (!pick<tx_extra_service_node_contributor>(nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR)) return false;
if (!pick<tx_extra_service_node_pubkey> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_PUBKEY)) return false;
if (!pick<tx_extra_tx_secret_key> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_SECRET_KEY)) return false;
if (!pick<tx_extra_tx_key_image_proofs> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS)) return false;
if (!pick<tx_extra_tx_key_image_unlock> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK)) return false;
if (!pick<tx_extra_service_node_register> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_REGISTER)) return false;
if (!pick<tx_extra_service_node_deregister_old> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_DEREG_OLD)) return false;
if (!pick<tx_extra_service_node_state_change> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_STATE_CHANGE)) return false;
if (!pick<tx_extra_service_node_winner> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_WINNER)) return false;
if (!pick<tx_extra_service_node_contributor> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR)) return false;
if (!pick<tx_extra_service_node_pubkey> (nar, tx_extra_fields, TX_EXTRA_TAG_SERVICE_NODE_PUBKEY)) return false;
if (!pick<tx_extra_tx_secret_key> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_SECRET_KEY)) return false;
if (!pick<tx_extra_tx_key_image_proofs> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS)) return false;
if (!pick<tx_extra_tx_key_image_unlock> (nar, tx_extra_fields, TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK)) return false;
if (!pick<tx_extra_merge_mining_tag>(nar, tx_extra_fields, TX_EXTRA_MERGE_MINING_TAG)) return false;
if (!pick<tx_extra_mysterious_minergate>(nar, tx_extra_fields, TX_EXTRA_MYSTERIOUS_MINERGATE_TAG)) return false;
if (!pick<tx_extra_padding>(nar, tx_extra_fields, TX_EXTRA_TAG_PADDING)) return false;
if (!pick<tx_extra_merge_mining_tag> (nar, tx_extra_fields, TX_EXTRA_MERGE_MINING_TAG)) return false;
if (!pick<tx_extra_mysterious_minergate> (nar, tx_extra_fields, TX_EXTRA_MYSTERIOUS_MINERGATE_TAG)) return false;
if (!pick<tx_extra_padding> (nar, tx_extra_fields, TX_EXTRA_TAG_PADDING)) return false;
// if not empty, someone added a new type and did not add a case above
if (!tx_extra_fields.empty())
@ -682,22 +683,25 @@ namespace cryptonote
memcpy(&tx_extra[start_pos], extra_nonce.data(), extra_nonce.size());
return true;
}
//---------------------------------------------------------------
bool add_service_node_deregister_to_tx_extra(std::vector<uint8_t>& tx_extra, const tx_extra_service_node_deregister& deregistration)
bool add_service_node_state_change_to_tx_extra(std::vector<uint8_t>& tx_extra, const tx_extra_service_node_state_change& state_change, const uint8_t hf_version)
{
tx_extra_field field = tx_extra_service_node_deregister{deregistration.block_height, deregistration.service_node_index, deregistration.votes};
std::ostringstream oss;
binary_archive<true> ar(oss);
bool r = ::do_serialize(ar, field);
CHECK_AND_ASSERT_MES(r, false, "failed to serialize tx extra service node deregister");
std::string tx_extra_str = oss.str();
size_t pos = tx_extra.size();
tx_extra.resize(tx_extra.size() + tx_extra_str.size());
memcpy(&tx_extra[pos], tx_extra_str.data(), tx_extra_str.size());
tx_extra_field field;
if (hf_version < network_version_12_checkpointing)
{
CHECK_AND_ASSERT_MES(state_change.state == service_nodes::new_state::deregister, false, "internal error: cannot construct an old deregistration for a non-deregistration state change (before hardfork v12)");
field = tx_extra_service_node_deregister_old{state_change};
}
else
{
field = state_change;
}
bool r = add_tx_extra_field_to_tx_extra(tx_extra, field);
CHECK_AND_ASSERT_MES(r, false, "failed to serialize tx extra service node state change");
return true;
}
//---------------------------------------------------------------
void add_service_node_pubkey_to_tx_extra(std::vector<uint8_t>& tx_extra, const crypto::public_key& pubkey)
{
@ -834,12 +838,27 @@ namespace cryptonote
add_data_to_tx_extra(tx_extra, reinterpret_cast<const char *>(&winner), sizeof(winner), TX_EXTRA_TAG_SERVICE_NODE_WINNER);
}
//---------------------------------------------------------------
bool get_service_node_deregister_from_tx_extra(const std::vector<uint8_t>& tx_extra, tx_extra_service_node_deregister &deregistration)
bool get_service_node_state_change_from_tx_extra(const std::vector<uint8_t>& tx_extra, tx_extra_service_node_state_change &state_change, const uint8_t hf_version)
{
std::vector<tx_extra_field> tx_extra_fields;
parse_tx_extra(tx_extra, tx_extra_fields);
bool result = find_tx_extra_field_by_type(tx_extra_fields, deregistration);
return result;
if (hf_version >= cryptonote::network_version_12_checkpointing) {
// Look for a new-style state change field:
if (find_tx_extra_field_by_type(tx_extra_fields, state_change))
return true;
}
else { // v11 or earlier; parse the old style and copy into a new style
tx_extra_service_node_deregister_old dereg;
if (find_tx_extra_field_by_type(tx_extra_fields, dereg))
{
state_change = tx_extra_service_node_state_change{
service_nodes::new_state::deregister, dereg.block_height, dereg.service_node_index, dereg.votes.begin(), dereg.votes.end()};
return true;
}
}
return false;
}
//---------------------------------------------------------------
crypto::public_key get_service_node_winner_from_tx_extra(const std::vector<uint8_t>& tx_extra)
@ -1209,7 +1228,7 @@ namespace cryptonote
if (vvc.m_invalid_block_height) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Invalid block height: %s, ", vote ? std::to_string(vote->block_height).c_str() : "??");
if (vvc.m_duplicate_voters) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Index in group was duplicated: %s, ", vote ? std::to_string(vote->index_in_group).c_str() : "??");
if (vvc.m_validator_index_out_of_bounds) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Validator index out of bounds");
if (vvc.m_worker_index_out_of_bounds) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Worker index out of bounds: %s, ", vote ? std::to_string(vote->deregister.worker_index).c_str() : "??");
if (vvc.m_worker_index_out_of_bounds) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Worker index out of bounds: %s, ", vote ? std::to_string(vote->state_change.worker_index).c_str() : "??");
if (vvc.m_signature_not_valid) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Signature not valid, ");
if (vvc.m_added_to_pool) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Added to pool, ");
if (vvc.m_not_enough_votes) bufPtr += snprintf(bufPtr, bufEnd - bufPtr, "Not enough votes, ");

View file

@ -92,9 +92,10 @@ namespace cryptonote
void add_tx_pub_key_to_extra(transaction_prefix& tx, const crypto::public_key& tx_pub_key);
void add_tx_pub_key_to_extra(std::vector<uint8_t>& tx_extra, const crypto::public_key& tx_pub_key);
bool add_service_node_deregister_to_tx_extra(std::vector<uint8_t>& tx_extra, const tx_extra_service_node_deregister& deregistration);
bool add_service_node_state_change_to_tx_extra(std::vector<uint8_t>& tx_extra, const tx_extra_service_node_state_change& state_change, uint8_t hf_version);
bool get_service_node_state_change_from_tx_extra(const std::vector<uint8_t>& tx_extra, tx_extra_service_node_state_change& state_change, uint8_t hf_version);
bool get_service_node_register_from_tx_extra(const std::vector<uint8_t>& tx_extra, tx_extra_service_node_register& registration);
bool get_service_node_deregister_from_tx_extra(const std::vector<uint8_t>& tx_extra, tx_extra_service_node_deregister& deregistration);
bool get_service_node_pubkey_from_tx_extra(const std::vector<uint8_t>& tx_extra, crypto::public_key& pubkey);
bool get_service_node_contributor_from_tx_extra(const std::vector<uint8_t>& tx_extra, cryptonote::account_public_address& address);
bool add_service_node_register_to_tx_extra(std::vector<uint8_t>& tx_extra, const std::vector<cryptonote::account_public_address>& addresses, uint64_t portions_for_operator, const std::vector<uint64_t>& portions, uint64_t expiration_timestamp, const crypto::signature& signature);

View file

@ -30,26 +30,45 @@
#pragma once
#define TX_EXTRA_PADDING_MAX_COUNT 255
#define TX_EXTRA_NONCE_MAX_COUNT 255
#include "serialization/serialization.h"
#include "serialization/binary_archive.h"
#include "serialization/variant.h"
#include "crypto/crypto.h"
#include <boost/variant.hpp>
#define TX_EXTRA_TAG_PADDING 0x00
#define TX_EXTRA_TAG_PUBKEY 0x01
#define TX_EXTRA_NONCE 0x02
#define TX_EXTRA_MERGE_MINING_TAG 0x03
#define TX_EXTRA_TAG_ADDITIONAL_PUBKEYS 0x04
#define TX_EXTRA_TAG_SERVICE_NODE_REGISTER 0x70
#define TX_EXTRA_TAG_SERVICE_NODE_DEREGISTER 0x71
#define TX_EXTRA_TAG_SERVICE_NODE_WINNER 0x72
#define TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR 0x73
#define TX_EXTRA_TAG_SERVICE_NODE_PUBKEY 0x74
#define TX_EXTRA_TAG_TX_SECRET_KEY 0x75
#define TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS 0x76
#define TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK 0x77
#define TX_EXTRA_MYSTERIOUS_MINERGATE_TAG 0xDE
#define TX_EXTRA_NONCE_PAYMENT_ID 0x00
#define TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID 0x01
#define TX_EXTRA_PADDING_MAX_COUNT 255
#define TX_EXTRA_NONCE_MAX_COUNT 255
#define TX_EXTRA_TAG_PADDING 0x00
#define TX_EXTRA_TAG_PUBKEY 0x01
#define TX_EXTRA_NONCE 0x02
#define TX_EXTRA_MERGE_MINING_TAG 0x03
#define TX_EXTRA_TAG_ADDITIONAL_PUBKEYS 0x04
#define TX_EXTRA_TAG_SERVICE_NODE_REGISTER 0x70
#define TX_EXTRA_TAG_SERVICE_NODE_DEREG_OLD 0x71
#define TX_EXTRA_TAG_SERVICE_NODE_WINNER 0x72
#define TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR 0x73
#define TX_EXTRA_TAG_SERVICE_NODE_PUBKEY 0x74
#define TX_EXTRA_TAG_TX_SECRET_KEY 0x75
#define TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS 0x76
#define TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK 0x77
#define TX_EXTRA_TAG_SERVICE_NODE_STATE_CHANGE 0x78
#define TX_EXTRA_MYSTERIOUS_MINERGATE_TAG 0xDE
#define TX_EXTRA_NONCE_PAYMENT_ID 0x00
#define TX_EXTRA_NONCE_ENCRYPTED_PAYMENT_ID 0x01
namespace service_nodes {
enum class new_state : uint16_t
{
deregister,
decommission,
recommission,
_count
};
};
namespace cryptonote
{
@ -235,7 +254,7 @@ namespace cryptonote
END_SERIALIZE()
};
struct tx_extra_service_node_deregister
struct tx_extra_service_node_state_change
{
struct vote
{
@ -243,12 +262,66 @@ namespace cryptonote
vote(crypto::signature const &signature, uint32_t validator_index): signature(signature), validator_index(validator_index) { }
crypto::signature signature;
uint32_t validator_index;
BEGIN_SERIALIZE()
VARINT_FIELD(validator_index);
FIELD(signature);
END_SERIALIZE()
};
service_nodes::new_state state;
uint64_t block_height;
uint32_t service_node_index;
std::vector<vote> votes;
tx_extra_service_node_state_change() = default;
template <typename... VotesArgs>
tx_extra_service_node_state_change(service_nodes::new_state state, uint64_t block_height, uint32_t service_node_index, VotesArgs &&...votes)
: state{state}, block_height{block_height}, service_node_index{service_node_index}, votes{std::forward<VotesArgs>(votes)...} {}
// Compares equal if this represents a state change of the same SN (does *not* require equality of stored votes)
bool operator==(const tx_extra_service_node_state_change &sc) const {
return state == sc.state && block_height == sc.block_height && service_node_index == sc.service_node_index;
}
BEGIN_SERIALIZE()
ENUM_FIELD(state, state < service_nodes::new_state::_count);
VARINT_FIELD(block_height);
VARINT_FIELD(service_node_index);
FIELD(votes);
END_SERIALIZE()
};
// Pre-Heimdall service node deregistration data; it doesn't carry the state change (it is only
// used for deregistrations), and is stored slightly less efficiently in the tx extra data.
struct tx_extra_service_node_deregister_old
{
#pragma pack(push, 4)
struct vote { // Not simply using state_change::vote because this gets blob serialized for v11 backwards compat
vote() = default;
vote(const tx_extra_service_node_state_change::vote &v) : signature{v.signature}, validator_index{v.validator_index} {}
crypto::signature signature;
uint32_t validator_index;
operator tx_extra_service_node_state_change::vote() const { return {signature, validator_index}; }
};
#pragma pack(pop)
static_assert(sizeof(vote) == sizeof(crypto::signature) + sizeof(uint32_t), "deregister_old tx extra vote size is not packed");
uint64_t block_height;
uint32_t service_node_index;
std::vector<vote> votes;
tx_extra_service_node_deregister_old() = default;
tx_extra_service_node_deregister_old(const tx_extra_service_node_state_change &state_change)
: block_height{state_change.block_height},
service_node_index{state_change.service_node_index},
votes{state_change.votes.begin(), state_change.votes.end()}
{
assert(state_change.state == service_nodes::new_state::deregister);
}
BEGIN_SERIALIZE()
FIELD(block_height)
FIELD(service_node_index)
@ -272,6 +345,7 @@ namespace cryptonote
crypto::key_image key_image;
crypto::signature signature;
};
static_assert(sizeof(proof) == sizeof(crypto::key_image) + sizeof(crypto::signature), "tx_extra key image proof data structure is not packed");
std::vector<proof> proofs;
@ -286,6 +360,9 @@ namespace cryptonote
crypto::signature signature;
uint32_t nonce;
// Compares equal if this represents the same key image unlock (but does *not* require equality of signature/nonce)
bool operator==(const tx_extra_tx_key_image_unlock &other) const { return key_image == other.key_image; }
BEGIN_SERIALIZE()
FIELD(key_image)
FIELD(signature)
@ -307,27 +384,29 @@ namespace cryptonote
tx_extra_service_node_register,
tx_extra_service_node_contributor,
tx_extra_service_node_winner,
tx_extra_service_node_deregister,
tx_extra_service_node_state_change,
tx_extra_service_node_deregister_old,
tx_extra_tx_secret_key,
tx_extra_tx_key_image_proofs,
tx_extra_tx_key_image_unlock
> tx_extra_field;
}
BLOB_SERIALIZER(cryptonote::tx_extra_service_node_deregister::vote);
BLOB_SERIALIZER(cryptonote::tx_extra_service_node_deregister_old::vote);
BLOB_SERIALIZER(cryptonote::tx_extra_tx_key_image_proofs::proof);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_padding, TX_EXTRA_TAG_PADDING);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_pub_key, TX_EXTRA_TAG_PUBKEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_nonce, TX_EXTRA_NONCE);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_merge_mining_tag, TX_EXTRA_MERGE_MINING_TAG);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_additional_pub_keys, TX_EXTRA_TAG_ADDITIONAL_PUBKEYS);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_mysterious_minergate, TX_EXTRA_MYSTERIOUS_MINERGATE_TAG);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_register, TX_EXTRA_TAG_SERVICE_NODE_REGISTER);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_deregister, TX_EXTRA_TAG_SERVICE_NODE_DEREGISTER);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_contributor, TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_winner, TX_EXTRA_TAG_SERVICE_NODE_WINNER);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_pubkey, TX_EXTRA_TAG_SERVICE_NODE_PUBKEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_secret_key, TX_EXTRA_TAG_TX_SECRET_KEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_key_image_proofs, TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_key_image_unlock, TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_padding, TX_EXTRA_TAG_PADDING);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_pub_key, TX_EXTRA_TAG_PUBKEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_nonce, TX_EXTRA_NONCE);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_merge_mining_tag, TX_EXTRA_MERGE_MINING_TAG);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_additional_pub_keys, TX_EXTRA_TAG_ADDITIONAL_PUBKEYS);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_mysterious_minergate, TX_EXTRA_MYSTERIOUS_MINERGATE_TAG);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_register, TX_EXTRA_TAG_SERVICE_NODE_REGISTER);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_state_change, TX_EXTRA_TAG_SERVICE_NODE_STATE_CHANGE);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_deregister_old, TX_EXTRA_TAG_SERVICE_NODE_DEREG_OLD);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_contributor, TX_EXTRA_TAG_SERVICE_NODE_CONTRIBUTOR);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_winner, TX_EXTRA_TAG_SERVICE_NODE_WINNER);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_service_node_pubkey, TX_EXTRA_TAG_SERVICE_NODE_PUBKEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_secret_key, TX_EXTRA_TAG_TX_SECRET_KEY);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_key_image_proofs, TX_EXTRA_TAG_TX_KEY_IMAGE_PROOFS);
VARIANT_TAG(binary_archive, cryptonote::tx_extra_tx_key_image_unlock, TX_EXTRA_TAG_TX_KEY_IMAGE_UNLOCK);

View file

@ -1275,8 +1275,8 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl
return false;
}
auto min_version = transaction::get_min_version_for_hf(version, nettype());
auto max_version = transaction::get_max_version_for_hf(version, nettype());
const auto min_version = transaction::get_min_version_for_hf(version, nettype());
const auto max_version = transaction::get_max_version_for_hf(version, nettype());
if (b.miner_tx.version < min_version || b.miner_tx.version > max_version)
{
MERROR_VER("Coinbase invalid version: " << b.miner_tx.version << " for hardfork: " << version << " min/max version: " << min_version << "/" << max_version);
@ -2831,12 +2831,12 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
if(pmax_used_block_height)
*pmax_used_block_height = 0;
const int hf_version = m_hardfork->get_current_version();
const auto hf_version = m_hardfork->get_current_version();
// Min/Max Type/Version Check
{
auto min_version = transaction::get_min_version_for_hf(hf_version, nettype());
auto max_version = transaction::get_max_version_for_hf(hf_version, nettype());
const auto min_version = transaction::get_min_version_for_hf(hf_version, nettype());
const auto max_version = transaction::get_max_version_for_hf(hf_version, nettype());
if (hf_version >= network_version_11_infinite_staking)
tvc.m_invalid_type |= (tx.type > txtype::key_image_unlock);
@ -3111,61 +3111,61 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
return false;
}
if (tx.type == txtype::deregister)
if (tx.type == txtype::state_change)
{
// Check the inputs (votes) of the transaction have not already been
// submitted to the blockchain under another transaction using a different
// combination of votes.
tx_extra_service_node_deregister deregister;
if (!get_service_node_deregister_from_tx_extra(tx.extra, deregister))
tx_extra_service_node_state_change state_change;
if (!get_service_node_state_change_from_tx_extra(tx.extra, state_change, hf_version))
{
MERROR_VER("Deregister TX did not have the deregister metadata in the tx_extra");
MERROR_VER("TX did not have the state change metadata in the tx_extra");
return false;
}
const std::shared_ptr<const service_nodes::testing_quorum> quorum = m_service_node_list.get_testing_quorum(service_nodes::quorum_type::deregister, deregister.block_height);
auto quorum = m_service_node_list.get_testing_quorum(service_nodes::quorum_type::obligations, state_change.block_height);
if (!quorum)
{
MERROR_VER("Deregister TX could not get quorum for height: " << deregister.block_height);
MERROR_VER("State change TX could not get quorum for height: " << state_change.block_height);
return false;
}
if (!service_nodes::verify_tx_deregister(deregister, tvc.m_vote_ctx, *quorum))
if (!service_nodes::verify_tx_state_change(state_change, tvc.m_vote_ctx, *quorum, hf_version))
{
tvc.m_verifivation_failed = true;
MERROR_VER("tx " << get_transaction_hash(tx) << ": deregister tx could not be completely verified reason: " << print_vote_verification_context(tvc.m_vote_ctx));
MERROR_VER("tx " << get_transaction_hash(tx) << ": state change tx could not be completely verified reason: " << print_vote_verification_context(tvc.m_vote_ctx));
return false;
}
// Check if deregister is too old or too new to hold onto
// Check if state change is too old or too new to hold onto
{
const uint64_t curr_height = get_current_blockchain_height();
if (deregister.block_height >= curr_height)
if (state_change.block_height >= curr_height)
{
LOG_PRINT_L1("Received deregister tx for height: " << deregister.block_height
<< " and service node: " << deregister.service_node_index
<< ", is newer than current height: " << curr_height
<< " blocks and has been rejected.");
LOG_PRINT_L1("Received state change tx for height: " << state_change.block_height
<< " and service node: " << state_change.service_node_index
<< ", is newer than current height: " << curr_height
<< " blocks and has been rejected.");
tvc.m_vote_ctx.m_invalid_block_height = true;
tvc.m_verifivation_failed = true;
return false;
}
uint64_t delta_height = curr_height - deregister.block_height;
if (delta_height >= service_nodes::DEREGISTER_TX_LIFETIME_IN_BLOCKS)
uint64_t delta_height = curr_height - state_change.block_height;
if (delta_height >= service_nodes::STATE_CHANGE_TX_LIFETIME_IN_BLOCKS)
{
LOG_PRINT_L1("Received deregister tx for height: " << deregister.block_height
<< " and service node: " << deregister.service_node_index
<< ", is older than: " << service_nodes::DEREGISTER_TX_LIFETIME_IN_BLOCKS
<< " blocks and has been rejected. The current height is: " << curr_height);
LOG_PRINT_L1("Received state change tx for height: " << state_change.block_height
<< " and service node: " << state_change.service_node_index
<< ", is older than: " << service_nodes::STATE_CHANGE_TX_LIFETIME_IN_BLOCKS
<< " blocks and has been rejected. The current height is: " << curr_height);
tvc.m_vote_ctx.m_invalid_block_height = true;
tvc.m_verifivation_failed = true;
return false;
}
}
const uint64_t height = deregister.block_height;
const size_t num_blocks_to_check = service_nodes::DEREGISTER_TX_LIFETIME_IN_BLOCKS;
const uint64_t height = state_change.block_height;
constexpr size_t num_blocks_to_check = service_nodes::STATE_CHANGE_TX_LIFETIME_IN_BLOCKS;
std::vector<std::pair<cryptonote::blobdata,block>> blocks;
std::vector<cryptonote::blobdata> txs;
@ -3183,27 +3183,27 @@ bool Blockchain::check_tx_inputs(transaction& tx, tx_verification_context &tvc,
continue;
}
if (existing_tx.type != txtype::deregister)
if (existing_tx.type != txtype::state_change)
continue;
tx_extra_service_node_deregister existing_deregister;
if (!get_service_node_deregister_from_tx_extra(existing_tx.extra, existing_deregister))
tx_extra_service_node_state_change existing_state_change;
if (!get_service_node_state_change_from_tx_extra(existing_tx.extra, existing_state_change, hf_version))
{
MERROR_VER("could not get service node deregister from tx extra, possibly corrupt tx");
MERROR_VER("could not get service node state change from tx extra, possibly corrupt tx");
continue;
}
const std::shared_ptr<const service_nodes::testing_quorum> existing_quorum = m_service_node_list.get_testing_quorum(service_nodes::quorum_type::deregister, existing_deregister.block_height);
const auto existing_quorum = m_service_node_list.get_testing_quorum(service_nodes::quorum_type::obligations, existing_state_change.block_height);
if (!existing_quorum)
{
MERROR_VER("could not get uptime quorum for recent deregister tx");
MERROR_VER("could not get obligations quorum for recent state change tx");
continue;
}
if (existing_quorum->workers[existing_deregister.service_node_index] ==
quorum->workers[deregister.service_node_index])
if (existing_quorum->workers[existing_state_change.service_node_index] ==
quorum->workers[state_change.service_node_index])
{
MERROR_VER("Already seen this deregister tx (aka double spend)");
MERROR_VER("Already seen this state change tx (aka double spend)");
tvc.m_double_spend = true;
return false;
}

View file

@ -641,10 +641,6 @@ namespace cryptonote
/**
* @brief check that a transaction's outputs conform to current standards
*
* This function checks, for example at the time of this writing, that
* each output is of the form a * 10^b (phrased differently, that if
* written out would have only one non-zero digit in base 10).
*
* @param tx the transaction to check the outputs of
* @param tvc returned info about tx verification
*
@ -1186,6 +1182,9 @@ namespace cryptonote
* Currently this function calls ring signature validation for each
* transaction.
*
* This fails if called on a non-standard metadata transaction such as a deregister; you
* generally want to call check_tx() instead, which calls this if appropriate.
*
* @param tx the transaction to validate
* @param tvc returned information about tx verification
* @param pmax_related_block_height return-by-pointer the height of the most recent block in the input set
@ -1194,6 +1193,22 @@ namespace cryptonote
*/
bool check_tx_inputs(transaction& tx, tx_verification_context &tvc, uint64_t* pmax_used_block_height = NULL);
/**
* @brief validates SN metadata transaction properties
*
* Checks the given service node metatransaction (i.e. deregister, unlock request, etc.) for
* validity. Fails if called with a non-metatransaction, or if there is something wrong with
* the metatransaction.
*
* This fails if called on a standard (non-meta) transaction such as a regular transaction or a
* SN registration; you generally want to call check_tx() instead, which calls this if
* appropriate.
*
* @param tx the transaction to validate
* @param tvc returned information about tx verification
*/
bool check_service_node_tx(transaction &tx, tx_verification_context &tvc);
/**
* @brief performs a blockchain reorganization according to the longest chain rule
*

View file

@ -2172,9 +2172,9 @@ namespace cryptonote
return m_service_node_list.get_testing_quorum(type, height);
}
//-----------------------------------------------------------------------------------------------
bool core::is_service_node(const crypto::public_key& pubkey) const
bool core::is_service_node(const crypto::public_key& pubkey, bool require_active) const
{
return m_service_node_list.is_service_node(pubkey);
return m_service_node_list.is_service_node(pubkey, require_active);
}
//-----------------------------------------------------------------------------------------------
const std::vector<service_nodes::key_image_blacklist_entry> &core::get_service_node_blacklisted_key_images() const

View file

@ -800,13 +800,15 @@ namespace cryptonote
std::vector<service_nodes::service_node_pubkey_info> get_service_node_list_state(const std::vector<crypto::public_key>& service_node_pubkeys) const;
/**
* @brief get whether `pubkey` is known as a service node
* @brief get whether `pubkey` is known as a service node.
*
* @param pubkey the public key to test
* @param require_active if true also require that the service node is active (fully funded
* and not decommissioned).
*
* @return whether `pubkey` is known as a service node
* @return whether `pubkey` is known as a (optionally active) service node
*/
bool is_service_node(const crypto::public_key& pubkey) const;
bool is_service_node(const crypto::public_key& pubkey, bool require_active) const;
uint32_t get_service_node_public_ip() const {

View file

@ -30,6 +30,8 @@
#include <random>
#include <algorithm>
#include <boost/endian/conversion.hpp>
#include "ringct/rctSigs.h"
#include "wallet/wallet2.h"
#include "cryptonote_tx_utils.h"
@ -51,16 +53,7 @@ namespace service_nodes
{
static int get_min_service_node_info_version_for_hf(int hf_version)
{
if (hf_version >= cryptonote::network_version_7 && hf_version <= cryptonote::network_version_9_service_nodes)
return service_node_info::version_0;
if (hf_version == cryptonote::network_version_10_bulletproofs)
return service_node_info::version_1_swarms;
if (hf_version == cryptonote::network_version_11_infinite_staking)
return service_node_info::version_2_infinite_staking;
return service_node_info::version_3_checkpointing;
return service_node_info::version_0_checkpointing; // Versioning reset with the full SN rescan in 4.0.0
}
service_node_list::service_node_list(cryptonote::Blockchain& blockchain)
@ -120,28 +113,37 @@ namespace service_nodes
LOG_PRINT_L0("Done recalculating service nodes list");
}
std::vector<crypto::public_key> service_node_list::get_service_nodes_pubkeys() const
{
std::vector<crypto::public_key> result;
for (const auto& iter : m_transient_state.service_nodes_infos)
if (iter.second.is_fully_funded())
result.push_back(iter.first);
template <typename UnaryPredicate>
static std::vector<service_nodes::pubkey_and_sninfo> sort_and_filter(const service_nodes_infos_t &sns_infos, UnaryPredicate p, bool reserve = true) {
std::vector<pubkey_and_sninfo> result;
if (reserve) result.reserve(sns_infos.size());
for (const pubkey_and_sninfo &key_info : sns_infos)
if (p(key_info.second))
result.push_back(key_info);
std::sort(result.begin(), result.end(),
[](const crypto::public_key &a, const crypto::public_key &b) {
[](const pubkey_and_sninfo &a, const pubkey_and_sninfo &b) {
return memcmp(reinterpret_cast<const void*>(&a), reinterpret_cast<const void*>(&b), sizeof(a)) < 0;
});
return result;
}
std::vector<pubkey_and_sninfo> service_node_list::transient_state::active_service_nodes_infos() const {
return sort_and_filter(service_nodes_infos, [](const service_node_info &info) { return info.is_active(); });
}
std::vector<pubkey_and_sninfo> service_node_list::transient_state::decommissioned_service_nodes_infos() const {
return sort_and_filter(service_nodes_infos, [](const service_node_info &info) { return info.is_decommissioned() && info.is_fully_funded(); }, /*reserve=*/ false);
}
std::shared_ptr<const testing_quorum> service_node_list::get_testing_quorum(quorum_type type, uint64_t height) const
{
std::lock_guard<boost::recursive_mutex> lock(m_sn_mutex);
const auto &it = m_transient_state.quorum_states.find(height);
if (it != m_transient_state.quorum_states.end())
{
if (type == quorum_type::deregister)
return it->second.deregister;
if (type == quorum_type::obligations)
return it->second.obligations;
else if (type == quorum_type::checkpointing)
return it->second.checkpointing;
else
@ -202,10 +204,11 @@ namespace service_nodes
m_service_node_pubkey = pub_key;
}
bool service_node_list::is_service_node(const crypto::public_key& pubkey) const
bool service_node_list::is_service_node(const crypto::public_key& pubkey, bool require_active) const
{
std::lock_guard<boost::recursive_mutex> lock(m_sn_mutex);
return m_transient_state.service_nodes_infos.find(pubkey) != m_transient_state.service_nodes_infos.end();
auto it = m_transient_state.service_nodes_infos.find(pubkey);
return it != m_transient_state.service_nodes_infos.end() && (!require_active || it->second.is_active());
}
bool service_node_list::is_key_image_locked(crypto::key_image const &check_image, uint64_t *unlock_height, service_node_info::contribution_t *the_locked_contribution) const
@ -295,70 +298,120 @@ namespace service_nodes
return money_transferred;
}
bool service_node_list::process_deregistration_tx(const cryptonote::transaction& tx, uint64_t block_height)
bool service_node_list::process_state_change_tx(const cryptonote::transaction& tx, uint64_t block_height)
{
if (tx.type != cryptonote::txtype::deregister)
if (tx.type != cryptonote::txtype::state_change)
return false;
cryptonote::tx_extra_service_node_deregister deregister;
if (!cryptonote::get_service_node_deregister_from_tx_extra(tx.extra, deregister))
uint8_t hard_fork_version = m_blockchain.get_hard_fork_version(block_height);
cryptonote::tx_extra_service_node_state_change state_change;
if (!cryptonote::get_service_node_state_change_from_tx_extra(tx.extra, state_change, hard_fork_version))
{
MERROR("Transaction deregister did not have deregister data in tx extra, possibly corrupt tx in blockchain");
MERROR("Transaction did not have valid state change data in tx extra, possibly corrupt tx in blockchain");
return false;
}
const auto state = get_testing_quorum(quorum_type::deregister, deregister.block_height);
const auto state = get_testing_quorum(quorum_type::obligations, state_change.block_height);
if (!state)
{
// TODO(loki): Not being able to find a quorum is fatal! We want better caching abilities.
MERROR("Uptime quorum for height: " << deregister.block_height << ", was not stored by the daemon");
MERROR("Uptime quorum for height: " << state_change.block_height << ", was not stored by the daemon");
return false;
}
if (deregister.service_node_index >= state->workers.size())
if (state_change.service_node_index >= state->workers.size())
{
MERROR("Service node index to vote off has become invalid, quorum rules have changed without a hardfork.");
return false;
}
const crypto::public_key& key = state->workers[deregister.service_node_index];
const crypto::public_key& key = state->workers[state_change.service_node_index];
auto iter = m_transient_state.service_nodes_infos.find(key);
if (iter == m_transient_state.service_nodes_infos.end())
if (iter == m_transient_state.service_nodes_infos.end()) {
LOG_PRINT_L2("Received state change tx for non-registered service node " << key << " (perhaps a delayed tx?)");
return false;
if (m_service_node_pubkey && *m_service_node_pubkey == key)
{
MGINFO_RED("Deregistration for service node (yours): " << key);
}
else
{
LOG_PRINT_L1("Deregistration for service node: " << key);
}
m_transient_state.rollback_events.push_back(std::unique_ptr<rollback_event>(new rollback_change(block_height, key, iter->second)));
auto &info = iter->second;
bool is_me = m_service_node_pubkey && *m_service_node_pubkey == key;
int hard_fork_version = m_blockchain.get_hard_fork_version(block_height);
if (hard_fork_version >= cryptonote::network_version_11_infinite_staking)
{
for (const auto &contributor : iter->second.contributors)
{
for (const auto &contribution : contributor.locked_contributions)
switch (state_change.state) {
case new_state::deregister:
if (is_me)
MGINFO_RED("Deregistration for service node (yours): " << key);
else
LOG_PRINT_L1("Deregistration for service node: " << key);
if (hard_fork_version >= cryptonote::network_version_11_infinite_staking)
{
key_image_blacklist_entry entry = {};
entry.version = get_min_service_node_info_version_for_hf(hard_fork_version);
entry.key_image = contribution.key_image;
entry.unlock_height = block_height + staking_num_lock_blocks(m_blockchain.nettype());
m_transient_state.key_image_blacklist.push_back(entry);
for (const auto &contributor : info.contributors)
{
for (const auto &contribution : contributor.locked_contributions)
{
key_image_blacklist_entry entry = {};
entry.version = get_min_service_node_info_version_for_hf(hard_fork_version);
entry.key_image = contribution.key_image;
entry.unlock_height = block_height + staking_num_lock_blocks(m_blockchain.nettype());
m_transient_state.key_image_blacklist.push_back(entry);
const bool adding_to_blacklist = true;
m_transient_state.rollback_events.push_back(std::unique_ptr<rollback_event>(new rollback_key_image_blacklist(block_height, entry, adding_to_blacklist)));
const bool adding_to_blacklist = true;
m_transient_state.rollback_events.push_back(std::unique_ptr<rollback_event>(new rollback_key_image_blacklist(block_height, entry, adding_to_blacklist)));
}
}
}
}
}
m_transient_state.service_nodes_infos.erase(iter);
return true;
m_transient_state.rollback_events.emplace_back(new rollback_change(block_height, key, info));
m_transient_state.service_nodes_infos.erase(iter);
return true;
case new_state::decommission:
if (hard_fork_version < cryptonote::network_version_12_checkpointing) {
MERROR("Invalid decommission transaction seen before network v12");
return false;
}
if (info.is_decommissioned()) {
LOG_PRINT_L2("Received decommission tx for already-decommissioned service node " << key << "; ignoring");
return false;
}
if (is_me)
MGINFO_RED("Temporary decommission for service node (yours): " << key);
else
LOG_PRINT_L1("Temporary decommission for service node: " << key);
m_transient_state.rollback_events.emplace_back(new rollback_change(block_height, key, info));
info.active_since_height = -info.active_since_height;
info.last_decommission_height = block_height;
info.decommission_count++;
return true;
case new_state::recommission:
if (hard_fork_version < cryptonote::network_version_12_checkpointing) {
MERROR("Invalid recommission transaction seen before network v12");
return false;
}
if (!info.is_decommissioned()) {
LOG_PRINT_L2("Received recommission tx for already-active service node " << key << "; ignoring");
return false;
}
if (is_me)
MGINFO_GREEN("Recommission for service node (yours): " << key);
else
LOG_PRINT_L1("Recommission for service node: " << key);
m_transient_state.rollback_events.emplace_back(new rollback_change(block_height, key, info));
info.active_since_height = block_height;
return true;
default:
// dev bug!
MERROR("BUG: Service node state change tx has unknown state " << static_cast<uint16_t>(state_change.state));
return false;
}
}
void service_node_list::update_swarms(uint64_t height) {
@ -370,12 +423,8 @@ namespace service_nodes
/// Gather existing swarms from infos
swarm_snode_map_t existing_swarms;
const std::vector<crypto::public_key> sorted_sn_pubkeys = get_service_nodes_pubkeys();
for (const crypto::public_key& pk : sorted_sn_pubkeys) {
const service_node_info& info = m_transient_state.service_nodes_infos.at(pk);
existing_swarms[info.swarm_id].push_back(pk);
}
for (const auto &key_info : m_transient_state.active_service_nodes_infos())
existing_swarms[key_info.second.swarm_id].push_back(key_info.first);
calc_swarm_changes(existing_swarms, seed);
@ -590,12 +639,13 @@ namespace service_nodes
info.registration_height = block_height;
info.last_reward_block_height = block_height;
info.last_reward_transaction_index = index;
info.active_since_height = 0;
info.last_decommission_height = 0;
info.decommission_count = 0;
info.total_contributed = 0;
info.total_reserved = 0;
info.version = get_min_service_node_info_version_for_hf(hf_version);
if (info.version >= service_node_info::version_1_swarms)
info.swarm_id = UNASSIGNED_SWARM_ID;
info.swarm_id = UNASSIGNED_SWARM_ID;
info.contributors.clear();
@ -690,19 +740,19 @@ namespace service_nodes
return true;
}
void service_node_list::process_contribution_tx(const cryptonote::transaction& tx, uint64_t block_height, uint32_t index)
bool service_node_list::process_contribution_tx(const cryptonote::transaction& tx, uint64_t block_height, uint32_t index)
{
crypto::public_key pubkey;
if (!cryptonote::get_service_node_pubkey_from_tx_extra(tx.extra, pubkey))
return; // Is not a contribution TX don't need to check it.
return false; // Is not a contribution TX don't need to check it.
parsed_tx_contribution parsed_contribution = {};
const int hf_version = m_blockchain.get_hard_fork_version(block_height);
if (!get_contribution(m_blockchain.nettype(), hf_version, tx, block_height, parsed_contribution))
{
LOG_PRINT_L1("Contribution TX: Could not decode contribution for service node: " << pubkey << " on height: " << block_height << " for tx: " << cryptonote::get_transaction_hash(tx));
return;
return false;
}
/// Service node must be registered
@ -713,7 +763,7 @@ namespace service_nodes
", but could not be found in the service node list on height: " << block_height <<
" for tx: " << cryptonote::get_transaction_hash(tx )<< "\n"
"This could mean that the service node was deregistered before the contribution was processed.");
return;
return false;
}
service_node_info& info = iter->second;
@ -722,13 +772,13 @@ namespace service_nodes
LOG_PRINT_L1("Contribution TX: Service node: " << pubkey <<
" is already fully funded, but contribution received on height: " << block_height <<
" for tx: " << cryptonote::get_transaction_hash(tx));
return;
return false;
}
if (!cryptonote::get_tx_secret_key_from_tx_extra(tx.extra, parsed_contribution.tx_key))
{
LOG_PRINT_L1("Contribution TX: Failed to get tx secret key from contribution received on height: " << block_height << " for tx: " << cryptonote::get_transaction_hash(tx));
return;
return false;
}
auto& contributors = info.contributors;
@ -745,7 +795,7 @@ namespace service_nodes
" for service node: " << pubkey <<
" on height: " << block_height <<
" for tx: " << cryptonote::get_transaction_hash(tx));
return;
return false;
}
/// Check that the contribution is large enough
@ -758,7 +808,7 @@ namespace service_nodes
" for service node: " << pubkey <<
" on height: " << block_height <<
" for tx: " << cryptonote::get_transaction_hash(tx));
return;
return false;
}
}
@ -817,6 +867,11 @@ namespace service_nodes
}
LOG_PRINT_L1("Contribution of " << parsed_contribution.transferred << " received for service node " << pubkey);
if (info.is_fully_funded()) {
info.active_since_height = block_height;
return true;
}
return false;
}
void service_node_list::block_added(const cryptonote::block& block, const std::vector<cryptonote::transaction>& txs)
@ -835,6 +890,8 @@ namespace service_nodes
if (hard_fork_version < 9)
return;
bool need_swarm_update = false;
//
// Remove old rollback events
//
@ -869,7 +926,6 @@ namespace service_nodes
//
// Expire Nodes
//
size_t expired_count = 0;
for (const crypto::public_key& pubkey : update_and_get_expired_nodes(txs, block_height))
{
auto i = m_transient_state.service_nodes_infos.find(pubkey);
@ -886,7 +942,7 @@ namespace service_nodes
m_transient_state.rollback_events.push_back(std::unique_ptr<rollback_event>(new rollback_change(block_height, pubkey, i->second)));
expired_count++;
need_swarm_update += i->second.is_active();
m_transient_state.service_nodes_infos.erase(i);
}
}
@ -912,22 +968,17 @@ namespace service_nodes
//
// Process TXs in the Block
//
size_t registrations = 0;
size_t deregistrations = 0;
for (uint32_t index = 0; index < txs.size(); ++index)
{
const cryptonote::transaction& tx = txs[index];
if (tx.type == cryptonote::txtype::standard)
{
if (process_registration_tx(tx, block.timestamp, block_height, index))
registrations++;
process_contribution_tx(tx, block_height, index);
process_registration_tx(tx, block.timestamp, block_height, index);
need_swarm_update += process_contribution_tx(tx, block_height, index);
}
else if (tx.type == cryptonote::txtype::deregister)
else if (tx.type == cryptonote::txtype::state_change)
{
if (process_deregistration_tx(tx, block_height))
deregistrations++;
need_swarm_update += process_state_change_tx(tx, block_height);
}
else if (tx.type == cryptonote::txtype::key_image_unlock)
{
@ -984,7 +1035,7 @@ namespace service_nodes
}
}
if (registrations || deregistrations || expired_count) {
if (need_swarm_update) {
update_swarms(block_height);
}
@ -1273,16 +1324,33 @@ namespace service_nodes
return true;
}
std::vector<size_t> generate_shuffled_service_node_index_list(std::vector<crypto::public_key> const &snode_list, crypto::hash const &block_hash, quorum_type type)
std::vector<size_t> generate_shuffled_service_node_index_list(
size_t list_size,
crypto::hash const &block_hash,
quorum_type type,
size_t sublist_size = 0,
size_t sublist_up_to = 0)
{
std::vector<size_t> result(snode_list.size());
for (size_t i = 0; i < snode_list.size(); i++) result[i] = i;
std::vector<size_t> result(list_size);
std::iota(result.begin(), result.end(), 0);
uint64_t seed = 0;
std::memcpy(&seed, block_hash.data, std::min(sizeof(seed), sizeof(block_hash.data)));
boost::endian::little_to_native_inplace(seed);
seed += static_cast<uint64_t>(type);
loki_shuffle(result, seed);
// 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
// reuse the same seed for both partial shuffles, but again, that isn't an issue.
if ((0 < sublist_size && sublist_size < list_size) && (0 < sublist_up_to && sublist_up_to < list_size)) {
assert(sublist_size <= sublist_up_to); // Can't select N random items from M items when M < N
loki_shuffle(result.begin(), result.begin() + sublist_up_to, seed);
loki_shuffle(result.begin() + sublist_size, result.end(), seed);
}
else {
loki_shuffle(result.begin(), result.end(), seed);
}
return result;
}
@ -1297,28 +1365,40 @@ namespace service_nodes
return;
}
std::vector<crypto::public_key> const snode_list = get_service_nodes_pubkeys();
quorum_manager &manager = m_transient_state.quorum_states[height];
for (int type_int = 0; type_int < (int)quorum_type::count; type_int++)
// The two quorums here have different selection criteria: the entire checkpoint quorum and the
// state change *validators* want only active service nodes, but the state change *workers*
// (i.e. the nodes to be tested) also include decommissioned service nodes. (Prior to v12 there
// are no decommissioned nodes, so this distinction is irrelevant for network concensus).
auto active_snode_list = m_transient_state.active_service_nodes_infos();
decltype(active_snode_list) decomm_snode_list;
if (hf_version >= cryptonote::network_version_12_checkpointing)
decomm_snode_list = m_transient_state.decommissioned_service_nodes_infos();
quorum_manager &manager = m_transient_state.quorum_states[height];
for (int type_int = 0; type_int < static_cast<int>(quorum_type::count); type_int++)
{
auto type = static_cast<quorum_type>(type_int);
size_t num_validators = 0, num_workers = 0;
auto quorum = std::make_shared<testing_quorum>();
std::vector<size_t> const pub_keys_indexes = generate_shuffled_service_node_index_list(snode_list, block_hash, type);
auto quorum = std::make_shared<testing_quorum>();
std::vector<size_t> pub_keys_indexes;
if (type == quorum_type::deregister)
if (type == quorum_type::obligations)
{
if (hf_version >= cryptonote::network_version_9_service_nodes)
{
num_validators = std::min(pub_keys_indexes.size(), DEREGISTER_QUORUM_SIZE);
size_t num_remaining_nodes = pub_keys_indexes.size() - num_validators;
num_workers = std::max(num_remaining_nodes/DEREGISTER_NTH_OF_THE_NETWORK_TO_TEST, std::min(DEREGISTER_MIN_NODES_TO_TEST, num_remaining_nodes));
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;
size_t num_remaining_nodes = total_nodes - num_validators;
num_workers = std::min(num_remaining_nodes, std::max(STATE_CHANGE_MIN_NODES_TO_TEST, num_remaining_nodes/STATE_CHANGE_NTH_OF_THE_NETWORK_TO_TEST));
}
}
else if (type == quorum_type::checkpointing)
{
if (hf_version >= cryptonote::network_version_12_checkpointing)
{
manager.checkpointing = quorum;
num_validators = std::min(pub_keys_indexes.size(), CHECKPOINT_QUORUM_SIZE);
size_t num_remaining_nodes = pub_keys_indexes.size() - num_validators;
num_workers = std::min(num_remaining_nodes, CHECKPOINT_QUORUM_SIZE);
@ -1330,30 +1410,23 @@ namespace service_nodes
continue;
}
quorum->validators.reserve(num_validators);
quorum->workers.reserve(num_workers);
size_t i = 0;
for (; i < num_validators; i++)
{
quorum->validators.resize(num_validators);
for (size_t i = 0; i < quorum->validators.size(); i++)
{
size_t node_index = pub_keys_indexes[i];
const crypto::public_key &key = snode_list[node_index];
quorum->validators[i] = key;
}
quorum->validators.push_back(active_snode_list[pub_keys_indexes[i]].first);
}
for (; i < num_validators + num_workers; i++)
{
quorum->workers.resize(num_workers);
for (size_t i = 0; i < quorum->workers.size(); i++)
{
size_t node_index = pub_keys_indexes[quorum->validators.size() + i];
const crypto::public_key &key = snode_list[node_index];
quorum->workers[i] = key;
}
size_t j = pub_keys_indexes[i];
if (j < active_snode_list.size())
quorum->workers.push_back(active_snode_list[j].first);
else
quorum->workers.push_back(decomm_snode_list[j - active_snode_list.size()].first);
}
if (type == quorum_type::deregister)
manager.deregister = quorum;
else
manager.checkpointing = quorum;
}
}
@ -1403,14 +1476,11 @@ namespace service_nodes
quorum.height = kv_pair.first;
quorum_manager const &manager = kv_pair.second;
if (manager.deregister)
quorum.quorums[(int)quorum_type::deregister] = *manager.deregister;
if (manager.obligations)
quorum.quorums[static_cast<uint8_t>(quorum_type::obligations)] = *manager.obligations;
if (quorum.version >= service_node_info::version_3_checkpointing)
{
if (manager.checkpointing)
quorum.quorums[(int)quorum_type::checkpointing] = *manager.checkpointing;
}
if (manager.checkpointing)
quorum.quorums[static_cast<uint8_t>(quorum_type::checkpointing)] = *manager.checkpointing;
data_to_store.quorum_states.push_back(quorum);
}
@ -1457,29 +1527,22 @@ namespace service_nodes
return true;
}
void service_node_list::get_all_service_nodes_public_keys(std::vector<crypto::public_key>& keys, bool fully_funded_nodes_only) const
void service_node_list::get_all_service_nodes_public_keys(std::vector<crypto::public_key>& keys, bool require_active) const
{
keys.clear();
keys.resize(m_transient_state.service_nodes_infos.size());
keys.reserve(m_transient_state.service_nodes_infos.size());
size_t i = 0;
if (fully_funded_nodes_only)
{
for (const auto &it : m_transient_state.service_nodes_infos)
{
service_node_info const &info = it.second;
if (info.is_fully_funded())
keys[i++] = it.first;
}
if (require_active) {
for (const auto &key_info : m_transient_state.service_nodes_infos)
if (key_info.second.is_active())
keys.push_back(key_info.first);
}
else
{
for (const auto &it : m_transient_state.service_nodes_infos)
keys[i++] = it.first;
else {
for (const auto &key_info : m_transient_state.service_nodes_infos)
keys.push_back(key_info.first);
}
}
void service_node_list::handle_uptime_proof(const cryptonote::NOTIFY_UPTIME_PROOF::request &proof) {
const uint8_t hf_version = m_blockchain.get_current_hard_fork_version();
@ -1523,16 +1586,11 @@ namespace service_nodes
for (const auto& states : data_in.quorum_states)
{
{
testing_quorum const &deregister = states.quorums[(int)quorum_type::deregister];
m_transient_state.quorum_states[states.height].deregister = std::make_shared<testing_quorum>(deregister);
}
testing_quorum const &obligations = states.quorums[static_cast<uint8_t>(quorum_type::obligations)];
m_transient_state.quorum_states[states.height].obligations = std::make_shared<testing_quorum>(obligations);
if (states.version >= service_node_info::version_3_checkpointing)
{
testing_quorum const &checkpointing = states.quorums[(int)quorum_type::checkpointing];
m_transient_state.quorum_states[states.height].checkpointing = std::make_shared<testing_quorum>(checkpointing);
}
testing_quorum const &checkpointing = states.quorums[static_cast<uint8_t>(quorum_type::checkpointing)];
m_transient_state.quorum_states[states.height].checkpointing = std::make_shared<testing_quorum>(checkpointing);
}
for (const auto& info : data_in.infos)

View file

@ -43,10 +43,7 @@ namespace service_nodes
{
enum version
{
version_0,
version_1_swarms,
version_2_infinite_staking,
version_3_checkpointing,
version_0_checkpointing, // versioning reset in 4.0.0 (data structure storage changed)
};
struct contribution_t
@ -85,8 +82,7 @@ namespace service_nodes
VARINT_FIELD(amount)
VARINT_FIELD(reserved)
FIELD(address)
if (version >= version_2_infinite_staking)
FIELD(locked_contributions)
FIELD(locked_contributions)
END_SERIALIZE()
};
@ -96,6 +92,9 @@ namespace service_nodes
// block_height and transaction_index are to record when the service node last received a reward.
uint64_t last_reward_block_height;
uint32_t last_reward_transaction_index;
uint32_t decommission_count; // How many times this service node has been decommissioned
int64_t active_since_height; // if decommissioned: equal to the *negative* height at which you became active before the decommission
uint64_t last_decommission_height; // The height at which the last (or current!) decommissioning started, or 0 if never decommissioned
std::vector<contributor_t> contributors;
uint64_t total_contributed;
uint64_t total_reserved;
@ -108,9 +107,10 @@ namespace service_nodes
service_node_info() = default;
bool is_fully_funded() const { return total_contributed >= staking_requirement; }
bool is_decommissioned() const { return active_since_height < 0; }
bool is_active() const { return is_fully_funded() && !is_decommissioned(); }
size_t total_num_locked_contributions() const;
int dummy; // FIXME(doyle)
BEGIN_SERIALIZE_OBJECT()
VARINT_FIELD(version)
VARINT_FIELD(registration_height)
@ -123,14 +123,11 @@ namespace service_nodes
VARINT_FIELD(staking_requirement)
VARINT_FIELD(portions_for_operator)
FIELD(operator_address)
if (version >= service_node_info::version_1_swarms)
VARINT_FIELD(swarm_id)
if (version >= service_node_info::version_3_checkpointing)
{
VARINT_FIELD(public_ip)
VARINT_FIELD(storage_port)
}
VARINT_FIELD(swarm_id)
VARINT_FIELD(public_ip)
VARINT_FIELD(storage_port)
VARINT_FIELD(active_since_height)
VARINT_FIELD(last_decommission_height)
END_SERIALIZE()
};
@ -158,19 +155,24 @@ namespace service_nodes
END_SERIALIZE()
};
template<typename T>
void loki_shuffle(std::vector<T>& a, uint64_t seed)
template<typename RandomIt>
void loki_shuffle(RandomIt begin, RandomIt end, uint64_t seed)
{
if (a.size() <= 1) return;
if (end <= begin + 1) return;
const size_t size = std::distance(begin, end);
std::mt19937_64 mersenne_twister(seed);
for (size_t i = 1; i < a.size(); i++)
for (size_t i = 1; i < size; i++)
{
size_t j = (size_t)uniform_distribution_portable(mersenne_twister, i+1);
if (i != j)
std::swap(a[i], a[j]);
using std::swap;
swap(begin[i], begin[j]);
}
}
using pubkey_and_sninfo = std::pair<crypto::public_key, service_node_info>;
using service_nodes_infos_t = std::unordered_map<crypto::public_key, service_node_info>;
class service_node_list
: public cryptonote::BlockAddedHook,
public cryptonote::BlockchainDetachedHook,
@ -186,7 +188,7 @@ namespace service_nodes
std::vector<std::pair<cryptonote::account_public_address, uint64_t>> get_winner_addresses_and_portions() const;
crypto::public_key select_winner() const;
bool is_service_node(const crypto::public_key& pubkey) const;
bool is_service_node(const crypto::public_key& pubkey, bool require_active = true) const;
bool is_key_image_locked(crypto::key_image const &check_image, uint64_t *unlock_height = nullptr, service_node_info::contribution_t *the_locked_contribution = nullptr) const;
void update_swarms(uint64_t height);
@ -201,7 +203,7 @@ namespace service_nodes
void set_my_service_node_keys(crypto::public_key const *pub_key);
bool store();
void get_all_service_nodes_public_keys(std::vector<crypto::public_key>& keys, bool fully_funded_nodes_only) const;
void get_all_service_nodes_public_keys(std::vector<crypto::public_key>& keys, bool require_active) const;
/// Record public ip and storage port and add them to the service node list
void handle_uptime_proof(const cryptonote::NOTIFY_UPTIME_PROOF::request &proof);
@ -298,14 +300,13 @@ namespace service_nodes
{
uint8_t version;
uint64_t height;
testing_quorum quorums[(size_t)quorum_type::count];
testing_quorum quorums[static_cast<uint8_t>(quorum_type::count)];
BEGIN_SERIALIZE()
FIELD(version)
FIELD(height)
FIELD_N("deregister_quorum", quorums[(size_t)quorum_type::deregister])
if (version >= service_node_info::version_3_checkpointing)
FIELD_N("checkpointing_quorum", quorums[(size_t)quorum_type::checkpointing])
FIELD_N("obligations_quorum", quorums[static_cast<uint8_t>(quorum_type::obligations)])
FIELD_N("checkpointing_quorum", quorums[static_cast<uint8_t>(quorum_type::checkpointing)])
END_SERIALIZE()
};
@ -324,21 +325,21 @@ namespace service_nodes
FIELD(infos)
FIELD(events)
FIELD(height)
if (version >= service_node_info::version_2_infinite_staking)
FIELD(key_image_blacklist)
FIELD(key_image_blacklist)
END_SERIALIZE()
};
private:
// Note(maxim): private methods don't have to be protected the mutex
// Returns true if there was a registration:
bool process_registration_tx(const cryptonote::transaction& tx, uint64_t block_timestamp, uint64_t block_height, uint32_t index);
void process_contribution_tx(const cryptonote::transaction& tx, uint64_t block_height, uint32_t index);
bool process_deregistration_tx(const cryptonote::transaction& tx, uint64_t block_height);
// Returns true if there was a successful contribution that fully funded a service node:
bool process_contribution_tx(const cryptonote::transaction& tx, uint64_t block_height, uint32_t index);
// Returns true if a service node changed state (deregistered, decommissioned, or recommissioned)
bool process_state_change_tx(const cryptonote::transaction& tx, uint64_t block_height);
void process_block(const cryptonote::block& block, const std::vector<cryptonote::transaction>& txs);
std::vector<crypto::public_key> get_service_nodes_pubkeys() const;
bool contribution_tx_output_has_correct_unlock_time(const cryptonote::transaction& tx, size_t i, uint64_t block_height) const;
void generate_quorums(cryptonote::block const &block);
@ -354,14 +355,27 @@ namespace service_nodes
cryptonote::BlockchainDB *m_db;
using block_height = uint64_t;
struct
struct transient_state
{
std::unordered_map<crypto::public_key, service_node_info> service_nodes_infos;
service_nodes_infos_t service_nodes_infos;
std::vector<key_image_blacklist_entry> key_image_blacklist;
std::map<block_height, quorum_manager> quorum_states;
std::list<std::unique_ptr<rollback_event>> rollback_events;
block_height height;
} m_transient_state;
// Returns a filtered, pubkey-sorted vector of service nodes that are active (fully funded and
// *not* decommissioned).
std::vector<pubkey_and_sninfo> active_service_nodes_infos() const;
// Similar to the above, but returns all nodes that are fully funded *and* decommissioned.
std::vector<pubkey_and_sninfo> decommissioned_service_nodes_infos() const;
};
transient_state m_transient_state;
template <typename T>
void load_transient(const rollback_event_variant &event)
{
m_transient_state.rollback_events.emplace_back(new T(boost::get<T>(event)));
}
};
bool reg_tx_extract_fields(const cryptonote::transaction& tx, std::vector<cryptonote::account_public_address>& addresses, uint64_t& portions_for_operator, std::vector<uint64_t>& portions, uint64_t& expiration_timestamp, crypto::public_key& service_node_key, crypto::signature& signature, crypto::public_key& tx_pub_key);

View file

@ -43,16 +43,16 @@
namespace service_nodes
{
static_assert(quorum_cop::REORG_SAFETY_BUFFER_IN_BLOCKS < DEREGISTER_VOTE_LIFETIME, "Safety buffer should always be less than the vote lifetime");
static_assert(quorum_cop::REORG_SAFETY_BUFFER_IN_BLOCKS < STATE_CHANGE_VOTE_LIFETIME, "Safety buffer should always be less than the vote lifetime");
quorum_cop::quorum_cop(cryptonote::core& core)
: m_core(core), m_uptime_proof_height(0), m_last_checkpointed_height(0)
: m_core(core), m_obligations_height(0), m_last_checkpointed_height(0)
{
}
void quorum_cop::init()
{
m_uptime_proof_height = 0;
m_obligations_height = 0;
m_last_checkpointed_height = 0;
m_uptime_proof_seen.clear();
@ -65,14 +65,26 @@ namespace service_nodes
process_quorums(blk);
}
// 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.
bool quorum_cop::check_service_node(const crypto::public_key &pubkey, const service_node_info &info) const
{
if (!m_uptime_proof_seen.count(pubkey))
return false;
// TODO: check for missing checkpoint quorum votes
return true;
}
void quorum_cop::blockchain_detached(uint64_t height)
{
// TODO(doyle): Assumes large reorgs that are no longer possible with checkpointing
if (m_uptime_proof_height >= height)
if (m_obligations_height >= height)
{
LOG_ERROR("The blockchain was detached to height: " << height << ", but quorum cop has already processed votes up to " << m_uptime_proof_height);
LOG_ERROR("The blockchain was detached to height: " << height << ", but quorum cop has already processed votes up to " << m_obligations_height);
LOG_ERROR("This implies a reorg occured that was over " << REORG_SAFETY_BUFFER_IN_BLOCKS << ". This should never happen! Please report this to the devs.");
m_uptime_proof_height = height;
m_obligations_height = height;
}
if (m_last_checkpointed_height >= height)
@ -111,7 +123,7 @@ namespace service_nodes
if (!m_core.get_service_node_keys(my_pubkey, my_seckey))
return;
if (!m_core.is_service_node(my_pubkey))
if (!m_core.is_service_node(my_pubkey, /*require_active=*/ true))
return;
uint64_t const height = cryptonote::get_block_height(block);
@ -137,16 +149,16 @@ namespace service_nodes
LOG_ERROR("Unhandled quorum type with value: " << (int)type);
} break;
case quorum_type::deregister:
case quorum_type::obligations:
{
if (hf_version >= cryptonote::network_version_9_service_nodes)
{
// NOTE: Wait atleast 2 hours before we're allowed to vote so that we collect uptimes from everyone on the network
time_t const now = time(nullptr);
#if defined(LOKI_ENABLE_INTEGRATION_TEST_HOOKS)
time_t const min_lifetime = 0;
constexpr time_t min_lifetime = 0;
#else
time_t const min_lifetime = 60 * 60 * 2;
constexpr time_t min_lifetime = UPTIME_PROOF_MAX_TIME_IN_SECONDS;
#endif
bool alive_for_min_time = (now - m_core.get_start_time()) >= min_lifetime;
if (!alive_for_min_time)
@ -154,42 +166,92 @@ namespace service_nodes
continue;
}
if (m_uptime_proof_height < start_voting_from_height)
m_uptime_proof_height = start_voting_from_height;
if (m_obligations_height < start_voting_from_height)
m_obligations_height = start_voting_from_height;
for (; m_uptime_proof_height < (height - REORG_SAFETY_BUFFER_IN_BLOCKS); m_uptime_proof_height++)
for (; m_obligations_height < (height - REORG_SAFETY_BUFFER_IN_BLOCKS); m_obligations_height++)
{
if (m_core.get_hard_fork_version(m_uptime_proof_height) < cryptonote::network_version_9_service_nodes) continue;
if (m_core.get_hard_fork_version(m_obligations_height) < cryptonote::network_version_9_service_nodes) continue;
const std::shared_ptr<const testing_quorum> quorum =
m_core.get_testing_quorum(quorum_type::deregister, m_uptime_proof_height);
m_core.get_testing_quorum(quorum_type::obligations, m_obligations_height);
if (!quorum)
{
// TODO(loki): Fatal error
LOG_ERROR("Uptime quorum for height: " << m_uptime_proof_height << " was not cached in daemon!");
LOG_ERROR("Obligations quorum for height: " << m_obligations_height << " was not cached in daemon!");
continue;
}
if (quorum->workers.empty()) continue;
int index_in_group = find_index_in_quorum_group(quorum->validators, my_pubkey);
if (index_in_group <= -1) continue;
//
// NOTE: I am in the quorum
//
for (size_t node_index = 0; node_index < quorum->workers.size(); ++node_index)
auto worker_states = m_core.get_service_node_list_state(quorum->workers);
auto worker_it = worker_states.begin();
CRITICAL_REGION_LOCAL(m_lock);
int good = 0, total = 0;
for (size_t node_index = 0; node_index < quorum->workers.size(); ++worker_it, ++node_index)
{
const crypto::public_key &node_key = quorum->workers[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++;
CRITICAL_REGION_LOCAL(m_lock);
bool vote_off_node = (m_uptime_proof_seen.find(node_key) == m_uptime_proof_seen.end());
if (!vote_off_node) continue;
const auto &node_key = worker_it->pubkey;
const auto &info = worker_it->info;
quorum_vote_t vote = service_nodes::make_deregister_vote(
m_uptime_proof_height, static_cast<uint16_t>(index_in_group), node_index, my_pubkey, my_seckey);
bool checks_passed = check_service_node(node_key, info);
new_state vote_for_state;
if (checks_passed) {
if (!info.is_decommissioned()) {
good++;
continue;
}
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 {
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, my_pubkey, my_seckey);
cryptonote::vote_verification_context vvc;
if (!handle_vote(vote, vvc))
LOG_ERROR("Failed to add uptime deregister vote reason: " << print_vote_verification_context(vvc, nullptr));
LOG_ERROR("Failed to add uptime check_state vote; reason: " << print_vote_verification_context(vvc, nullptr));
}
if (good > 0)
LOG_PRINT_L2(good << " of " << total << " service nodes are active and passing checks; no state change votes required");
}
}
}
@ -257,8 +319,8 @@ namespace service_nodes
{
process_quorums(block);
// Since our age checks for deregister votes is now (age >=
// DEREGISTER_VOTE_LIFETIME_IN_BLOCKS) where age is
// Since our age checks for state change votes is now (age >=
// STATE_CHANGE_VOTE_LIFETIME_IN_BLOCKS) where age is
// get_current_blockchain_height() which gives you the height that you are
// currently mining for, i.e. (height + 1).
@ -266,7 +328,7 @@ namespace service_nodes
// go around P2Ping votes due to passing around old votes
uint64_t const height = cryptonote::get_block_height(block) + 1;
m_vote_pool.remove_expired_votes(height);
m_vote_pool.remove_used_votes(txs);
m_vote_pool.remove_used_votes(txs, block.major_version);
}
bool quorum_cop::handle_vote(quorum_vote_t const &vote, cryptonote::vote_verification_context &vvc)
@ -281,7 +343,8 @@ namespace service_nodes
return false;
};
case quorum_type::deregister: break;
case quorum_type::obligations:
break;
case quorum_type::checkpointing:
{
cryptonote::block block;
@ -319,45 +382,47 @@ namespace service_nodes
return false;
};
case quorum_type::deregister:
case quorum_type::obligations:
{
if (votes.size() >= DEREGISTER_MIN_VOTES_TO_KICK_SERVICE_NODE)
if (votes.size() >= STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE)
{
cryptonote::tx_extra_service_node_deregister deregister;
deregister.block_height = vote.block_height;
deregister.service_node_index = vote.deregister.worker_index;
deregister.votes.reserve(votes.size());
cryptonote::tx_extra_service_node_state_change state_change{
vote.state_change.state, vote.block_height, vote.state_change.worker_index};
state_change.votes.reserve(votes.size());
std::transform(votes.begin(), votes.end(), std::back_inserter(deregister.votes), [](pool_vote_entry const &pool_vote) {
auto result = cryptonote::tx_extra_service_node_deregister::vote(pool_vote.vote.signature, pool_vote.vote.index_in_group);
return result;
std::transform(votes.begin(), votes.end(), std::back_inserter(state_change.votes), [](pool_vote_entry const &pool_vote) {
return cryptonote::tx_extra_service_node_state_change::vote{pool_vote.vote.signature, pool_vote.vote.index_in_group};
});
cryptonote::transaction deregister_tx = {};
if (cryptonote::add_service_node_deregister_to_tx_extra(deregister_tx.extra, deregister))
cryptonote::transaction state_change_tx = {};
int hf_version = m_core.get_blockchain_storage().get_current_hard_fork_version();
if (cryptonote::add_service_node_state_change_to_tx_extra(state_change_tx.extra, state_change, hf_version))
{
int hf_version = m_core.get_blockchain_storage().get_current_hard_fork_version();
deregister_tx.version = cryptonote::transaction::get_max_version_for_hf(hf_version, m_core.get_nettype());
deregister_tx.type = cryptonote::txtype::deregister;
state_change_tx.version = cryptonote::transaction::get_max_version_for_hf(hf_version, m_core.get_nettype());
state_change_tx.type = cryptonote::txtype::state_change;
cryptonote::tx_verification_context tvc = {};
cryptonote::blobdata const tx_blob = cryptonote::tx_to_blob(deregister_tx);
cryptonote::blobdata const tx_blob = cryptonote::tx_to_blob(state_change_tx);
result &= m_core.handle_incoming_tx(tx_blob, tvc, false /*keeped_by_block*/, false /*relayed*/, false /*do_not_relay*/);
if (!result || tvc.m_verifivation_failed)
{
LOG_PRINT_L1("A full deregister tx for height: " << vote.block_height <<
" and service node: " << vote.deregister.worker_index <<
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, &deregister_tx));
print_tx_verification_context(tvc, &state_change_tx));
}
}
else
{
LOG_PRINT_L1("Failed to add deregister to tx extra for height: "
<< vote.block_height << " and service node: " << vote.deregister.worker_index);
LOG_PRINT_L1("Failed to add state change to tx extra for height: "
<< vote.block_height << " and service node: " << vote.state_change.worker_index);
}
}
else
{
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");
}
}
break;
@ -381,6 +446,9 @@ namespace service_nodes
m_core.get_blockchain_storage().update_checkpoint(checkpoint);
}
{
LOG_PRINT_L2("Don't have enough votes yet to submit a checkpoint: have " << votes.size() << " of " << CHECKPOINT_MIN_VOTES << " required");
}
}
break;
}
@ -390,6 +458,7 @@ namespace service_nodes
/// NOTE(maxim): we can remove this after hardfork
static crypto::hash make_hash(crypto::public_key const &pubkey, uint64_t timestamp)
{
boost::endian::native_to_little(timestamp);
char buf[44] = "SUP"; // Meaningless magic bytes
crypto::hash result;
memcpy(buf + 4, reinterpret_cast<const void *>(&pubkey), sizeof(pubkey));
@ -432,24 +501,39 @@ namespace service_nodes
const uint32_t public_ip = proof.public_ip;
const uint16_t storage_port = proof.storage_port;
if ((timestamp < now - UPTIME_PROOF_BUFFER_IN_SECONDS) || (timestamp > now + UPTIME_PROOF_BUFFER_IN_SECONDS))
if ((timestamp < now - UPTIME_PROOF_BUFFER_IN_SECONDS) || (timestamp > now + UPTIME_PROOF_BUFFER_IN_SECONDS)) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": timestamp is too far from now");
return false;
}
if (!m_core.is_service_node(pubkey))
if (!m_core.is_service_node(pubkey, /*require_active=*/ false)) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": no such service node is currently registered");
return false;
}
uint64_t height = m_core.get_current_blockchain_height();
int version = m_core.get_hard_fork_version(height);
// NOTE: Only care about major version for now
if (version >= cryptonote::network_version_11_infinite_staking && proof.snode_version_major < 3)
// FIXME(Jason): remove this `false` before release!
if (false && version >= cryptonote::network_version_12_checkpointing && proof.snode_version_major < 4) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": v4+ loki version is required for v12+ network proofs");
return false;
else if (version >= cryptonote::network_version_10_bulletproofs && proof.snode_version_major < 2)
}
else if (version >= cryptonote::network_version_11_infinite_staking && proof.snode_version_major < 3) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": v3+ loki version is required for v11+ network proofs");
return false;
}
else if (version >= cryptonote::network_version_10_bulletproofs && proof.snode_version_major < 2) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": v2+ loki version is required for v10+ network proofs");
return false;
}
CRITICAL_REGION_LOCAL(m_lock);
if (m_uptime_proof_seen[pubkey].timestamp >= now - (UPTIME_PROOF_FREQUENCY_IN_SECONDS / 2))
return false; // already received one uptime proof for this node recently.
if (m_uptime_proof_seen[pubkey].timestamp >= now - (UPTIME_PROOF_FREQUENCY_IN_SECONDS / 2)) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": already received one uptime proof for this node recently");
return false;
}
const uint64_t hf12_height = m_core.get_earliest_ideal_height_for_version(cryptonote::network_version_12_checkpointing);
@ -479,10 +563,12 @@ namespace service_nodes
}
if (!signature_ok) {
LOG_PRINT_L2("Rejecting uptime proof from " << pubkey << ": signature validation failed");
return false;
}
m_uptime_proof_seen[pubkey] = {now, proof.snode_version_major, proof.snode_version_minor, proof.snode_version_patch};
LOG_PRINT_L2("Accepted uptime proof from " << pubkey);
return true;
}
@ -542,4 +628,35 @@ namespace service_nodes
return it->second;
}
// Calculate the decommission credit for a service node. If the SN is current decommissioned this
// returns the number of blocks remaining in the credit; otherwise this is the number of currently
// 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.
int64_t blocks_up;
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 earned from being up for `blocks_up` blocks
int64_t credit = 0;
if (blocks_up >= 0) {
credit = blocks_up * DECOMMISSION_CREDIT_PER_DAY / BLOCKS_EXPECTED_IN_HOURS(24);
if (info.decommission_count <= info.is_decommissioned()) // Has never been decommissioned (or is currently in the first decommission), so add initial starting credit
credit += DECOMMISSION_INITIAL_CREDIT;
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;
}
}

View file

@ -41,6 +41,8 @@ namespace cryptonote
namespace service_nodes
{
struct service_node_info;
struct proof_info
{
uint64_t timestamp;
@ -60,7 +62,7 @@ namespace service_nodes
struct quorum_manager
{
std::shared_ptr<const testing_quorum> deregister;
std::shared_ptr<const testing_quorum> obligations;
std::shared_ptr<const testing_quorum> checkpointing;
};
@ -87,12 +89,16 @@ namespace service_nodes
proof_info get_uptime_proof(const crypto::public_key &pubkey) const;
void generate_uptime_proof_request(cryptonote::NOTIFY_UPTIME_PROOF::request& req) const;
static int64_t calculate_decommission_credit(const service_node_info &info, uint64_t current_height);
bool check_service_node(const crypto::public_key &pubkey, const service_node_info &info) const;
private:
void process_quorums(cryptonote::block const &block);
cryptonote::core& m_core;
voting_pool m_vote_pool;
uint64_t m_uptime_proof_height;
uint64_t m_obligations_height;
uint64_t m_last_checkpointed_height;
std::unordered_map<crypto::public_key, proof_info> m_uptime_proof_seen;

View file

@ -7,11 +7,37 @@
#include <random>
namespace service_nodes {
constexpr size_t DEREGISTER_QUORUM_SIZE = 10;
constexpr size_t DEREGISTER_MIN_VOTES_TO_KICK_SERVICE_NODE = 7;
constexpr size_t DEREGISTER_NTH_OF_THE_NETWORK_TO_TEST = 100;
constexpr size_t DEREGISTER_MIN_NODES_TO_TEST = 50;
constexpr uint64_t DEREGISTER_VOTE_LIFETIME = BLOCKS_EXPECTED_IN_HOURS(2);
// State change quorums are in charge of policing the network by changing the state of a service
// node on the network: temporary decommissioning, recommissioning, and permanent deregistration.
constexpr size_t STATE_CHANGE_QUORUM_SIZE = 10;
constexpr size_t STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE = 7;
constexpr size_t STATE_CHANGE_NTH_OF_THE_NETWORK_TO_TEST = 100;
constexpr size_t STATE_CHANGE_MIN_NODES_TO_TEST = 50;
constexpr uint64_t STATE_CHANGE_VOTE_LIFETIME = BLOCKS_EXPECTED_IN_HOURS(2);
static_assert(STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE <= STATE_CHANGE_QUORUM_SIZE, "The number of votes required to kick can't exceed the actual quorum size, otherwise we never kick.");
// Service node decommissioning: as service nodes stay up they earn "credits" (measured in blocks)
// towards a future outage. A new service node starts out with INITIAL_CREDIT, and then builds up
// CREDIT_PER_DAY for each day the service node remains active up to a maximum of
// DECOMMISSION_MAX_CREDIT.
//
// If a service node stops sending uptime proofs, a quorum will consider whether the service node
// has built up enough credits (at least MINIMUM): if so, instead of submitting a deregistration,
// it instead submits a decommission. This removes the service node from the list of active
// service nodes both for rewards and for any active network duties. If the service node comes
// back online (i.e. starts sending the required performance proofs again) before the credits run
// out then a quorum will reinstate the service node using a recommission transaction, which adds
// the service node back to the bottom of the service node reward list, and resets its accumulated
// credits to 0. If it does not come back online within the required number of blocks (i.e. the
// accumulated credit at the point of decommissioning) then a quorum will send a permanent
// deregistration transaction to the network, starting a 30-day deregistration count down.
constexpr int64_t DECOMMISSION_CREDIT_PER_DAY = BLOCKS_EXPECTED_IN_HOURS(24) / 30;
constexpr int64_t DECOMMISSION_INITIAL_CREDIT = BLOCKS_EXPECTED_IN_HOURS(0);
constexpr int64_t DECOMMISSION_MAX_CREDIT = BLOCKS_EXPECTED_IN_HOURS(24);
constexpr int64_t DECOMMISSION_MINIMUM = BLOCKS_EXPECTED_IN_HOURS(8);
static_assert(DECOMMISSION_INITIAL_CREDIT <= DECOMMISSION_MAX_CREDIT, "Initial registration decommission credit cannot be larger than the maximum decommission credit");
constexpr uint64_t CHECKPOINT_INTERVAL = 4; // Checkpoint every 4 blocks and prune when too old except if (height % CHECKPOINT_STORE_PERSISTENTLY_INTERVAL == 0)
constexpr uint64_t CHECKPOINT_STORE_PERSISTENTLY_INTERVAL = 60; // Persistently store the checkpoints at these intervals
@ -24,7 +50,6 @@ namespace service_nodes {
constexpr size_t CHECKPOINT_MIN_VOTES = 18;
#endif
static_assert(DEREGISTER_MIN_VOTES_TO_KICK_SERVICE_NODE <= DEREGISTER_QUORUM_SIZE, "The number of votes required to kick can't exceed the actual quorum size, otherwise we never kick.");
static_assert(CHECKPOINT_MIN_VOTES <= CHECKPOINT_QUORUM_SIZE, "The number of votes required to kick can't exceed the actual quorum size, otherwise we never kick.");
constexpr size_t MAX_SWARM_SIZE = 10;
@ -40,15 +65,15 @@ namespace service_nodes {
constexpr size_t NEW_SWARM_SIZE = IDEAL_SWARM_SIZE;
// The lower swarm percentile that will be randomly filled with new service nodes
constexpr size_t FILL_SWARM_LOWER_PERCENTILE = 25;
// Redistribute decommissioned snodes to the smallest swarms
// Redistribute snodes from decommissioned swarms to the smallest swarms
constexpr size_t DECOMMISSIONED_REDISTRIBUTION_LOWER_PERCENTILE = 0;
// The upper swarm percentile that will be randomly selected during stealing
constexpr size_t STEALING_SWARM_UPPER_PERCENTILE = 75;
constexpr int MAX_KEY_IMAGES_PER_CONTRIBUTOR = 1;
constexpr uint64_t KEY_IMAGE_AWAITING_UNLOCK_HEIGHT = 0;
constexpr uint64_t DEREGISTER_TX_LIFETIME_IN_BLOCKS = DEREGISTER_VOTE_LIFETIME;
constexpr size_t QUORUM_LIFETIME = (6 * DEREGISTER_TX_LIFETIME_IN_BLOCKS);
constexpr uint64_t STATE_CHANGE_TX_LIFETIME_IN_BLOCKS = STATE_CHANGE_VOTE_LIFETIME;
constexpr size_t QUORUM_LIFETIME = (6 * STATE_CHANGE_TX_LIFETIME_IN_BLOCKS);
using swarm_id_t = uint64_t;
@ -68,7 +93,7 @@ namespace service_nodes {
{
switch (type)
{
case quorum_type::deregister: return DEREGISTER_VOTE_LIFETIME;
case quorum_type::obligations: return STATE_CHANGE_VOTE_LIFETIME;
case quorum_type::checkpointing: return CHECKPOINT_VOTE_LIFETIME;
default:
{

View file

@ -41,6 +41,8 @@
#include <string>
#include <vector>
#include <boost/endian/conversion.hpp>
#undef LOKI_DEFAULT_LOG_CATEGORY
#define LOKI_DEFAULT_LOG_CATEGORY "service_nodes"
@ -48,11 +50,11 @@ namespace service_nodes
{
bool convert_deregister_vote_to_legacy(quorum_vote_t const &vote, legacy_deregister_vote &legacy_vote)
{
if (vote.type != quorum_type::deregister)
if (vote.type != quorum_type::obligations || vote.state_change.state != new_state::deregister)
return false;
legacy_vote.block_height = vote.block_height;
legacy_vote.service_node_index = vote.deregister.worker_index;
legacy_vote.service_node_index = vote.state_change.worker_index;
legacy_vote.voters_quorum_index = vote.index_in_group;
legacy_vote.signature = vote.signature;
return true;
@ -60,26 +62,36 @@ namespace service_nodes
quorum_vote_t convert_legacy_deregister_vote(legacy_deregister_vote const &vote)
{
quorum_vote_t result = {};
result.type = quorum_type::deregister;
result.block_height = vote.block_height;
result.signature = vote.signature;
result.group = quorum_group::validator;
result.index_in_group = vote.voters_quorum_index;
result.deregister.worker_index = vote.service_node_index;
quorum_vote_t result = {};
result.type = quorum_type::obligations;
result.block_height = vote.block_height;
result.signature = vote.signature;
result.group = quorum_group::validator;
result.index_in_group = vote.voters_quorum_index;
result.state_change.worker_index = vote.service_node_index;
result.state_change.state = new_state::deregister;
return result;
}
static crypto::hash make_deregister_vote_hash(uint64_t block_height, uint32_t service_node_index)
static crypto::hash make_state_change_vote_hash(uint64_t block_height, uint32_t service_node_index, new_state state)
{
const int buf_size = sizeof(block_height) + sizeof(service_node_index);
char buf[buf_size];
uint16_t state_int = static_cast<uint16_t>(state);
memcpy(buf, reinterpret_cast<void *>(&block_height), sizeof(block_height));
memcpy(buf + sizeof(block_height), reinterpret_cast<void *>(&service_node_index), sizeof(service_node_index));
char buf[sizeof(block_height) + sizeof(service_node_index) + sizeof(state_int)];
boost::endian::native_to_little_inplace(block_height);
boost::endian::native_to_little_inplace(service_node_index);
boost::endian::native_to_little_inplace(state_int);
memcpy(buf, &block_height, sizeof(block_height));
memcpy(buf + sizeof(block_height), &service_node_index, sizeof(service_node_index));
memcpy(buf + sizeof(block_height) + sizeof(service_node_index), &state_int, sizeof(state_int));
auto size = sizeof(buf);
if (state == new_state::deregister)
size -= sizeof(uint16_t); // Don't include state value for deregs (to be backwards compatible with pre-v12 dereg votes)
crypto::hash result;
crypto::cn_fast_hash(buf, buf_size, result);
crypto::cn_fast_hash(buf, size, result);
return result;
}
@ -95,9 +107,9 @@ namespace service_nodes
return result;
};
case quorum_type::deregister:
case quorum_type::obligations:
{
crypto::hash hash = make_deregister_vote_hash(vote.block_height, vote.deregister.worker_index);
crypto::hash hash = make_state_change_vote_hash(vote.block_height, vote.state_change.worker_index, vote.state_change.state);
crypto::generate_signature(hash, pub, sec, result);
}
break;
@ -113,15 +125,15 @@ namespace service_nodes
return result;
}
crypto::signature make_signature_from_tx_deregister(cryptonote::tx_extra_service_node_deregister const &deregister, crypto::public_key const &pub, crypto::secret_key const &sec)
crypto::signature make_signature_from_tx_state_change(cryptonote::tx_extra_service_node_state_change const &state_change, crypto::public_key const &pub, crypto::secret_key const &sec)
{
crypto::signature result;
crypto::hash hash = make_deregister_vote_hash(deregister.block_height, deregister.service_node_index);
crypto::hash hash = make_state_change_vote_hash(state_change.block_height, state_change.service_node_index, state_change.state);
crypto::generate_signature(hash, pub, sec, result);
return result;
}
static bool bounds_check_worker_index(service_nodes::testing_quorum const &quorum, size_t worker_index, cryptonote::vote_verification_context &vvc)
static bool bounds_check_worker_index(service_nodes::testing_quorum const &quorum, uint32_t worker_index, cryptonote::vote_verification_context &vvc)
{
if (worker_index >= quorum.workers.size())
{
@ -132,7 +144,7 @@ namespace service_nodes
return true;
}
static bool bounds_check_validator_index(service_nodes::testing_quorum const &quorum, size_t validator_index, cryptonote::vote_verification_context &vvc)
static bool bounds_check_validator_index(service_nodes::testing_quorum const &quorum, uint32_t validator_index, cryptonote::vote_verification_context &vvc)
{
if (validator_index >= quorum.validators.size())
{
@ -143,29 +155,42 @@ namespace service_nodes
return true;
}
bool verify_tx_deregister(const cryptonote::tx_extra_service_node_deregister &deregister,
cryptonote::vote_verification_context &vvc,
const service_nodes::testing_quorum &quorum)
bool verify_tx_state_change(const cryptonote::tx_extra_service_node_state_change &state_change,
cryptonote::vote_verification_context &vvc,
const service_nodes::testing_quorum &quorum,
const uint8_t hf_version)
{
if (deregister.votes.size() < service_nodes::DEREGISTER_MIN_VOTES_TO_KICK_SERVICE_NODE)
if (state_change.state != new_state::deregister && hf_version < cryptonote::network_version_12_checkpointing)
{
LOG_PRINT_L1("Non-deregister state changes are invalid before v12");
return false;
}
if (state_change.state > new_state::recommission)
{
LOG_PRINT_L1("Unknown state change to new state: " << static_cast<uint16_t>(state_change.state));
return false;
}
if (state_change.votes.size() < service_nodes::STATE_CHANGE_MIN_VOTES_TO_CHANGE_STATE)
{
LOG_PRINT_L1("Not enough votes");
vvc.m_not_enough_votes = true;
return false;
}
if (deregister.votes.size() > service_nodes::DEREGISTER_QUORUM_SIZE)
if (state_change.votes.size() > service_nodes::STATE_CHANGE_QUORUM_SIZE)
{
LOG_PRINT_L1("Too many votes");
return false;
}
if (!bounds_check_worker_index(quorum, deregister.service_node_index, vvc))
if (!bounds_check_worker_index(quorum, state_change.service_node_index, vvc))
return false;
crypto::hash const hash = make_deregister_vote_hash(deregister.block_height, deregister.service_node_index);
std::array<int, service_nodes::DEREGISTER_QUORUM_SIZE> validator_set = {};
for (const cryptonote::tx_extra_service_node_deregister::vote& vote : deregister.votes)
crypto::hash const hash = make_state_change_vote_hash(state_change.block_height, state_change.service_node_index, state_change.state);
std::array<int, service_nodes::STATE_CHANGE_QUORUM_SIZE> validator_set = {};
for (const auto &vote : state_change.votes)
{
if (!bounds_check_validator_index(quorum, vote.validator_index, vvc))
return false;
@ -189,15 +214,16 @@ namespace service_nodes
return true;
}
quorum_vote_t make_deregister_vote(uint64_t block_height, uint16_t validator_index, uint16_t worker_index, crypto::public_key const &pub_key, crypto::secret_key const &sec_key)
quorum_vote_t make_state_change_vote(uint64_t block_height, uint16_t validator_index, uint16_t worker_index, new_state state, crypto::public_key const &pub_key, crypto::secret_key const &sec_key)
{
quorum_vote_t result = {};
result.type = quorum_type::deregister;
result.block_height = block_height;
result.group = quorum_group::validator;
result.index_in_group = validator_index;
result.deregister.worker_index = worker_index;
result.signature = make_signature_from_vote(result, pub_key, sec_key);
quorum_vote_t result = {};
result.type = quorum_type::obligations;
result.block_height = block_height;
result.group = quorum_group::validator;
result.index_in_group = validator_index;
result.state_change.worker_index = worker_index;
result.state_change.state = state;
result.signature = make_signature_from_vote(result, pub_key, sec_key);
return result;
}
@ -228,7 +254,7 @@ namespace service_nodes
return false;
};
case quorum_type::deregister:
case quorum_type::obligations:
{
if (vote.group != quorum_group::validator)
{
@ -238,10 +264,10 @@ namespace service_nodes
}
key = quorum.validators[vote.index_in_group];
max_vote_age = service_nodes::DEREGISTER_VOTE_LIFETIME;
hash = make_deregister_vote_hash(vote.block_height, vote.deregister.worker_index);
max_vote_age = service_nodes::STATE_CHANGE_VOTE_LIFETIME;
hash = make_state_change_vote_hash(vote.block_height, vote.state_change.worker_index, vote.state_change.state);
bool result = bounds_check_worker_index(quorum, vote.deregister.worker_index, vvc);
bool result = bounds_check_worker_index(quorum, vote.state_change.worker_index, vvc);
if (!result)
return result;
}
@ -304,6 +330,34 @@ namespace service_nodes
return result;
}
template <typename T>
static std::vector<pool_vote_entry> *find_vote_in_pool(std::vector<T> &pool, const quorum_vote_t &vote, bool create) {
T typed_vote{vote};
auto it = std::find(pool.begin(), pool.end(), typed_vote);
if (it != pool.end())
return &it->votes;
if (!create)
return nullptr;
pool.push_back(std::move(typed_vote));
return &pool.back().votes;
}
std::vector<pool_vote_entry> *voting_pool::find_vote_pool(const quorum_vote_t &find_vote, bool create_if_not_found) {
switch(find_vote.type)
{
default:
LOG_PRINT_L1("Unhandled find_vote type with value: " << (int)find_vote.type);
assert("Unhandled find_vote type" == 0);
return nullptr;
case quorum_type::obligations:
return find_vote_in_pool(m_obligations_pool, find_vote, create_if_not_found);
case quorum_type::checkpointing:
return find_vote_in_pool(m_checkpoint_pool, find_vote, create_if_not_found);
}
}
void voting_pool::set_relayed(const std::vector<quorum_vote_t>& votes)
{
CRITICAL_REGION_LOCAL(m_lock);
@ -311,47 +365,11 @@ namespace service_nodes
for (const quorum_vote_t &find_vote : votes)
{
std::vector<pool_vote_entry> *vote_pool = nullptr;
switch(find_vote.type)
{
default:
{
LOG_PRINT_L1("Unhandled find_vote type with value: " << (int)find_vote.type);
assert("Unhandled find_vote type" == 0);
break;
};
case quorum_type::deregister:
{
auto it = std::find_if(m_deregister_pool.begin(), m_deregister_pool.end(), [find_vote](deregister_pool_entry const &entry) {
return (entry.height == find_vote.block_height &&
entry.worker_index == find_vote.deregister.worker_index);
});
if (it == m_deregister_pool.end())
break;
vote_pool = &it->votes;
}
break;
case quorum_type::checkpointing:
{
auto it = std::find_if(m_checkpoint_pool.begin(), m_checkpoint_pool.end(), [find_vote](checkpoint_pool_entry const &entry) {
return (entry.height == find_vote.block_height && entry.hash == find_vote.checkpoint.block_hash);
});
if (it == m_checkpoint_pool.end())
break;
vote_pool = &it->votes;
}
break;
}
std::vector<pool_vote_entry> *vote_pool = find_vote_pool(find_vote);
if (vote_pool) // We found the group that this vote belongs to
{
auto vote = std::find_if(vote_pool->begin(), vote_pool->end(), [find_vote](pool_vote_entry const &entry) {
auto vote = std::find_if(vote_pool->begin(), vote_pool->end(), [&find_vote](pool_vote_entry const &entry) {
return (find_vote.index_in_group == entry.vote.index_in_group);
});
@ -363,6 +381,14 @@ namespace service_nodes
}
}
template <typename T>
static void append_relayable_votes(std::vector<quorum_vote_t> &result, const T &pool, const time_t now, const time_t threshold) {
for (const auto &pool_entry : pool)
for (const auto &vote_entry : pool_entry.votes)
if (now > (time_t)vote_entry.time_last_sent_p2p + threshold)
result.push_back(vote_entry.vote);
}
std::vector<quorum_vote_t> voting_pool::get_relayable_votes() const
{
CRITICAL_REGION_LOCAL(m_lock);
@ -376,48 +402,26 @@ namespace service_nodes
time_t const now = time(nullptr);
std::vector<quorum_vote_t> result;
for (deregister_pool_entry const &pool_entry : m_deregister_pool)
{
for (pool_vote_entry const &vote_entry : pool_entry.votes)
{
const time_t last_sent = now - vote_entry.time_last_sent_p2p;
if (last_sent > TIME_BETWEEN_RELAY)
result.push_back(vote_entry.vote);
}
}
for (checkpoint_pool_entry const &pool_entry : m_checkpoint_pool)
{
for (pool_vote_entry const &vote_entry : pool_entry.votes)
{
const time_t last_sent = now - vote_entry.time_last_sent_p2p;
if (last_sent > TIME_BETWEEN_RELAY)
result.push_back(vote_entry.vote);
}
}
append_relayable_votes(result, m_obligations_pool, now, TIME_BETWEEN_RELAY);
append_relayable_votes(result, m_checkpoint_pool, now, TIME_BETWEEN_RELAY);
return result;
}
// return: True if the vote was unique
template <typename T>
static bool add_vote_to_pool_if_unique(T &vote_pool, quorum_vote_t const &vote)
static bool add_vote_to_pool_if_unique(std::vector<pool_vote_entry> &votes, quorum_vote_t const &vote)
{
auto vote_it = std::find_if(vote_pool.votes.begin(), vote_pool.votes.end(), [&vote](pool_vote_entry const &pool_entry) {
auto vote_it = std::find_if(votes.begin(), votes.end(), [&vote](pool_vote_entry const &pool_entry) {
assert(pool_entry.vote.group == vote.group);
return (pool_entry.vote.index_in_group == vote.index_in_group);
});
bool result = false;
if (vote_it == vote_pool.votes.end())
if (vote_it == votes.end())
{
pool_vote_entry entry = {};
entry.vote = vote;
vote_pool.votes.push_back(entry);
result = true;
votes.push_back({vote});
return true;
}
return result;
return false;
}
std::vector<pool_vote_entry> voting_pool::add_pool_vote_if_unique(uint64_t latest_height,
@ -425,115 +429,67 @@ namespace service_nodes
cryptonote::vote_verification_context& vvc,
const service_nodes::testing_quorum &quorum)
{
std::vector<pool_vote_entry> result = {};
if (!verify_vote(vote, latest_height, vvc, quorum))
{
LOG_PRINT_L1("Signature verification failed for deregister vote");
return result;
return {};
}
CRITICAL_REGION_LOCAL(m_lock);
switch(vote.type)
{
default:
{
LOG_PRINT_L1("Unhandled vote type with value: " << (int)vote.type);
assert("Unhandled vote type" == 0);
return result;
};
auto *votes = find_vote_pool(vote, /*create_if_not_found=*/ true);
if (!votes)
return {};
case quorum_type::deregister:
{
time_t const now = time(NULL);
auto it = std::find_if(m_deregister_pool.begin(), m_deregister_pool.end(), [&vote](deregister_pool_entry const &entry) {
return (entry.height == vote.block_height && entry.worker_index == vote.deregister.worker_index);
});
if (it == m_deregister_pool.end())
{
m_deregister_pool.emplace_back(vote.block_height, vote.deregister.worker_index);
it = (m_deregister_pool.end() - 1);
}
deregister_pool_entry &pool_entry = (*it);
vvc.m_added_to_pool = add_vote_to_pool_if_unique(pool_entry, vote);
result = pool_entry.votes;
}
break;
case quorum_type::checkpointing:
{
// Get Matching Checkpoint
auto it = std::find_if(m_checkpoint_pool.begin(), m_checkpoint_pool.end(), [&vote](checkpoint_pool_entry const &entry) {
return (entry.height == vote.block_height && entry.hash == vote.checkpoint.block_hash);
});
if (it == m_checkpoint_pool.end())
{
m_checkpoint_pool.emplace_back(vote.block_height, vote.checkpoint.block_hash);
it = (m_checkpoint_pool.end() - 1);
}
checkpoint_pool_entry &pool_entry = (*it);
vvc.m_added_to_pool = add_vote_to_pool_if_unique(pool_entry, vote);
result = pool_entry.votes;
}
break;
}
return result;
vvc.m_added_to_pool = add_vote_to_pool_if_unique(*votes, vote);
return *votes;
}
void voting_pool::remove_used_votes(std::vector<cryptonote::transaction> const &txs)
void voting_pool::remove_used_votes(std::vector<cryptonote::transaction> const &txs, uint8_t hard_fork_version)
{
// TODO(doyle): Cull checkpoint votes
CRITICAL_REGION_LOCAL(m_lock);
if (m_deregister_pool.empty())
if (m_obligations_pool.empty())
return;
for (const cryptonote::transaction &tx : txs)
for (const auto &tx : txs)
{
if (tx.type != cryptonote::txtype::deregister)
if (tx.type != cryptonote::txtype::state_change)
continue;
cryptonote::tx_extra_service_node_deregister deregister;
if (!get_service_node_deregister_from_tx_extra(tx.extra, deregister))
cryptonote::tx_extra_service_node_state_change state_change;
if (!get_service_node_state_change_from_tx_extra(tx.extra, state_change, hard_fork_version))
{
LOG_ERROR("Could not get deregister from tx, possibly corrupt tx");
LOG_ERROR("Could not get state change from tx, possibly corrupt tx");
continue;
}
auto it = std::find_if(m_deregister_pool.begin(), m_deregister_pool.end(), [&deregister](deregister_pool_entry const &entry){
return (entry.height == deregister.block_height) && (entry.worker_index == deregister.service_node_index);
});
auto it = std::find(m_obligations_pool.begin(), m_obligations_pool.end(), state_change);
if (it != m_deregister_pool.end())
m_deregister_pool.erase(it);
if (it != m_obligations_pool.end())
m_obligations_pool.erase(it);
}
}
template <typename T>
static void cull_votes(std::vector<T> &vote_pool, uint64_t min_height, uint64_t max_height)
{
for (auto it = vote_pool.begin(); it != vote_pool.end(); ++it)
for (auto it = vote_pool.begin(); it != vote_pool.end(); )
{
const T &pool_entry = *it;
if (pool_entry.height < min_height || pool_entry.height > max_height)
{
it = vote_pool.erase(it);
it--;
}
else
++it;
}
}
void voting_pool::remove_expired_votes(uint64_t height)
{
CRITICAL_REGION_LOCAL(m_lock);
uint64_t deregister_min_height = (height < DEREGISTER_VOTE_LIFETIME) ? 0 : height - DEREGISTER_VOTE_LIFETIME;
cull_votes(m_deregister_pool, deregister_min_height, height);
uint64_t deregister_min_height = (height < STATE_CHANGE_VOTE_LIFETIME) ? 0 : height - STATE_CHANGE_VOTE_LIFETIME;
cull_votes(m_obligations_pool, deregister_min_height, height);
uint64_t checkpoint_min_height = (height < CHECKPOINT_VOTE_LIFETIME) ? 0 : height - CHECKPOINT_VOTE_LIFETIME;
uint64_t checkpoint_min_height = (height < CHECKPOINT_VOTE_LIFETIME) ? 0 : height - CHECKPOINT_VOTE_LIFETIME;
cull_votes(m_checkpoint_pool, checkpoint_min_height, height);
}
}; // namespace service_nodes

View file

@ -35,6 +35,7 @@
#include "crypto/crypto.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "cryptonote_basic/blobdatatype.h"
#include "cryptonote_basic/tx_extra.h"
#include "math_helper.h"
#include "syncobj.h"
@ -42,7 +43,6 @@
namespace cryptonote
{
struct vote_verification_context;
struct tx_extra_service_node_deregister;
};
namespace service_nodes
@ -61,11 +61,11 @@ namespace service_nodes
};
struct checkpoint_vote { crypto::hash block_hash; };
struct deregister_vote { uint16_t worker_index; };
struct state_change_vote { uint16_t worker_index; new_state state; };
enum struct quorum_type : uint8_t
{
deregister = 0,
obligations = 0,
checkpointing,
count,
};
@ -82,17 +82,17 @@ namespace service_nodes
union
{
deregister_vote deregister;
checkpoint_vote checkpoint;
state_change_vote state_change;
checkpoint_vote checkpoint;
};
};
quorum_vote_t make_deregister_vote(uint64_t block_height, uint16_t index_in_group, uint16_t worker_index, crypto::public_key const &pub_key, crypto::secret_key const &secret_key);
quorum_vote_t make_state_change_vote(uint64_t block_height, uint16_t index_in_group, uint16_t worker_index, new_state state, crypto::public_key const &pub_key, crypto::secret_key const &secret_key);
bool verify_tx_deregister (const cryptonote::tx_extra_service_node_deregister& deregister, cryptonote::vote_verification_context& vvc, const service_nodes::testing_quorum &quorum);
bool verify_vote (const quorum_vote_t& vote, uint64_t latest_height, cryptonote::vote_verification_context &vvc, const service_nodes::testing_quorum &quorum);
crypto::signature make_signature_from_vote (quorum_vote_t const &vote, const crypto::public_key& pub, const crypto::secret_key& sec);
crypto::signature make_signature_from_tx_deregister(cryptonote::tx_extra_service_node_deregister const &deregister, crypto::public_key const &pub, crypto::secret_key const &sec);
bool verify_tx_state_change (const cryptonote::tx_extra_service_node_state_change& state_change, cryptonote::vote_verification_context& vvc, const service_nodes::testing_quorum &quorum, uint8_t hf_version);
bool verify_vote (const quorum_vote_t& vote, uint64_t latest_height, cryptonote::vote_verification_context &vvc, const service_nodes::testing_quorum &quorum);
crypto::signature make_signature_from_vote (quorum_vote_t const &vote, const crypto::public_key& pub, const crypto::secret_key& sec);
crypto::signature make_signature_from_tx_state_change(cryptonote::tx_extra_service_node_state_change const &state_change, crypto::public_key const &pub, crypto::secret_key const &sec);
// NOTE: This preserves the deregister vote format pre-checkpointing so that
// up to the hardfork, we can still deserialize and serialize until we switch
@ -125,25 +125,37 @@ namespace service_nodes
// TODO(loki): Review relay behaviour and all the cases when it should be triggered
void set_relayed (const std::vector<quorum_vote_t>& votes);
void remove_expired_votes(uint64_t height);
void remove_used_votes (std::vector<cryptonote::transaction> const &txs);
void remove_used_votes (std::vector<cryptonote::transaction> const &txs, uint8_t hard_fork_version);
std::vector<quorum_vote_t> get_relayable_votes () const;
private:
struct deregister_pool_entry
std::vector<pool_vote_entry> *find_vote_pool(const quorum_vote_t &vote, bool create_if_not_found = false);
struct obligations_pool_entry
{
deregister_pool_entry(uint64_t height, uint32_t worker_index): height(height), worker_index(worker_index) {}
explicit obligations_pool_entry(const quorum_vote_t &vote)
: height{vote.block_height}, worker_index{vote.state_change.worker_index}, state{vote.state_change.state} {}
obligations_pool_entry(const cryptonote::tx_extra_service_node_state_change &sc)
: height{sc.block_height}, worker_index{sc.service_node_index}, state{sc.state} {}
uint64_t height;
uint32_t worker_index;
new_state state;
std::vector<pool_vote_entry> votes;
bool operator==(const obligations_pool_entry &e) const { return height == e.height && worker_index == e.worker_index && state == e.state; }
};
std::vector<deregister_pool_entry> m_deregister_pool;
std::vector<obligations_pool_entry> m_obligations_pool;
struct checkpoint_pool_entry
{
explicit checkpoint_pool_entry(const quorum_vote_t &vote) : height{vote.block_height}, hash{vote.checkpoint.block_hash} {}
checkpoint_pool_entry(uint64_t height, crypto::hash const &hash): height(height), hash(hash) {}
uint64_t height;
crypto::hash hash;
std::vector<pool_vote_entry> votes;
bool operator==(const checkpoint_pool_entry &e) const { return height == e.height && hash == e.hash; }
};
std::vector<checkpoint_pool_entry> m_checkpoint_pool;

View file

@ -115,17 +115,17 @@ namespace cryptonote
}
//---------------------------------------------------------------------------------
bool tx_memory_pool::have_duplicated_non_standard_tx(transaction const &tx) const
bool tx_memory_pool::have_duplicated_non_standard_tx(transaction const &tx, uint8_t hard_fork_version) const
{
if (tx.type == txtype::standard)
return false;
if (tx.type == txtype::deregister)
if (tx.type == txtype::state_change)
{
tx_extra_service_node_deregister deregister;
if (!get_service_node_deregister_from_tx_extra(tx.extra, deregister))
tx_extra_service_node_state_change state_change;
if (!get_service_node_state_change_from_tx_extra(tx.extra, state_change, hard_fork_version))
{
MERROR("Could not get service node deregister from tx, possibly corrupt tx in your blockchain, rejecting malformed deregister");
MERROR("Could not get service node state change from tx, possibly corrupt tx in your blockchain, rejecting malformed state change");
return true;
}
@ -133,21 +133,18 @@ namespace cryptonote
get_transactions(pool_txs);
for (const transaction& pool_tx : pool_txs)
{
if (pool_tx.type != txtype::deregister)
if (pool_tx.type != txtype::state_change)
continue;
tx_extra_service_node_deregister pool_tx_deregister;
if (!get_service_node_deregister_from_tx_extra(pool_tx.extra, pool_tx_deregister))
tx_extra_service_node_state_change pool_tx_state_change;
if (!get_service_node_state_change_from_tx_extra(pool_tx.extra, pool_tx_state_change, hard_fork_version))
{
MERROR("Could not get service node deregister from tx, possibly corrupt tx in your blockchain");
MERROR("Could not get service node state change from tx, possibly corrupt tx in your blockchain");
continue;
}
if ((pool_tx_deregister.block_height == deregister.block_height) &&
(pool_tx_deregister.service_node_index == deregister.service_node_index))
{
if (state_change == pool_tx_state_change)
return true;
}
}
}
else if (tx.type == txtype::key_image_unlock)
@ -173,9 +170,9 @@ namespace cryptonote
return true;
}
if (unlock.key_image == pool_unlock.key_image)
if (unlock == pool_unlock)
{
MERROR("There was atleast one TX in the pool that is requesting to unlock the same key image already.");
MWARNING("There was atleast one TX in the pool that is requesting to unlock the same key image already.");
return true;
}
}
@ -286,7 +283,7 @@ namespace cryptonote
tvc.m_double_spend = true;
return false;
}
if (have_duplicated_non_standard_tx(tx))
if (have_duplicated_non_standard_tx(tx, version))
{
mark_double_spend(tx);
LOG_PRINT_L1("Transaction with id= "<< id << " already has a duplicate tx for height");
@ -331,7 +328,7 @@ namespace cryptonote
meta.last_relayed_time = time(NULL);
meta.relayed = relayed;
meta.do_not_relay = do_not_relay;
meta.double_spend_seen = (have_tx_keyimges_as_spent(tx) || have_duplicated_non_standard_tx(tx));
meta.double_spend_seen = (have_tx_keyimges_as_spent(tx) || have_duplicated_non_standard_tx(tx, version));
meta.bf_padding = 0;
memset(meta.padding, 0, sizeof(meta.padding));
try
@ -503,7 +500,9 @@ namespace cryptonote
}
// this will never remove the first one, but we don't care
auto it = --m_txs_by_fee_and_receive_time.end();
auto it = m_txs_by_fee_and_receive_time.end();
if (it != m_txs_by_fee_and_receive_time.begin())
it = std::prev(it);
while (it != m_txs_by_fee_and_receive_time.begin())
{
if (m_txpool_weight <= bytes)
@ -749,12 +748,13 @@ namespace cryptonote
return true;
}
if (tx.type != txtype::deregister)
if (tx.type != txtype::state_change)
return true;
tx_verification_context tvc;
uint64_t max_used_block_height = 0;
crypto::hash max_used_block_id = null_hash;
// FIXME: not check_tx_inputs anymore
if (!m_blockchain.check_tx_inputs(tx, max_used_block_height, max_used_block_id, tvc, /*kept_by_block*/ false))
{
LOG_PRINT_L1("TX type: " << tx.type << " considered for relaying failed tx inputs check, txid: " << txid << ", reason: " << print_tx_verification_context(tvc, &tx));

View file

@ -461,7 +461,7 @@ namespace cryptonote
* @return true if it already exists
*
*/
bool have_duplicated_non_standard_tx(transaction const &tx) const;
bool have_duplicated_non_standard_tx(transaction const &tx, uint8_t hard_fork_version) const;
/**
* @brief check if any spent key image in a transaction is in the pool

View file

@ -2283,6 +2283,12 @@ bool t_rpc_command_executor::sync_info()
return true;
}
static std::string to_string_rounded(double d, int precision) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(precision) << d;
return ss.str();
}
static void append_printable_service_node_list_entry(cryptonote::network_type nettype, int hard_fork_version, uint64_t curr_height, uint64_t entry_index, cryptonote::COMMAND_RPC_GET_SERVICE_NODES::response::entry const &entry, std::string &buffer)
{
const char indent1[] = " ";
@ -2290,6 +2296,8 @@ static void append_printable_service_node_list_entry(cryptonote::network_type ne
const char indent3[] = " ";
bool is_registered = entry.total_contributed >= entry.staking_requirement;
buffer.reserve(buffer.size() + 2048);
// Print Funding Status
{
buffer.append(indent1);
@ -2371,7 +2379,7 @@ static void append_printable_service_node_list_entry(cryptonote::network_type ne
{
buffer.append(indent2);
buffer.append("Operator Cut (\% Of Reward): ");
buffer.append(std::to_string((entry.portions_for_operator / (double)STAKING_PORTIONS) * 100.0));
buffer.append(to_string_rounded((entry.portions_for_operator / (double)STAKING_PORTIONS) * 100.0, 2));
buffer.append("%\n");
buffer.append(indent2);
@ -2405,6 +2413,18 @@ static void append_printable_service_node_list_entry(cryptonote::network_type ne
buffer.append(std::to_string(entry.storage_port));
}
buffer.append("\n");
buffer.append(indent2);
if (entry.active) {
buffer.append("Downtime Credits: " + std::to_string(entry.earned_downtime_blocks) + " blocks");
buffer.append(" (about " + to_string_rounded(entry.earned_downtime_blocks / (double) BLOCKS_EXPECTED_IN_HOURS(1), 2) + " hours)");
if (entry.earned_downtime_blocks < service_nodes::DECOMMISSION_MINIMUM)
buffer.append(" (Note: " + std::to_string(service_nodes::DECOMMISSION_MINIMUM) + " blocks required to enable deregistration delay)");
} else {
buffer.append("Current Status: DECOMMISSIONED\n");
buffer.append(indent2);
buffer.append("Remaining Decommission Time Until DEREGISTRATION: " + std::to_string(entry.earned_downtime_blocks) + " blocks");
}
buffer.append("\n");
}
// Print contributors

View file

@ -233,7 +233,7 @@ namespace nodetool
return false;
peers_indexed::index<by_time>::type& by_time_index = m_peers_white.get<by_time>();
p = *epee::misc_utils::move_it_backward(--by_time_index.end(), i);
p = *epee::misc_utils::move_it_backward(std::prev(by_time_index.end()), i);
return true;
}
//--------------------------------------------------------------------------------------------------
@ -245,7 +245,7 @@ namespace nodetool
return false;
peers_indexed::index<by_time>::type& by_time_index = m_peers_gray.get<by_time>();
p = *epee::misc_utils::move_it_backward(--by_time_index.end(), i);
p = *epee::misc_utils::move_it_backward(std::prev(by_time_index.end()), i);
return true;
}
//--------------------------------------------------------------------------------------------------
@ -418,7 +418,7 @@ namespace nodetool
size_t random_index = crypto::rand_idx(m_peers_gray.size());
peers_indexed::index<by_time>::type& by_time_index = m_peers_gray.get<by_time>();
pe = *epee::misc_utils::move_it_backward(--by_time_index.end(), random_index);
pe = *epee::misc_utils::move_it_backward(std::prev(by_time_index.end()), random_index);
return true;

View file

@ -2488,7 +2488,7 @@ namespace cryptonote
PERF_TIMER(on_get_quorum_state);
bool r;
const auto uptime_quorum = m_core.get_testing_quorum(service_nodes::quorum_type::deregister, req.height);
const auto uptime_quorum = m_core.get_testing_quorum(service_nodes::quorum_type::obligations, req.height);
r = (uptime_quorum != nullptr);
if (r)
{
@ -2534,7 +2534,7 @@ namespace cryptonote
res.quorum_entries.reserve(height_end - height_begin + 1);
for (auto h = height_begin; h <= height_end; ++h)
{
const auto uptime_quorum = m_core.get_testing_quorum(service_nodes::quorum_type::deregister, h);
const auto uptime_quorum = m_core.get_testing_quorum(service_nodes::quorum_type::obligations, h);
if (!uptime_quorum) {
failed_height = h;
@ -2701,7 +2701,7 @@ namespace cryptonote
}
//------------------------------------------------------------------------------------------------------------------------------
template<typename response>
void core_rpc_server::fill_sn_response_entry(response &entry, const service_nodes::service_node_pubkey_info &sn_info) {
void core_rpc_server::fill_sn_response_entry(response &entry, const service_nodes::service_node_pubkey_info &sn_info, uint64_t current_height) {
const auto proof = m_core.get_uptime_proof(sn_info.pubkey);
@ -2711,6 +2711,9 @@ namespace cryptonote
entry.last_reward_block_height = sn_info.info.last_reward_block_height;
entry.last_reward_transaction_index = sn_info.info.last_reward_transaction_index;
entry.last_uptime_proof = proof.timestamp;
entry.active = sn_info.info.is_active();
entry.earned_downtime_blocks = service_nodes::quorum_cop::calculate_decommission_credit(sn_info.info, current_height);
entry.decommission_count = sn_info.info.decommission_count;
entry.service_node_version = {proof.version_major, proof.version_minor, proof.version_patch};
entry.public_ip = string_tools::get_ip_string_from_int32(sn_info.info.public_ip);
entry.storage_port = sn_info.info.storage_port;
@ -2763,11 +2766,11 @@ namespace cryptonote
}
}
std::vector<service_nodes::service_node_pubkey_info> pubkey_info_list = m_core.get_service_node_list_state(pubkeys);
auto pubkey_info_list = m_core.get_service_node_list_state(pubkeys);
res.status = CORE_RPC_STATUS_OK;
res.service_node_states.reserve(pubkey_info_list.size());
if (req.include_json)
{
res.as_json = "{\n}";
@ -2782,7 +2785,7 @@ namespace cryptonote
for (auto &pubkey_info : pubkey_info_list)
{
COMMAND_RPC_GET_SERVICE_NODES::response::entry entry = {};
fill_sn_response_entry(entry, pubkey_info);
fill_sn_response_entry(entry, pubkey_info, res.height);
res.service_node_states.push_back(entry);
}
@ -2819,10 +2822,12 @@ namespace cryptonote
res.service_node_states.reserve(sn_infos.size());
uint64_t height = m_core.get_current_blockchain_height();
for (auto &pubkey_info : sn_infos) {
COMMAND_RPC_GET_N_SERVICE_NODES::response::entry entry = {res.fields};
fill_sn_response_entry(entry, pubkey_info);
fill_sn_response_entry(entry, pubkey_info, height);
res.service_node_states.push_back(entry);
}

View file

@ -324,7 +324,7 @@ private:
bool check_core_ready();
template<typename response>
void fill_sn_response_entry(response &entry, const service_nodes::service_node_pubkey_info &sn_info);
void fill_sn_response_entry(response &entry, const service_nodes::service_node_pubkey_info &sn_info, uint64_t current_height);
//utils
uint64_t get_block_reward(const block& blk);

View file

@ -2777,6 +2777,9 @@ namespace cryptonote
uint64_t last_reward_block_height; // The last height at which this Service Node received a reward.
uint32_t last_reward_transaction_index; // When multiple Service Nodes register on the same height, the order the transaction arrive dictate the order you receive rewards.
uint64_t last_uptime_proof; // The last time this Service Node's uptime proof was relayed by atleast 1 Service Node other than itself in unix epoch time.
bool active; // True if fully funded and not currently decommissioned
uint32_t decommission_count; // The number of times the Service Node has been decommissioned since registration
int64_t earned_downtime_blocks; // The number of blocks earned towards decommissioning, or the number of blocks remaining until deregistration if currently decommissioned
std::vector<uint16_t> service_node_version; // The major, minor, patch version of the Service Node respectively.
std::vector<contributor> contributors; // Array of contributors, contributing to this Service Node.
uint64_t total_contributed; // The total amount of Loki in atomic units contributed to this Service Node.
@ -2795,6 +2798,9 @@ namespace cryptonote
KV_SERIALIZE(last_reward_block_height)
KV_SERIALIZE(last_reward_transaction_index)
KV_SERIALIZE(last_uptime_proof)
KV_SERIALIZE(active)
KV_SERIALIZE(decommission_count)
KV_SERIALIZE(earned_downtime_blocks)
KV_SERIALIZE(service_node_version)
KV_SERIALIZE(contributors)
KV_SERIALIZE(total_contributed)
@ -2846,6 +2852,9 @@ namespace cryptonote
bool last_reward_block_height;
bool last_reward_transaction_index;
bool last_uptime_proof;
bool active;
bool decommission_count;
bool earned_downtime_blocks;
bool service_node_version;
bool contributors;
@ -2868,6 +2877,9 @@ namespace cryptonote
KV_SERIALIZE_OPT2(last_reward_block_height, false)
KV_SERIALIZE_OPT2(last_reward_transaction_index, false)
KV_SERIALIZE_OPT2(last_uptime_proof, false)
KV_SERIALIZE_OPT2(active, false)
KV_SERIALIZE_OPT2(decommission_count, false)
KV_SERIALIZE_OPT2(earned_downtime_blocks, false)
KV_SERIALIZE_OPT2(service_node_version, false)
KV_SERIALIZE_OPT2(contributors, false)
KV_SERIALIZE_OPT2(total_contributed, false)
@ -2916,6 +2928,9 @@ namespace cryptonote
uint64_t last_reward_block_height; // The last height at which this Service Node received a reward.
uint32_t last_reward_transaction_index; // When multiple Service Nodes register on the same height, the order the transaction arrive dictate the order you receive rewards.
uint64_t last_uptime_proof; // The last time this Service Node's uptime proof was relayed by atleast 1 Service Node other than itself in unix epoch time.
bool active; // True if fully funded and not currently decommissioned
uint32_t decommission_count; // The number of times the Service Node has been decommissioned since registration
int64_t earned_downtime_blocks; // The number of blocks earned towards decommissioning, or the number of blocks remaining until deregistration if currently decommissioned
std::vector<uint16_t> service_node_version; // The major, minor, patch version of the Service Node respectively.
std::vector<contributor> contributors; // Array of contributors, contributing to this Service Node.
uint64_t total_contributed; // The total amount of Loki in atomic units contributed to this Service Node.
@ -2934,6 +2949,9 @@ namespace cryptonote
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(last_reward_block_height);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(last_reward_transaction_index);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(last_uptime_proof);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(active);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(decommission_count);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(earned_downtime_blocks);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(service_node_version);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(contributors);
KV_SERIALIZE_ENTRY_FIELD_IF_REQUESTED(total_contributed);

View file

@ -5945,6 +5945,7 @@ bool wallet2::is_transfer_unlocked(uint64_t unlock_time, uint64_t block_height,
binary_buf.reserve(sizeof(crypto::key_image));
{
boost::optional<std::string> failed;
// FIXME: can just check one here by adding a is_key_image_blacklisted
std::vector<cryptonote::COMMAND_RPC_GET_SERVICE_NODE_BLACKLISTED_KEY_IMAGES::entry> blacklist = m_node_rpc_proxy.get_service_node_blacklisted_key_images(failed);
if (failed)
{