diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -1318,6 +1318,7 @@ uint64_t nKeyedNetGroup; bool prefer_evict; bool m_is_local; + double availabilityScore; }; [[nodiscard]] std::optional diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -928,6 +928,17 @@ return a.nTimeConnected > b.nTimeConnected; } +static bool CompareNodeAvailabilityScore(const NodeEvictionCandidate &a, + const NodeEvictionCandidate &b) { + // Equality can happen if the nodes have no score or it has not been + // computed yet. + if (a.availabilityScore != b.availabilityScore) { + return a.availabilityScore < b.availabilityScore; + } + + return a.nTimeConnected > b.nTimeConnected; +} + //! Sort an array by the specified comparator, then erase the last K elements. template static void EraseLastKElements(std::vector &elements, Comparator comparator, @@ -981,6 +992,13 @@ // An attacker cannot manipulate this metric without performing useful work. EraseLastKElements(vEvictionCandidates, CompareNodeBlockTime, 4); + // Protect up to 128 nodes that have the highest avalanche availability + // score. + EraseLastKElementsIf(vEvictionCandidates, CompareNodeAvailabilityScore, 128, + [](NodeEvictionCandidate const &n) { + return n.availabilityScore > 0.; + }); + // 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. @@ -1076,6 +1094,7 @@ peer_relay_txes = node->m_tx_relay->fRelayTxes; peer_filter_not_null = node->m_tx_relay->pfilter != nullptr; } + NodeEvictionCandidate candidate = { node->GetId(), node->nTimeConnected, @@ -1088,7 +1107,10 @@ peer_filter_not_null, node->nKeyedNetGroup, node->m_prefer_evict, - node->addr.IsLocal()}; + node->addr.IsLocal(), + node->m_avalanche_state + ? node->m_avalanche_state->getAvailabilityScore() + : -std::numeric_limits::infinity()}; 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 @@ -25,6 +25,7 @@ #include #include #include +#include #include class CAddrManSerializationMock : public CAddrMan { @@ -792,6 +793,7 @@ /* nKeyedNetGroup */ random_context.randrange(100), /* prefer_evict */ random_context.randbool(), /* m_is_local */ random_context.randbool(), + /* availabilityScore */ double(random_context.randrange(-1)), }); } return candidates; @@ -926,12 +928,25 @@ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, random_context)); - // An eviction is expected given >= 33 random eviction candidates. + // 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)); + + // An eviction is expected given >= 161 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, and four more peers by last novel block time. - if (number_of_nodes >= 33) { + // novel block time, four by last novel block time, and 128 more by + // avalanche availability score. + if (number_of_nodes >= 161) { 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 @@ -15,9 +15,9 @@ import time -from test_framework.avatools import create_coinbase_stakes +from test_framework.avatools import AvaP2PInterface, create_coinbase_stakes from test_framework.blocktools import create_block, create_coinbase -from test_framework.key import ECKey +from test_framework.key import ECKey, bytes_to_wif from test_framework.messages import ( AvalancheProof, CTransaction, @@ -43,15 +43,23 @@ self.send_message(msg_pong(message.nonce)) +class SlowAvaP2PInterface(AvaP2PInterface): + def on_ping(self, message): + time.sleep(0.1) + self.send_message(msg_pong(message.nonce)) + + class P2PEvict(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - # The choice of maxconnections=36 results in a maximum of 25 inbound connections - # (36 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction: + # The choice of maxconnections=164 results in a maximum of 153 inbound + # connections (164 - 10 outbound - 1 feeler). 152 inbound peers are + # protected from eviction: # 4 by netgroup, 4 that sent us blocks, 4 that sent us proofs, 4 that - # sent us transactions and 8 via lowest ping time - self.extra_args = [['-maxconnections=36', "-enableavalanche=1"]] + # sent us transactions, 8 via lowest ping time, 128 with the best + # avalanche availability score + self.extra_args = [['-maxconnections=164', "-enableavalanche=1"]] def run_test(self): # peers that we expect to be protected from eviction @@ -140,6 +148,24 @@ 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(), + ) + + for _ in range(128): + avapeer = node.add_p2p_connection(SlowAvaP2PInterface()) + current_peer += 1 + avapeer.sync_with_ping() + avapeer.send_avahello(delegation, privkey) + # Make sure by asking the node what the actual min pings are peerinfo = node.getpeerinfo() pings = {}