1#!/usr/bin/env python3
2# Copyright (c) 2019-2020 The Bitcoin Core 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
8When the number of peers has reached the limit of maximum connections,
9the next connecting inbound peer will trigger the eviction mechanism.
10We cannot currently test the parts of the eviction logic that are based on
11address/netgroup since in the current framework, all peers are connecting from
12the same local address. See Issue #14210 for more info.
13Therefore, this test is limited to the remaining protection criteria.
14"""
15
16import time
17
18from test_framework.blocktools import (
19    COINBASE_MATURITY,
20    create_block,
21    create_coinbase,
22)
23from test_framework.messages import (
24    msg_pong,
25    msg_tx,
26    tx_from_hex,
27)
28from test_framework.p2p import P2PDataStore, P2PInterface
29from test_framework.test_framework import BitcoinTestFramework
30from test_framework.util import assert_equal
31
32
33class SlowP2PDataStore(P2PDataStore):
34    def on_ping(self, message):
35        time.sleep(0.1)
36        self.send_message(msg_pong(message.nonce))
37
38class SlowP2PInterface(P2PInterface):
39    def on_ping(self, message):
40        time.sleep(0.1)
41        self.send_message(msg_pong(message.nonce))
42
43class P2PEvict(BitcoinTestFramework):
44    def set_test_params(self):
45        self.setup_clean_chain = True
46        self.num_nodes = 1
47        # The choice of maxconnections=32 results in a maximum of 21 inbound connections
48        # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction:
49        # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
50        self.extra_args = [['-maxconnections=32']]
51
52    def run_test(self):
53        protected_peers = set()  # peers that we expect to be protected from eviction
54        current_peer = -1
55        node = self.nodes[0]
56        node.generatetoaddress(COINBASE_MATURITY + 1, node.get_deterministic_priv_key().address)
57
58        self.log.info("Create 4 peers and protect them from eviction by sending us a block")
59        for _ in range(4):
60            block_peer = node.add_p2p_connection(SlowP2PDataStore())
61            current_peer += 1
62            block_peer.sync_with_ping()
63            best_block = node.getbestblockhash()
64            tip = int(best_block, 16)
65            best_block_time = node.getblock(best_block)['time']
66            block = create_block(tip, create_coinbase(node.getblockcount() + 1), best_block_time + 1)
67            block.solve()
68            block_peer.send_blocks_and_test([block], node, success=True)
69            protected_peers.add(current_peer)
70
71        self.log.info("Create 5 slow-pinging peers, making them eviction candidates")
72        for _ in range(5):
73            node.add_p2p_connection(SlowP2PInterface())
74            current_peer += 1
75
76        self.log.info("Create 4 peers and protect them from eviction by sending us a tx")
77        for i in range(4):
78            txpeer = node.add_p2p_connection(SlowP2PInterface())
79            current_peer += 1
80            txpeer.sync_with_ping()
81
82            prevtx = node.getblock(node.getblockhash(i + 1), 2)['tx'][0]
83            rawtx = node.createrawtransaction(
84                inputs=[{'txid': prevtx['txid'], 'vout': 0}],
85                outputs=[{node.get_deterministic_priv_key().address: 50 - 0.00125}],
86            )
87            sigtx = node.signrawtransactionwithkey(
88                hexstring=rawtx,
89                privkeys=[node.get_deterministic_priv_key().key],
90                prevtxs=[{
91                    'txid': prevtx['txid'],
92                    'vout': 0,
93                    'scriptPubKey': prevtx['vout'][0]['scriptPubKey']['hex'],
94                }],
95            )['hex']
96            txpeer.send_message(msg_tx(tx_from_hex(sigtx)))
97            protected_peers.add(current_peer)
98
99        self.log.info("Create 8 peers and protect them from eviction by having faster pings")
100        for _ in range(8):
101            fastpeer = node.add_p2p_connection(P2PInterface())
102            current_peer += 1
103            self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
104
105        # Make sure by asking the node what the actual min pings are
106        peerinfo = node.getpeerinfo()
107        pings = {}
108        for i in range(len(peerinfo)):
109            pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] else 1000000
110        sorted_pings = sorted(pings.items(), key=lambda x: x[1])
111
112        # Usually the 8 fast peers are protected. In rare case of unreliable pings,
113        # one of the slower peers might have a faster min ping though.
114        for i in range(8):
115            protected_peers.add(sorted_pings[i][0])
116
117        self.log.info("Create peer that triggers the eviction mechanism")
118        node.add_p2p_connection(SlowP2PInterface())
119
120        # One of the non-protected peers must be evicted. We can't be sure which one because
121        # 4 peers are protected via netgroup, which is identical for all peers,
122        # and the eviction mechanism doesn't preserve the order of identical elements.
123        evicted_peers = []
124        for i in range(len(node.p2ps)):
125            if not node.p2ps[i].is_connected:
126                evicted_peers.append(i)
127
128        self.log.info("Test that one peer was evicted")
129        self.log.debug("{} evicted peer: {}".format(len(evicted_peers), set(evicted_peers)))
130        assert_equal(len(evicted_peers), 1)
131
132        self.log.info("Test that no peer expected to be protected was evicted")
133        self.log.debug("{} protected peers: {}".format(len(protected_peers), protected_peers))
134        assert evicted_peers[0] not in protected_peers
135
136if __name__ == '__main__':
137    P2PEvict().main()
138