62
62
#endif
63
63
64
64
#include < algorithm>
65
+ #include < array>
65
66
#include < cstdint>
66
67
#include < functional>
67
68
#include < unordered_map>
@@ -913,18 +914,6 @@ static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate& a, cons
913
914
return a.nTimeConnected > b.nTimeConnected ;
914
915
}
915
916
916
- static bool CompareLocalHostTimeConnected (const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
917
- {
918
- if (a.m_is_local != b.m_is_local ) return b.m_is_local ;
919
- return a.nTimeConnected > b.nTimeConnected ;
920
- }
921
-
922
- static bool CompareOnionTimeConnected (const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
923
- {
924
- if (a.m_is_onion != b.m_is_onion ) return b.m_is_onion ;
925
- return a.nTimeConnected > b.nTimeConnected ;
926
- }
927
-
928
917
static bool CompareNetGroupKeyed (const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
929
918
return a.nKeyedNetGroup < b.nKeyedNetGroup ;
930
919
}
@@ -955,6 +944,26 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
955
944
return a.nTimeConnected > b.nTimeConnected ;
956
945
}
957
946
947
+ /* *
948
+ * Sort eviction candidates by network/localhost and connection uptime.
949
+ * Candidates near the beginning are more likely to be evicted, and those
950
+ * near the end are more likely to be protected, e.g. less likely to be evicted.
951
+ * - First, nodes that are not `is_local` and that do not belong to `network`,
952
+ * sorted by increasing uptime (from most recently connected to connected longer).
953
+ * - Then, nodes that are `is_local` or belong to `network`, sorted by increasing uptime.
954
+ */
955
+ struct CompareNodeNetworkTime {
956
+ const bool m_is_local;
957
+ const Network m_network;
958
+ CompareNodeNetworkTime (bool is_local, Network network) : m_is_local(is_local), m_network(network) {}
959
+ bool operator ()(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b) const
960
+ {
961
+ if (m_is_local && a.m_is_local != b.m_is_local ) return b.m_is_local ;
962
+ if ((a.m_network == m_network) != (b.m_network == m_network)) return b.m_network == m_network;
963
+ return a.nTimeConnected > b.nTimeConnected ;
964
+ };
965
+ };
966
+
958
967
// ! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
959
968
template <typename T, typename Comparator>
960
969
static void EraseLastKElements (
@@ -966,40 +975,72 @@ static void EraseLastKElements(
966
975
elements.erase (std::remove_if (elements.end () - eraseSize, elements.end (), predicate), elements.end ());
967
976
}
968
977
969
- void ProtectEvictionCandidatesByRatio (std::vector<NodeEvictionCandidate>& vEvictionCandidates )
978
+ void ProtectEvictionCandidatesByRatio (std::vector<NodeEvictionCandidate>& eviction_candidates )
970
979
{
971
980
// Protect the half of the remaining nodes which have been connected the longest.
972
981
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
973
- // To favorise the diversity of our peer connections, reserve up to ( half + 2) of
974
- // these protected spots for onion and localhost peers, if any, even if they're not
975
- // longest uptime overall. This helps protect tor peers, which tend to be otherwise
982
+ // To favorise the diversity of our peer connections, reserve up to half of these protected
983
+ // spots for Tor/ onion, localhost and I2P peers, even if they're not longest uptime overall.
984
+ // This helps protect these higher-latency peers that tend to be otherwise
976
985
// disadvantaged under our eviction criteria.
977
- const size_t initial_size = vEvictionCandidates.size ();
978
- size_t total_protect_size = initial_size / 2 ;
979
- const size_t onion_protect_size = total_protect_size / 2 ;
980
-
981
- if (onion_protect_size) {
982
- // Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
983
- EraseLastKElements (vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
984
- [](const NodeEvictionCandidate& n) { return n.m_is_onion ; });
985
- }
986
-
987
- const size_t localhost_min_protect_size{2 };
988
- if (onion_protect_size >= localhost_min_protect_size) {
989
- // Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
990
- // to localhost peers, sorted by longest uptime, as manually configured
991
- // hidden services not using `-bind=addr[:port]=onion` will not be detected
992
- // as inbound onion connections.
993
- const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size ())};
994
- const size_t localhost_protect_size{std::max (remaining_tor_slots, localhost_min_protect_size)};
995
- EraseLastKElements (vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
996
- [](const NodeEvictionCandidate& n) { return n.m_is_local ; });
986
+ const size_t initial_size = eviction_candidates.size ();
987
+ const size_t total_protect_size{initial_size / 2 };
988
+
989
+ // Disadvantaged networks to protect: I2P, localhost, Tor/onion. In case of equal counts, earlier
990
+ // array members have first opportunity to recover unused slots from the previous iteration.
991
+ struct Net { bool is_local; Network id; size_t count; };
992
+ std::array<Net, 3 > networks{
993
+ {{false , NET_I2P, 0 }, {/* localhost */ true , NET_MAX, 0 }, {false , NET_ONION, 0 }}};
994
+
995
+ // Count and store the number of eviction candidates per network.
996
+ for (Net& n : networks) {
997
+ n.count = std::count_if (eviction_candidates.cbegin (), eviction_candidates.cend (),
998
+ [&n](const NodeEvictionCandidate& c) {
999
+ return n.is_local ? c.m_is_local : c.m_network == n.id ;
1000
+ });
1001
+ }
1002
+ // Sort `networks` by ascending candidate count, to give networks having fewer candidates
1003
+ // the first opportunity to recover unused protected slots from the previous iteration.
1004
+ std::stable_sort (networks.begin (), networks.end (), [](Net a, Net b) { return a.count < b.count ; });
1005
+
1006
+ // Protect up to 25% of the eviction candidates by disadvantaged network.
1007
+ const size_t max_protect_by_network{total_protect_size / 2 };
1008
+ size_t num_protected{0 };
1009
+
1010
+ while (num_protected < max_protect_by_network) {
1011
+ const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
1012
+ const size_t protect_per_network{
1013
+ std::max (disadvantaged_to_protect / networks.size (), static_cast <size_t >(1 ))};
1014
+
1015
+ // Early exit flag if there are no remaining candidates by disadvantaged network.
1016
+ bool protected_at_least_one{false };
1017
+
1018
+ for (const Net& n : networks) {
1019
+ if (n.count == 0 ) continue ;
1020
+ const size_t before = eviction_candidates.size ();
1021
+ EraseLastKElements (eviction_candidates, CompareNodeNetworkTime (n.is_local , n.id ),
1022
+ protect_per_network, [&n](const NodeEvictionCandidate& c) {
1023
+ return n.is_local ? c.m_is_local : c.m_network == n.id ;
1024
+ });
1025
+ const size_t after = eviction_candidates.size ();
1026
+ if (before > after) {
1027
+ protected_at_least_one = true ;
1028
+ num_protected += before - after;
1029
+ if (num_protected >= max_protect_by_network) {
1030
+ break ;
1031
+ }
1032
+ }
1033
+ }
1034
+ if (!protected_at_least_one) {
1035
+ break ;
1036
+ }
997
1037
}
998
1038
999
1039
// Calculate how many we removed, and update our total number of peers that
1000
1040
// we want to protect based on uptime accordingly.
1001
- total_protect_size -= initial_size - vEvictionCandidates.size ();
1002
- EraseLastKElements (vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
1041
+ assert (num_protected == initial_size - eviction_candidates.size ());
1042
+ const size_t remaining_to_protect{total_protect_size - num_protected};
1043
+ EraseLastKElements (eviction_candidates, ReverseCompareNodeTimeConnected, remaining_to_protect);
1003
1044
}
1004
1045
1005
1046
[[nodiscard]] std::optional<NodeId> SelectNodeToEvict (std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
@@ -1016,8 +1057,7 @@ void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvict
1016
1057
// An attacker cannot manipulate this metric without performing useful work.
1017
1058
EraseLastKElements (vEvictionCandidates, CompareNodeTXTime, 4 );
1018
1059
// Protect up to 8 non-tx-relay peers that have sent us novel blocks.
1019
- const size_t erase_size = std::min (size_t (8 ), vEvictionCandidates.size ());
1020
- EraseLastKElements (vEvictionCandidates, CompareNodeBlockRelayOnlyTime, erase_size,
1060
+ EraseLastKElements (vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8 ,
1021
1061
[](const NodeEvictionCandidate& n) { return !n.m_relay_txs && n.fRelevantServices ; });
1022
1062
1023
1063
// Protect 4 nodes that most recently sent us novel blocks.
@@ -1109,7 +1149,7 @@ bool CConnman::AttemptToEvictConnection()
1109
1149
HasAllDesirableServiceFlags (node->nServices ),
1110
1150
node->m_relays_txs .load (), node->m_bloom_filter_loaded .load (),
1111
1151
node->nKeyedNetGroup , node->m_prefer_evict , node->addr .IsLocal (),
1112
- node->m_inbound_onion };
1152
+ node->ConnectedThroughNetwork () };
1113
1153
vEvictionCandidates.push_back (candidate);
1114
1154
}
1115
1155
}
0 commit comments