diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -1318,6 +1318,8 @@ uint64_t nKeyedNetGroup; bool prefer_evict; bool m_is_local; + bool fAvalanche; + 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,20 @@ return a.nTimeConnected > b.nTimeConnected; } +static bool CompareNodeAvailabilityScore(const NodeEvictionCandidate &a, + const NodeEvictionCandidate &b) { + if (a.fAvalanche != b.fAvalanche) { + return b.fAvalanche; + } + + // Equality can happen if the score 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, @@ -937,6 +951,19 @@ elements.erase(elements.end() - eraseSize, elements.end()); } +//! Sort an array by the specified comparator, then erase up to K last elements +//! which verify the condition. +template +static void +EraseLastKElementsIf(std::vector &elements, Comparator comparator, size_t k, + std::function cond) { + 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(), cond), + elements.end()); +} + [[nodiscard]] std::optional SelectNodeToEvict(std::vector &&vEvictionCandidates) { // Protect connections with certain characteristics @@ -959,20 +986,18 @@ // enabled for pre-consensus. EraseLastKElements(vEvictionCandidates, CompareNodeProofTime, 4); // Protect up to 8 non-tx-relay peers that have sent us novel blocks. - std::sort(vEvictionCandidates.begin(), vEvictionCandidates.end(), - CompareNodeBlockRelayOnlyTime); - size_t erase_size = std::min(size_t(8), vEvictionCandidates.size()); - vEvictionCandidates.erase( - std::remove_if(vEvictionCandidates.end() - erase_size, - vEvictionCandidates.end(), - [](NodeEvictionCandidate const &n) { - return !n.fRelayTxes && n.fRelevantServices; - }), - vEvictionCandidates.end()); - + EraseLastKElementsIf(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8, + [](NodeEvictionCandidate const &n) { + return !n.fRelayTxes && 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 up to 16 nodes that have the highest avalanche availability + // score. + EraseLastKElementsIf( + vEvictionCandidates, CompareNodeAvailabilityScore, 16, + [](NodeEvictionCandidate const &n) { return n.fAvalanche; }); // Protect the half of the remaining nodes which have been connected the // longest. This replicates the non-eviction implicit behavior, and @@ -984,15 +1009,10 @@ size_t total_protect_size = initial_size / 2; // Pick out up to 1/4 peers that are localhost, sorted by longest uptime. - std::sort(vEvictionCandidates.begin(), vEvictionCandidates.end(), - CompareLocalHostTimeConnected); - size_t local_erase_size = total_protect_size / 2; - vEvictionCandidates.erase( - std::remove_if( - vEvictionCandidates.end() - local_erase_size, - vEvictionCandidates.end(), - [](NodeEvictionCandidate const &n) { return n.m_is_local; }), - vEvictionCandidates.end()); + EraseLastKElementsIf( + vEvictionCandidates, CompareLocalHostTimeConnected, + total_protect_size / 2, + [](NodeEvictionCandidate const &n) { return n.m_is_local; }); // Calculate how many we removed, and update our total number of peers that // we want to protect based on uptime accordingly. total_protect_size -= initial_size - vEvictionCandidates.size(); @@ -1074,6 +1094,14 @@ peer_relay_txes = node->m_tx_relay->fRelayTxes; peer_filter_not_null = node->m_tx_relay->pfilter != nullptr; } + + double availabilityScore = std::numeric_limits::lowest(); + bool fAvalanche = node->m_avalanche_state != nullptr; + if (fAvalanche) { + availabilityScore = + node->m_avalanche_state->getAvailabilityScore(); + } + NodeEvictionCandidate candidate = { node->GetId(), node->nTimeConnected, @@ -1086,7 +1114,9 @@ peer_filter_not_null, node->nKeyedNetGroup, node->m_prefer_evict, - node->addr.IsLocal()}; + node->addr.IsLocal(), + fAvalanche, + availabilityScore}; 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 @@ -792,6 +792,8 @@ /* nKeyedNetGroup */ random_context.randrange(100), /* prefer_evict */ random_context.randbool(), /* m_is_local */ random_context.randbool(), + /* fAvalanche */ 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. + // 16 peers with the highest availability score should be protected + // from eviction. + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.fAvalanche = true; + candidate.availabilityScore = + double(number_of_nodes - candidate.id); + }, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + 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 // 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 16 more by + // avalanche availability score. + if (number_of_nodes >= 49) { 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=52 results in a maximum of 41 inbound + # connections (52 - 10 outbound - 1 feeler). 40 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, 16 with the best + # avalanche availability score + self.extra_args = [['-maxconnections=52', "-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 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(), + ) + + for _ in range(16): + 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 = {}