interface_ipc.py
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 from io import BytesIO 8 from pathlib import Path 9 import shutil 10 from test_framework.messages import (CBlock, CTransaction, ser_uint256, COIN) 11 from test_framework.test_framework import BitcoinTestFramework 12 from test_framework.util import ( 13 assert_equal, 14 assert_not_equal 15 ) 16 from test_framework.wallet import MiniWallet 17 18 # Test may be skipped and not have capnp installed 19 try: 20 import capnp # type: ignore[import] # noqa: F401 21 except ImportError: 22 pass 23 24 25 class IPCInterfaceTest(BitcoinTestFramework): 26 27 def skip_test_if_missing_module(self): 28 self.skip_if_no_ipc() 29 self.skip_if_no_py_capnp() 30 31 def load_capnp_modules(self): 32 if capnp_bin := shutil.which("capnp"): 33 # Add the system cap'nproto path so include/capnp/c++.capnp can be found. 34 capnp_dir = Path(capnp_bin).resolve().parent.parent / "include" 35 else: 36 # If there is no system cap'nproto, the pycapnp module should have its own "bundled" 37 # includes at this location. If pycapnp was installed with bundled capnp, 38 # capnp/c++.capnp can be found here. 39 capnp_dir = Path(capnp.__path__[0]).parent 40 src_dir = Path(self.config['environment']['SRCDIR']) / "src" 41 mp_dir = src_dir / "ipc" / "libmultiprocess" / "include" 42 # List of import directories. Note: it is important for mp_dir to be 43 # listed first, in case there are other libmultiprocess installations on 44 # the system, to ensure that `import "/mp/proxy.capnp"` lines load the 45 # same file as capnp.load() loads directly below, and there are not 46 # "failed: Duplicate ID @0xcc316e3f71a040fb" errors. 47 imports = [str(mp_dir), str(capnp_dir), str(src_dir)] 48 return { 49 "proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports), 50 "init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports), 51 "echo": capnp.load(str(src_dir / "ipc" / "capnp" / "echo.capnp"), imports=imports), 52 "mining": capnp.load(str(src_dir / "ipc" / "capnp" / "mining.capnp"), imports=imports), 53 } 54 55 def set_test_params(self): 56 self.num_nodes = 2 57 58 def setup_nodes(self): 59 self.extra_init = [{"ipcbind": True}, {}] 60 super().setup_nodes() 61 # Use this function to also load the capnp modules (we cannot use set_test_params for this, 62 # as it is being called before knowing whether capnp is available). 63 self.capnp_modules = self.load_capnp_modules() 64 65 async def make_capnp_init_ctx(self): 66 node = self.nodes[0] 67 # Establish a connection, and create Init proxy object. 68 connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path) 69 client = capnp.TwoPartyClient(connection) 70 init = client.bootstrap().cast_as(self.capnp_modules['init'].Init) 71 # Create a remote thread on the server for the IPC calls to be executed in. 72 threadmap = init.construct().threadMap 73 thread = threadmap.makeThread("pythread").result 74 ctx = self.capnp_modules['proxy'].Context() 75 ctx.thread = thread 76 # Return both. 77 return ctx, init 78 79 async def parse_and_deserialize_block(self, block_template, ctx): 80 block_data = BytesIO((await block_template.result.getBlock(ctx)).result) 81 block = CBlock() 82 block.deserialize(block_data) 83 return block 84 85 async def parse_and_deserialize_coinbase_tx(self, block_template, ctx): 86 coinbase_data = BytesIO((await block_template.result.getCoinbaseTx(ctx)).result) 87 tx = CTransaction() 88 tx.deserialize(coinbase_data) 89 return tx 90 91 def run_echo_test(self): 92 self.log.info("Running echo test") 93 async def async_routine(): 94 ctx, init = await self.make_capnp_init_ctx() 95 self.log.debug("Create Echo proxy object") 96 echo = init.makeEcho(ctx).result 97 self.log.debug("Test a few invocations of echo") 98 for s in ["hallo", "", "haha"]: 99 result_eval = (await echo.echo(ctx, s)).result 100 assert_equal(s, result_eval) 101 self.log.debug("Destroy the Echo object") 102 echo.destroy(ctx) 103 asyncio.run(capnp.run(async_routine())) 104 105 def run_mining_test(self): 106 self.log.info("Running mining test") 107 block_hash_size = 32 108 block_header_size = 80 109 timeout = 1000.0 # 1000 milliseconds 110 miniwallet = MiniWallet(self.nodes[0]) 111 112 async def async_routine(): 113 ctx, init = await self.make_capnp_init_ctx() 114 self.log.debug("Create Mining proxy object") 115 mining = init.makeMining(ctx) 116 self.log.debug("Test simple inspectors") 117 assert (await mining.result.isTestChain(ctx)) 118 assert (await mining.result.isInitialBlockDownload(ctx)) 119 blockref = await mining.result.getTip(ctx) 120 assert blockref.hasResult 121 assert_equal(len(blockref.result.hash), block_hash_size) 122 current_block_height = self.nodes[0].getchaintips()[0]["height"] 123 assert blockref.result.height == current_block_height 124 self.log.debug("Mine a block") 125 wait = mining.result.waitTipChanged(ctx, blockref.result.hash, ) 126 self.generate(self.nodes[0], 1) 127 newblockref = await wait 128 assert_equal(len(newblockref.result.hash), block_hash_size) 129 assert_equal(newblockref.result.height, current_block_height + 1) 130 self.log.debug("Wait for timeout") 131 wait = mining.result.waitTipChanged(ctx, newblockref.result.hash, timeout) 132 oldblockref = await wait 133 assert_equal(len(newblockref.result.hash), block_hash_size) 134 assert_equal(oldblockref.result.hash, newblockref.result.hash) 135 assert_equal(oldblockref.result.height, newblockref.result.height) 136 137 self.log.debug("Create a template") 138 opts = self.capnp_modules['mining'].BlockCreateOptions() 139 opts.useMempool = True 140 opts.blockReservedWeight = 4000 141 opts.coinbaseOutputMaxAdditionalSigops = 0 142 template = mining.result.createNewBlock(opts) 143 self.log.debug("Test some inspectors of Template") 144 header = await template.result.getBlockHeader(ctx) 145 assert_equal(len(header.result), block_header_size) 146 block = await self.parse_and_deserialize_block(template, ctx) 147 assert_equal(ser_uint256(block.hashPrevBlock), newblockref.result.hash) 148 assert len(block.vtx) >= 1 149 txfees = await template.result.getTxFees(ctx) 150 assert_equal(len(txfees.result), 0) 151 txsigops = await template.result.getTxSigops(ctx) 152 assert_equal(len(txsigops.result), 0) 153 coinbase_data = BytesIO((await template.result.getCoinbaseTx(ctx)).result) 154 coinbase = CTransaction() 155 coinbase.deserialize(coinbase_data) 156 assert_equal(coinbase.vin[0].prevout.hash, 0) 157 self.log.debug("Wait for a new template") 158 waitoptions = self.capnp_modules['mining'].BlockWaitOptions() 159 waitoptions.timeout = timeout 160 waitoptions.feeThreshold = 1 161 waitnext = template.result.waitNext(ctx, waitoptions) 162 self.generate(self.nodes[0], 1) 163 template2 = await waitnext 164 block2 = await self.parse_and_deserialize_block(template2, ctx) 165 assert_equal(len(block2.vtx), 1) 166 self.log.debug("Wait for another, but time out") 167 template3 = await template2.result.waitNext(ctx, waitoptions) 168 assert_equal(template3.to_dict(), {}) 169 self.log.debug("Wait for another, get one after increase in fees in the mempool") 170 waitnext = template2.result.waitNext(ctx, waitoptions) 171 miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) 172 template4 = await waitnext 173 block3 = await self.parse_and_deserialize_block(template4, ctx) 174 assert_equal(len(block3.vtx), 2) 175 self.log.debug("Wait again, this should return the same template, since the fee threshold is zero") 176 waitoptions.feeThreshold = 0 177 template5 = await template4.result.waitNext(ctx, waitoptions) 178 block4 = await self.parse_and_deserialize_block(template5, ctx) 179 assert_equal(len(block4.vtx), 2) 180 waitoptions.feeThreshold = 1 181 self.log.debug("Wait for another, get one after increase in fees in the mempool") 182 waitnext = template5.result.waitNext(ctx, waitoptions) 183 miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) 184 template6 = await waitnext 185 block4 = await self.parse_and_deserialize_block(template6, ctx) 186 assert_equal(len(block4.vtx), 3) 187 self.log.debug("Wait for another, but time out, since the fee threshold is set now") 188 template7 = await template6.result.waitNext(ctx, waitoptions) 189 assert_equal(template7.to_dict(), {}) 190 191 self.log.debug("interruptWait should abort the current wait") 192 wait_started = asyncio.Event() 193 async def wait_for_block(): 194 new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions() 195 new_waitoptions.timeout = waitoptions.timeout * 60 # 1 minute wait 196 new_waitoptions.feeThreshold = 1 197 wait_started.set() 198 return await template6.result.waitNext(ctx, new_waitoptions) 199 200 async def interrupt_wait(): 201 await wait_started.wait() # Wait for confirmation wait started 202 await asyncio.sleep(0.1) # Minimal buffer 203 template6.result.interruptWait() 204 miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) 205 206 wait_task = asyncio.create_task(wait_for_block()) 207 interrupt_task = asyncio.create_task(interrupt_wait()) 208 209 result = await wait_task 210 await interrupt_task 211 assert_equal(result.to_dict(), {}) 212 213 current_block_height = self.nodes[0].getchaintips()[0]["height"] 214 check_opts = self.capnp_modules['mining'].BlockCheckOptions() 215 template = await mining.result.createNewBlock(opts) 216 block = await self.parse_and_deserialize_block(template, ctx) 217 coinbase = await self.parse_and_deserialize_coinbase_tx(template, ctx) 218 balance = miniwallet.get_balance() 219 coinbase.vout[0].scriptPubKey = miniwallet.get_output_script() 220 coinbase.vout[0].nValue = COIN 221 block.vtx[0] = coinbase 222 block.hashMerkleRoot = block.calc_merkle_root() 223 original_version = block.nVersion 224 self.log.debug("Submit a block with a bad version") 225 block.nVersion = 0 226 block.solve() 227 res = await mining.result.checkBlock(block.serialize(), check_opts) 228 assert_equal(res.result, False) 229 assert_equal(res.reason, "bad-version(0x00000000)") 230 res = await template.result.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize()) 231 assert_equal(res.result, False) 232 self.log.debug("Submit a valid block") 233 block.nVersion = original_version 234 block.solve() 235 236 self.log.debug("First call checkBlock()") 237 res = await mining.result.checkBlock(block.serialize(), check_opts) 238 assert_equal(res.result, True) 239 240 # The remote template block will be mutated, capture the original: 241 remote_block_before = await self.parse_and_deserialize_block(template, ctx) 242 243 self.log.debug("Submitted coinbase must include witness") 244 assert_not_equal(coinbase.serialize_without_witness().hex(), coinbase.serialize().hex()) 245 res = await template.result.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize_without_witness()) 246 assert_equal(res.result, False) 247 248 self.log.debug("Even a rejected submitBlock() mutates the template's block") 249 # Can be used by clients to download and inspect the (rejected) 250 # reconstructed block. 251 remote_block_after = await self.parse_and_deserialize_block(template, ctx) 252 assert_not_equal(remote_block_before.serialize().hex(), remote_block_after.serialize().hex()) 253 254 self.log.debug("Submit again, with the witness") 255 res = await template.result.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize()) 256 assert_equal(res.result, True) 257 258 self.log.debug("Block should propagate") 259 # Check that the IPC node actually updates its own chain 260 assert_equal(self.nodes[0].getchaintips()[0]["height"], current_block_height + 1) 261 # Stalls if a regression causes submitBlock() to accept an invalid block: 262 self.sync_all() 263 # Check that the other node accepts the block 264 assert_equal(self.nodes[0].getchaintips()[0], self.nodes[1].getchaintips()[0]) 265 266 miniwallet.rescan_utxos() 267 assert_equal(miniwallet.get_balance(), balance + 1) 268 self.log.debug("Check block should fail now, since it is a duplicate") 269 res = await mining.result.checkBlock(block.serialize(), check_opts) 270 assert_equal(res.result, False) 271 assert_equal(res.reason, "inconclusive-not-best-prevblk") 272 273 self.log.debug("Destroy template objects") 274 template.result.destroy(ctx) 275 template2.result.destroy(ctx) 276 template3.result.destroy(ctx) 277 template4.result.destroy(ctx) 278 template5.result.destroy(ctx) 279 template6.result.destroy(ctx) 280 template7.result.destroy(ctx) 281 asyncio.run(capnp.run(async_routine())) 282 283 def run_test(self): 284 self.run_echo_test() 285 self.run_mining_test() 286 287 if __name__ == '__main__': 288 IPCInterfaceTest(__file__).main()