p2p_node_network_limited.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2017-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 """Tests NODE_NETWORK_LIMITED.
6
7 Tests that a node configured with -prune=550 signals NODE_NETWORK_LIMITED correctly
8 and that it responds to getdata requests for blocks correctly:
9 - send a block within 288 + 2 of the tip
10 - disconnect peers who request blocks older than that."""
11 from test_framework.messages import (
12 CInv,
13 MSG_BLOCK,
14 NODE_REDUCED_DATA,
15 NODE_NETWORK_LIMITED,
16 NODE_P2P_V2,
17 NODE_WITNESS,
18 msg_getdata,
19 )
20 from test_framework.p2p import P2PInterface
21 from test_framework.test_framework import LimenkaTestFramework
22 from test_framework.util import (
23 assert_equal,
24 assert_raises_rpc_error,
25 try_rpc
26 )
27
28 # Minimum blocks required to signal NODE_NETWORK_LIMITED #
29 NODE_NETWORK_LIMITED_MIN_BLOCKS = 288
30
31 class P2PIgnoreInv(P2PInterface):
32 firstAddrnServices = 0
33 def on_inv(self, message):
34 # The node will send us invs for other blocks. Ignore them.
35 pass
36 def on_addr(self, message):
37 self.firstAddrnServices = message.addrs[0].nServices
38 def wait_for_addr(self, timeout=5):
39 test_function = lambda: self.last_message.get("addr")
40 self.wait_until(test_function, timeout=timeout)
41 def send_getdata_for_block(self, blockhash):
42 getdata_request = msg_getdata()
43 getdata_request.inv.append(CInv(MSG_BLOCK, int(blockhash, 16)))
44 self.send_message(getdata_request)
45
46 class NodeNetworkLimitedTest(LimenkaTestFramework):
47 def set_test_params(self):
48 self.setup_clean_chain = True
49 self.num_nodes = 3
50 self.extra_args = [
51 ['-prune=550', '-peerbloomfilters=0'],
52 [],
53 [],
54 ]
55
56 def disconnect_all(self):
57 self.disconnect_nodes(0, 1)
58 self.disconnect_nodes(0, 2)
59 self.disconnect_nodes(1, 2)
60
61 def setup_network(self):
62 self.add_nodes(self.num_nodes, self.extra_args)
63 self.start_nodes()
64
65 def test_avoid_requesting_historical_blocks(self):
66 self.log.info("Test full node does not request blocks beyond the limited peer threshold")
67 pruned_node = self.nodes[0]
68 miner = self.nodes[1]
69 full_node = self.nodes[2]
70
71 # Connect and generate block to ensure IBD=false
72 self.connect_nodes(1, 0)
73 self.connect_nodes(1, 2)
74 self.generate(miner, 1)
75
76 # Verify peers are out of IBD
77 for node in self.nodes:
78 assert not node.getblockchaininfo()['initialblockdownload']
79
80 # Isolate full_node (the node will remain out of IBD)
81 full_node.setnetworkactive(False)
82 self.wait_until(lambda: len(full_node.getpeerinfo()) == 0)
83
84 # Mine blocks and sync the pruned node. Surpass the NETWORK_NODE_LIMITED threshold.
85 # Blocks deeper than the threshold are considered "historical blocks"
86 num_historial_blocks = 12
87 self.generate(miner, NODE_NETWORK_LIMITED_MIN_BLOCKS + num_historial_blocks, sync_fun=self.no_op)
88 self.sync_blocks([miner, pruned_node])
89
90 # Connect full_node to prune_node and check peers don't disconnect right away.
91 # (they will disconnect if full_node, which is chain-wise behind, request blocks
92 # older than NODE_NETWORK_LIMITED_MIN_BLOCKS)
93 start_height_full_node = full_node.getblockcount()
94 full_node.setnetworkactive(True)
95 self.connect_nodes(2, 0)
96 assert_equal(len(full_node.getpeerinfo()), 1)
97
98 # Wait until the full_node is headers-wise sync
99 best_block_hash = pruned_node.getbestblockhash()
100 default_value = {'status': ''} # No status
101 self.wait_until(lambda: next(filter(lambda x: x['hash'] == best_block_hash, full_node.getchaintips()), default_value)['status'] == "headers-only")
102
103 # Now, since the node aims to download a window of 1024 blocks,
104 # ensure it requests the blocks below the threshold only (with a
105 # 2-block buffer). And also, ensure it does not request any
106 # historical block.
107 tip_height = pruned_node.getblockcount()
108 limit_buffer = 2
109 # Prevent races by waiting for the tip to arrive first
110 self.wait_until(lambda: not try_rpc(-1, "Block not available (not fully downloaded)", full_node.getblock, pruned_node.getbestblockhash()))
111 for height in range(start_height_full_node + 1, tip_height + 1):
112 if height <= tip_height - (NODE_NETWORK_LIMITED_MIN_BLOCKS - limit_buffer):
113 assert_raises_rpc_error(-1, "Block not available (not fully downloaded)", full_node.getblock, pruned_node.getblockhash(height))
114 else:
115 full_node.getblock(pruned_node.getblockhash(height)) # just assert it does not throw an exception
116
117 # Lastly, ensure the full_node is not sync and verify it can get synced by
118 # establishing a connection with another full node capable of providing them.
119 assert_equal(full_node.getblockcount(), start_height_full_node)
120 self.connect_nodes(2, 1)
121 self.sync_blocks([miner, full_node])
122
123 def run_test(self):
124 node = self.nodes[0].add_p2p_connection(P2PIgnoreInv())
125
126 expected_services = NODE_WITNESS | NODE_NETWORK_LIMITED | NODE_REDUCED_DATA
127 if self.options.v2transport:
128 expected_services |= NODE_P2P_V2
129
130 self.log.info("Check that node has signalled expected services.")
131 assert_equal(node.nServices, expected_services)
132
133 self.log.info("Check that the localservices is as expected.")
134 assert_equal(int(self.nodes[0].getnetworkinfo()['localservices'], 16), expected_services)
135
136 self.log.info("Mine enough blocks to reach the NODE_NETWORK_LIMITED range.")
137 self.connect_nodes(0, 1)
138 blocks = self.generate(self.nodes[1], 292, sync_fun=lambda: self.sync_blocks([self.nodes[0], self.nodes[1]]))
139
140 self.log.info("Make sure we can max retrieve block at tip-288.")
141 node.send_getdata_for_block(blocks[1]) # last block in valid range
142 node.wait_for_block(int(blocks[1], 16), timeout=3)
143
144 self.log.info("Requesting block at height 2 (tip-289) must fail (ignored).")
145 node.send_getdata_for_block(blocks[0]) # first block outside of the 288+2 limit
146 node.wait_for_disconnect(timeout=5)
147 self.nodes[0].disconnect_p2ps()
148
149 # connect unsynced node 2 with pruned NODE_NETWORK_LIMITED peer
150 # because node 2 is in IBD and node 0 is a NODE_NETWORK_LIMITED peer, sync must not be possible
151 self.connect_nodes(0, 2)
152 try:
153 self.sync_blocks([self.nodes[0], self.nodes[2]], timeout=5)
154 except Exception:
155 pass
156 # node2 must remain at height 0
157 assert_equal(self.nodes[2].getblockheader(self.nodes[2].getbestblockhash())['height'], 0)
158
159 # now connect also to node 1 (non pruned)
160 self.connect_nodes(1, 2)
161
162 # sync must be possible
163 self.sync_blocks()
164
165 # disconnect all peers
166 self.disconnect_all()
167
168 # mine 10 blocks on node 0 (pruned node)
169 self.generate(self.nodes[0], 10, sync_fun=self.no_op)
170
171 # connect node1 (non pruned) with node0 (pruned) and check if the can sync
172 self.connect_nodes(0, 1)
173
174 # sync must be possible, node 1 is no longer in IBD and should therefore connect to node 0 (NODE_NETWORK_LIMITED)
175 self.sync_blocks([self.nodes[0], self.nodes[1]])
176
177 self.test_avoid_requesting_historical_blocks()
178
179 if __name__ == '__main__':
180 NodeNetworkLimitedTest(__file__).main()
181