Skip to content

Commit 45eff75

Browse files
committed
Add functional test for P2P eviction logic of inbound peers
1 parent 4af01b3 commit 45eff75

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

test/functional/p2p_eviction.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
'rpc_preciousblock.py',
189189
'wallet_importprunedfunds.py',
190190
'p2p_leak_tx.py',
191+
'p2p_eviction.py',
191192
'rpc_signmessage.py',
192193
'rpc_generateblock.py',
193194
'wallet_balance.py',

0 commit comments

Comments
 (0)