1 #!/usr/bin/env python3
2 # Copyright (c) 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 that the correct active block is chosen in complex reorgs."""
6 7 from test_framework.blocktools import create_block
8 from test_framework.messages import CBlockHeader
9 from test_framework.p2p import P2PDataStore
10 from test_framework.test_framework import LimenkaTestFramework
11 from test_framework.util import assert_equal
12 13 class ChainTiebreaksTest(LimenkaTestFramework):
14 def set_test_params(self):
15 self.num_nodes = 2
16 self.setup_clean_chain = True
17 18 @staticmethod
19 def send_headers(node, blocks):
20 """Submit headers for blocks to node."""
21 for block in blocks:
22 # Use RPC rather than P2P, to prevent the message from being interpreted as a block
23 # announcement.
24 node.submitheader(hexdata=CBlockHeader(block).serialize().hex())
25 26 def test_chain_split_in_memory(self):
27 node = self.nodes[0]
28 # Add P2P connection to limenkad
29 peer = node.add_p2p_connection(P2PDataStore())
30 31 self.log.info('Precomputing blocks')
32 #
33 # /- B3 -- B7
34 # B1 \- B8
35 # / \
36 # / \ B4 -- B9
37 # B0 \- B10
38 # \
39 # \ /- B5
40 # B2
41 # \- B6
42 #
43 blocks = []
44 45 # Construct B0, building off genesis.
46 start_height = node.getblockcount()
47 blocks.append(create_block(
48 hashprev=int(node.getbestblockhash(), 16),
49 tmpl={"height": start_height + 1}
50 ))
51 blocks[-1].solve()
52 53 # Construct B1-B10.
54 for i in range(1, 11):
55 blocks.append(create_block(
56 hashprev=int(blocks[(i - 1) >> 1].hash, 16),
57 tmpl={
58 "height": start_height + (i + 1).bit_length(),
59 # Make sure each block has a different hash.
60 "curtime": blocks[-1].nTime + 1,
61 }
62 ))
63 blocks[-1].solve()
64 65 self.log.info('Make sure B0 is accepted normally')
66 peer.send_blocks_and_test([blocks[0]], node, success=True)
67 # B0 must be active chain now.
68 assert_equal(node.getbestblockhash(), blocks[0].hash)
69 70 self.log.info('Send B1 and B2 headers, and then blocks in opposite order')
71 self.send_headers(node, blocks[1:3])
72 peer.send_blocks_and_test([blocks[2]], node, success=True)
73 peer.send_blocks_and_test([blocks[1]], node, success=False)
74 # B2 must be active chain now, as full data for B2 was received first.
75 assert_equal(node.getbestblockhash(), blocks[2].hash)
76 77 self.log.info('Send all further headers in order')
78 self.send_headers(node, blocks[3:])
79 # B2 is still the active chain, headers don't change this.
80 assert_equal(node.getbestblockhash(), blocks[2].hash)
81 82 self.log.info('Send blocks B7-B10')
83 peer.send_blocks_and_test([blocks[7]], node, success=False)
84 peer.send_blocks_and_test([blocks[8]], node, success=False)
85 peer.send_blocks_and_test([blocks[9]], node, success=False)
86 peer.send_blocks_and_test([blocks[10]], node, success=False)
87 # B2 is still the active chain, as B7-B10 have missing parents.
88 assert_equal(node.getbestblockhash(), blocks[2].hash)
89 90 self.log.info('Send parents B3-B4 of B8-B10 in reverse order')
91 peer.send_blocks_and_test([blocks[4]], node, success=False, force_send=True)
92 peer.send_blocks_and_test([blocks[3]], node, success=False, force_send=True)
93 # B9 is now active. Despite B7 being received earlier, the missing parent.
94 assert_equal(node.getbestblockhash(), blocks[9].hash)
95 96 self.log.info('Invalidate B9-B10')
97 node.invalidateblock(blocks[9].hash)
98 node.invalidateblock(blocks[10].hash)
99 # B7 is now active.
100 assert_equal(node.getbestblockhash(), blocks[7].hash)
101 102 # Invalidate blocks to start fresh on the next test
103 node.invalidateblock(blocks[0].hash)
104 105 def test_chain_split_from_disk(self):
106 node = self.nodes[0]
107 peer = node.add_p2p_connection(P2PDataStore())
108 109 self.log.info('Precomputing blocks')
110 #
111 # A1
112 # /
113 # G
114 # \
115 # A2
116 #
117 blocks = []
118 119 # Construct two blocks building from genesis.
120 start_height = node.getblockcount()
121 genesis_block = node.getblock(node.getblockhash(start_height))
122 prev_time = genesis_block["time"]
123 124 for i in range(0, 2):
125 blocks.append(create_block(
126 hashprev=int(genesis_block["hash"], 16),
127 tmpl={"height": start_height + 1,
128 # Make sure each block has a different hash.
129 "curtime": prev_time + i + 1,
130 }
131 ))
132 blocks[-1].solve()
133 134 # Send blocks and test the last one is not connected
135 self.log.info('Send A1 and A2. Make sure that only the former connects')
136 peer.send_blocks_and_test([blocks[0]], node, success=True)
137 peer.send_blocks_and_test([blocks[1]], node, success=False)
138 139 self.log.info('Restart the node and check that the best tip before restarting matched the ones afterwards')
140 # Restart and check enough times for this to eventually fail if the logic is broken
141 for _ in range(10):
142 self.restart_node(0)
143 assert_equal(blocks[0].hash, node.getbestblockhash())
144 145 def run_test(self):
146 self.test_chain_split_in_memory()
147 self.test_chain_split_from_disk()
148 149 150 if __name__ == '__main__':
151 ChainTiebreaksTest(__file__).main()
152