interface_ipc.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) interface."""
   6  import asyncio
   7  
   8  from contextlib import ExitStack
   9  from test_framework.test_framework import BitcoinTestFramework
  10  from test_framework.util import assert_equal
  11  from test_framework.ipc_util import (
  12      load_capnp_modules,
  13      make_capnp_init_ctx,
  14      make_mining_ctx,
  15  )
  16  
  17  # Test may be skipped and not have capnp installed
  18  try:
  19      import capnp  # type: ignore[import] # noqa: F401
  20  except ModuleNotFoundError:
  21      pass
  22  
  23  
  24  class IPCInterfaceTest(BitcoinTestFramework):
  25  
  26      def skip_test_if_missing_module(self):
  27          self.skip_if_no_ipc()
  28          self.skip_if_no_py_capnp()
  29  
  30      def set_test_params(self):
  31          self.num_nodes = 1
  32  
  33      def setup_nodes(self):
  34          self.extra_init = [{"ipcbind": True}]
  35          super().setup_nodes()
  36          # Use this function to also load the capnp modules (we cannot use set_test_params for this,
  37          # as it is being called before knowing whether capnp is available).
  38          self.capnp_modules = load_capnp_modules(self.config)
  39  
  40      def run_echo_test(self):
  41          self.log.info("Running echo test")
  42          async def async_routine():
  43              ctx, init = await make_capnp_init_ctx(self)
  44              self.log.debug("Create Echo proxy object")
  45              echo = init.makeEcho(ctx).result
  46              self.log.debug("Test a few invocations of echo")
  47              for s in ["hallo", "", "haha"]:
  48                  result_eval = (await echo.echo(ctx, s)).result
  49                  assert_equal(s, result_eval)
  50              self.log.debug("Destroy the Echo object")
  51              echo.destroy(ctx)
  52          asyncio.run(capnp.run(async_routine()))
  53  
  54      def run_mining_test(self):
  55          self.log.info("Running mining test")
  56          block_hash_size = 32
  57  
  58          async def async_routine():
  59              ctx, mining = await make_mining_ctx(self)
  60              self.log.debug("Test simple inspectors")
  61              assert (await mining.isTestChain(ctx)).result
  62              assert not (await mining.isInitialBlockDownload(ctx)).result
  63              blockref = await mining.getTip(ctx)
  64              assert blockref.hasResult
  65              assert_equal(len(blockref.result.hash), block_hash_size)
  66              current_block_height = self.nodes[0].getchaintips()[0]["height"]
  67              assert_equal(blockref.result.height, current_block_height)
  68  
  69          asyncio.run(capnp.run(async_routine()))
  70  
  71      def run_deprecated_mining_test(self):
  72          self.log.info("Running deprecated mining interface test")
  73          async def async_routine():
  74              node = self.nodes[0]
  75              connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path)
  76              init = capnp.TwoPartyClient(connection).bootstrap().cast_as(self.capnp_modules['init'].Init)
  77              self.log.debug("Calling deprecated makeMiningOld2 should raise an error")
  78              try:
  79                  await init.makeMiningOld2()
  80                  raise AssertionError("makeMiningOld2 unexpectedly succeeded")
  81              except capnp.KjException as e:
  82                  assert_equal(e.description, "remote exception: std::exception: Old mining interface (@2) not supported. Please update your client!")
  83                  assert_equal(e.type, "FAILED")
  84          asyncio.run(capnp.run(async_routine()))
  85  
  86      def run_unclean_disconnect_test(self):
  87          """Test behavior when disconnecting during an IPC call that later
  88          returns a non-null interface pointer. This used to cause a crash as
  89          reported https://github.com/bitcoin/bitcoin/issues/34250, but now just
  90          results in a cancellation log message"""
  91          node = self.nodes[0]
  92          self.log.info("Running disconnect during BlockTemplate.waitNext")
  93          timeout = self.rpc_timeout * 1000.0
  94          disconnected_log_check = ExitStack()
  95  
  96          async def async_routine():
  97              ctx, mining = await make_mining_ctx(self)
  98              self.log.debug("Create a template")
  99              opts = self.capnp_modules['mining'].BlockCreateOptions()
 100              template = (await mining.createNewBlock(ctx, opts)).result
 101  
 102              self.log.debug("Wait for a new template")
 103              waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
 104              waitoptions.timeout = timeout
 105              waitoptions.feeThreshold = 1
 106              with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
 107                  promise = template.waitNext(ctx, waitoptions)
 108                  await asyncio.sleep(0.1)
 109              disconnected_log_check.enter_context(node.assert_debug_log(expected_msgs=["IPC server: socket disconnected", "canceled while executing"], timeout=2))
 110              del promise
 111  
 112          asyncio.run(capnp.run(async_routine()))
 113  
 114          # Wait for socket disconnected log message, then generate a block to
 115          # cause the waitNext() call to return a new template. Look for a
 116          # canceled IPC log message after waitNext returns.
 117          with node.assert_debug_log(expected_msgs=["interrupted (canceled)"], timeout=2):
 118              disconnected_log_check.close()
 119              self.generate(node, 1)
 120  
 121      def run_thread_busy_test(self):
 122          """Test behavior when sending multiple calls to the same server thread
 123          which used to cause a crash as reported
 124          https://github.com/bitcoin/bitcoin/issues/33923."""
 125          node = self.nodes[0]
 126          self.log.info("Running thread busy test")
 127          timeout = self.rpc_timeout * 1000.0
 128  
 129          async def async_routine():
 130              ctx, mining = await make_mining_ctx(self)
 131              self.log.debug("Create a template")
 132              opts = self.capnp_modules['mining'].BlockCreateOptions()
 133              template = (await mining.createNewBlock(ctx, opts)).result
 134  
 135              self.log.debug("Wait for a new template")
 136              waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
 137              waitoptions.timeout = timeout
 138              waitoptions.feeThreshold = 1
 139  
 140              # Make multiple waitNext calls where the first will start to
 141              # execute, and the second and third will be posted waiting to
 142              # execute. Previously, the third call would fail calling
 143              # mp::Waiter::post() because the waiting function slot is occupied,
 144              # but now posts are queued.
 145              with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
 146                  promise1 = template.waitNext(ctx, waitoptions)
 147                  await asyncio.sleep(0.1)
 148              with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
 149                  promise2 = template.waitNext(ctx, waitoptions)
 150                  await asyncio.sleep(0.1)
 151              with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
 152                  promise3 = template.waitNext(ctx, waitoptions)
 153                  await asyncio.sleep(0.1)
 154  
 155              # Generate a new block to make the active waitNext calls return, then clean up.
 156              with node.assert_debug_log(expected_msgs=["IPC server send response"], timeout=2):
 157                  self.generate(node, 1, sync_fun=self.no_op)
 158              await ((await promise1).result).destroy(ctx)
 159              await ((await promise2).result).destroy(ctx)
 160              await ((await promise3).result).destroy(ctx)
 161              await template.destroy(ctx)
 162  
 163          asyncio.run(capnp.run(async_routine()))
 164  
 165      def run_test(self):
 166          self.run_echo_test()
 167          self.run_mining_test()
 168          self.run_deprecated_mining_test()
 169          self.run_unclean_disconnect_test()
 170          self.run_thread_busy_test()
 171  
 172  if __name__ == '__main__':
 173      IPCInterfaceTest(__file__).main()
 174