Skip to content

Commit 5c4f0c4

Browse files
committed
Merge bitcoin/bitcoin#21261: p2p: update inbound eviction protection for multiple networks, add I2P peers
1b1088d test: add combined I2P/onion/localhost eviction protection tests (Jon Atack) 7c2284e test: add tests for inbound eviction protection of I2P peers (Jon Atack) ce02dd1 p2p: extend inbound eviction protection by network to I2P peers (Jon Atack) 70bbc62 test: add combined onion/localhost eviction protection coverage (Jon Atack) 045cb40 p2p: remove unused m_is_onion member from NodeEvictionCandidate struct (Jon Atack) 310fab4 p2p: remove unused CompareLocalHostTimeConnected() (Jon Atack) 9e889e8 p2p: remove unused CompareOnionTimeConnected() (Jon Atack) 787d46b p2p: update ProtectEvictionCandidatesByRatio() doxygen docs (Jon Atack) 1e15acf p2p: make ProtectEvictionCandidatesByRatio() fully ratio-based (Jon Atack) 3f8105c test: remove combined onion/localhost eviction protection tests (Jon Atack) 38a81a8 p2p: add CompareNodeNetworkTime() comparator struct (Jon Atack) 4ee7aec p2p: add m_network to NodeEvictionCandidate struct (Jon Atack) 7321e6f p2p, refactor: rename vEvictionCandidates to eviction_candidates (Jon Atack) ec590f1 p2p, refactor: improve constness in ProtectEvictionCandidatesByRatio() (Jon Atack) 4a19f50 test: add ALL_NETWORKS to test utilities (Jon Atack) 519e76b test: speed up and simplify peer_eviction_test (Jon Atack) 1cde800 p2p, refactor: rm redundant erase_size calculation in SelectNodeToEvict() (Jon Atack) Pull request description: Continuing the work in #20197 and #20685, this pull updates and abstracts our inbound eviction protection to make it fully ratio-based and easily extensible to peers connected via high-latency privacy networks that we newly support, like I2P and perhaps others soon, as these peers are disadvantaged by the latency criteria of our eviction logic. It then adds eviction protection for peers connected over I2P. As described in bitcoin/bitcoin#20685 (comment), we've observed over the past few months that I2P peers have a min ping latency similar to or greater than that of onion peers. The algorithm is a basically a multi-pass knapsack: - Count the number of eviction candidates in each of the disadvantaged privacy networks. - Sort the networks from lower to higher candidate counts, so that a network with fewer candidates will have the first opportunity for any unused slots remaining from the previous iteration. In the case of a tie in candidate counts, priority is given by array member order from first to last, guesstimated to favor more unusual networks. - Iterate through the networks in this order. On each iteration, allocate each network an equal number of protected slots targeting a total number of candidates to protect, provided any slots remain in the knapsack. - Protect the candidates in that network having the longest uptime, if any in that network are present. - Continue iterating as long as we have non-allocated slots remaining and candidates available to protect. The goal of this logic is to favorise the diversity of our peer connections. The individual commit messages describe each change in more detail. Special thank you to Vasil Dimov for the excellent review feedback and the algorithm improvement that made this change much better than it would have been otherwise. Thanks also to Antoine Riard, whose review feedback nudged this change to protect disadvantaged networks having fewer, rather than more, eviction candidates. ACKs for top commit: laanwj: Code review re-ACK 1b1088d vasild: ACK 1b1088d Tree-SHA512: 722f790ff11f2969c79e45a5e0e938d94df78df8687e77002f32e3ef5c72a9ac10ebf8c7a9eb7f71882c97ab0e67b2778191effdb747d9ca54d7c23c2ed19a90
2 parents de5512e + 1b1088d commit 5c4f0c4

File tree

5 files changed

+472
-194
lines changed

5 files changed

+472
-194
lines changed

src/net.cpp

Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#endif
4343

4444
#include <algorithm>
45+
#include <array>
4546
#include <cstdint>
4647
#include <functional>
4748
#include <optional>
@@ -841,18 +842,6 @@ static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate &a, cons
841842
return a.nTimeConnected > b.nTimeConnected;
842843
}
843844

844-
static bool CompareLocalHostTimeConnected(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
845-
{
846-
if (a.m_is_local != b.m_is_local) return b.m_is_local;
847-
return a.nTimeConnected > b.nTimeConnected;
848-
}
849-
850-
static bool CompareOnionTimeConnected(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
851-
{
852-
if (a.m_is_onion != b.m_is_onion) return b.m_is_onion;
853-
return a.nTimeConnected > b.nTimeConnected;
854-
}
855-
856845
static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
857846
return a.nKeyedNetGroup < b.nKeyedNetGroup;
858847
}
@@ -883,6 +872,26 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
883872
return a.nTimeConnected > b.nTimeConnected;
884873
}
885874

875+
/**
876+
* Sort eviction candidates by network/localhost and connection uptime.
877+
* Candidates near the beginning are more likely to be evicted, and those
878+
* near the end are more likely to be protected, e.g. less likely to be evicted.
879+
* - First, nodes that are not `is_local` and that do not belong to `network`,
880+
* sorted by increasing uptime (from most recently connected to connected longer).
881+
* - Then, nodes that are `is_local` or belong to `network`, sorted by increasing uptime.
882+
*/
883+
struct CompareNodeNetworkTime {
884+
const bool m_is_local;
885+
const Network m_network;
886+
CompareNodeNetworkTime(bool is_local, Network network) : m_is_local(is_local), m_network(network) {}
887+
bool operator()(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b) const
888+
{
889+
if (m_is_local && a.m_is_local != b.m_is_local) return b.m_is_local;
890+
if ((a.m_network == m_network) != (b.m_network == m_network)) return b.m_network == m_network;
891+
return a.nTimeConnected > b.nTimeConnected;
892+
};
893+
};
894+
886895
//! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
887896
template <typename T, typename Comparator>
888897
static void EraseLastKElements(
@@ -894,40 +903,72 @@ static void EraseLastKElements(
894903
elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
895904
}
896905

897-
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates)
906+
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& eviction_candidates)
898907
{
899908
// Protect the half of the remaining nodes which have been connected the longest.
900909
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
901-
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
902-
// these protected spots for onion and localhost peers, if any, even if they're not
903-
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
910+
// To favorise the diversity of our peer connections, reserve up to half of these protected
911+
// spots for Tor/onion, localhost and I2P peers, even if they're not longest uptime overall.
912+
// This helps protect these higher-latency peers that tend to be otherwise
904913
// disadvantaged under our eviction criteria.
905-
const size_t initial_size = vEvictionCandidates.size();
906-
size_t total_protect_size = initial_size / 2;
907-
const size_t onion_protect_size = total_protect_size / 2;
908-
909-
if (onion_protect_size) {
910-
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
911-
EraseLastKElements(vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
912-
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
913-
}
914-
915-
const size_t localhost_min_protect_size{2};
916-
if (onion_protect_size >= localhost_min_protect_size) {
917-
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
918-
// to localhost peers, sorted by longest uptime, as manually configured
919-
// hidden services not using `-bind=addr[:port]=onion` will not be detected
920-
// as inbound onion connections.
921-
const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size())};
922-
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
923-
EraseLastKElements(vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
924-
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
914+
const size_t initial_size = eviction_candidates.size();
915+
const size_t total_protect_size{initial_size / 2};
916+
917+
// Disadvantaged networks to protect: I2P, localhost, Tor/onion. In case of equal counts, earlier
918+
// array members have first opportunity to recover unused slots from the previous iteration.
919+
struct Net { bool is_local; Network id; size_t count; };
920+
std::array<Net, 3> networks{
921+
{{false, NET_I2P, 0}, {/* localhost */ true, NET_MAX, 0}, {false, NET_ONION, 0}}};
922+
923+
// Count and store the number of eviction candidates per network.
924+
for (Net& n : networks) {
925+
n.count = std::count_if(eviction_candidates.cbegin(), eviction_candidates.cend(),
926+
[&n](const NodeEvictionCandidate& c) {
927+
return n.is_local ? c.m_is_local : c.m_network == n.id;
928+
});
929+
}
930+
// Sort `networks` by ascending candidate count, to give networks having fewer candidates
931+
// the first opportunity to recover unused protected slots from the previous iteration.
932+
std::stable_sort(networks.begin(), networks.end(), [](Net a, Net b) { return a.count < b.count; });
933+
934+
// Protect up to 25% of the eviction candidates by disadvantaged network.
935+
const size_t max_protect_by_network{total_protect_size / 2};
936+
size_t num_protected{0};
937+
938+
while (num_protected < max_protect_by_network) {
939+
const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
940+
const size_t protect_per_network{
941+
std::max(disadvantaged_to_protect / networks.size(), static_cast<size_t>(1))};
942+
943+
// Early exit flag if there are no remaining candidates by disadvantaged network.
944+
bool protected_at_least_one{false};
945+
946+
for (const Net& n : networks) {
947+
if (n.count == 0) continue;
948+
const size_t before = eviction_candidates.size();
949+
EraseLastKElements(eviction_candidates, CompareNodeNetworkTime(n.is_local, n.id),
950+
protect_per_network, [&n](const NodeEvictionCandidate& c) {
951+
return n.is_local ? c.m_is_local : c.m_network == n.id;
952+
});
953+
const size_t after = eviction_candidates.size();
954+
if (before > after) {
955+
protected_at_least_one = true;
956+
num_protected += before - after;
957+
if (num_protected >= max_protect_by_network) {
958+
break;
959+
}
960+
}
961+
}
962+
if (!protected_at_least_one) {
963+
break;
964+
}
925965
}
926966

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

933974
[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
@@ -944,8 +985,7 @@ void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvict
944985
// An attacker cannot manipulate this metric without performing useful work.
945986
EraseLastKElements(vEvictionCandidates, CompareNodeTXTime, 4);
946987
// Protect up to 8 non-tx-relay peers that have sent us novel blocks.
947-
const size_t erase_size = std::min(size_t(8), vEvictionCandidates.size());
948-
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, erase_size,
988+
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8,
949989
[](const NodeEvictionCandidate& n) { return !n.fRelayTxes && n.fRelevantServices; });
950990

951991
// Protect 4 nodes that most recently sent us novel blocks.
@@ -1024,7 +1064,7 @@ bool CConnman::AttemptToEvictConnection()
10241064
HasAllDesirableServiceFlags(node->nServices),
10251065
peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
10261066
node->m_prefer_evict, node->addr.IsLocal(),
1027-
node->m_inbound_onion};
1067+
node->ConnectedThroughNetwork()};
10281068
vEvictionCandidates.push_back(candidate);
10291069
}
10301070
}

src/net.h

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ struct NodeEvictionCandidate
12091209
uint64_t nKeyedNetGroup;
12101210
bool prefer_evict;
12111211
bool m_is_local;
1212-
bool m_is_onion;
1212+
Network m_network;
12131213
};
12141214

12151215
/**
@@ -1227,20 +1227,20 @@ struct NodeEvictionCandidate
12271227
* longest, to replicate the non-eviction implicit behavior and preclude attacks
12281228
* that start later.
12291229
*
1230-
* Half of these protected spots (1/4 of the total) are reserved for onion peers
1231-
* connected via our tor control service, if any, sorted by longest uptime, even
1232-
* if they're not longest uptime overall. Any remaining slots of the 1/4 are
1233-
* then allocated to protect localhost peers, if any (or up to 2 localhost peers
1234-
* if no slots remain and 2 or more onion peers were protected), sorted by
1235-
* longest uptime, as manually configured hidden services not using
1236-
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections.
1230+
* Half of these protected spots (1/4 of the total) are reserved for the
1231+
* following categories of peers, sorted by longest uptime, even if they're not
1232+
* longest uptime overall:
1233+
*
1234+
* - onion peers connected via our tor control service
1235+
*
1236+
* - localhost peers, as manually configured hidden services not using
1237+
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections
12371238
*
1238-
* This helps protect onion peers, which tend to be otherwise disadvantaged
1239-
* under our eviction criteria for their higher min ping times relative to IPv4
1240-
* and IPv6 peers, and favorise the diversity of peer connections.
1239+
* - I2P peers
12411240
*
1242-
* This function was extracted from SelectNodeToEvict() to be able to test the
1243-
* ratio-based protection logic deterministically.
1241+
* This helps protect these privacy network peers, which tend to be otherwise
1242+
* disadvantaged under our eviction criteria for their higher min ping times
1243+
* relative to IPv4/IPv6 peers, and favorise the diversity of peer connections.
12441244
*/
12451245
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates);
12461246

src/test/fuzz/node_eviction.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ FUZZ_TARGET(node_eviction)
3131
/* nKeyedNetGroup */ fuzzed_data_provider.ConsumeIntegral<uint64_t>(),
3232
/* prefer_evict */ fuzzed_data_provider.ConsumeBool(),
3333
/* m_is_local */ fuzzed_data_provider.ConsumeBool(),
34-
/* m_is_onion */ fuzzed_data_provider.ConsumeBool(),
34+
/* m_network */ fuzzed_data_provider.PickValueInArray(ALL_NETWORKS),
3535
});
3636
}
3737
// Make a copy since eviction_candidates may be in some valid but otherwise

0 commit comments

Comments
 (0)