diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -1261,6 +1261,7 @@ int64_t nTimeConnected; int64_t nMinPingUsecTime; int64_t nLastBlockTime; + int64_t nLastProofTime; int64_t nLastTXTime; bool fRelevantServices; bool fRelayTxes; diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -895,6 +895,18 @@ return a.nTimeConnected > b.nTimeConnected; } +static bool CompareNodeProofTime(const NodeEvictionCandidate &a, + const NodeEvictionCandidate &b) { + // There is a fall-through here because it is common for a node to have more + // than a few peers that have not yet relayed proofs. This fallback is also + // used in the case avalanche is not enabled. + if (a.nLastProofTime != b.nLastProofTime) { + return a.nLastProofTime < b.nLastProofTime; + } + + return a.nTimeConnected > b.nTimeConnected; +} + // Pick out the potential block-relay only peers, and sort them by last block // time. static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, @@ -938,6 +950,12 @@ // into our mempool. An attacker cannot manipulate this metric without // performing useful work. EraseLastKElements(vEvictionCandidates, CompareNodeTXTime, 4); + // Protect 4 nodes that most recently sent us novel proofs accepted + // into our proof pool. An attacker cannot manipulate this metric without + // performing useful work. + // TODO this filter must happen before the last tx time once avalanche is + // 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); @@ -1059,6 +1077,7 @@ node->nTimeConnected, node->nMinPingUsecTime, node->nLastBlockTime, + node->nLastProofTime, node->nLastTXTime, HasAllDesirableServiceFlags(node->nServices), peer_relay_txes, 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 @@ -781,6 +781,8 @@ static_cast(random_context.randrange(100)), /* nLastBlockTime */ static_cast(random_context.randrange(100)), + /* nLastProofTime */ + static_cast(random_context.randrange(100)), /* nLastTXTime */ static_cast(random_context.randrange(100)), /* fRelevantServices */ random_context.randbool(), @@ -862,6 +864,15 @@ }, {0, 1, 2, 3}, random_context)); + // Four nodes that most recently sent us novel proofs accepted + // into our proof pool should be protected from eviction. + BOOST_CHECK(!IsEvicted( + number_of_nodes, + [number_of_nodes](NodeEvictionCandidate &candidate) { + candidate.nLastProofTime = number_of_nodes - candidate.id; + }, + {0, 1, 2, 3}, random_context)); + // Up to eight non-tx-relay peers that most recently sent us novel // blocks should be protected from eviction. BOOST_CHECK(!IsEvicted( @@ -912,21 +923,21 @@ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, random_context)); - // An eviction is expected given >= 29 random eviction candidates. + // An eviction is expected given >= 33 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, 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 >= 29) { + // 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) { BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates( number_of_nodes, random_context))); } - // No eviction is expected given <= 20 random eviction candidates. + // No eviction is expected given <= 24 random eviction candidates. // The eviction logic protects at least four peers by net group, - // eight by lowest ping time, four by last time of novel tx and four - // peers by last novel block time. - if (number_of_nodes <= 20) { + // eight by lowest ping time, four by last time of novel tx, four by + // last time of novel proof, four peers by last novel block time. + if (number_of_nodes <= 24) { 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,8 +15,17 @@ import time +from test_framework.avatools import create_coinbase_stakes from test_framework.blocktools import create_block, create_coinbase -from test_framework.messages import CTransaction, FromHex, msg_pong, msg_tx +from test_framework.key import ECKey +from test_framework.messages import ( + AvalancheProof, + CTransaction, + FromHex, + msg_avaproof, + msg_pong, + msg_tx, +) from test_framework.p2p import P2PDataStore, P2PInterface from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, wait_until @@ -38,17 +47,19 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - # The choice of maxconnections=32 results in a maximum of 21 inbound connections - # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction: - # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and - # 8 via lowest ping time - self.extra_args = [['-maxconnections=32']] + # 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: + # 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"]] def run_test(self): - protected_peers = set() # peers that we expect to be protected from eviction + # peers that we expect to be protected from eviction + protected_peers = set() current_peer = -1 node = self.nodes[0] - node.generatetoaddress(101, node.get_deterministic_priv_key().address) + blocks = node.generatetoaddress( + 101, node.get_deterministic_priv_key().address) self.log.info( "Create 4 peers and protect them from eviction by sending us a block") @@ -68,6 +79,28 @@ block_peer.send_blocks_and_test([block], node, success=True) protected_peers.add(current_peer) + self.log.info( + "Create 4 peers and protect them from eviction by sending us a proof") + privkey = ECKey() + privkey.generate() + pubkey = privkey.get_pubkey() + + stakes = create_coinbase_stakes( + node, blocks, node.get_deterministic_priv_key().key) + + for i in range(4): + proof_peer = node.add_p2p_connection(SlowP2PDataStore()) + current_peer += 1 + proof_peer.sync_with_ping() + + proof = node.buildavalancheproof( + 42, 2000000000, pubkey.get_bytes().hex(), [stakes[i]]) + + avaproof_msg = msg_avaproof() + avaproof_msg.proof = FromHex(AvalancheProof(), proof) + proof_peer.send_message(avaproof_msg) + protected_peers.add(current_peer) + self.log.info( "Create 5 slow-pinging peers, making them eviction candidates") for _ in range(5):