Skip to content

Commit b440c33

Browse files
author
MarcoFalke
committed
Merge #20477: net: Add unit testing of node eviction logic
fee8823 Assert eviction at >= 29 candidates. Assert non-eviction at <= 20 candidates. (practicalswift) 685c428 test: Add unit testing of node eviction logic (practicalswift) ed73f8c net: Move eviction node selection logic to SelectNodeToEvict(...) (practicalswift) Pull request description: Add unit testing of node eviction logic. Closes #19966. ACKs for top commit: jonatack: ACK fee8823 MarcoFalke: ACK fee8823 🐼 Tree-SHA512: 0827c35609122ca42bfabb17feaaee35c191ab4dc2e428282af425a6c176eaeaff2789b74a4f7eb4ca6b8cb10603068e90ca800e6ef3bc3904d50e76616f7a2b
2 parents dff0f6f + fee8823 commit b440c33

File tree

3 files changed

+210
-54
lines changed

3 files changed

+210
-54
lines changed

src/net.cpp

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <net_permissions.h>
1717
#include <netbase.h>
1818
#include <node/ui_interface.h>
19+
#include <optional.h>
1920
#include <protocol.h>
2021
#include <random.h>
2122
#include <scheduler.h>
@@ -844,21 +845,6 @@ size_t CConnman::SocketSendData(CNode *pnode) const EXCLUSIVE_LOCKS_REQUIRED(pno
844845
return nSentSize;
845846
}
846847

847-
struct NodeEvictionCandidate
848-
{
849-
NodeId id;
850-
int64_t nTimeConnected;
851-
int64_t nMinPingUsecTime;
852-
int64_t nLastBlockTime;
853-
int64_t nLastTXTime;
854-
bool fRelevantServices;
855-
bool fRelayTxes;
856-
bool fBloomFilter;
857-
uint64_t nKeyedNetGroup;
858-
bool prefer_evict;
859-
bool m_is_local;
860-
};
861-
862848
static bool ReverseCompareNodeMinPingTime(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
863849
{
864850
return a.nMinPingUsecTime > b.nMinPingUsecTime;
@@ -914,43 +900,8 @@ static void EraseLastKElements(std::vector<T> &elements, Comparator comparator,
914900
elements.erase(elements.end() - eraseSize, elements.end());
915901
}
916902

917-
/** Try to find a connection to evict when the node is full.
918-
* Extreme care must be taken to avoid opening the node to attacker
919-
* triggered network partitioning.
920-
* The strategy used here is to protect a small number of peers
921-
* for each of several distinct characteristics which are difficult
922-
* to forge. In order to partition a node the attacker must be
923-
* simultaneously better at all of them than honest peers.
924-
*/
925-
bool CConnman::AttemptToEvictConnection()
903+
[[nodiscard]] Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
926904
{
927-
std::vector<NodeEvictionCandidate> vEvictionCandidates;
928-
{
929-
LOCK(cs_vNodes);
930-
931-
for (const CNode* node : vNodes) {
932-
if (node->HasPermission(PF_NOBAN))
933-
continue;
934-
if (!node->IsInboundConn())
935-
continue;
936-
if (node->fDisconnect)
937-
continue;
938-
bool peer_relay_txes = false;
939-
bool peer_filter_not_null = false;
940-
if (node->m_tx_relay != nullptr) {
941-
LOCK(node->m_tx_relay->cs_filter);
942-
peer_relay_txes = node->m_tx_relay->fRelayTxes;
943-
peer_filter_not_null = node->m_tx_relay->pfilter != nullptr;
944-
}
945-
NodeEvictionCandidate candidate = {node->GetId(), node->nTimeConnected, node->nMinPingUsecTime,
946-
node->nLastBlockTime, node->nLastTXTime,
947-
HasAllDesirableServiceFlags(node->nServices),
948-
peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
949-
node->m_prefer_evict, node->addr.IsLocal()};
950-
vEvictionCandidates.push_back(candidate);
951-
}
952-
}
953-
954905
// Protect connections with certain characteristics
955906

956907
// Deterministically select 4 peers to protect by netgroup.
@@ -988,7 +939,7 @@ bool CConnman::AttemptToEvictConnection()
988939
total_protect_size -= initial_size - vEvictionCandidates.size();
989940
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
990941

991-
if (vEvictionCandidates.empty()) return false;
942+
if (vEvictionCandidates.empty()) return nullopt;
992943

993944
// If any remaining peers are preferred for eviction consider only them.
994945
// This happens after the other preferences since if a peer is really the best by other criteria (esp relaying blocks)
@@ -1020,10 +971,52 @@ bool CConnman::AttemptToEvictConnection()
1020971
vEvictionCandidates = std::move(mapNetGroupNodes[naMostConnections]);
1021972

1022973
// Disconnect from the network group with the most connections
1023-
NodeId evicted = vEvictionCandidates.front().id;
974+
return vEvictionCandidates.front().id;
975+
}
976+
977+
/** Try to find a connection to evict when the node is full.
978+
* Extreme care must be taken to avoid opening the node to attacker
979+
* triggered network partitioning.
980+
* The strategy used here is to protect a small number of peers
981+
* for each of several distinct characteristics which are difficult
982+
* to forge. In order to partition a node the attacker must be
983+
* simultaneously better at all of them than honest peers.
984+
*/
985+
bool CConnman::AttemptToEvictConnection()
986+
{
987+
std::vector<NodeEvictionCandidate> vEvictionCandidates;
988+
{
989+
990+
LOCK(cs_vNodes);
991+
for (const CNode* node : vNodes) {
992+
if (node->HasPermission(PF_NOBAN))
993+
continue;
994+
if (!node->IsInboundConn())
995+
continue;
996+
if (node->fDisconnect)
997+
continue;
998+
bool peer_relay_txes = false;
999+
bool peer_filter_not_null = false;
1000+
if (node->m_tx_relay != nullptr) {
1001+
LOCK(node->m_tx_relay->cs_filter);
1002+
peer_relay_txes = node->m_tx_relay->fRelayTxes;
1003+
peer_filter_not_null = node->m_tx_relay->pfilter != nullptr;
1004+
}
1005+
NodeEvictionCandidate candidate = {node->GetId(), node->nTimeConnected, node->nMinPingUsecTime,
1006+
node->nLastBlockTime, node->nLastTXTime,
1007+
HasAllDesirableServiceFlags(node->nServices),
1008+
peer_relay_txes, peer_filter_not_null, node->nKeyedNetGroup,
1009+
node->m_prefer_evict, node->addr.IsLocal()};
1010+
vEvictionCandidates.push_back(candidate);
1011+
}
1012+
}
1013+
const Optional<NodeId> node_id_to_evict = SelectNodeToEvict(std::move(vEvictionCandidates));
1014+
if (!node_id_to_evict) {
1015+
return false;
1016+
}
10241017
LOCK(cs_vNodes);
10251018
for (CNode* pnode : vNodes) {
1026-
if (pnode->GetId() == evicted) {
1019+
if (pnode->GetId() == *node_id_to_evict) {
10271020
pnode->fDisconnect = true;
10281021
return true;
10291022
}

src/net.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <map>
3434
#include <memory>
3535
#include <thread>
36+
#include <vector>
3637

3738
class CScheduler;
3839
class CNode;
@@ -1239,4 +1240,21 @@ inline std::chrono::microseconds PoissonNextSend(std::chrono::microseconds now,
12391240
return std::chrono::microseconds{PoissonNextSend(now.count(), average_interval.count())};
12401241
}
12411242

1243+
struct NodeEvictionCandidate
1244+
{
1245+
NodeId id;
1246+
int64_t nTimeConnected;
1247+
int64_t nMinPingUsecTime;
1248+
int64_t nLastBlockTime;
1249+
int64_t nLastTXTime;
1250+
bool fRelevantServices;
1251+
bool fRelayTxes;
1252+
bool fBloomFilter;
1253+
uint64_t nKeyedNetGroup;
1254+
bool prefer_evict;
1255+
bool m_is_local;
1256+
};
1257+
1258+
[[nodiscard]] Optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates);
1259+
12421260
#endif // BITCOIN_NET_H

src/test/net_tests.cpp

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <cstdint>
1010
#include <net.h>
1111
#include <netbase.h>
12+
#include <optional.h>
1213
#include <serialize.h>
1314
#include <span.h>
1415
#include <streams.h>
@@ -21,6 +22,7 @@
2122

2223
#include <boost/test/unit_test.hpp>
2324

25+
#include <algorithm>
2426
#include <ios>
2527
#include <memory>
2628
#include <string>
@@ -781,4 +783,147 @@ BOOST_AUTO_TEST_CASE(PoissonNextSend)
781783
g_mock_deterministic_tests = false;
782784
}
783785

786+
std::vector<NodeEvictionCandidate> GetRandomNodeEvictionCandidates(const int n_candidates, FastRandomContext& random_context)
787+
{
788+
std::vector<NodeEvictionCandidate> candidates;
789+
for (int id = 0; id < n_candidates; ++id) {
790+
candidates.push_back({
791+
/* id */ id,
792+
/* nTimeConnected */ static_cast<int64_t>(random_context.randrange(100)),
793+
/* nMinPingUsecTime */ static_cast<int64_t>(random_context.randrange(100)),
794+
/* nLastBlockTime */ static_cast<int64_t>(random_context.randrange(100)),
795+
/* nLastTXTime */ static_cast<int64_t>(random_context.randrange(100)),
796+
/* fRelevantServices */ random_context.randbool(),
797+
/* fRelayTxes */ random_context.randbool(),
798+
/* fBloomFilter */ random_context.randbool(),
799+
/* nKeyedNetGroup */ random_context.randrange(100),
800+
/* prefer_evict */ random_context.randbool(),
801+
/* m_is_local */ random_context.randbool(),
802+
});
803+
}
804+
return candidates;
805+
}
806+
807+
// Returns true if any of the node ids in node_ids are selected for eviction.
808+
bool IsEvicted(std::vector<NodeEvictionCandidate> candidates, const std::vector<NodeId>& node_ids, FastRandomContext& random_context)
809+
{
810+
Shuffle(candidates.begin(), candidates.end(), random_context);
811+
const Optional<NodeId> evicted_node_id = SelectNodeToEvict(std::move(candidates));
812+
if (!evicted_node_id) {
813+
return false;
814+
}
815+
return std::find(node_ids.begin(), node_ids.end(), *evicted_node_id) != node_ids.end();
816+
}
817+
818+
// Create number_of_nodes random nodes, apply setup function candidate_setup_fn,
819+
// apply eviction logic and then return true if any of the node ids in node_ids
820+
// are selected for eviction.
821+
bool IsEvicted(const int number_of_nodes, std::function<void(NodeEvictionCandidate&)> candidate_setup_fn, const std::vector<NodeId>& node_ids, FastRandomContext& random_context)
822+
{
823+
std::vector<NodeEvictionCandidate> candidates = GetRandomNodeEvictionCandidates(number_of_nodes, random_context);
824+
for (NodeEvictionCandidate& candidate : candidates) {
825+
candidate_setup_fn(candidate);
826+
}
827+
return IsEvicted(candidates, node_ids, random_context);
828+
}
829+
830+
namespace {
831+
constexpr int NODE_EVICTION_TEST_ROUNDS{10};
832+
constexpr int NODE_EVICTION_TEST_UP_TO_N_NODES{200};
833+
} // namespace
834+
835+
BOOST_AUTO_TEST_CASE(node_eviction_test)
836+
{
837+
FastRandomContext random_context{true};
838+
839+
for (int i = 0; i < NODE_EVICTION_TEST_ROUNDS; ++i) {
840+
for (int number_of_nodes = 0; number_of_nodes < NODE_EVICTION_TEST_UP_TO_N_NODES; ++number_of_nodes) {
841+
// Four nodes with the highest keyed netgroup values should be
842+
// protected from eviction.
843+
BOOST_CHECK(!IsEvicted(
844+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
845+
candidate.nKeyedNetGroup = number_of_nodes - candidate.id;
846+
},
847+
{0, 1, 2, 3}, random_context));
848+
849+
// Eight nodes with the lowest minimum ping time should be protected
850+
// from eviction.
851+
BOOST_CHECK(!IsEvicted(
852+
number_of_nodes, [](NodeEvictionCandidate& candidate) {
853+
candidate.nMinPingUsecTime = candidate.id;
854+
},
855+
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
856+
857+
// Four nodes that most recently sent us novel transactions accepted
858+
// into our mempool should be protected from eviction.
859+
BOOST_CHECK(!IsEvicted(
860+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
861+
candidate.nLastTXTime = number_of_nodes - candidate.id;
862+
},
863+
{0, 1, 2, 3}, random_context));
864+
865+
// Up to eight non-tx-relay peers that most recently sent us novel
866+
// blocks should be protected from eviction.
867+
BOOST_CHECK(!IsEvicted(
868+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
869+
candidate.nLastBlockTime = number_of_nodes - candidate.id;
870+
if (candidate.id <= 7) {
871+
candidate.fRelayTxes = false;
872+
candidate.fRelevantServices = true;
873+
}
874+
},
875+
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
876+
877+
// Four peers that most recently sent us novel blocks should be
878+
// protected from eviction.
879+
BOOST_CHECK(!IsEvicted(
880+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
881+
candidate.nLastBlockTime = number_of_nodes - candidate.id;
882+
},
883+
{0, 1, 2, 3}, random_context));
884+
885+
// Combination of the previous two tests.
886+
BOOST_CHECK(!IsEvicted(
887+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
888+
candidate.nLastBlockTime = number_of_nodes - candidate.id;
889+
if (candidate.id <= 7) {
890+
candidate.fRelayTxes = false;
891+
candidate.fRelevantServices = true;
892+
}
893+
},
894+
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, random_context));
895+
896+
// Combination of all tests above.
897+
BOOST_CHECK(!IsEvicted(
898+
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
899+
candidate.nKeyedNetGroup = number_of_nodes - candidate.id; // 4 protected
900+
candidate.nMinPingUsecTime = candidate.id; // 8 protected
901+
candidate.nLastTXTime = number_of_nodes - candidate.id; // 4 protected
902+
candidate.nLastBlockTime = number_of_nodes - candidate.id; // 4 protected
903+
},
904+
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, random_context));
905+
906+
// An eviction is expected given >= 29 random eviction candidates. The eviction logic protects at most
907+
// four peers by net group, eight by lowest ping time, four by last time of novel tx, up to eight non-tx-relay
908+
// peers by last novel block time, and four more peers by last novel block time.
909+
if (number_of_nodes >= 29) {
910+
BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
911+
}
912+
913+
// No eviction is expected given <= 20 random eviction candidates. The eviction logic protects at least
914+
// four peers by net group, eight by lowest ping time, four by last time of novel tx and four peers by last
915+
// novel block time.
916+
if (number_of_nodes <= 20) {
917+
BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
918+
}
919+
920+
// Cases left to test:
921+
// * "Protect the half of the remaining nodes which have been connected the longest. [...]"
922+
// * "Pick out up to 1/4 peers that are localhost, sorted by longest uptime. [...]"
923+
// * "If any remaining peers are preferred for eviction consider only them. [...]"
924+
// * "Identify the network group with the most connections and youngest member. [...]"
925+
}
926+
}
927+
}
928+
784929
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)