Add penalty for switching IPs

This adds a new obligations quorum vote "ip_change_penalty" that gets
triggered if the quorum has received multiple IPs advertised in uptime
proofs from a service node in the past 24 hours.  Upon reception of such
a transaction the SN gets bumped to the bottom of the reward list.
This commit is contained in:
Jason Rhinelander 2019-06-26 01:01:40 -03:00
parent 661bfe3f1b
commit 6130c1b976
7 changed files with 105 additions and 21 deletions

View File

@ -43,6 +43,9 @@ namespace crypto { \
inline bool operator!=(const type &_v1, const type &_v2) { \
return !operator==(_v1, _v2); \
} \
inline bool operator<(const type &_v1, const type &_v2) { \
return memcmp(&_v1, &_v2, sizeof(_v1)); \
} \
}
#define CRYPTO_MAKE_COMPARABLE_CONSTANT_TIME(type) \

View File

@ -66,6 +66,7 @@ namespace service_nodes {
deregister,
decommission,
recommission,
ip_change_penalty,
_count
};
};

View File

@ -413,6 +413,30 @@ namespace service_nodes
info.last_reward_transaction_index = std::numeric_limits<uint32_t>::max();
return true;
case new_state::ip_change_penalty:
if (hard_fork_version < cryptonote::network_version_12_checkpointing) {
MERROR("Invalid ip_change_penalty transaction seen before network v12");
return false;
}
if (info.is_decommissioned()) {
LOG_PRINT_L2("Received reset position tx for service node " << key << " but it is already decommissioned; ignoring");
return false;
}
if (is_me)
MGINFO_RED("Reward position reset for service node (yours): " << key);
else
LOG_PRINT_L1("Reward position reset for service node: " << key);
m_transient_state.rollback_events.emplace_back(new rollback_change(block_height, key, info));
// Move the SN at the back of the list as if it had just registered (or just won)
info.last_reward_block_height = block_height;
info.last_reward_transaction_index = std::numeric_limits<uint32_t>::max();
info.last_ip_change_height = block_height;
default:
// dev bug!
MERROR("BUG: Service node state change tx has unknown state " << static_cast<uint16_t>(state_change.state));
@ -650,8 +674,11 @@ namespace service_nodes
info.decommission_count = 0;
info.total_contributed = 0;
info.total_reserved = 0;
info.version = get_min_service_node_info_version_for_hf(hf_version);
info.swarm_id = UNASSIGNED_SWARM_ID;
info.public_ip = 0;
info.storage_port = 0;
info.last_ip_change_height = block_height;
info.version = get_min_service_node_info_version_for_hf(hf_version);
info.contributors.clear();
@ -1253,19 +1280,17 @@ namespace service_nodes
crypto::public_key service_node_list::select_winner() const
{
std::lock_guard<boost::recursive_mutex> lock(m_sn_mutex);
auto oldest_waiting = std::pair<uint64_t, uint32_t>(std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint32_t>::max());
crypto::public_key key = crypto::null_pkey;
auto oldest_waiting = std::make_tuple(std::numeric_limits<uint64_t>::max(), std::numeric_limits<uint32_t>::max(), crypto::null_pkey);
for (const auto& info : m_transient_state.service_nodes_infos)
if (info.second.is_active())
{
auto waiting_since = std::make_pair(info.second.last_reward_block_height, info.second.last_reward_transaction_index);
auto waiting_since = std::make_tuple(info.second.last_reward_block_height, info.second.last_reward_transaction_index, info.first);
if (waiting_since < oldest_waiting)
{
oldest_waiting = waiting_since;
key = info.first;
}
}
return key;
return std::get<2>(oldest_waiting);
}
bool service_node_list::validate_miner_tx(const crypto::hash& prev_id, const cryptonote::transaction& miner_tx, uint64_t height, int hard_fork_version, cryptonote::block_reward_parts const &reward_parts) const
@ -1554,6 +1579,23 @@ namespace service_nodes
sn_info.public_ip = proof.public_ip;
sn_info.storage_port = proof.storage_port;
// Track any IP changes (so that the obligations quorum can penalize for IP changes)
//
// First prune any stale (>1w) ip info. 1 week is probably excessive, but IP switches should be
// rare and this could, in theory, be useful for diagnostics.
auto &ips = sn_info.proof_public_ips;
const auto now = static_cast<uint64_t>(time(nullptr));
const auto expiry = now - IP_CHANGE_WINDOW_IN_SECONDS;
ips.erase(std::remove_if(ips.begin(), ips.end(),
[expiry](const std::pair<uint32_t, uint64_t> &ip_time) { return ip_time.second < expiry; }));
auto it = std::find_if(ips.begin(), ips.end(),
[&proof](const std::pair<uint32_t, uint64_t> &ip_time) { return ip_time.first == proof.public_ip; });
if (it == ips.end())
ips.emplace_back(proof.public_ip, now);
else if (now > it->second)
it->second = now;
}

View File

@ -104,6 +104,8 @@ namespace service_nodes
cryptonote::account_public_address operator_address;
uint32_t public_ip;
uint16_t storage_port;
uint64_t last_ip_change_height; // The height of the last quorum penalty for changing IPs
std::vector<std::pair<uint32_t, uint64_t>> proof_public_ips; // (not serialized)
service_node_info() = default;
bool is_fully_funded() const { return total_contributed >= staking_requirement; }
@ -117,6 +119,9 @@ namespace service_nodes
VARINT_FIELD(requested_unlock_height)
VARINT_FIELD(last_reward_block_height)
VARINT_FIELD(last_reward_transaction_index)
VARINT_FIELD(decommission_count)
VARINT_FIELD(active_since_height)
VARINT_FIELD(last_decommission_height)
FIELD(contributors)
VARINT_FIELD(total_contributed)
VARINT_FIELD(total_reserved)
@ -126,8 +131,7 @@ namespace service_nodes
VARINT_FIELD(swarm_id)
VARINT_FIELD(public_ip)
VARINT_FIELD(storage_port)
VARINT_FIELD(active_since_height)
VARINT_FIELD(last_decommission_height)
VARINT_FIELD(last_ip_change_height)
END_SERIALIZE()
};

View File

@ -67,14 +67,34 @@ namespace service_nodes
// 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
service_node_test_results 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;
service_node_test_results results; // Defaults to true for individual tests
// TODO: check for missing checkpoint quorum votes
// Basic uptime proof check
if (!m_uptime_proof_seen.count(pubkey))
results.uptime_proved = false;
return true;
// IP change checks
if (info.proof_public_ips.size() > 1) {
// Figure out when we last had a blockchain-level IP change penalty (or when we registered);
// we only consider IP changes starting two hours after the last IP penalty.
std::vector<cryptonote::block> blocks;
if (m_core.get_blocks(info.last_ip_change_height, 1, blocks)) {
uint64_t find_changes_since = std::max(
uint64_t(std::time(nullptr)) - IP_CHANGE_WINDOW_IN_SECONDS,
uint64_t(blocks[0].timestamp) + IP_CHANGE_BUFFER_IN_SECONDS);
auto num_ips = std::count_if(info.proof_public_ips.begin(), info.proof_public_ips.end(),
[find_changes_since](const std::pair<uint32_t, uint64_t> &ip_time) { return ip_time.second > find_changes_since; });
if (num_ips > 1)
results.single_ip = false;
}
}
// TODO: check for missing checkpoint quorum votes
return results;
}
void quorum_cop::blockchain_detached(uint64_t height)
@ -203,17 +223,23 @@ namespace service_nodes
const auto &node_key = worker_it->pubkey;
const auto &info = worker_it->info;
bool checks_passed = check_service_node(node_key, info);
auto test_results = check_service_node(node_key, info);
new_state vote_for_state;
if (checks_passed) {
if (!info.is_decommissioned()) {
good++;
continue;
if (test_results.uptime_proved) {
if (info.is_decommissioned()) {
vote_for_state = new_state::recommission;
LOG_PRINT_L2("Decommissioned service node " << quorum->workers[node_index] << " is now passing required checks; voting to recommission");
} else if (!test_results.single_ip) {
// Don't worry about this if the SN is getting recommissioned (above) -- it'll
// already reenter at the bottom.
vote_for_state = new_state::ip_change_penalty;
LOG_PRINT_L2("Service node " << quorum->workers[node_index] << " was observed with multiple IPs recently; voting to reset reward position");
} else {
good++;
continue;
}
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);

View File

@ -68,6 +68,11 @@ namespace service_nodes
std::shared_ptr<const testing_quorum> checkpointing;
};
struct service_node_test_results {
bool uptime_proved = true;
bool single_ip = true;
};
class quorum_cop
: public cryptonote::BlockAddedHook,
public cryptonote::BlockchainDetachedHook,
@ -93,7 +98,7 @@ namespace service_nodes
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;
service_node_test_results check_service_node(const crypto::public_key &pubkey, const service_node_info &info) const;
private:
void process_quorums(cryptonote::block const &block);

View File

@ -52,6 +52,9 @@ namespace service_nodes {
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 uint64_t IP_CHANGE_WINDOW_IN_SECONDS = 24*60*60; // How far back an obligations quorum looks for multiple IPs (unless the following buffer is more recent)
constexpr uint64_t IP_CHANGE_BUFFER_IN_SECONDS = 2*60*60; // After we bump a SN for an IP change we don't bump again for changes within this time period
constexpr size_t MAX_SWARM_SIZE = 10;
// We never create a new swarm unless there are SWARM_BUFFER extra nodes
// available in the queue.