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