mirror of https://github.com/oxen-io/oxen-core.git
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:
parent
661bfe3f1b
commit
6130c1b976
|
@ -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) \
|
||||
|
|
|
@ -66,6 +66,7 @@ namespace service_nodes {
|
|||
deregister,
|
||||
decommission,
|
||||
recommission,
|
||||
ip_change_penalty,
|
||||
_count
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue