1 #!/usr/bin/env python3
2 # Copyright (c) 2019-2021 The Limenka developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 6 """ Test node eviction logic
7 8 When the number of peers has reached the limit of maximum connections,
9 the next connecting inbound peer will trigger the eviction mechanism.
10 We cannot currently test the parts of the eviction logic that are based on
11 address/netgroup since in the current framework, all peers are connecting from
12 the same local address. See Issue #14210 for more info.
13 Therefore, this test is limited to the remaining protection criteria.
14 """
15 import time
16 17 from test_framework.blocktools import (
18 create_block,
19 create_coinbase,
20 )
21 from test_framework.messages import (
22 msg_pong,
23 msg_tx,
24 )
25 from test_framework.p2p import (
26 P2PDataStore,
27 P2PInterface,
28 )
29 from test_framework.test_framework import LimenkaTestFramework
30 from test_framework.util import assert_equal
31 from test_framework.wallet import MiniWallet
32 33 34 class SlowP2PDataStore(P2PDataStore):
35 def on_ping(self, message):
36 time.sleep(0.1)
37 self.send_message(msg_pong(message.nonce))
38 39 40 class SlowP2PInterface(P2PInterface):
41 def on_ping(self, message):
42 time.sleep(0.1)
43 self.send_message(msg_pong(message.nonce))
44 45 46 class P2PEvict(LimenkaTestFramework):
47 def set_test_params(self):
48 self.num_nodes = 1
49 # The choice of maxconnections=32 results in a maximum of 21 inbound connections
50 # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction:
51 # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
52 self.extra_args = [['-maxconnections=32']]
53 54 def run_test(self):
55 protected_peers = set() # peers that we expect to be protected from eviction
56 current_peer = -1
57 node = self.nodes[0]
58 self.wallet = MiniWallet(node)
59 60 self.log.info("Create 4 peers and protect them from eviction by sending us a block")
61 for _ in range(4):
62 block_peer = node.add_p2p_connection(SlowP2PDataStore())
63 current_peer += 1
64 block_peer.sync_with_ping()
65 best_block = node.getbestblockhash()
66 tip = int(best_block, 16)
67 best_block_time = node.getblock(best_block)['time']
68 block = create_block(tip, create_coinbase(node.getblockcount() + 1), best_block_time + 1)
69 block.solve()
70 block_peer.send_blocks_and_test([block], node, success=True)
71 protected_peers.add(current_peer)
72 73 self.log.info("Create 5 slow-pinging peers, making them eviction candidates")
74 for _ in range(5):
75 node.add_p2p_connection(SlowP2PInterface())
76 current_peer += 1
77 78 self.log.info("Create 4 peers and protect them from eviction by sending us a tx")
79 for i in range(4):
80 txpeer = node.add_p2p_connection(SlowP2PInterface())
81 current_peer += 1
82 txpeer.sync_with_ping()
83 84 tx = self.wallet.create_self_transfer()['tx']
85 txpeer.send_message(msg_tx(tx))
86 protected_peers.add(current_peer)
87 88 self.log.info("Create 8 peers and protect them from eviction by having faster pings")
89 for _ in range(8):
90 fastpeer = node.add_p2p_connection(P2PInterface())
91 current_peer += 1
92 self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
93 94 # Make sure by asking the node what the actual min pings are
95 peerinfo = node.getpeerinfo()
96 pings = {}
97 for i in range(len(peerinfo)):
98 pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] else 1000000
99 sorted_pings = sorted(pings.items(), key=lambda x: x[1])
100 101 # Usually the 8 fast peers are protected. In rare case of unreliable pings,
102 # one of the slower peers might have a faster min ping though.
103 for i in range(8):
104 protected_peers.add(sorted_pings[i][0])
105 106 self.log.info("Create peer that triggers the eviction mechanism")
107 node.add_p2p_connection(SlowP2PInterface())
108 109 # One of the non-protected peers must be evicted. We can't be sure which one because
110 # 4 peers are protected via netgroup, which is identical for all peers,
111 # and the eviction mechanism doesn't preserve the order of identical elements.
112 evicted_peers = []
113 for i in range(len(node.p2ps)):
114 if not node.p2ps[i].is_connected:
115 evicted_peers.append(i)
116 117 self.log.info("Test that one peer was evicted")
118 self.log.debug("{} evicted peer: {}".format(len(evicted_peers), set(evicted_peers)))
119 assert_equal(len(evicted_peers), 1)
120 121 self.log.info("Test that no peer expected to be protected was evicted")
122 self.log.debug("{} protected peers: {}".format(len(protected_peers), protected_peers))
123 assert evicted_peers[0] not in protected_peers
124 125 self.log.info("Test that whitebind inbounds get extra eviction power")
126 # Allow 10 inbound connections, set whitebind and forceinbound
127 self.restart_node(0, extra_args=['-maxconnections=21', '-whitebind=127.0.0.1:30201', '-whitebind=forceinbound@127.0.0.1:30202'])
128 self.log.debug("Fill connections with unprivileged peers")
129 for i in range(10):
130 node.add_p2p_connection(P2PInterface())
131 132 # Create a peer that expects to be rejected
133 # FIXME: "multiprocess, i686, DEBUG" CI task has a reliable timeout issue for v2transport
134 class RejectedPeer(P2PInterface):
135 def connection_lost(self, exc):
136 return
137 138 allowed_peers = []
139 140 self.log.debug("Generic inbound gets rejected when full")
141 with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
142 node.add_p2p_connection(RejectedPeer(), wait_for_verack=False, supports_v2_p2p=False)
143 144 self.log.debug("Default whitebind inbound gets rejected, even when full")
145 with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
146 node.add_p2p_connection(RejectedPeer(), wait_for_verack=False, supports_v2_p2p=False, dstport=30201)
147 148 self.log.debug("ForceInbound whitebind inbound gets connected, even when full")
149 allowed_peers.append(node.add_p2p_connection(P2PInterface(), dstport=30202))
150 151 peerinfo = node.getpeerinfo()
152 assert_equal(len(peerinfo), 10)
153 for peer in peerinfo:
154 if "30202" in peer["addrbind"]:
155 assert peer["forced_inbound"]
156 else:
157 assert not peer["forced_inbound"]
158 159 self.log.debug("Generic inbound gets rejected when whitebind peer is filling inbound slot")
160 with node.assert_debug_log(["failed to find an eviction candidate - connection dropped (full)"]):
161 node.add_p2p_connection(RejectedPeer(), supports_v2_p2p=False, wait_for_verack=False)
162 163 if __name__ == '__main__':
164 P2PEvict(__file__).main()
165