diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -11,6 +11,7 @@ #include #include #include +#include // for PeerId #include #include #include @@ -1319,6 +1320,7 @@ bool prefer_evict; bool m_is_local; double availabilityScore; + PeerId peerid; }; [[nodiscard]] std::optional diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -939,6 +941,41 @@ return a.nTimeConnected > b.nTimeConnected; } +static void +ProtectAvalancheNodes(std::vector &candidates) { + std::map preferredNodes; + + for (auto &c : candidates) { + if (c.peerid == NO_PEER) { + continue; + } + + auto handle = preferredNodes.extract(c.peerid); + if (!handle) { + preferredNodes.emplace(c.peerid, c); + continue; + } + + auto &mappedCandidate = handle.mapped(); + if (c.availabilityScore > mappedCandidate.availabilityScore) { + mappedCandidate = c; + } + preferredNodes.insert(std::move(handle)); + } + + // Remove the nodes while keeping the ordering of the other elements, unless + // the score is too low. + for (auto cit = candidates.begin(); cit != candidates.end();) { + auto nit = preferredNodes.find(cit->peerid); + if (nit != preferredNodes.end() && nit->second.id == cit->id && + cit->availabilityScore >= 0.) { + cit = candidates.erase(cit); + } else { + ++cit; + } + } +} + //! Sort an array by the specified comparator, then erase the last K elements. template static void EraseLastKElements(std::vector &elements, Comparator comparator, @@ -992,6 +1029,9 @@ // An attacker cannot manipulate this metric without performing useful work. EraseLastKElements(vEvictionCandidates, CompareNodeBlockTime, 4); + // Protect the node with the highest availability score for each avalanche + // peer + ProtectAvalancheNodes(vEvictionCandidates); // Protect up to 128 nodes that have the highest avalanche availability // score. EraseLastKElementsIf(vEvictionCandidates, CompareNodeAvailabilityScore, 128, @@ -1095,6 +1135,18 @@ peer_filter_not_null = node->m_tx_relay->pfilter != nullptr; } + PeerId peerid = NO_PEER; + if (node->m_avalanche_state) { + g_avalanche->withPeerManager( + [&](const avalanche::PeerManager &pm) { + pm.forNode(node->GetId(), + [&](const avalanche::Node &n) { + peerid = n.peerid; + return true; + }); + }); + } + NodeEvictionCandidate candidate = { node->GetId(), node->nTimeConnected, @@ -1110,7 +1162,9 @@ node->addr.IsLocal(), node->m_avalanche_state ? node->m_avalanche_state->getAvailabilityScore() - : -std::numeric_limits::infinity()}; + : -std::numeric_limits::infinity(), + peerid, + }; vEvictionCandidates.push_back(candidate); } } diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -794,6 +794,7 @@ /* prefer_evict */ random_context.randbool(), /* m_is_local */ random_context.randbool(), /* availabilityScore */ double(random_context.randrange(-1)), + /* peerid */ PeerId(random_context.randrange(4)), }); } return candidates; @@ -928,25 +929,66 @@ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, random_context)); - // 128 peers with the highest availability score should be protected - // from eviction. - std::vector protectedNodes(128); - std::iota(protectedNodes.begin(), protectedNodes.end(), 0); - BOOST_CHECK(!IsEvicted( - number_of_nodes, - [number_of_nodes](NodeEvictionCandidate &candidate) { - candidate.availabilityScore = - double(number_of_nodes - candidate.id); - }, - protectedNodes, random_context)); + { + // The best node from each avalanche peer should be protected + // from eviction. + std::vector protectedNodes; + for (NodeId id = 0; id < NODE_EVICTION_TEST_UP_TO_N_NODES; + id += 10) { + protectedNodes.emplace_back(id); + } + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(candidate.id / 10); + }, + protectedNodes, random_context)); + } + + { + // 128 peers with the highest availability score should be + // protected from eviction. + std::vector protectedNodes(128); + std::iota(protectedNodes.begin(), protectedNodes.end(), 0); + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(); + }, + protectedNodes, random_context)); + } + + { + // Combination of the previous two tests. We expect 132 + // protected nodes (1 best node from each of the 4 avalanche + // peers + 128 best overall nodes from the remaining ones). + // Nodes 0, 42, 84, 126 and 168 are the best for their peer, + // nodes 1 to 132 are the best from the remaining nodes. + std::vector protectedNodes(131); + std::iota(protectedNodes.begin(), protectedNodes.end(), 0); + protectedNodes.push_back(168); + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(candidate.id / 42); + }, + protectedNodes, random_context)); + } - // An eviction is expected given >= 161 random eviction candidates. + // An eviction is expected given >= 165 random eviction candidates. // The eviction logic protects at most four peers by net group, // eight by lowest ping time, four by last time of novel tx, four by // last time of novel proof, up to eight non-tx-relay peers by last - // novel block time, four by last novel block time, and 128 more by - // avalanche availability score. - if (number_of_nodes >= 161) { + // novel block time, four by last novel block time, 128 more by + // avalanche availability score and the best node for each of 4 + // avalanche peer. + if (number_of_nodes >= 165) { BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates( number_of_nodes, random_context))); } diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py --- a/test/functional/p2p_eviction.py +++ b/test/functional/p2p_eviction.py @@ -53,13 +53,14 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - # The choice of maxconnections=164 results in a maximum of 153 inbound - # connections (164 - 10 outbound - 1 feeler). 152 inbound peers are + # The choice of maxconnections=168 results in a maximum of 157 inbound + # connections (164 - 10 outbound - 1 feeler). 156 inbound peers are # protected from eviction: # 4 by netgroup, 4 that sent us blocks, 4 that sent us proofs, 4 that # sent us transactions, 8 via lowest ping time, 128 with the best - # avalanche availability score - self.extra_args = [['-maxconnections=164', "-enableavalanche=1"]] + # avalanche availability score, 4 with the best availability score from + # each of 4 avalanche peers + self.extra_args = [['-maxconnections=168', "-enableavalanche=1"]] def run_test(self): # peers that we expect to be protected from eviction @@ -148,24 +149,36 @@ current_peer += 1 wait_until(lambda: "ping" in fastpeer.last_message, timeout=10) - self.log.info( - "Create 128 peers and protect them from eviction by sending an avahello message") - - proof = node.buildavalancheproof( - 42, 2000000000, pubkey.get_bytes().hex(), [stakes[0]]) - proof_obj = FromHex(AvalancheProof(), proof) - delegation = node.delegateavalancheproof( - f"{proof_obj.limited_proofid:064x}", - bytes_to_wif(privkey.get_bytes()), - pubkey.get_bytes().hex(), - ) + def build_proof_and_delegation(stakes): + proof = node.buildavalancheproof( + 42, 2000000000, pubkey.get_bytes().hex(), stakes) + proof_obj = FromHex(AvalancheProof(), proof) + delegation = node.delegateavalancheproof( + f"{proof_obj.limited_proofid:064x}", + bytes_to_wif(privkey.get_bytes()), + pubkey.get_bytes().hex(), + ) + return proof, delegation - for _ in range(128): + def add_avalanche_connection(): avapeer = node.add_p2p_connection(SlowAvaP2PInterface()) - current_peer += 1 avapeer.sync_with_ping() avapeer.send_avahello(delegation, privkey) + self.log.info( + "Create 128 peers and protect them from eviction by sending an avahello message") + proof, delegation = build_proof_and_delegation([stakes[0]]) + for _ in range(128): + add_avalanche_connection() + current_peer += 1 + + self.log.info( + "Create 4 more peers, each with a different proof known by our node") + for i in range(1, 5): + proof, delegation = build_proof_and_delegation([stakes[i]]) + add_avalanche_connection() + current_peer += 1 + # Make sure by asking the node what the actual min pings are peerinfo = node.getpeerinfo() pings = {} diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh --- a/test/lint/lint-circular-dependencies.sh +++ b/test/lint/lint-circular-dependencies.sh @@ -33,6 +33,7 @@ "pow/aserti32d -> validation -> pow/aserti32d" "pow/aserti32d -> validation -> pow/pow -> pow/aserti32d" "avalanche/orphanproofpool -> avalanche/peermanager -> avalanche/orphanproofpool" + "avalanche/processor -> net -> avalanche/processor" ) EXIT_CODE=0