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 """Test various fingerprinting protections.
6 7 If a stale block more than a month old or its header are requested by a peer,
8 the node should pretend that it does not have it to avoid fingerprinting.
9 """
10 11 import time
12 13 from test_framework.blocktools import (create_block, create_coinbase)
14 from test_framework.messages import CInv, MSG_BLOCK
15 from test_framework.p2p import (
16 P2PInterface,
17 msg_headers,
18 msg_block,
19 msg_getdata,
20 msg_getheaders,
21 p2p_lock,
22 )
23 from test_framework.test_framework import LimenkaTestFramework
24 from test_framework.util import (
25 assert_equal,
26 )
27 28 29 class P2PFingerprintTest(LimenkaTestFramework):
30 def set_test_params(self):
31 self.setup_clean_chain = True
32 self.num_nodes = 1
33 34 # Build a chain of blocks on top of given one
35 def build_chain(self, nblocks, prev_hash, prev_height, prev_median_time):
36 blocks = []
37 for _ in range(nblocks):
38 coinbase = create_coinbase(prev_height + 1)
39 block_time = prev_median_time + 1
40 block = create_block(int(prev_hash, 16), coinbase, block_time)
41 block.solve()
42 43 blocks.append(block)
44 prev_hash = block.hash
45 prev_height += 1
46 prev_median_time = block_time
47 return blocks
48 49 # Send a getdata request for a given block hash
50 def send_block_request(self, block_hash, node):
51 msg = msg_getdata()
52 msg.inv.append(CInv(MSG_BLOCK, block_hash))
53 node.send_message(msg)
54 55 # Send a getheaders request for a given single block hash
56 def send_header_request(self, block_hash, node):
57 msg = msg_getheaders()
58 msg.hashstop = block_hash
59 node.send_message(msg)
60 61 # Checks that stale blocks timestamped more than a month ago are not served
62 # by the node while recent stale blocks and old active chain blocks are.
63 # This does not currently test that stale blocks timestamped within the
64 # last month but that have over a month's worth of work are also withheld.
65 def run_test(self):
66 node0 = self.nodes[0].add_p2p_connection(P2PInterface())
67 68 # Set node time to 60 days ago
69 self.nodes[0].setmocktime(int(time.time()) - 60 * 24 * 60 * 60)
70 71 # Generating a chain of 10 blocks
72 block_hashes = self.generatetoaddress(self.nodes[0], 10, self.nodes[0].get_deterministic_priv_key().address)
73 74 # Create longer chain starting 2 blocks before current tip
75 height = len(block_hashes) - 2
76 block_hash = block_hashes[height - 1]
77 block_time = self.nodes[0].getblockheader(block_hash)["mediantime"] + 1
78 new_blocks = self.build_chain(5, block_hash, height, block_time)
79 80 # Force reorg to a longer chain
81 node0.send_message(msg_headers(new_blocks))
82 node0.wait_for_getdata([x.sha256 for x in new_blocks])
83 for block in new_blocks:
84 node0.send_and_ping(msg_block(block))
85 86 # Check that reorg succeeded
87 assert_equal(self.nodes[0].getblockcount(), 13)
88 89 stale_hash = int(block_hashes[-1], 16)
90 91 # Check that getdata request for stale block succeeds
92 self.send_block_request(stale_hash, node0)
93 node0.wait_for_block(stale_hash, timeout=3)
94 95 # Check that getheader request for stale block header succeeds
96 self.send_header_request(stale_hash, node0)
97 node0.wait_for_header(hex(stale_hash), timeout=3)
98 99 # Longest chain is extended so stale is much older than chain tip
100 self.nodes[0].setmocktime(0)
101 block_hash = int(self.generatetoaddress(self.nodes[0], 1, self.nodes[0].get_deterministic_priv_key().address)[-1], 16)
102 assert_equal(self.nodes[0].getblockcount(), 14)
103 node0.wait_for_block(block_hash, timeout=3)
104 105 # Request for very old stale block should now fail
106 with p2p_lock:
107 node0.last_message.pop("block", None)
108 self.send_block_request(stale_hash, node0)
109 node0.sync_with_ping()
110 assert "block" not in node0.last_message
111 112 # Request for very old stale block header should now fail
113 with p2p_lock:
114 node0.last_message.pop("headers", None)
115 self.send_header_request(stale_hash, node0)
116 node0.sync_with_ping()
117 assert "headers" not in node0.last_message
118 119 # Verify we can fetch very old blocks and headers on the active chain
120 block_hash = int(block_hashes[2], 16)
121 self.send_block_request(block_hash, node0)
122 self.send_header_request(block_hash, node0)
123 node0.sync_with_ping()
124 125 self.send_block_request(block_hash, node0)
126 node0.wait_for_block(block_hash, timeout=3)
127 128 self.send_header_request(block_hash, node0)
129 node0.wait_for_header(hex(block_hash), timeout=3)
130 131 132 if __name__ == '__main__':
133 P2PFingerprintTest(__file__).main()
134