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