Skip to content

Commit 1e15acf

Browse files
jonatackvasild
andcommitted
p2p: make ProtectEvictionCandidatesByRatio() fully ratio-based
with a more abstract framework to allow easily extending inbound eviction protection to peers connected through new higher-latency networks that are disadvantaged by our inbound eviction criteria, such as I2P and perhaps other BIP155 networks in the future like CJDNS. This is a change in behavior. 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. Localhost peers are treated as a network like Tor or I2P by aliasing them to an unused Network enumerator: Network::NET_MAX. The goal is to favorise diversity of our inbound connections. Credit to Vasil Dimov for improving the algorithm from single-pass to multi-pass to better allocate unused protection slots. Co-authored-by: Vasil Dimov <[email protected]>
1 parent 3f8105c commit 1e15acf

File tree

2 files changed

+63
-26
lines changed

2 files changed

+63
-26
lines changed

src/net.cpp

Lines changed: 53 additions & 21 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>
@@ -918,35 +919,66 @@ void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& evicti
918919
{
919920
// Protect the half of the remaining nodes which have been connected the longest.
920921
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
921-
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
922-
// these protected spots for onion and localhost peers, if any, even if they're not
923-
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
922+
// To favorise the diversity of our peer connections, reserve up to half of these protected
923+
// spots for Tor/onion and localhost peers, even if they're not longest uptime overall.
924+
// This helps protect these higher-latency peers that tend to be otherwise
924925
// disadvantaged under our eviction criteria.
925926
const size_t initial_size = eviction_candidates.size();
926927
const size_t total_protect_size{initial_size / 2};
927-
const size_t onion_protect_size = total_protect_size / 2;
928928

929-
if (onion_protect_size) {
930-
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
931-
EraseLastKElements(eviction_candidates, CompareOnionTimeConnected, onion_protect_size,
932-
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
933-
}
934-
935-
const size_t localhost_min_protect_size{2};
936-
if (onion_protect_size >= localhost_min_protect_size) {
937-
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
938-
// to localhost peers, sorted by longest uptime, as manually configured
939-
// hidden services not using `-bind=addr[:port]=onion` will not be detected
940-
// as inbound onion connections.
941-
const size_t remaining_tor_slots{onion_protect_size - (initial_size - eviction_candidates.size())};
942-
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
943-
EraseLastKElements(eviction_candidates, CompareLocalHostTimeConnected, localhost_protect_size,
944-
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
929+
// Disadvantaged networks to protect: localhost and Tor/onion. In case of equal counts, earlier
930+
// array members have first opportunity to recover unused slots from the previous iteration.
931+
struct Net { bool is_local; Network id; size_t count; };
932+
std::array<Net, 3> networks{{{/* localhost */ true, NET_MAX, 0}, {false, NET_ONION, 0}}};
933+
934+
// Count and store the number of eviction candidates per network.
935+
for (Net& n : networks) {
936+
n.count = std::count_if(eviction_candidates.cbegin(), eviction_candidates.cend(),
937+
[&n](const NodeEvictionCandidate& c) {
938+
return n.is_local ? c.m_is_local : c.m_network == n.id;
939+
});
940+
}
941+
// Sort `networks` by ascending candidate count, to give networks having fewer candidates
942+
// the first opportunity to recover unused protected slots from the previous iteration.
943+
std::stable_sort(networks.begin(), networks.end(), [](Net a, Net b) { return a.count < b.count; });
944+
945+
// Protect up to 25% of the eviction candidates by disadvantaged network.
946+
const size_t max_protect_by_network{total_protect_size / 2};
947+
size_t num_protected{0};
948+
949+
while (num_protected < max_protect_by_network) {
950+
const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
951+
const size_t protect_per_network{
952+
std::max(disadvantaged_to_protect / networks.size(), static_cast<size_t>(1))};
953+
954+
// Early exit flag if there are no remaining candidates by disadvantaged network.
955+
bool protected_at_least_one{false};
956+
957+
for (const Net& n : networks) {
958+
if (n.count == 0) continue;
959+
const size_t before = eviction_candidates.size();
960+
EraseLastKElements(eviction_candidates, CompareNodeNetworkTime(n.is_local, n.id),
961+
protect_per_network, [&n](const NodeEvictionCandidate& c) {
962+
return n.is_local ? c.m_is_local : c.m_network == n.id;
963+
});
964+
const size_t after = eviction_candidates.size();
965+
if (before > after) {
966+
protected_at_least_one = true;
967+
num_protected += before - after;
968+
if (num_protected >= max_protect_by_network) {
969+
break;
970+
}
971+
}
972+
}
973+
if (!protected_at_least_one) {
974+
break;
975+
}
945976
}
946977

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

src/test/net_peer_eviction_tests.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
9393
num_peers, [](NodeEvictionCandidate& c) {
9494
c.nTimeConnected = c.id;
9595
c.m_is_onion = c.m_is_local = false;
96+
c.m_network = NET_IPV4;
9697
},
9798
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5},
9899
/* unprotected_peer_ids */ {6, 7, 8, 9, 10, 11},
@@ -103,6 +104,7 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
103104
num_peers, [num_peers](NodeEvictionCandidate& c) {
104105
c.nTimeConnected = num_peers - c.id;
105106
c.m_is_onion = c.m_is_local = false;
107+
c.m_network = NET_IPV6;
106108
},
107109
/* protected_peer_ids */ {6, 7, 8, 9, 10, 11},
108110
/* unprotected_peer_ids */ {0, 1, 2, 3, 4, 5},
@@ -111,22 +113,23 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
111113
// Test protection of onion and localhost peers...
112114

113115
// Expect 1/4 onion peers to be protected from eviction,
114-
// independently of other characteristics.
116+
// if no localhost peers.
115117
BOOST_CHECK(IsProtected(
116118
num_peers, [](NodeEvictionCandidate& c) {
117-
c.m_is_onion = (c.id == 3 || c.id == 8 || c.id == 9);
119+
c.m_is_local = false;
120+
c.m_network = (c.id == 3 || c.id == 8 || c.id == 9) ? NET_ONION : NET_IPV4;
118121
},
119122
/* protected_peer_ids */ {3, 8, 9},
120123
/* unprotected_peer_ids */ {},
121124
random_context));
122125

123-
// Expect 1/4 onion peers and 1/4 of the others to be protected
124-
// from eviction, sorted by longest uptime (lowest nTimeConnected).
126+
// Expect 1/4 onion peers and 1/4 of the other peers to be protected,
127+
// sorted by longest uptime (lowest nTimeConnected), if no localhost peers.
125128
BOOST_CHECK(IsProtected(
126129
num_peers, [](NodeEvictionCandidate& c) {
127130
c.nTimeConnected = c.id;
128131
c.m_is_local = false;
129-
c.m_is_onion = (c.id == 3 || c.id > 7);
132+
c.m_network = (c.id == 3 || c.id > 7) ? NET_ONION : NET_IPV6;
130133
},
131134
/* protected_peer_ids */ {0, 1, 2, 3, 8, 9},
132135
/* unprotected_peer_ids */ {4, 5, 6, 7, 10, 11},
@@ -138,6 +141,7 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
138141
num_peers, [](NodeEvictionCandidate& c) {
139142
c.m_is_onion = false;
140143
c.m_is_local = (c.id == 1 || c.id == 9 || c.id == 11);
144+
c.m_network = NET_IPV4;
141145
},
142146
/* protected_peer_ids */ {1, 9, 11},
143147
/* unprotected_peer_ids */ {},
@@ -150,6 +154,7 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
150154
c.nTimeConnected = c.id;
151155
c.m_is_onion = false;
152156
c.m_is_local = (c.id > 6);
157+
c.m_network = NET_IPV6;
153158
},
154159
/* protected_peer_ids */ {0, 1, 2, 7, 8, 9},
155160
/* unprotected_peer_ids */ {3, 4, 5, 6, 10, 11},

0 commit comments

Comments
 (0)