/ test / functional / feature_assumevalid.py
feature_assumevalid.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2014-present 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  """Test logic for skipping signature validation on old blocks.
  6  
  7  Test logic for skipping signature validation on blocks which we've assumed
  8  valid (https://github.com/bitcoin/bitcoin/pull/9484)
  9  
 10  We build a chain that includes an invalid signature for one of the transactions:
 11  
 12      0:        genesis block
 13      1:        block 1 with coinbase transaction output.
 14      2-101:    bury that block with 100 blocks so the coinbase transaction
 15                output can be spent
 16      102:      a block containing a transaction spending the coinbase
 17                transaction output. The transaction has an invalid signature.
 18      103-2202: bury the bad block with just over two weeks' worth of blocks
 19                (2100 blocks)
 20  
 21  Start a few nodes:
 22  
 23      - node0 has no -assumevalid parameter. Try to sync to block 2202. It will
 24        reject block 102 and only sync as far as block 101
 25      - node1 has -assumevalid set to the hash of block 102. Try to sync to
 26        block 2202. node1 will sync all the way to block 2202.
 27      - node2 has -assumevalid set to the hash of block 102. Try to sync to
 28        block 200. node2 will reject block 102 since it's assumed valid, but it
 29        isn't buried by at least two weeks' work.
 30      - node3 has -assumevalid set to the hash of block 102. Feed a longer
 31        competing headers-only branch so block #1 is not on the best header chain.
 32      - node4 has -assumevalid set to the hash of block 102. Submit an alternative
 33        block #1 that is not part of the assumevalid chain.
 34      - node5 starts with no -assumevalid parameter. Reindex to hit
 35        "assumevalid hash not in headers" and "below minimum chainwork".
 36  """
 37  
 38  from test_framework.blocktools import (
 39      COINBASE_MATURITY,
 40      create_block,
 41      create_coinbase,
 42  )
 43  from test_framework.messages import (
 44      CBlockHeader,
 45      COutPoint,
 46      CTransaction,
 47      CTxIn,
 48      CTxOut,
 49      msg_block,
 50      msg_headers,
 51  )
 52  from test_framework.p2p import P2PInterface
 53  from test_framework.script import (
 54      CScript,
 55      OP_TRUE,
 56  )
 57  from test_framework.test_framework import BitcoinTestFramework
 58  from test_framework.util import assert_equal
 59  from test_framework.wallet_util import generate_keypair
 60  
 61  
 62  class BaseNode(P2PInterface):
 63      def send_header_for_blocks(self, new_blocks):
 64          headers_message = msg_headers()
 65          headers_message.headers = [CBlockHeader(b) for b in new_blocks]
 66          self.send_without_ping(headers_message)
 67  
 68  
 69  class AssumeValidTest(BitcoinTestFramework):
 70      def set_test_params(self):
 71          self.setup_clean_chain = True
 72          self.num_nodes = 6
 73          self.rpc_timeout = 120
 74  
 75      def setup_network(self):
 76          self.add_nodes(self.num_nodes)
 77          # Start node0. We don't start the other nodes yet since
 78          # we need to pre-mine a block with an invalid transaction
 79          # signature so we can pass in the block hash as assumevalid.
 80          self.start_node(0)
 81  
 82      def run_test(self):
 83          # Build the blockchain
 84          self.tip = int(self.nodes[0].getbestblockhash(), 16)
 85          self.block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time'] + 1
 86  
 87          self.blocks = []
 88  
 89          # Get a pubkey for the coinbase TXO
 90          _, coinbase_pubkey = generate_keypair()
 91  
 92          # Create the first block with a coinbase output to our key
 93          height = 1
 94          block = create_block(self.tip, create_coinbase(height, coinbase_pubkey), self.block_time)
 95          self.blocks.append(block)
 96          self.block_time += 1
 97          block.solve()
 98          # Save the coinbase for later
 99          self.block1 = block
100          self.tip = block.hash_int
101          height += 1
102  
103          # Bury the block 100 deep so the coinbase output is spendable
104          for _ in range(100):
105              block = create_block(self.tip, create_coinbase(height), self.block_time)
106              block.solve()
107              self.blocks.append(block)
108              self.tip = block.hash_int
109              self.block_time += 1
110              height += 1
111  
112          # Create a transaction spending the coinbase output with an invalid (null) signature
113          tx = CTransaction()
114          tx.vin.append(CTxIn(COutPoint(self.block1.vtx[0].txid_int, 0), scriptSig=b""))
115          tx.vout.append(CTxOut(49 * 100000000, CScript([OP_TRUE])))
116  
117          block102 = create_block(self.tip, create_coinbase(height), self.block_time, txlist=[tx])
118          self.block_time += 1
119          block102.solve()
120          self.blocks.append(block102)
121          self.tip = block102.hash_int
122          self.block_time += 1
123          height += 1
124  
125          # Bury the assumed valid block 2100 deep
126          for _ in range(2100):
127              block = create_block(self.tip, create_coinbase(height), self.block_time)
128              block.solve()
129              self.blocks.append(block)
130              self.tip = block.hash_int
131              self.block_time += 1
132              height += 1
133          block_1_hash = self.blocks[0].hash_hex
134  
135          self.start_node(1, extra_args=[f"-assumevalid={block102.hash_hex}"])
136          self.start_node(2, extra_args=[f"-assumevalid={block102.hash_hex}"])
137          self.start_node(3, extra_args=[f"-assumevalid={block102.hash_hex}"])
138          self.start_node(4, extra_args=[f"-assumevalid={block102.hash_hex}"])
139          self.start_node(5)
140  
141          # nodes[0]
142          self.log.info("Send blocks to node0. Block 102 will be rejected.")
143          p2p0 = self.nodes[0].add_p2p_connection(BaseNode())
144          p2p0.send_header_for_blocks(self.blocks[0:2000])
145          p2p0.send_header_for_blocks(self.blocks[2000:])
146          with self.nodes[0].assert_debug_log(expected_msgs=[
147              f"Enabling script verification at block #1 ({block_1_hash}): assumevalid=0 (always verify).",
148          ]):
149              p2p0.send_and_ping(msg_block(self.blocks[0]))
150          with self.nodes[0].assert_debug_log(expected_msgs=[
151              "Block validation error: block-script-verify-flag-failed",
152          ]):
153              for i in range(1, 103):
154                  p2p0.send_without_ping(msg_block(self.blocks[i]))
155              p2p0.wait_for_disconnect()
156              assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1)
157              assert_equal(next(filter(lambda x: x["hash"] == self.blocks[-1].hash_hex, self.nodes[0].getchaintips()))["status"], "invalid")
158  
159          # nodes[1]
160          self.log.info("Send all blocks to node1. All blocks will be accepted.")
161          p2p1 = self.nodes[1].add_p2p_connection(BaseNode())
162          p2p1.send_header_for_blocks(self.blocks[0:2000])
163          p2p1.send_header_for_blocks(self.blocks[2000:])
164          with self.nodes[1].assert_debug_log(expected_msgs=[
165              f"Disabling script verification at block #1 ({self.blocks[0].hash_hex}).",
166          ]):
167              p2p1.send_and_ping(msg_block(self.blocks[0]))
168          with self.nodes[1].assert_debug_log(expected_msgs=[
169              f"Enabling script verification at block #103 ({self.blocks[102].hash_hex}): block height above assumevalid height.",
170          ]):
171              for i in range(1, 2202):
172                  p2p1.send_without_ping(msg_block(self.blocks[i]))
173              # Syncing 2200 blocks can take a while on slow systems. Give it plenty of time to sync.
174              p2p1.sync_with_ping(timeout=960)
175              assert_equal(self.nodes[1].getblockcount(), 2202)
176  
177          # nodes[2]
178          self.log.info("Send blocks to node2. Block 102 will be rejected.")
179          p2p2 = self.nodes[2].add_p2p_connection(BaseNode())
180          p2p2.send_header_for_blocks(self.blocks[0:200])
181          with self.nodes[2].assert_debug_log(expected_msgs=[
182              f"Enabling script verification at block #1 ({block_1_hash}): block too recent relative to best header.",
183          ]):
184              p2p2.send_and_ping(msg_block(self.blocks[0]))
185          with self.nodes[2].assert_debug_log(expected_msgs=[
186              "Block validation error: block-script-verify-flag-failed",
187          ]):
188              for i in range(1, 103):
189                  p2p2.send_without_ping(msg_block(self.blocks[i]))
190              p2p2.wait_for_disconnect()
191              assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1)
192              assert_equal(next(filter(lambda x: x["hash"] == self.blocks[199].hash_hex, self.nodes[2].getchaintips()))["status"], "invalid")
193  
194          # nodes[3]
195          self.log.info("Send two header chains, and a block not in the best header chain to node3.")
196          best_hash = self.nodes[3].getbestblockhash()
197          tip_block = self.nodes[3].getblock(best_hash)
198          second_chain_tip, second_chain_time, second_chain_height = int(best_hash, 16), tip_block["time"] + 1, tip_block["height"] + 1
199          second_chain = []
200          for _ in range(150):
201              block = create_block(second_chain_tip, create_coinbase(second_chain_height), second_chain_time)
202              block.solve()
203              second_chain.append(block)
204              second_chain_tip, second_chain_time, second_chain_height = block.hash_int, second_chain_time + 1, second_chain_height + 1
205          p2p3 = self.nodes[3].add_p2p_connection(BaseNode())
206          p2p3.send_header_for_blocks(second_chain)
207          p2p3.send_header_for_blocks(self.blocks[0:103])
208          with self.nodes[3].assert_debug_log(expected_msgs=[
209              f"Enabling script verification at block #1 ({block_1_hash}): block not in best header chain.",
210          ]):
211              p2p3.send_and_ping(msg_block(self.blocks[0]))
212              assert_equal(self.nodes[3].getblockcount(), 1)
213  
214          # nodes[4]
215          self.log.info("Send a block not in the assumevalid header chain to node4.")
216          genesis_hash = self.nodes[4].getbestblockhash()
217          genesis_time = self.nodes[4].getblock(genesis_hash)['time']
218          alt1 = create_block(int(genesis_hash, 16), create_coinbase(1), genesis_time + 2)
219          alt1.solve()
220          p2p4 = self.nodes[4].add_p2p_connection(BaseNode())
221          p2p4.send_header_for_blocks(self.blocks[0:103])
222          with self.nodes[4].assert_debug_log(expected_msgs=[
223              f"Enabling script verification at block #1 ({alt1.hash_hex}): block not in assumevalid chain.",
224          ]):
225              p2p4.send_and_ping(msg_block(alt1))
226              assert_equal(self.nodes[4].getblockcount(), 1)
227  
228          # nodes[5]
229          self.log.info("Reindex to hit specific assumevalid gates (no races with header downloads/chainwork during startup).")
230          p2p5 = self.nodes[5].add_p2p_connection(BaseNode())
231          p2p5.send_header_for_blocks(self.blocks[0:200])
232          p2p5.send_without_ping(msg_block(self.blocks[0]))
233          self.wait_until(lambda: self.nodes[5].getblockcount() == 1)
234          with self.nodes[5].assert_debug_log(expected_msgs=[
235              f"Enabling script verification at block #1 ({block_1_hash}): assumevalid hash not in headers.",
236          ]):
237              self.restart_node(5, extra_args=["-reindex-chainstate", "-assumevalid=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"])
238              assert_equal(self.nodes[5].getblockcount(), 1)
239          with self.nodes[5].assert_debug_log(expected_msgs=[
240              f"Enabling script verification at block #1 ({block_1_hash}): best header chainwork below minimumchainwork.",
241          ]):
242              self.restart_node(5, extra_args=["-reindex-chainstate", f"-assumevalid={block102.hash_hex}", "-minimumchainwork=0xffff"])
243              assert_equal(self.nodes[5].getblockcount(), 1)
244  
245  
246  if __name__ == '__main__':
247      AssumeValidTest(__file__).main()