Skip to content

Commit

Permalink
merge bitcoin#21261: update inbound eviction protection for multiple …
Browse files Browse the repository at this point in the history
…networks, add I2P peers
  • Loading branch information
kwvg committed May 26, 2024
1 parent 0b16b50 commit b2d8656
Show file tree
Hide file tree
Showing 5 changed files with 472 additions and 194 deletions.
122 changes: 81 additions & 41 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
#endif

#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <unordered_map>
Expand Down Expand Up @@ -913,18 +914,6 @@ static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate& a, cons
return a.nTimeConnected > b.nTimeConnected;
}

static bool CompareLocalHostTimeConnected(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
{
if (a.m_is_local != b.m_is_local) return b.m_is_local;
return a.nTimeConnected > b.nTimeConnected;
}

static bool CompareOnionTimeConnected(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
{
if (a.m_is_onion != b.m_is_onion) return b.m_is_onion;
return a.nTimeConnected > b.nTimeConnected;
}

static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
return a.nKeyedNetGroup < b.nKeyedNetGroup;
}
Expand Down Expand Up @@ -955,6 +944,26 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
return a.nTimeConnected > b.nTimeConnected;
}

/**
* Sort eviction candidates by network/localhost and connection uptime.
* Candidates near the beginning are more likely to be evicted, and those
* near the end are more likely to be protected, e.g. less likely to be evicted.
* - First, nodes that are not `is_local` and that do not belong to `network`,
* sorted by increasing uptime (from most recently connected to connected longer).
* - Then, nodes that are `is_local` or belong to `network`, sorted by increasing uptime.
*/
struct CompareNodeNetworkTime {
const bool m_is_local;
const Network m_network;
CompareNodeNetworkTime(bool is_local, Network network) : m_is_local(is_local), m_network(network) {}
bool operator()(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b) const
{
if (m_is_local && a.m_is_local != b.m_is_local) return b.m_is_local;
if ((a.m_network == m_network) != (b.m_network == m_network)) return b.m_network == m_network;
return a.nTimeConnected > b.nTimeConnected;
};
};

//! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
template <typename T, typename Comparator>
static void EraseLastKElements(
Expand All @@ -966,40 +975,72 @@ static void EraseLastKElements(
elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
}

void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates)
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& eviction_candidates)
{
// Protect the half of the remaining nodes which have been connected the longest.
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
// these protected spots for onion and localhost peers, if any, even if they're not
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
// To favorise the diversity of our peer connections, reserve up to half of these protected
// spots for Tor/onion, localhost and I2P peers, even if they're not longest uptime overall.
// This helps protect these higher-latency peers that tend to be otherwise
// disadvantaged under our eviction criteria.
const size_t initial_size = vEvictionCandidates.size();
size_t total_protect_size = initial_size / 2;
const size_t onion_protect_size = total_protect_size / 2;

if (onion_protect_size) {
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
EraseLastKElements(vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
}

const size_t localhost_min_protect_size{2};
if (onion_protect_size >= localhost_min_protect_size) {
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
// to localhost peers, sorted by longest uptime, as manually configured
// hidden services not using `-bind=addr[:port]=onion` will not be detected
// as inbound onion connections.
const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size())};
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
EraseLastKElements(vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
const size_t initial_size = eviction_candidates.size();
const size_t total_protect_size{initial_size / 2};

// Disadvantaged networks to protect: I2P, localhost, Tor/onion. In case of equal counts, earlier
// array members have first opportunity to recover unused slots from the previous iteration.
struct Net { bool is_local; Network id; size_t count; };
std::array<Net, 3> networks{
{{false, NET_I2P, 0}, {/* localhost */ true, NET_MAX, 0}, {false, NET_ONION, 0}}};

// Count and store the number of eviction candidates per network.
for (Net& n : networks) {
n.count = std::count_if(eviction_candidates.cbegin(), eviction_candidates.cend(),
[&n](const NodeEvictionCandidate& c) {
return n.is_local ? c.m_is_local : c.m_network == n.id;
});
}
// Sort `networks` by ascending candidate count, to give networks having fewer candidates
// the first opportunity to recover unused protected slots from the previous iteration.
std::stable_sort(networks.begin(), networks.end(), [](Net a, Net b) { return a.count < b.count; });

// Protect up to 25% of the eviction candidates by disadvantaged network.
const size_t max_protect_by_network{total_protect_size / 2};
size_t num_protected{0};

while (num_protected < max_protect_by_network) {
const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
const size_t protect_per_network{
std::max(disadvantaged_to_protect / networks.size(), static_cast<size_t>(1))};

// Early exit flag if there are no remaining candidates by disadvantaged network.
bool protected_at_least_one{false};

for (const Net& n : networks) {
if (n.count == 0) continue;
const size_t before = eviction_candidates.size();
EraseLastKElements(eviction_candidates, CompareNodeNetworkTime(n.is_local, n.id),
protect_per_network, [&n](const NodeEvictionCandidate& c) {
return n.is_local ? c.m_is_local : c.m_network == n.id;
});
const size_t after = eviction_candidates.size();
if (before > after) {
protected_at_least_one = true;
num_protected += before - after;
if (num_protected >= max_protect_by_network) {
break;
}
}
}
if (!protected_at_least_one) {
break;
}
}

// Calculate how many we removed, and update our total number of peers that
// we want to protect based on uptime accordingly.
total_protect_size -= initial_size - vEvictionCandidates.size();
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
assert(num_protected == initial_size - eviction_candidates.size());
const size_t remaining_to_protect{total_protect_size - num_protected};
EraseLastKElements(eviction_candidates, ReverseCompareNodeTimeConnected, remaining_to_protect);
}

[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
Expand All @@ -1016,8 +1057,7 @@ void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvict
// An attacker cannot manipulate this metric without performing useful work.
EraseLastKElements(vEvictionCandidates, CompareNodeTXTime, 4);
// Protect up to 8 non-tx-relay peers that have sent us novel blocks.
const size_t erase_size = std::min(size_t(8), vEvictionCandidates.size());
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, erase_size,
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8,
[](const NodeEvictionCandidate& n) { return !n.m_relay_txs && n.fRelevantServices; });

// Protect 4 nodes that most recently sent us novel blocks.
Expand Down Expand Up @@ -1109,7 +1149,7 @@ bool CConnman::AttemptToEvictConnection()
HasAllDesirableServiceFlags(node->nServices),
node->m_relays_txs.load(), node->m_bloom_filter_loaded.load(),
node->nKeyedNetGroup, node->m_prefer_evict, node->addr.IsLocal(),
node->m_inbound_onion};
node->ConnectedThroughNetwork()};
vEvictionCandidates.push_back(candidate);
}
}
Expand Down
26 changes: 13 additions & 13 deletions src/net.h
Original file line number Diff line number Diff line change
Expand Up @@ -1582,7 +1582,7 @@ struct NodeEvictionCandidate
uint64_t nKeyedNetGroup;
bool prefer_evict;
bool m_is_local;
bool m_is_onion;
Network m_network;
};

/**
Expand Down Expand Up @@ -1612,20 +1612,20 @@ size_t GetRequestedObjectCount(NodeId nodeId) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
* longest, to replicate the non-eviction implicit behavior and preclude attacks
* that start later.
*
* Half of these protected spots (1/4 of the total) are reserved for onion peers
* connected via our tor control service, if any, sorted by longest uptime, even
* if they're not longest uptime overall. Any remaining slots of the 1/4 are
* then allocated to protect localhost peers, if any (or up to 2 localhost peers
* if no slots remain and 2 or more onion peers were protected), sorted by
* longest uptime, as manually configured hidden services not using
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections.
* Half of these protected spots (1/4 of the total) are reserved for the
* following categories of peers, sorted by longest uptime, even if they're not
* longest uptime overall:
*
* - onion peers connected via our tor control service
*
* - localhost peers, as manually configured hidden services not using
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections
*
* This helps protect onion peers, which tend to be otherwise disadvantaged
* under our eviction criteria for their higher min ping times relative to IPv4
* and IPv6 peers, and favorise the diversity of peer connections.
* - I2P peers
*
* This function was extracted from SelectNodeToEvict() to be able to test the
* ratio-based protection logic deterministically.
* This helps protect these privacy network peers, which tend to be otherwise
* disadvantaged under our eviction criteria for their higher min ping times
* relative to IPv4/IPv6 peers, and favorise the diversity of peer connections.
*/
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates);

Expand Down
2 changes: 1 addition & 1 deletion src/test/fuzz/node_eviction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ FUZZ_TARGET(node_eviction)
/* nKeyedNetGroup */ fuzzed_data_provider.ConsumeIntegral<uint64_t>(),
/* prefer_evict */ fuzzed_data_provider.ConsumeBool(),
/* m_is_local */ fuzzed_data_provider.ConsumeBool(),
/* m_is_onion */ fuzzed_data_provider.ConsumeBool(),
/* m_network */ fuzzed_data_provider.PickValueInArray(ALL_NETWORKS),
});
}
// Make a copy since eviction_candidates may be in some valid but otherwise
Expand Down
Loading

0 comments on commit b2d8656

Please sign in to comment.