aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordergoegge <n.goeggi@gmail.com>2022-05-26 16:07:04 +0200
committerdergoegge <n.goeggi@gmail.com>2022-07-06 18:13:54 +0200
commit0101d2bc3c3bcf698d6cc2a237a680fc52395987 (patch)
tree7b5429e73afc057679b828d907034c3bf9be9273 /src
parentc741d748d4d9836940b99091cc7be09c65efcb79 (diff)
downloadbitcoin-0101d2bc3c3bcf698d6cc2a237a680fc52395987.tar.xz
[net] Move eviction logic to its own file
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am2
-rw-r--r--src/net.cpp227
-rw-r--r--src/net.h52
-rw-r--r--src/node/eviction.cpp240
-rw-r--r--src/node/eviction.h69
-rw-r--r--src/test/util/net.cpp1
-rw-r--r--src/test/util/net.h1
7 files changed, 314 insertions, 278 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index edda8617d2..da777e2301 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -149,6 +149,7 @@ BITCOIN_CORE_H = \
dbwrapper.h \
deploymentinfo.h \
deploymentstatus.h \
+ node/eviction.h \
external_signer.h \
flatfile.h \
fs.h \
@@ -371,6 +372,7 @@ libbitcoin_node_a_SOURCES = \
node/coin.cpp \
node/connection_types.cpp \
node/context.cpp \
+ node/eviction.cpp \
node/interfaces.cpp \
node/miner.cpp \
node/minisketchwrapper.cpp \
diff --git a/src/net.cpp b/src/net.cpp
index bde1c049f8..c37d90519c 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -16,6 +16,7 @@
#include <compat.h>
#include <consensus/consensus.h>
#include <crypto/sha256.h>
+#include <node/eviction.h>
#include <fs.h>
#include <i2p.h>
#include <net_permissions.h>
@@ -857,232 +858,6 @@ size_t CConnman::SocketSendData(CNode& node) const
return nSentSize;
}
-static bool ReverseCompareNodeMinPingTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
-{
- return a.m_min_ping_time > b.m_min_ping_time;
-}
-
-static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
-{
- return a.m_connected > b.m_connected;
-}
-
-static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
- return a.nKeyedNetGroup < b.nKeyedNetGroup;
-}
-
-static bool CompareNodeBlockTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
-{
- // There is a fall-through here because it is common for a node to have many peers which have not yet relayed a block.
- if (a.m_last_block_time != b.m_last_block_time) return a.m_last_block_time < b.m_last_block_time;
- if (a.fRelevantServices != b.fRelevantServices) return b.fRelevantServices;
- return a.m_connected > b.m_connected;
-}
-
-static bool CompareNodeTXTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
-{
- // There is a fall-through here because it is common for a node to have more than a few peers that have not yet relayed txn.
- if (a.m_last_tx_time != b.m_last_tx_time) return a.m_last_tx_time < b.m_last_tx_time;
- if (a.m_relay_txs != b.m_relay_txs) return b.m_relay_txs;
- if (a.fBloomFilter != b.fBloomFilter) return a.fBloomFilter;
- return a.m_connected > b.m_connected;
-}
-
-// Pick out the potential block-relay only peers, and sort them by last block time.
-static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
-{
- if (a.m_relay_txs != b.m_relay_txs) return a.m_relay_txs;
- if (a.m_last_block_time != b.m_last_block_time) return a.m_last_block_time < b.m_last_block_time;
- if (a.fRelevantServices != b.fRelevantServices) return b.fRelevantServices;
- return a.m_connected > b.m_connected;
-}
-
-/**
- * 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.m_connected > b.m_connected;
- };
-};
-
-//! 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(
- std::vector<T>& elements, Comparator comparator, size_t k,
- std::function<bool(const NodeEvictionCandidate&)> predicate = [](const NodeEvictionCandidate& n) { return true; })
-{
- std::sort(elements.begin(), elements.end(), comparator);
- size_t eraseSize = std::min(k, elements.size());
- elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
-}
-
-void ProtectNoBanConnections(std::vector<NodeEvictionCandidate>& eviction_candidates)
-{
- eviction_candidates.erase(std::remove_if(eviction_candidates.begin(), eviction_candidates.end(),
- [](NodeEvictionCandidate const& n) {
- return n.m_noban;
- }),
- eviction_candidates.end());
-}
-
-void ProtectOutboundConnections(std::vector<NodeEvictionCandidate>& eviction_candidates)
-{
- eviction_candidates.erase(std::remove_if(eviction_candidates.begin(), eviction_candidates.end(),
- [](NodeEvictionCandidate const& n) {
- return n.m_conn_type != ConnectionType::INBOUND;
- }),
- eviction_candidates.end());
-}
-
-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 of these protected
- // spots for Tor/onion, localhost, I2P, and CJDNS 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 = eviction_candidates.size();
- const size_t total_protect_size{initial_size / 2};
-
- // Disadvantaged networks to protect. In the case of equal counts, earlier array members
- // have the first opportunity to recover unused slots from the previous iteration.
- struct Net { bool is_local; Network id; size_t count; };
- std::array<Net, 4> networks{
- {{false, NET_CJDNS, 0}, {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) {
- // Count the number of disadvantaged networks from which we have peers to protect.
- auto num_networks = std::count_if(networks.begin(), networks.end(), [](const Net& n) { return n.count; });
- if (num_networks == 0) {
- break;
- }
- const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
- const size_t protect_per_network{std::max(disadvantaged_to_protect / num_networks, static_cast<size_t>(1))};
- // Early exit flag if there are no remaining candidates by disadvantaged network.
- bool protected_at_least_one{false};
-
- for (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;
- const size_t delta{before - after};
- num_protected += delta;
- if (num_protected >= max_protect_by_network) {
- break;
- }
- n.count -= delta;
- }
- }
- 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.
- 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)
-{
- // Protect connections with certain characteristics
-
- ProtectNoBanConnections(vEvictionCandidates);
-
- ProtectOutboundConnections(vEvictionCandidates);
-
- // Deterministically select 4 peers to protect by netgroup.
- // An attacker cannot predict which netgroups will be protected
- EraseLastKElements(vEvictionCandidates, CompareNetGroupKeyed, 4);
- // Protect the 8 nodes with the lowest minimum ping time.
- // An attacker cannot manipulate this metric without physically moving nodes closer to the target.
- EraseLastKElements(vEvictionCandidates, ReverseCompareNodeMinPingTime, 8);
- // Protect 4 nodes that most recently sent us novel transactions accepted into our mempool.
- // 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.
- EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8,
- [](const NodeEvictionCandidate& n) { return !n.m_relay_txs && n.fRelevantServices; });
-
- // Protect 4 nodes that most recently sent us novel blocks.
- // An attacker cannot manipulate this metric without performing useful work.
- EraseLastKElements(vEvictionCandidates, CompareNodeBlockTime, 4);
-
- // Protect some of the remaining eviction candidates by ratios of desirable
- // or disadvantaged characteristics.
- ProtectEvictionCandidatesByRatio(vEvictionCandidates);
-
- if (vEvictionCandidates.empty()) return std::nullopt;
-
- // If any remaining peers are preferred for eviction consider only them.
- // This happens after the other preferences since if a peer is really the best by other criteria (esp relaying blocks)
- // then we probably don't want to evict it no matter what.
- if (std::any_of(vEvictionCandidates.begin(),vEvictionCandidates.end(),[](NodeEvictionCandidate const &n){return n.prefer_evict;})) {
- vEvictionCandidates.erase(std::remove_if(vEvictionCandidates.begin(),vEvictionCandidates.end(),
- [](NodeEvictionCandidate const &n){return !n.prefer_evict;}),vEvictionCandidates.end());
- }
-
- // Identify the network group with the most connections and youngest member.
- // (vEvictionCandidates is already sorted by reverse connect time)
- uint64_t naMostConnections;
- unsigned int nMostConnections = 0;
- std::chrono::seconds nMostConnectionsTime{0};
- std::map<uint64_t, std::vector<NodeEvictionCandidate> > mapNetGroupNodes;
- for (const NodeEvictionCandidate &node : vEvictionCandidates) {
- std::vector<NodeEvictionCandidate> &group = mapNetGroupNodes[node.nKeyedNetGroup];
- group.push_back(node);
- const auto grouptime{group[0].m_connected};
-
- if (group.size() > nMostConnections || (group.size() == nMostConnections && grouptime > nMostConnectionsTime)) {
- nMostConnections = group.size();
- nMostConnectionsTime = grouptime;
- naMostConnections = node.nKeyedNetGroup;
- }
- }
-
- // Reduce to the network group with the most connections
- vEvictionCandidates = std::move(mapNetGroupNodes[naMostConnections]);
-
- // Disconnect from the network group with the most connections
- return vEvictionCandidates.front().id;
-}
-
/** Try to find a connection to evict when the node is full.
* Extreme care must be taken to avoid opening the node to attacker
* triggered network partitioning.
diff --git a/src/net.h b/src/net.h
index 03b8de26b1..6453ad1dc7 100644
--- a/src/net.h
+++ b/src/net.h
@@ -1176,56 +1176,4 @@ extern std::function<void(const CAddress& addr,
bool is_incoming)>
CaptureMessage;
-struct NodeEvictionCandidate
-{
- NodeId id;
- std::chrono::seconds m_connected;
- std::chrono::microseconds m_min_ping_time;
- std::chrono::seconds m_last_block_time;
- std::chrono::seconds m_last_tx_time;
- bool fRelevantServices;
- bool m_relay_txs;
- bool fBloomFilter;
- uint64_t nKeyedNetGroup;
- bool prefer_evict;
- bool m_is_local;
- Network m_network;
- bool m_noban;
- ConnectionType m_conn_type;
-};
-
-/**
- * Select an inbound peer to evict after filtering out (protecting) peers having
- * distinct, difficult-to-forge characteristics. The protection logic picks out
- * fixed numbers of desirable peers per various criteria, followed by (mostly)
- * ratios of desirable or disadvantaged peers. If any eviction candidates
- * remain, the selection logic chooses a peer to evict.
- */
-[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates);
-
-/** Protect desirable or disadvantaged inbound peers from eviction by ratio.
- *
- * This function protects half of the peers which have been connected the
- * 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 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
- *
- * - I2P peers
- *
- * - CJDNS peers
- *
- * 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);
-
#endif // BITCOIN_NET_H
diff --git a/src/node/eviction.cpp b/src/node/eviction.cpp
new file mode 100644
index 0000000000..33406931d4
--- /dev/null
+++ b/src/node/eviction.cpp
@@ -0,0 +1,240 @@
+// Copyright (c) 2022 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <node/eviction.h>
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cstdint>
+#include <functional>
+#include <map>
+#include <vector>
+
+
+static bool ReverseCompareNodeMinPingTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
+{
+ return a.m_min_ping_time > b.m_min_ping_time;
+}
+
+static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
+{
+ return a.m_connected > b.m_connected;
+}
+
+static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
+ return a.nKeyedNetGroup < b.nKeyedNetGroup;
+}
+
+static bool CompareNodeBlockTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
+{
+ // There is a fall-through here because it is common for a node to have many peers which have not yet relayed a block.
+ if (a.m_last_block_time != b.m_last_block_time) return a.m_last_block_time < b.m_last_block_time;
+ if (a.fRelevantServices != b.fRelevantServices) return b.fRelevantServices;
+ return a.m_connected > b.m_connected;
+}
+
+static bool CompareNodeTXTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
+{
+ // There is a fall-through here because it is common for a node to have more than a few peers that have not yet relayed txn.
+ if (a.m_last_tx_time != b.m_last_tx_time) return a.m_last_tx_time < b.m_last_tx_time;
+ if (a.m_relay_txs != b.m_relay_txs) return b.m_relay_txs;
+ if (a.fBloomFilter != b.fBloomFilter) return a.fBloomFilter;
+ return a.m_connected > b.m_connected;
+}
+
+// Pick out the potential block-relay only peers, and sort them by last block time.
+static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
+{
+ if (a.m_relay_txs != b.m_relay_txs) return a.m_relay_txs;
+ if (a.m_last_block_time != b.m_last_block_time) return a.m_last_block_time < b.m_last_block_time;
+ if (a.fRelevantServices != b.fRelevantServices) return b.fRelevantServices;
+ return a.m_connected > b.m_connected;
+}
+
+/**
+ * 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.m_connected > b.m_connected;
+ };
+};
+
+//! 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(
+ std::vector<T>& elements, Comparator comparator, size_t k,
+ std::function<bool(const NodeEvictionCandidate&)> predicate = [](const NodeEvictionCandidate& n) { return true; })
+{
+ std::sort(elements.begin(), elements.end(), comparator);
+ size_t eraseSize = std::min(k, elements.size());
+ elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
+}
+
+void ProtectNoBanConnections(std::vector<NodeEvictionCandidate>& eviction_candidates)
+{
+ eviction_candidates.erase(std::remove_if(eviction_candidates.begin(), eviction_candidates.end(),
+ [](NodeEvictionCandidate const& n) {
+ return n.m_noban;
+ }),
+ eviction_candidates.end());
+}
+
+void ProtectOutboundConnections(std::vector<NodeEvictionCandidate>& eviction_candidates)
+{
+ eviction_candidates.erase(std::remove_if(eviction_candidates.begin(), eviction_candidates.end(),
+ [](NodeEvictionCandidate const& n) {
+ return n.m_conn_type != ConnectionType::INBOUND;
+ }),
+ eviction_candidates.end());
+}
+
+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 of these protected
+ // spots for Tor/onion, localhost, I2P, and CJDNS 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 = eviction_candidates.size();
+ const size_t total_protect_size{initial_size / 2};
+
+ // Disadvantaged networks to protect. In the case of equal counts, earlier array members
+ // have the first opportunity to recover unused slots from the previous iteration.
+ struct Net { bool is_local; Network id; size_t count; };
+ std::array<Net, 4> networks{
+ {{false, NET_CJDNS, 0}, {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) {
+ // Count the number of disadvantaged networks from which we have peers to protect.
+ auto num_networks = std::count_if(networks.begin(), networks.end(), [](const Net& n) { return n.count; });
+ if (num_networks == 0) {
+ break;
+ }
+ const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
+ const size_t protect_per_network{std::max(disadvantaged_to_protect / num_networks, static_cast<size_t>(1))};
+ // Early exit flag if there are no remaining candidates by disadvantaged network.
+ bool protected_at_least_one{false};
+
+ for (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;
+ const size_t delta{before - after};
+ num_protected += delta;
+ if (num_protected >= max_protect_by_network) {
+ break;
+ }
+ n.count -= delta;
+ }
+ }
+ 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.
+ 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)
+{
+ // Protect connections with certain characteristics
+
+ ProtectNoBanConnections(vEvictionCandidates);
+
+ ProtectOutboundConnections(vEvictionCandidates);
+
+ // Deterministically select 4 peers to protect by netgroup.
+ // An attacker cannot predict which netgroups will be protected
+ EraseLastKElements(vEvictionCandidates, CompareNetGroupKeyed, 4);
+ // Protect the 8 nodes with the lowest minimum ping time.
+ // An attacker cannot manipulate this metric without physically moving nodes closer to the target.
+ EraseLastKElements(vEvictionCandidates, ReverseCompareNodeMinPingTime, 8);
+ // Protect 4 nodes that most recently sent us novel transactions accepted into our mempool.
+ // 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.
+ EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8,
+ [](const NodeEvictionCandidate& n) { return !n.m_relay_txs && n.fRelevantServices; });
+
+ // Protect 4 nodes that most recently sent us novel blocks.
+ // An attacker cannot manipulate this metric without performing useful work.
+ EraseLastKElements(vEvictionCandidates, CompareNodeBlockTime, 4);
+
+ // Protect some of the remaining eviction candidates by ratios of desirable
+ // or disadvantaged characteristics.
+ ProtectEvictionCandidatesByRatio(vEvictionCandidates);
+
+ if (vEvictionCandidates.empty()) return std::nullopt;
+
+ // If any remaining peers are preferred for eviction consider only them.
+ // This happens after the other preferences since if a peer is really the best by other criteria (esp relaying blocks)
+ // then we probably don't want to evict it no matter what.
+ if (std::any_of(vEvictionCandidates.begin(),vEvictionCandidates.end(),[](NodeEvictionCandidate const &n){return n.prefer_evict;})) {
+ vEvictionCandidates.erase(std::remove_if(vEvictionCandidates.begin(),vEvictionCandidates.end(),
+ [](NodeEvictionCandidate const &n){return !n.prefer_evict;}),vEvictionCandidates.end());
+ }
+
+ // Identify the network group with the most connections and youngest member.
+ // (vEvictionCandidates is already sorted by reverse connect time)
+ uint64_t naMostConnections;
+ unsigned int nMostConnections = 0;
+ std::chrono::seconds nMostConnectionsTime{0};
+ std::map<uint64_t, std::vector<NodeEvictionCandidate> > mapNetGroupNodes;
+ for (const NodeEvictionCandidate &node : vEvictionCandidates) {
+ std::vector<NodeEvictionCandidate> &group = mapNetGroupNodes[node.nKeyedNetGroup];
+ group.push_back(node);
+ const auto grouptime{group[0].m_connected};
+
+ if (group.size() > nMostConnections || (group.size() == nMostConnections && grouptime > nMostConnectionsTime)) {
+ nMostConnections = group.size();
+ nMostConnectionsTime = grouptime;
+ naMostConnections = node.nKeyedNetGroup;
+ }
+ }
+
+ // Reduce to the network group with the most connections
+ vEvictionCandidates = std::move(mapNetGroupNodes[naMostConnections]);
+
+ // Disconnect from the network group with the most connections
+ return vEvictionCandidates.front().id;
+}
diff --git a/src/node/eviction.h b/src/node/eviction.h
new file mode 100644
index 0000000000..1bb32e5327
--- /dev/null
+++ b/src/node/eviction.h
@@ -0,0 +1,69 @@
+// Copyright (c) 2022 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_NODE_EVICTION_H
+#define BITCOIN_NODE_EVICTION_H
+
+#include <node/connection_types.h>
+#include <net_permissions.h>
+
+#include <chrono>
+#include <cstdint>
+#include <optional>
+#include <vector>
+
+typedef int64_t NodeId;
+
+struct NodeEvictionCandidate {
+ NodeId id;
+ std::chrono::seconds m_connected;
+ std::chrono::microseconds m_min_ping_time;
+ std::chrono::seconds m_last_block_time;
+ std::chrono::seconds m_last_tx_time;
+ bool fRelevantServices;
+ bool m_relay_txs;
+ bool fBloomFilter;
+ uint64_t nKeyedNetGroup;
+ bool prefer_evict;
+ bool m_is_local;
+ Network m_network;
+ bool m_noban;
+ ConnectionType m_conn_type;
+};
+
+/**
+ * Select an inbound peer to evict after filtering out (protecting) peers having
+ * distinct, difficult-to-forge characteristics. The protection logic picks out
+ * fixed numbers of desirable peers per various criteria, followed by (mostly)
+ * ratios of desirable or disadvantaged peers. If any eviction candidates
+ * remain, the selection logic chooses a peer to evict.
+ */
+[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates);
+
+/** Protect desirable or disadvantaged inbound peers from eviction by ratio.
+ *
+ * This function protects half of the peers which have been connected the
+ * 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 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
+ *
+ * - I2P peers
+ *
+ * - CJDNS peers
+ *
+ * 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);
+
+#endif // BITCOIN_NODE_EVICTION_H
diff --git a/src/test/util/net.cpp b/src/test/util/net.cpp
index bbcee6a5c8..071193b550 100644
--- a/src/test/util/net.cpp
+++ b/src/test/util/net.cpp
@@ -5,6 +5,7 @@
#include <test/util/net.h>
#include <chainparams.h>
+#include <node/eviction.h>
#include <net.h>
#include <span.h>
diff --git a/src/test/util/net.h b/src/test/util/net.h
index c5dbaeca3e..34ab9958c6 100644
--- a/src/test/util/net.h
+++ b/src/test/util/net.h
@@ -6,6 +6,7 @@
#define BITCOIN_TEST_UTIL_NET_H
#include <compat.h>
+#include <node/eviction.h>
#include <netaddress.h>
#include <net.h>
#include <util/sock.h>