interface_ipc_mining.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 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 the IPC (multiprocess) Mining interface."""
   6  import asyncio
   7  import time
   8  from contextlib import AsyncExitStack
   9  from copy import deepcopy
  10  from decimal import Decimal
  11  from io import BytesIO
  12  from test_framework.blocktools import (
  13      NULL_OUTPOINT,
  14      script_BIP34_coinbase_height,
  15      WITNESS_COMMITMENT_HEADER,
  16  )
  17  from test_framework.messages import (
  18      CBlockHeader,
  19      COIN,
  20      CTransaction,
  21      CTxIn,
  22      CTxInWitness,
  23      CTxOut,
  24      DEFAULT_BLOCK_RESERVED_WEIGHT,
  25      MAX_BLOCK_SIGOPS_COST,
  26      MAX_BLOCK_WEIGHT,
  27      from_hex,
  28      msg_headers,
  29      ser_uint256,
  30  )
  31  from test_framework.test_framework import BitcoinTestFramework
  32  from test_framework.util import (
  33      assert_equal,
  34      assert_greater_than_or_equal,
  35      assert_not_equal,
  36  )
  37  from test_framework.wallet import MiniWallet
  38  from test_framework.p2p import P2PInterface
  39  from test_framework.ipc_util import (
  40      assert_capnp_failed,
  41      assert_create_new_block_fails,
  42      destroying,
  43      load_capnp_modules,
  44      make_mining_ctx,
  45      mining_create_block_template,
  46      mining_get_block,
  47      mining_get_coinbase_tx,
  48      mining_wait_next_template,
  49      wait_and_do,
  50  )
  51  
  52  # Test may be skipped and not have capnp installed
  53  try:
  54      import capnp  # type: ignore[import] # noqa: F401
  55  except ModuleNotFoundError:
  56      pass
  57  
  58  
  59  class IPCMiningTest(BitcoinTestFramework):
  60  
  61      def skip_test_if_missing_module(self):
  62          self.skip_if_no_ipc()
  63          self.skip_if_no_py_capnp()
  64  
  65      def set_test_params(self):
  66          self.num_nodes = 3
  67  
  68      def setup_nodes(self):
  69          self.extra_init = [{"ipcbind": True}, {}, {"ipcbind": True}]
  70          super().setup_nodes()
  71          # Use this function to also load the capnp modules (we cannot use set_test_params for this,
  72          # as it is being called before knowing whether capnp is available).
  73          self.capnp_modules = load_capnp_modules(self.config)
  74  
  75      async def build_coinbase_test(self, template, ctx, miniwallet, extra_nonce=b""):
  76          self.log.debug("Build coinbase transaction using getCoinbaseTx()")
  77          assert template is not None
  78          coinbase_res = await mining_get_coinbase_tx(template, ctx)
  79          coinbase_tx = CTransaction()
  80          coinbase_tx.version = coinbase_res.version
  81          coinbase_tx.vin = [CTxIn()]
  82          coinbase_tx.vin[0].prevout = NULL_OUTPOINT
  83          coinbase_tx.vin[0].nSequence = coinbase_res.sequence
  84  
  85          # Verify there's no dummy extraNonce in the coinbase scriptSig
  86          current_block_height = self.nodes[0].getchaintips()[0]["height"]
  87          bip34_prefix = script_BIP34_coinbase_height(current_block_height + 1, padding=False)
  88          assert_equal(coinbase_res.scriptSigPrefix, bip34_prefix)
  89  
  90          # Typically a mining pool appends its name and an extraNonce
  91          coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix + extra_nonce
  92  
  93          # We currently always provide a coinbase witness, even for empty
  94          # blocks, but this may change, so always check:
  95          has_witness = coinbase_res.witness is not None
  96          if has_witness:
  97              coinbase_tx.wit.vtxinwit = [CTxInWitness()]
  98              coinbase_tx.wit.vtxinwit[0].scriptWitness.stack = [coinbase_res.witness]
  99  
 100          # First output is our payout
 101          coinbase_tx.vout = [CTxOut()]
 102          coinbase_tx.vout[0].scriptPubKey = miniwallet.get_output_script()
 103          coinbase_tx.vout[0].nValue = coinbase_res.blockRewardRemaining
 104          # Add SegWit OP_RETURN. This is currently always present even for
 105          # empty blocks, but this may change.
 106          for output_data in coinbase_res.requiredOutputs:
 107              output = CTxOut()
 108              output.deserialize(BytesIO(output_data))
 109              coinbase_tx.vout.append(output)
 110  
 111          coinbase_tx.nLockTime = coinbase_res.lockTime
 112          return coinbase_tx
 113  
 114      async def build_candidate_block(self, template, ctx):
 115          """Build a complete block from a remote BlockTemplate."""
 116          block = await mining_get_block(template, ctx)
 117          coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet)
 118          # Reduce payout for balance comparison simplicity.
 119          coinbase.vout[0].nValue = COIN
 120          block.vtx[0] = coinbase
 121          block.hashMerkleRoot = block.calc_merkle_root()
 122          return block
 123  
 124      async def assert_submit_block(self, mining, ctx, block, *, result, reason="", debug=""):
 125          submit = await mining.submitBlock(ctx, block.serialize())
 126          assert_equal(submit.result, result)
 127          assert_equal(submit.reason, reason)
 128          assert_equal(submit.debug, debug)
 129  
 130      def run_mining_interface_test(self):
 131          """Test Mining interface methods."""
 132          self.log.info("Running Mining interface test")
 133          block_hash_size = 32
 134  
 135          async def async_routine():
 136              ctx, mining = await make_mining_ctx(self)
 137              blockref = await mining.getTip(ctx)
 138              current_block_height = self.nodes[0].getchaintips()[0]["height"]
 139              assert_equal(blockref.result.height, current_block_height)
 140  
 141              self.log.debug("Mine a block")
 142              newblockref = (await wait_and_do(
 143                  mining.waitTipChanged(ctx, blockref.result.hash, self.default_ipc_timeout),
 144                  lambda: self.generate(self.nodes[0], 1))).result
 145              assert_equal(len(newblockref.hash), block_hash_size)
 146              assert_equal(newblockref.height, current_block_height + 1)
 147              self.log.debug("Wait for timeout")
 148              oldblockref = (await mining.waitTipChanged(ctx, newblockref.hash, self.default_ipc_timeout)).result
 149              assert_equal(len(newblockref.hash), block_hash_size)
 150              assert_equal(oldblockref.hash, newblockref.hash)
 151              assert_equal(oldblockref.height, newblockref.height)
 152  
 153              self.log.debug("interrupt() should abort waitTipChanged()")
 154              async def wait_for_tip():
 155                  long_timeout = max(self.default_ipc_timeout, 60000.0)  # at least 1 minute
 156                  result = (await mining.waitTipChanged(ctx, newblockref.hash, long_timeout)).result
 157                  # Unlike a timeout, interrupt() returns an empty BlockRef.
 158                  assert_equal(len(result.hash), 0)
 159              await wait_and_do(wait_for_tip(), mining.interrupt())
 160  
 161          asyncio.run(capnp.run(async_routine()))
 162  
 163      def run_early_startup_test(self):
 164          """Make sure mining.createNewBlock safely returns on early startup as
 165          soon as mining interface is available """
 166          self.log.info("Running Mining interface early startup test")
 167  
 168          node = self.nodes[0]
 169          self.stop_node(node.index)
 170          node.start()
 171  
 172          async def async_routine():
 173              while True:
 174                  try:
 175                      ctx, mining = await make_mining_ctx(self)
 176                      break
 177                  except (ConnectionRefusedError, FileNotFoundError):
 178                      # Poll quickly to connect as soon as socket becomes
 179                      # available but without using a lot of CPU
 180                      await asyncio.sleep(0.005)
 181  
 182              opts = self.capnp_modules['mining'].BlockCreateOptions()
 183              await mining.createNewBlock(ctx, opts)
 184  
 185          asyncio.run(capnp.run(async_routine()))
 186  
 187          # Reconnect nodes so next tests are happy
 188          node.wait_for_rpc_connection()
 189          self.connect_nodes(1, 0)
 190          # Restarting node 0 drops its P2P connection to the rest of the test
 191          # chain. Restore the synced 0-1-2 topology before later tests split
 192          # node 2 off for submitBlock checks.
 193          self.sync_all()
 194  
 195      def run_block_template_test(self):
 196          """Test BlockTemplate interface methods."""
 197          self.log.info("Running BlockTemplate interface test")
 198          block_header_size = 80
 199  
 200          async def async_routine():
 201              ctx, mining = await make_mining_ctx(self)
 202  
 203              async with AsyncExitStack() as stack:
 204                  self.log.debug("createNewBlock() should wait if tip is still updating")
 205                  self.disconnect_nodes(0, 1)
 206                  node1_block_hash = self.generate(self.nodes[1], 1, sync_fun=self.no_op)[0]
 207                  header = from_hex(CBlockHeader(), self.nodes[1].getblockheader(node1_block_hash, False))
 208                  header_only_peer = self.nodes[0].add_p2p_connection(P2PInterface())
 209                  header_only_peer.send_and_ping(msg_headers([header]))
 210                  start = time.time()
 211                  async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx):
 212                      pass
 213                  # Lower-bound only: a heavily loaded CI host might still exceed 0.9s
 214                  # even without the cooldown, so this can miss regressions but avoids
 215                  # spurious failures.
 216                  assert_greater_than_or_equal(time.time() - start, 0.9)
 217  
 218                  self.log.debug("createNewBlock() should wake up promptly after tip advances")
 219                  success = False
 220                  duration = 0.0
 221                  async def wait_fn():
 222                      nonlocal success, duration
 223                      start = time.time()
 224                      res = await mining.createNewBlock(ctx, self.default_block_create_options)
 225                      duration = time.time() - start
 226                      success = res._has("result")
 227                  def do_fn():
 228                      block_hex = self.nodes[1].getblock(node1_block_hash, False)
 229                      self.nodes[0].submitblock(block_hex)
 230                  await wait_and_do(wait_fn(), do_fn)
 231                  assert_equal(success, True)
 232                  if self.options.timeout_factor <= 1:
 233                      assert duration < 3.0, f"createNewBlock took {duration:.2f}s, did not wake up promptly after tip advances"
 234                  else:
 235                      self.log.debug("Skipping strict wake-up duration check because timeout_factor > 1")
 236  
 237                  self.log.debug("interrupt() should abort createNewBlock() during cooldown")
 238                  async def create_block():
 239                      result = await mining.createNewBlock(ctx, self.default_block_create_options)
 240                      # interrupt() causes createNewBlock to return nullptr
 241                      assert_equal(result._has("result"), False)
 242  
 243                  await wait_and_do(create_block(), mining.interrupt())
 244  
 245                  header_only_peer.peer_disconnect()
 246                  self.connect_nodes(0, 1)
 247                  self.sync_all()
 248  
 249                  self.log.debug("Create a template")
 250                  template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
 251                  assert template is not None
 252  
 253                  self.log.debug("Test some inspectors of Template")
 254                  header = (await template.getBlockHeader(ctx)).result
 255                  assert_equal(len(header), block_header_size)
 256                  block = await mining_get_block(template, ctx)
 257                  current_tip = self.nodes[0].getbestblockhash()
 258                  assert_equal(ser_uint256(block.hashPrevBlock), ser_uint256(int(current_tip, 16)))
 259                  assert_greater_than_or_equal(len(block.vtx), 1)
 260                  txfees = await template.getTxFees(ctx)
 261                  assert_equal(len(txfees.result), 0)
 262                  txsigops = await template.getTxSigops(ctx)
 263                  assert_equal(len(txsigops.result), 0)
 264  
 265                  self.log.debug("Wait for a new template")
 266                  waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
 267                  waitoptions.timeout = self.default_ipc_timeout
 268                  waitoptions.feeThreshold = 1
 269                  template2 = await wait_and_do(
 270                      mining_wait_next_template(template, stack, ctx, waitoptions),
 271                      lambda: self.generate(self.nodes[0], 1))
 272                  assert template2 is not None
 273                  block2 = await mining_get_block(template2, ctx)
 274                  assert_equal(len(block2.vtx), 1)
 275  
 276                  self.log.debug("Wait for another, but time out")
 277                  template3 = await mining_wait_next_template(template2, stack, ctx, waitoptions)
 278                  assert template3 is None
 279  
 280                  self.log.debug("Wait for another, get one after increase in fees in the mempool")
 281                  template4 = await wait_and_do(
 282                      mining_wait_next_template(template2, stack, ctx, waitoptions),
 283                      lambda: self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]))
 284                  assert template4 is not None
 285                  block3 = await mining_get_block(template4, ctx)
 286                  assert_equal(len(block3.vtx), 2)
 287  
 288                  self.log.debug("Wait again, this should return the same template, since the fee threshold is zero")
 289                  waitoptions.feeThreshold = 0
 290                  template5 = await mining_wait_next_template(template4, stack, ctx, waitoptions)
 291                  assert template5 is not None
 292                  block4 = await mining_get_block(template5, ctx)
 293                  assert_equal(len(block4.vtx), 2)
 294                  waitoptions.feeThreshold = 1
 295  
 296                  self.log.debug("Wait for another, get one after increase in fees in the mempool")
 297                  template6 = await wait_and_do(
 298                      mining_wait_next_template(template5, stack, ctx, waitoptions),
 299                      lambda: self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]))
 300                  assert template6 is not None
 301                  block4 = await mining_get_block(template6, ctx)
 302                  assert_equal(len(block4.vtx), 3)
 303  
 304                  self.log.debug("Wait for another, but time out, since the fee threshold is set now")
 305                  template7 = await mining_wait_next_template(template6, stack, ctx, waitoptions)
 306                  assert template7 is None
 307  
 308                  self.log.debug("interruptWait should abort the current wait")
 309                  async def wait_for_block():
 310                      new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
 311                      new_waitoptions.timeout = max(self.default_ipc_timeout, 60000.0)  # at least 1 minute
 312                      new_waitoptions.feeThreshold = 1
 313                      template7 = await mining_wait_next_template(template6, stack, ctx, new_waitoptions)
 314                      assert template7 is None
 315                  await wait_and_do(wait_for_block(), template6.interruptWait())
 316  
 317          asyncio.run(capnp.run(async_routine()))
 318  
 319      def run_ipc_option_override_test(self):
 320          self.log.info("Running IPC option override test")
 321          # Confirm that BlockCreateOptions.blockReservedWeight takes precedence
 322          # over -blockreservedweight. Set an absurdly high -blockreservedweight
 323          # value that would result in empty blocks to verify this. IPC clients set
 324          # blockReservedWeight per template request and are unaffected; later in
 325          # the test the IPC template includes a mempool transaction.
 326          self.restart_node(0, extra_args=[f"-blockreservedweight={MAX_BLOCK_WEIGHT}"])
 327          self.miniwallet.rescan_utxos()
 328  
 329          async def async_routine():
 330              ctx, mining = await make_mining_ctx(self)
 331              self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
 332  
 333              async with AsyncExitStack() as stack:
 334                  opts = self.capnp_modules['mining'].BlockCreateOptions()
 335                  template = await mining_create_block_template(mining, stack, ctx, opts)
 336                  assert template is not None
 337                  block = await mining_get_block(template, ctx)
 338                  assert_equal(len(block.vtx), 2)
 339  
 340                  self.log.debug("Use absurdly large reserved weight to force an empty template")
 341                  opts.blockReservedWeight = MAX_BLOCK_WEIGHT
 342                  empty_template = await mining_create_block_template(mining, stack, ctx, opts)
 343                  assert empty_template is not None
 344                  empty_block = await mining_get_block(empty_template, ctx)
 345                  assert_equal(len(empty_block.vtx), 1)
 346  
 347              self.log.debug("Enforce minimum reserved weight for IPC clients too")
 348              opts.blockReservedWeight = 0
 349              await assert_create_new_block_fails(ctx, mining, opts,
 350                  "block_reserved_weight (0) is lower than minimum safety value of (2000)")
 351  
 352          async def async_routine_check_max_reserved_weight():
 353              self.log.debug("Enforce maximum reserved weight for IPC clients too")
 354              ctx, mining = await make_mining_ctx(self)
 355              opts = self.capnp_modules['mining'].BlockCreateOptions()
 356              opts.blockReservedWeight = MAX_BLOCK_WEIGHT + 1
 357              await assert_create_new_block_fails(ctx, mining, opts,
 358                  f"block_reserved_weight ({MAX_BLOCK_WEIGHT + 1}) exceeds consensus maximum block weight ({MAX_BLOCK_WEIGHT})")
 359  
 360          async def async_routine_check_sigops_limit():
 361              self.log.debug("Enforce sigops limit for IPC clients too")
 362              ctx, mining = await make_mining_ctx(self)
 363              opts = self.capnp_modules['mining'].BlockCreateOptions()
 364              opts.coinbaseOutputMaxAdditionalSigops = MAX_BLOCK_SIGOPS_COST + 1
 365              await assert_create_new_block_fails(ctx, mining, opts,
 366                  f"coinbase_output_max_additional_sigops ({MAX_BLOCK_SIGOPS_COST + 1}) exceeds consensus maximum block sigops cost ({MAX_BLOCK_SIGOPS_COST})")
 367  
 368          asyncio.run(capnp.run(async_routine()))
 369          asyncio.run(capnp.run(async_routine_check_max_reserved_weight()))
 370          asyncio.run(capnp.run(async_routine_check_sigops_limit()))
 371          self.restart_node(0)
 372          self.connect_nodes(0, 1)
 373          self.miniwallet.rescan_utxos()
 374  
 375      def run_waitnext_mining_policy_test(self):
 376          """Verify that waitNext() preserves the mining policy from -blockmintxfee
 377          instead of falling back to defaults."""
 378          self.log.info("Running waitNext mining policy test")
 379          block_min_tx_fee = Decimal("0.00002000")
 380          below_block_min_tx_fee = Decimal("0.00001000")
 381          above_block_min_tx_fee = Decimal("0.00003000")
 382  
 383          self.restart_node(0, extra_args=[
 384              f"-blockmintxfee={block_min_tx_fee:.8f}",
 385              "-minrelaytxfee=0",
 386              "-persistmempool=0",
 387          ])
 388  
 389          async def async_routine():
 390              ctx, mining = await make_mining_ctx(self)
 391  
 392              self.log.debug("Create a below -blockmintxfee transaction")
 393              low_fee_tx = self.miniwallet.send_self_transfer(
 394                  fee_rate=below_block_min_tx_fee,
 395                  from_node=self.nodes[0],
 396                  confirmed_only=True,
 397              )
 398              assert low_fee_tx["txid"] in self.nodes[0].getrawmempool()
 399  
 400              async with AsyncExitStack() as stack:
 401                  self.log.debug("createNewBlock should respect -blockmintxfee")
 402                  template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
 403                  assert template is not None
 404                  block = await mining_get_block(template, ctx)
 405                  assert low_fee_tx["txid"] not in {tx.txid_hex for tx in block.vtx[1:]}
 406  
 407                  self.log.debug("waitNext should preserve the same mining policy")
 408                  high_fee_tx = self.miniwallet.send_self_transfer(
 409                      fee_rate=above_block_min_tx_fee,
 410                      from_node=self.nodes[0],
 411                      confirmed_only=True,
 412                  )
 413                  mempool_txids = self.nodes[0].getrawmempool()
 414                  assert high_fee_tx["txid"] in mempool_txids
 415                  assert low_fee_tx["txid"] in mempool_txids
 416                  template_next = await mining_wait_next_template(template, stack, ctx, self.default_block_wait_options)
 417                  assert template_next is not None
 418  
 419                  block_next = await mining_get_block(template_next, ctx)
 420                  block_next_txids = {tx.txid_hex for tx in block_next.vtx[1:]}
 421                  assert high_fee_tx["txid"] in block_next_txids
 422                  assert low_fee_tx["txid"] not in block_next_txids
 423  
 424          asyncio.run(capnp.run(async_routine()))
 425  
 426      def run_block_max_weight_test(self):
 427          """Verify IPC createNewBlock() and waitNext() preserve the -blockmaxweight policy."""
 428          self.log.info("Running block_max_weight test")
 429  
 430          # Cap that leaves room for only a handful of mempool transactions
 431          # above DEFAULT_BLOCK_RESERVED_WEIGHT (8000). Well below MAX_BLOCK_WEIGHT
 432          # (4_000_000), so any truncation observed here is attributable to the
 433          # cap, not to consensus limits or wallet chain limits.
 434          small_cap = DEFAULT_BLOCK_RESERVED_WEIGHT + 4000
 435          NUM_TXS = 20
 436  
 437          self.restart_node(0, extra_args=[
 438              f"-blockmaxweight={small_cap}",
 439              "-minrelaytxfee=0",
 440              "-persistmempool=0",
 441          ])
 442          # Refresh miniwallet's UTXO view from the chain after restart.
 443          self.miniwallet.rescan_utxos()
 444  
 445          # Fill the mempool enough that the configured block weight cap forces
 446          # template truncation.
 447          for _ in range(NUM_TXS):
 448              self.miniwallet.send_self_transfer(from_node=self.nodes[0], confirmed_only=True)
 449          assert_equal(self.nodes[0].getmempoolinfo()["size"], NUM_TXS)
 450  
 451          async def async_routine():
 452              ctx, mining = await make_mining_ctx(self)
 453              async with AsyncExitStack() as stack:
 454                  template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
 455                  assert template is not None
 456                  block = await mining_get_block(template, ctx)
 457                  assert_greater_than_or_equal(small_cap, block.get_weight())
 458                  # Exclude the coinbase; the cap must have forced truncation.
 459                  initial_included = len(block.vtx) - 1
 460                  assert initial_included < NUM_TXS, (
 461                      f"Expected -blockmaxweight={small_cap} to truncate; "
 462                      f"included {initial_included}/{NUM_TXS} mempool txs"
 463                  )
 464  
 465                  self.log.debug("waitNext should preserve -blockmaxweight")
 466                  high_fee_tx = self.miniwallet.send_self_transfer(
 467                      from_node=self.nodes[0],
 468                      confirmed_only=True,
 469                      fee_rate=10,
 470                  )
 471                  template_next = await mining_wait_next_template(template, stack, ctx, self.default_block_wait_options)
 472                  assert template_next is not None
 473  
 474                  block_next = await mining_get_block(template_next, ctx)
 475                  assert_greater_than_or_equal(small_cap, block_next.get_weight())
 476                  assert high_fee_tx["txid"] in {tx.txid_hex for tx in block_next.vtx[1:]}
 477                  next_included = len(block_next.vtx) - 1
 478                  assert next_included < NUM_TXS + 1, (
 479                      f"Expected -blockmaxweight={small_cap} to remain capped after waitNext; "
 480                      f"included {next_included}/{NUM_TXS + 1} mempool txs"
 481                  )
 482  
 483          asyncio.run(capnp.run(async_routine()))
 484  
 485      def run_coinbase_and_submission_test(self):
 486          """Test coinbase construction (getCoinbaseTx) and block submission (submitSolution)."""
 487          self.log.info("Running coinbase construction and submission test")
 488  
 489          async def async_routine():
 490              ctx, mining = await make_mining_ctx(self)
 491              # Node 0 drives the checkBlock() and submitSolution() checks. Node
 492              # 2 has a separate IPC interface and starts synced through node 1,
 493              # so it can be isolated below to test submitBlock() without
 494              # changing node 0's template and chain state first.
 495              ctx2, mining2 = await make_mining_ctx(self, node_index=2)
 496  
 497              current_block_height = self.nodes[0].getchaintips()[0]["height"]
 498              check_opts = self.capnp_modules['mining'].BlockCheckOptions()
 499  
 500              # Send a real transaction so the template includes it.
 501              self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
 502  
 503              async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as template:
 504                  block = await self.build_candidate_block(template, ctx)
 505                  coinbase = block.vtx[0]
 506                  self.log.debug("Template should include a mempool transaction")
 507                  assert len(block.vtx) >= 2, "Block should include at least the coinbase and the mempool tx"
 508                  balance = self.miniwallet.get_balance()
 509                  original_version = block.nVersion
 510  
 511                  self.log.debug("Disconnect node 2 before block submission tests")
 512                  # The default topology is 2 -> 1 -> 0. Splitting the 1-2 edge
 513                  # lets node 2 accept/reject complete blocks independently.
 514                  self.disconnect_nodes(1, 2)
 515  
 516                  self.log.debug("submitSolution should reject an empty coinbase")
 517                  submitted = (await template.submitSolution(ctx, 0, 0, 0, b"")).result
 518                  assert_equal(submitted, False)
 519  
 520                  self.log.debug("Submit solution that can't be deserialized")
 521                  try:
 522                      await template.submitSolution(ctx, 0, 0, 0, b"\x00")
 523                      raise AssertionError("submitSolution unexpectedly succeeded")
 524                  except capnp.lib.capnp.KjException as e:
 525                      assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
 526  
 527                  self.log.debug("Submit a block with a bad version")
 528                  block.nVersion = 0
 529                  block.solve()
 530                  check = await mining.checkBlock(ctx, block.serialize(), check_opts)
 531                  assert_equal(check.result, False)
 532                  assert_equal(check.reason, "bad-version(0x00000000)")
 533                  assert_equal(check.debug, "rejected nVersion=0x00000000 block")
 534                  self.log.debug("submitSolution should reject a bad-version block")
 535                  submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
 536                  assert_equal(submitted, False)
 537                  self.log.debug("submitBlock should reject a bad-version block")
 538                  await self.assert_submit_block(
 539                      mining2,
 540                      ctx2,
 541                      block,
 542                      result=False,
 543                      reason="bad-version(0x00000000)",
 544                      debug="rejected nVersion=0x00000000 block",
 545                  )
 546                  self.log.debug("Submit a valid block")
 547                  block.nVersion = original_version
 548                  block.solve()
 549  
 550                  self.log.debug("First call checkBlock()")
 551                  block_valid = (await mining.checkBlock(ctx, block.serialize(), check_opts)).result
 552                  assert_equal(block_valid, True)
 553  
 554                  # The remote template block will be mutated, capture the original:
 555                  remote_block_before = await mining_get_block(template, ctx)
 556  
 557                  self.log.debug("Submitted coinbase must include witness")
 558                  assert_not_equal(coinbase.serialize_without_witness().hex(), coinbase.serialize().hex())
 559                  missing_witness_block = deepcopy(block)
 560                  missing_witness_block.vtx[0].wit.vtxinwit = []
 561                  has_witness_commitment = any(
 562                      len(o.scriptPubKey) >= 38 and o.scriptPubKey[2:6] == WITNESS_COMMITMENT_HEADER
 563                      for o in missing_witness_block.vtx[0].vout
 564                  )
 565                  assert has_witness_commitment, "Coinbase should have a witness commitment output"
 566                  missing_witness_block.hashMerkleRoot = missing_witness_block.calc_merkle_root()
 567                  missing_witness_block.solve()
 568                  self.log.debug("submitSolution should reject a coinbase missing witness")
 569                  submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize_without_witness())).result
 570                  assert_equal(submitted, False)
 571  
 572                  self.log.debug("Even a rejected submitSolution() mutates the template's block")
 573                  # Can be used by clients to download and inspect the (rejected)
 574                  # reconstructed block.
 575                  remote_block_after = await mining_get_block(template, ctx)
 576                  assert_not_equal(remote_block_before.serialize().hex(), remote_block_after.serialize().hex())
 577  
 578                  self.log.debug("submitBlock should reject a block missing coinbase witness")
 579                  await self.assert_submit_block(
 580                      mining2,
 581                      ctx2,
 582                      missing_witness_block,
 583                      result=False,
 584                      reason="bad-witness-nonce-size",
 585                      debug="CheckWitnessMalleation : invalid witness reserved value size",
 586                  )
 587  
 588                  self.log.debug("Submit again, with the witness")
 589                  submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
 590                  assert_equal(submitted, True)
 591  
 592                  self.log.debug("Submit a valid complete block through the disconnected node")
 593                  await self.assert_submit_block(mining2, ctx2, block, result=True)
 594                  assert_equal(self.nodes[2].getchaintips()[0]["height"], current_block_height + 1)
 595                  self.log.debug("submitBlock should reject the duplicate complete block")
 596                  await self.assert_submit_block(mining2, ctx2, block, result=False, reason="duplicate")
 597  
 598              self.log.debug("Block should propagate")
 599              # Check that the IPC node actually updates its own chain
 600              assert_equal(self.nodes[0].getchaintips()[0]["height"], current_block_height + 1)
 601              # Stalls if a regression causes submitSolution() to accept an invalid block:
 602              self.sync_blocks(self.nodes[:2])
 603              # Rejoin node 2 and verify the submitBlock result converges with
 604              # the submitSolution result from node 0.
 605              self.connect_nodes(1, 2)
 606              self.sync_all()
 607              # Check that the other node accepts the block
 608              assert_equal(self.nodes[0].getchaintips()[0], self.nodes[1].getchaintips()[0])
 609  
 610              self.miniwallet.rescan_utxos()
 611              assert_equal(self.miniwallet.get_balance(), balance + 1)
 612              self.log.debug("Check block should fail now, since it is a duplicate")
 613              check = await mining.checkBlock(ctx, block.serialize(), check_opts)
 614              assert_equal(check.result, False)
 615              assert_equal(check.reason, "inconclusive-not-best-prevblk")
 616              self.log.debug("submitBlock on the same node should fail with duplicate after submitSolution succeeds")
 617              await self.assert_submit_block(mining, ctx, block, result=False, reason="duplicate")
 618  
 619              self.log.debug("submitSolution should still return True for a duplicate after submitBlock succeeds")
 620              async with destroying((await mining2.createNewBlock(ctx2, self.default_block_create_options)).result, ctx2) as template2:
 621                  duplicate_block = await self.build_candidate_block(template2, ctx2)
 622                  duplicate_coinbase = duplicate_block.vtx[0]
 623                  duplicate_block.solve()
 624                  self.log.debug("Submit a valid complete block before duplicate submitSolution")
 625                  await self.assert_submit_block(mining2, ctx2, duplicate_block, result=True)
 626                  self.nodes[2].waitforblockheight(current_block_height + 2)
 627                  self.log.debug("submitSolution should accept the duplicate block")
 628                  submitted = (await template2.submitSolution(ctx2, duplicate_block.nVersion, duplicate_block.nTime, duplicate_block.nNonce, duplicate_coinbase.serialize())).result
 629                  assert_equal(submitted, True)
 630              self.sync_all()
 631  
 632              self.log.debug("Submit the same invalid block twice")
 633              async with destroying((await mining2.createNewBlock(ctx2, self.default_block_create_options)).result, ctx2) as template2:
 634                  invalid_block = await self.build_candidate_block(template2, ctx2)
 635                  invalid_block.vtx[0].nLockTime = 2**32 - 1
 636                  invalid_block.hashMerkleRoot = invalid_block.calc_merkle_root()
 637                  invalid_block.solve()
 638                  self.log.debug("submitBlock should reject the non-final block")
 639                  await self.assert_submit_block(
 640                      mining2,
 641                      ctx2,
 642                      invalid_block,
 643                      result=False,
 644                      reason="bad-txns-nonfinal",
 645                      debug="non-final transaction",
 646                  )
 647                  self.log.debug("submitBlock should report duplicate-invalid for the same block")
 648                  await self.assert_submit_block(
 649                      mining2,
 650                      ctx2,
 651                      invalid_block,
 652                      result=False,
 653                      reason="duplicate-invalid",
 654                      debug=f"block {invalid_block.hash_hex} was previously marked invalid",
 655                  )
 656  
 657              self.log.debug("Submit a malformed complete block")
 658              try:
 659                  await mining2.submitBlock(ctx2, block.serialize()[:-15])
 660                  raise AssertionError("submitBlock unexpectedly succeeded")
 661              except capnp.lib.capnp.KjException as e:
 662                  assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
 663  
 664              self.log.debug("Submit empty block data")
 665              try:
 666                  await mining2.submitBlock(ctx2, b"")
 667                  raise AssertionError("submitBlock unexpectedly succeeded")
 668              except capnp.lib.capnp.KjException as e:
 669                  assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
 670              assert_equal(self.nodes[2].is_node_stopped(), False)
 671  
 672          asyncio.run(capnp.run(async_routine()))
 673  
 674      def run_transaction_lookup_test(self):
 675          """Test getTransactionsByTxID() and getTransactionsByWitnessID()."""
 676          self.log.info("Running transaction lookup test")
 677  
 678          async def async_routine():
 679              ctx, mining = await make_mining_ctx(self)
 680              tx1 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
 681              tx2 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0], utxo_to_spend=tx1["new_utxo"])
 682  
 683              self.log.debug("getTransactionsByTxID() returns mempool txs and nulls")
 684              raw_txs_txid = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid, tx2["tx"].txid, bytes(32)])
 685              assert_equal(len(raw_txs_txid.result), 3)
 686              assert_equal(raw_txs_txid.result[0].hex(), tx1["hex"])
 687              assert_equal(raw_txs_txid.result[1].hex(), tx2["hex"])
 688              assert_equal(raw_txs_txid.result[2], b'')
 689  
 690              self.log.debug("getTransactionsByWitnessID() returns mempool txs and nulls")
 691              raw_txs_wtxid = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid, tx2["tx"].wtxid, bytes(32)])
 692              assert_equal(len(raw_txs_wtxid.result), 3)
 693              assert_equal(raw_txs_wtxid.result[0].hex(), tx1["hex"])
 694              assert_equal(raw_txs_wtxid.result[1].hex(), tx2["hex"])
 695              assert_equal(raw_txs_wtxid.result[2], b'')
 696  
 697              self.log.debug("Mined transactions are not returned")
 698              self.generate(self.nodes[0], 1)
 699              self.sync_all()
 700              raw_txs = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid])
 701              assert_equal(raw_txs.result[0], b'')
 702              raw_txs = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid])
 703              assert_equal(raw_txs.result[0], b'')
 704  
 705          asyncio.run(capnp.run(async_routine()))
 706  
 707      def run_low_height_test(self):
 708          """Test that IPC createNewBlock() works at low block heights on a
 709          clean chain, in particular with regard to bad-cb-length.
 710  
 711          createNewBlock pads the scriptSig with a dummy extranonce to pass
 712          its internal CheckBlock(). This dummy is omitted from the
 713          getCoinbaseTx() script_sig_prefix field. The client provides its
 714          own extraNonce via submitSolution()."""
 715          self.log.info("Running low block height test")
 716  
 717          node = self.nodes[0]
 718          self.stop_node(0)
 719          # Clear chain data to start from genesis
 720          self.cleanup_folder(node.chain_path)
 721          node.start()
 722          node.wait_for_rpc_connection()
 723          assert_equal(node.getblockcount(), 0)
 724  
 725          async def async_routine():
 726              ctx, mining = await make_mining_ctx(self)
 727              opts = self.capnp_modules['mining'].BlockCreateOptions()
 728  
 729              # Mine blocks 1-17 to exercise the boundary at height 16, where the
 730              # internal scriptSig padding is no longer needed.
 731              for height in range(1, 18):
 732                  async with AsyncExitStack() as stack:
 733                      # Disable cooldown to avoid hanging in the IBD loop on a fresh chain
 734                      template = await mining_create_block_template(mining, stack, ctx, opts, cooldown=False)
 735                      assert template is not None
 736                      block = await mining_get_block(template, ctx)
 737                      # Heights <= 16 need extra nonce padding.
 738                      extra_nonce = b'\xaa\xbb\xcc\xdd' if height <= 16 else b""
 739                      coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet, extra_nonce=extra_nonce)
 740                      block.vtx[0] = coinbase
 741                      block.hashMerkleRoot = block.calc_merkle_root()
 742                      block.solve()
 743                      submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
 744                      assert_equal(submitted, True)
 745                      assert_equal(node.getblockcount(), height)
 746  
 747          asyncio.run(capnp.run(async_routine()))
 748  
 749      def run_test(self):
 750          self.miniwallet = MiniWallet(self.nodes[0])
 751          # Amount of time in milliseconds the test is allowed to wait or be idle before it should fail.
 752          self.default_ipc_timeout = 1000.0 * self.options.timeout_factor
 753          self.default_block_create_options = self.capnp_modules['mining'].BlockCreateOptions()
 754          self.default_block_wait_options = self.capnp_modules['mining'].BlockWaitOptions()
 755          self.default_block_wait_options.timeout = self.default_ipc_timeout
 756          self.default_block_wait_options.feeThreshold = 1
 757          self.run_mining_interface_test()
 758          self.run_early_startup_test()
 759          self.run_block_template_test()
 760          self.run_coinbase_and_submission_test()
 761          self.run_waitnext_mining_policy_test()
 762          self.run_block_max_weight_test()
 763          self.run_ipc_option_override_test()
 764          self.run_transaction_lookup_test()
 765  
 766          # Needs to run last because it resets the chain.
 767          self.run_low_height_test()
 768  
 769  
 770  if __name__ == '__main__':
 771      IPCMiningTest(__file__).main()
 772