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 @@ -1320,6 +1321,7 @@ bool m_is_local; bool fAvalanche; 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 @@ -942,6 +944,39 @@ return a.nTimeConnected > b.nTimeConnected; } +static void +ProtectAvalancheNodes(std::vector &candidates) { + std::map preferredNodes; + + for (auto &c : candidates) { + if (!c.fAvalanche || 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 + for (auto cit = candidates.begin(); cit != candidates.end();) { + auto nit = preferredNodes.find(cit->peerid); + if (nit != preferredNodes.end() && nit->second.id == cit->id) { + 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, @@ -993,6 +1028,9 @@ // 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 the node with the highest availability score for each avalanche + // peer + ProtectAvalancheNodes(vEvictionCandidates); // Protect up to 16 nodes that have the highest avalanche availability // score. EraseLastKElementsIf( @@ -1096,10 +1134,19 @@ } double availabilityScore = std::numeric_limits::lowest(); + PeerId peerid = NO_PEER; bool fAvalanche = node->m_avalanche_state != nullptr; if (fAvalanche) { availabilityScore = node->m_avalanche_state->getAvailabilityScore(); + g_avalanche->withPeerManager( + [&node, &peerid](const avalanche::PeerManager &pm) { + pm.forNode(node->GetId(), + [&peerid](const avalanche::Node &n) { + peerid = n.peerid; + return true; + }); + }); } NodeEvictionCandidate candidate = { @@ -1116,7 +1163,9 @@ node->m_prefer_evict, node->addr.IsLocal(), fAvalanche, - availabilityScore}; + availabilityScore, + 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 @@ /* m_is_local */ random_context.randbool(), /* fAvalanche */ random_context.randbool(), /* availabilityScore */ double(random_context.randrange(-1)), + /* peerid */ PeerId(random_context.randrange(100)), }); } return candidates; @@ -928,6 +929,23 @@ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, 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.fAvalanche = true; + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(candidate.id / 10); + }, + protectedNodes, random_context)); + // 16 peers with the highest availability score should be protected // from eviction. BOOST_CHECK(!IsEvicted( @@ -936,10 +954,28 @@ candidate.fAvalanche = true; candidate.availabilityScore = double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(); }, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, random_context)); + // Combination of the previous two tests. We expect 20 protected + // nodes (1 best node from each of the 4 avalanche peers + 16 best + // overall nodes from the remaining ones). + // Nodes 0, 42, 84, 126 and 168 are the best for their peer, nodes + // 1 to 16 are the best from the remaining nodes. + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.fAvalanche = true; + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + candidate.peerid = PeerId(candidate.id / 42); + }, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 42, 84, 126, 168}, + random_context)); + // An eviction is expected given >= 49 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 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 @@ -58,8 +58,9 @@ # 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, 16 with the best - # avalanche availability score - self.extra_args = [['-maxconnections=52', "-enableavalanche=1"]] + # avalanche availability score, 4 with the best availability score from + # each of 4 avalanche peers + self.extra_args = [['-maxconnections=56', "-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 16 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(16): + 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 16 peers and protect them from eviction by sending an avahello message") + proof, delegation = build_proof_and_delegation([stakes[0]]) + for _ in range(16): + 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