mining_template_verification.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2024-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 getblocktemplate RPC in proposal mode
   6  
   7  Generate several blocks and test them against the getblocktemplate RPC.
   8  """
   9  
  10  from concurrent.futures import ThreadPoolExecutor
  11  
  12  import copy
  13  
  14  from test_framework.blocktools import (
  15      create_block,
  16      add_witness_commitment,
  17      NORMAL_GBT_REQUEST_PARAMS,
  18  )
  19  
  20  from test_framework.test_framework import BitcoinTestFramework
  21  from test_framework.util import (
  22      assert_equal,
  23      assert_raises_rpc_error,
  24  )
  25  
  26  from test_framework.messages import (
  27      BLOCK_HEADER_SIZE,
  28      uint256_from_compact,
  29  )
  30  
  31  from test_framework.wallet import (
  32      MiniWallet,
  33  )
  34  
  35  def assert_template(node, block, expect, *, rehash=True, submit=True, solve=True, expect_submit=None):
  36      if rehash:
  37          block.hashMerkleRoot = block.calc_merkle_root()
  38  
  39      rsp = node.getblocktemplate(template_request={
  40          'data': block.serialize().hex(),
  41          'mode': 'proposal',
  42          **NORMAL_GBT_REQUEST_PARAMS,
  43      })
  44      assert_equal(rsp, expect)
  45      # Only attempt to submit invalid templates
  46      if submit and expect is not None:
  47          # submitblock runs checks in a different order, so may not return
  48          # the same error
  49          if expect_submit is None:
  50              expect_submit = expect
  51          if solve:
  52              block.solve()
  53          assert_equal(node.submitblock(block.serialize().hex()), expect_submit)
  54  
  55  class MiningTemplateVerificationTest(BitcoinTestFramework):
  56  
  57      def set_test_params(self):
  58          self.num_nodes = 1
  59  
  60      def valid_block_test(self, node, block):
  61          self.log.info("Valid block")
  62          assert_template(node, block, None)
  63  
  64      def cb_missing_test(self, node, block):
  65          self.log.info("Bad input hash for coinbase transaction")
  66          bad_block = copy.deepcopy(block)
  67          bad_block.vtx[0].vin[0].prevout.hash += 1
  68          assert_template(node, bad_block, 'bad-cb-missing')
  69  
  70      def block_without_transactions_test(self, node, block):
  71          self.log.info("Block with no transactions")
  72  
  73          no_tx_block = copy.deepcopy(block)
  74          no_tx_block.vtx.clear()
  75          no_tx_block.hashMerkleRoot = 0
  76          no_tx_block.solve()
  77          assert_template(node, no_tx_block, 'bad-blk-length', rehash=False)
  78  
  79      def truncated_final_transaction_test(self, node, block):
  80          self.log.info("Truncated final transaction")
  81          assert_raises_rpc_error(-22, "Block decode failed", node.getblocktemplate,
  82              template_request={
  83                  "data": block.serialize()[:-1].hex(),
  84                  "mode": "proposal",
  85                  **NORMAL_GBT_REQUEST_PARAMS,
  86              }
  87          )
  88  
  89      def duplicate_transaction_test(self, node, block):
  90          self.log.info("Duplicate transaction")
  91          bad_block = copy.deepcopy(block)
  92          bad_block.vtx.append(bad_block.vtx[0])
  93          assert_template(node, bad_block, 'bad-txns-duplicate')
  94  
  95      def thin_air_spending_test(self, node, block):
  96          self.log.info("Transaction that spends from thin air")
  97          bad_block = copy.deepcopy(block)
  98          bad_tx = copy.deepcopy(bad_block.vtx[0])
  99          bad_tx.vin[0].prevout.hash = 255
 100          bad_block.vtx.append(bad_tx)
 101          assert_template(node, bad_block, 'bad-txns-inputs-missingorspent')
 102  
 103      def non_final_transaction_test(self, node, block):
 104          self.log.info("Non-final transaction")
 105          bad_block = copy.deepcopy(block)
 106          bad_block.vtx[0].nLockTime = 2**32 - 1
 107          assert_template(node, bad_block, 'bad-txns-nonfinal')
 108  
 109      def bad_tx_count_test(self, node, block):
 110          self.log.info("Bad tx count")
 111          # The tx count is immediately after the block header
 112          bad_block_sn = bytearray(block.serialize())
 113          assert_equal(bad_block_sn[BLOCK_HEADER_SIZE], 1)
 114          bad_block_sn[BLOCK_HEADER_SIZE] += 1
 115          assert_raises_rpc_error(-22, "Block decode failed", node.getblocktemplate, {
 116              'data': bad_block_sn.hex(),
 117              'mode': 'proposal',
 118              **NORMAL_GBT_REQUEST_PARAMS,
 119          })
 120  
 121      def nbits_test(self, node, block):
 122          self.log.info("Extremely high nBits")
 123          bad_block = copy.deepcopy(block)
 124          bad_block.nBits = 469762303  # impossible in the real world
 125          assert_template(node, bad_block, "bad-diffbits", solve=False, expect_submit="high-hash")
 126  
 127          self.log.info("Lowering nBits should make the block invalid")
 128          bad_block = copy.deepcopy(block)
 129          bad_block.nBits -= 1
 130          assert_template(node, bad_block, "bad-diffbits")
 131  
 132      def merkle_root_test(self, node, block):
 133          self.log.info("Bad merkle root")
 134          bad_block = copy.deepcopy(block)
 135          bad_block.hashMerkleRoot += 1
 136          assert_template(node, bad_block, 'bad-txnmrklroot', rehash=False)
 137  
 138      def bad_timestamp_test(self, node, block):
 139          self.log.info("Bad timestamps")
 140          bad_block = copy.deepcopy(block)
 141          bad_block.nTime = 2**32 - 1
 142          assert_template(node, bad_block, 'time-too-new')
 143          bad_block.nTime = 0
 144          assert_template(node, bad_block, 'time-too-old')
 145  
 146      def current_tip_test(self, node, block):
 147          self.log.info("Block must build on the current tip")
 148          bad_block = copy.deepcopy(block)
 149          bad_block.hashPrevBlock = 123
 150          bad_block.solve()
 151  
 152          assert_template(node, bad_block, "inconclusive-not-best-prevblk", expect_submit="prev-blk-not-found")
 153  
 154      def pow_test(self, node, block):
 155          '''Modifies block with the generated PoW'''
 156          self.log.info("Generate a block")
 157          target = uint256_from_compact(block.nBits)
 158          # Ensure that it doesn't meet the target by coincidence
 159          while block.hash_int <= target:
 160              block.nNonce += 1
 161          self.log.debug("Found a nonce")
 162  
 163          self.log.info("A block template doesn't need PoW")
 164          assert_template(node, block, None)
 165  
 166          self.log.info("Add proof of work")
 167          block.solve()
 168          assert_template(node, block, None)
 169  
 170      def submit_test(self, node, block_0_height, block):
 171          self.log.info("getblocktemplate call in previous tests did not submit the block")
 172          assert_equal(node.getblockcount(), block_0_height + 1)
 173  
 174          self.log.info("Submitting this block should succeed")
 175          assert_equal(node.submitblock(block.serialize().hex()), None)
 176          node.waitforblockheight(2)
 177  
 178      def transaction_test(self, node, block_0_height, tx):
 179          self.log.info("make block template with a transaction")
 180  
 181          block_1 = node.getblock(node.getblockhash(block_0_height + 1))
 182          block_2_hash = node.getblockhash(block_0_height + 2)
 183  
 184          block_3 = create_block(
 185              int(block_2_hash, 16),
 186              height=block_0_height + 3,
 187              ntime=block_1["mediantime"] + 1,
 188              txlist=[tx["hex"]],
 189          )
 190          assert_equal(len(block_3.vtx), 2)
 191          add_witness_commitment(block_3)
 192          block_3.solve()
 193          assert_template(node, block_3, None)
 194  
 195          self.log.info("checking block validity did not update the UTXO set")
 196          # Call again to ensure the UTXO set wasn't updated
 197          assert_template(node, block_3, None)
 198  
 199      def overspending_transaction_test(self, node, block_0_height, tx):
 200          self.log.info("Add an transaction that spends too much")
 201  
 202          block_1 = node.getblock(node.getblockhash(block_0_height + 1))
 203          block_2_hash = node.getblockhash(block_0_height + 2)
 204  
 205          bad_tx = copy.deepcopy(tx)
 206          bad_tx["tx"].vout[0].nValue = 10000000000
 207          bad_tx_hex = bad_tx["tx"].serialize().hex()
 208          assert_equal(
 209              node.testmempoolaccept([bad_tx_hex])[0]["reject-reason"],
 210              "bad-txns-in-belowout",
 211          )
 212          block_3 = create_block(
 213              int(block_2_hash, 16),
 214              height=block_0_height + 3,
 215              ntime=block_1["mediantime"] + 1,
 216              txlist=[bad_tx_hex],
 217          )
 218          assert_equal(len(block_3.vtx), 2)
 219          add_witness_commitment(block_3)
 220          block_3.solve()
 221  
 222          assert_template(node, block_3, "bad-txns-in-belowout")
 223  
 224      def spend_twice_test(self, node, block_0_height, tx):
 225          block_1 = node.getblock(node.getblockhash(block_0_height + 1))
 226          block_2_hash = node.getblockhash(block_0_height + 2)
 227  
 228          self.log.info("Can't spend coins twice")
 229          tx_hex = tx["tx"].serialize().hex()
 230          tx_2 = copy.deepcopy(tx)
 231          tx_2_hex = tx_2["tx"].serialize().hex()
 232          # Nothing wrong with these transactions individually
 233          assert_equal(node.testmempoolaccept([tx_hex])[0]["allowed"], True)
 234          assert_equal(node.testmempoolaccept([tx_2_hex])[0]["allowed"], True)
 235          # But can't be combined
 236          assert_equal(
 237              node.testmempoolaccept([tx_hex, tx_2_hex])[0]["package-error"],
 238              "package-contains-duplicates",
 239          )
 240          block_3 = create_block(
 241              int(block_2_hash, 16),
 242              height=block_0_height + 3,
 243              ntime=block_1["mediantime"] + 1,
 244              txlist=[tx_hex, tx_2_hex],
 245          )
 246          assert_equal(len(block_3.vtx), 3)
 247          add_witness_commitment(block_3)
 248  
 249          assert_template(node, block_3, "bad-txns-inputs-missingorspent", submit=False)
 250  
 251          return block_3
 252  
 253      def parallel_test(self, node, block_3):
 254          # Ensure that getblocktemplate can be called concurrently by many threads.
 255          self.log.info("Check blocks in parallel")
 256          check_50_blocks = lambda n: [
 257              assert_template(n, block_3, "bad-txns-inputs-missingorspent", submit=False)
 258              for _ in range(50)
 259          ]
 260          rpcs = [node.cli for _ in range(6)]
 261          with ThreadPoolExecutor(max_workers=len(rpcs)) as threads:
 262              list(threads.map(check_50_blocks, rpcs))
 263  
 264      def run_test(self):
 265          node = self.nodes[0]
 266  
 267          block_0_height = node.getblockcount()
 268          self.generate(node, sync_fun=self.no_op, nblocks=1)
 269          block_1 = node.getblock(node.getbestblockhash())
 270          block_2 = create_block(
 271              int(block_1["hash"], 16),
 272              height=block_0_height + 2,
 273              ntime=block_1["mediantime"] + 1,
 274          )
 275  
 276          self.valid_block_test(node, block_2)
 277          self.cb_missing_test(node, block_2)
 278          self.block_without_transactions_test(node, block_2)
 279          self.truncated_final_transaction_test(node, block_2)
 280          self.duplicate_transaction_test(node, block_2)
 281          self.thin_air_spending_test(node, block_2)
 282          self.non_final_transaction_test(node, block_2)
 283          self.bad_tx_count_test(node, block_2)
 284          self.nbits_test(node, block_2)
 285          self.merkle_root_test(node, block_2)
 286          self.bad_timestamp_test(node, block_2)
 287          self.current_tip_test(node, block_2)
 288          # This sets the PoW for the next test
 289          self.pow_test(node, block_2)
 290          self.submit_test(node, block_0_height, block_2)
 291  
 292          self.log.info("Generate a transaction")
 293          tx = MiniWallet(node).create_self_transfer()
 294  
 295          self.transaction_test(node, block_0_height, tx)
 296          self.overspending_transaction_test(node, block_0_height, tx)
 297          block_3 = self.spend_twice_test(node, block_0_height, tx)
 298          self.parallel_test(node, block_3)
 299  
 300  if __name__ == "__main__":
 301      MiningTemplateVerificationTest(__file__).main()
 302