|
| 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) |
| 11 | +from test_framework.test_framework import (BitcoinTestFramework, assert_equal) |
| 12 | +from test_framework.wallet import MiniWallet |
| 13 | + |
| 14 | +# Test may be skipped and not have capnp installed |
| 15 | +try: |
| 16 | + import capnp # type: ignore[import] # noqa: F401 |
| 17 | +except ImportError: |
| 18 | + pass |
| 19 | + |
| 20 | + |
| 21 | +class IPCInterfaceTest(BitcoinTestFramework): |
| 22 | + |
| 23 | + def skip_test_if_missing_module(self): |
| 24 | + self.skip_if_no_ipc() |
| 25 | + self.skip_if_no_py_capnp() |
| 26 | + |
| 27 | + def load_capnp_modules(self): |
| 28 | + if capnp_bin := shutil.which("capnp"): |
| 29 | + # Add the system cap'nproto path so include/capnp/c++.capnp can be found. |
| 30 | + capnp_dir = Path(capnp_bin).parent.parent / "include" |
| 31 | + else: |
| 32 | + # If there is no system cap'nproto, the pycapnp module should have its own "bundled" |
| 33 | + # includes at this location. If pycapnp was installed with bundled capnp, |
| 34 | + # capnp/c++.capnp can be found here. |
| 35 | + capnp_dir = Path(capnp.__path__[0]).parent |
| 36 | + src_dir = Path(self.config['environment']['SRCDIR']) / "src" |
| 37 | + mp_dir = src_dir / "ipc" / "libmultiprocess" / "include" |
| 38 | + imports = [str(capnp_dir), str(src_dir), str(mp_dir)] |
| 39 | + return { |
| 40 | + "proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports), |
| 41 | + "init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports), |
| 42 | + "echo": capnp.load(str(src_dir / "ipc" / "capnp" / "echo.capnp"), imports=imports), |
| 43 | + "mining": capnp.load(str(src_dir / "ipc" / "capnp" / "mining.capnp"), imports=imports), |
| 44 | + } |
| 45 | + |
| 46 | + def set_test_params(self): |
| 47 | + self.num_nodes = 1 |
| 48 | + |
| 49 | + def setup_nodes(self): |
| 50 | + self.extra_init = [{"ipcbind": True}] |
| 51 | + super().setup_nodes() |
| 52 | + # Use this function to also load the capnp modules (we cannot use set_test_params for this, |
| 53 | + # as it is being called before knowing whether capnp is available). |
| 54 | + self.capnp_modules = self.load_capnp_modules() |
| 55 | + |
| 56 | + async def make_capnp_init_ctx(self): |
| 57 | + node = self.nodes[0] |
| 58 | + # Establish a connection, and create Init proxy object. |
| 59 | + connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path) |
| 60 | + client = capnp.TwoPartyClient(connection) |
| 61 | + init = client.bootstrap().cast_as(self.capnp_modules['init'].Init) |
| 62 | + # Create a remote thread on the server for the IPC calls to be executed in. |
| 63 | + threadmap = init.construct().threadMap |
| 64 | + thread = threadmap.makeThread("pythread").result |
| 65 | + ctx = self.capnp_modules['proxy'].Context() |
| 66 | + ctx.thread = thread |
| 67 | + # Return both. |
| 68 | + return ctx, init |
| 69 | + |
| 70 | + async def parse_and_deserialize_block(self, block_template, ctx): |
| 71 | + block_data = BytesIO((await block_template.result.getBlock(ctx)).result) |
| 72 | + block = CBlock() |
| 73 | + block.deserialize(block_data) |
| 74 | + return block |
| 75 | + |
| 76 | + def run_echo_test(self): |
| 77 | + self.log.info("Running echo test") |
| 78 | + async def async_routine(): |
| 79 | + ctx, init = await self.make_capnp_init_ctx() |
| 80 | + self.log.debug("Create Echo proxy object") |
| 81 | + echo = init.makeEcho(ctx).result |
| 82 | + self.log.debug("Test a few invocations of echo") |
| 83 | + for s in ["hallo", "", "haha"]: |
| 84 | + result_eval = (await echo.echo(ctx, s)).result |
| 85 | + assert_equal(s, result_eval) |
| 86 | + self.log.debug("Destroy the Echo object") |
| 87 | + echo.destroy(ctx) |
| 88 | + asyncio.run(capnp.run(async_routine())) |
| 89 | + |
| 90 | + def run_mining_test(self): |
| 91 | + self.log.info("Running mining test") |
| 92 | + block_hash_size = 32 |
| 93 | + block_header_size = 80 |
| 94 | + timeout = 1000.0 # 1000 milliseconds |
| 95 | + miniwallet = MiniWallet(self.nodes[0]) |
| 96 | + |
| 97 | + async def async_routine(): |
| 98 | + ctx, init = await self.make_capnp_init_ctx() |
| 99 | + self.log.debug("Create Mining proxy object") |
| 100 | + mining = init.makeMining(ctx) |
| 101 | + self.log.debug("Test simple inspectors") |
| 102 | + assert (await mining.result.isTestChain(ctx)) |
| 103 | + assert (await mining.result.isInitialBlockDownload(ctx)) |
| 104 | + blockref = await mining.result.getTip(ctx) |
| 105 | + assert blockref.hasResult |
| 106 | + assert_equal(len(blockref.result.hash), block_hash_size) |
| 107 | + current_block_height = self.nodes[0].getchaintips()[0]["height"] |
| 108 | + assert blockref.result.height == current_block_height |
| 109 | + self.log.debug("Mine a block") |
| 110 | + wait = mining.result.waitTipChanged(ctx, blockref.result.hash, ) |
| 111 | + self.generate(self.nodes[0], 1) |
| 112 | + newblockref = await wait |
| 113 | + assert_equal(len(newblockref.result.hash), block_hash_size) |
| 114 | + assert_equal(newblockref.result.height, current_block_height + 1) |
| 115 | + self.log.debug("Wait for timeout") |
| 116 | + wait = mining.result.waitTipChanged(ctx, newblockref.result.hash, timeout) |
| 117 | + oldblockref = await wait |
| 118 | + assert_equal(len(newblockref.result.hash), block_hash_size) |
| 119 | + assert_equal(oldblockref.result.hash, newblockref.result.hash) |
| 120 | + assert_equal(oldblockref.result.height, newblockref.result.height) |
| 121 | + |
| 122 | + self.log.debug("Create a template") |
| 123 | + opts = self.capnp_modules['mining'].BlockCreateOptions() |
| 124 | + opts.useMempool = True |
| 125 | + opts.blockReservedWeight = 4000 |
| 126 | + opts.coinbaseOutputMaxAdditionalSigops = 0 |
| 127 | + template = mining.result.createNewBlock(opts) |
| 128 | + self.log.debug("Test some inspectors of Template") |
| 129 | + header = await template.result.getBlockHeader(ctx) |
| 130 | + assert_equal(len(header.result), block_header_size) |
| 131 | + block = await self.parse_and_deserialize_block(template, ctx) |
| 132 | + assert_equal(ser_uint256(block.hashPrevBlock), newblockref.result.hash) |
| 133 | + assert len(block.vtx) >= 1 |
| 134 | + txfees = await template.result.getTxFees(ctx) |
| 135 | + assert_equal(len(txfees.result), 0) |
| 136 | + txsigops = await template.result.getTxSigops(ctx) |
| 137 | + assert_equal(len(txsigops.result), 0) |
| 138 | + coinbase_data = BytesIO((await template.result.getCoinbaseTx(ctx)).result) |
| 139 | + coinbase = CTransaction() |
| 140 | + coinbase.deserialize(coinbase_data) |
| 141 | + assert_equal(coinbase.vin[0].prevout.hash, 0) |
| 142 | + self.log.debug("Wait for a new template") |
| 143 | + waitoptions = self.capnp_modules['mining'].BlockWaitOptions() |
| 144 | + waitoptions.timeout = timeout |
| 145 | + waitnext = template.result.waitNext(ctx, waitoptions) |
| 146 | + self.generate(self.nodes[0], 1) |
| 147 | + template2 = await waitnext |
| 148 | + block2 = await self.parse_and_deserialize_block(template2, ctx) |
| 149 | + assert_equal(len(block2.vtx), 1) |
| 150 | + self.log.debug("Wait for another, but time out") |
| 151 | + template3 = await template2.result.waitNext(ctx, waitoptions) |
| 152 | + assert_equal(template3.to_dict(), {}) |
| 153 | + self.log.debug("Wait for another, get one after increase in fees in the mempool") |
| 154 | + waitnext = template2.result.waitNext(ctx, waitoptions) |
| 155 | + miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) |
| 156 | + template4 = await waitnext |
| 157 | + block3 = await self.parse_and_deserialize_block(template4, ctx) |
| 158 | + assert_equal(len(block3.vtx), 2) |
| 159 | + self.log.debug("Wait again, this should return the same template, since the fee threshold is zero") |
| 160 | + template5 = await template4.result.waitNext(ctx, waitoptions) |
| 161 | + block4 = await self.parse_and_deserialize_block(template5, ctx) |
| 162 | + assert_equal(len(block4.vtx), 2) |
| 163 | + waitoptions.feeThreshold = 1 |
| 164 | + self.log.debug("Wait for another, get one after increase in fees in the mempool") |
| 165 | + waitnext = template5.result.waitNext(ctx, waitoptions) |
| 166 | + miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) |
| 167 | + template6 = await waitnext |
| 168 | + block4 = await self.parse_and_deserialize_block(template6, ctx) |
| 169 | + assert_equal(len(block4.vtx), 3) |
| 170 | + self.log.debug("Wait for another, but time out, since the fee threshold is set now") |
| 171 | + template7 = await template6.result.waitNext(ctx, waitoptions) |
| 172 | + assert_equal(template7.to_dict(), {}) |
| 173 | + self.log.debug("Destroy template objects") |
| 174 | + template.result.destroy(ctx) |
| 175 | + template2.result.destroy(ctx) |
| 176 | + template3.result.destroy(ctx) |
| 177 | + template4.result.destroy(ctx) |
| 178 | + template5.result.destroy(ctx) |
| 179 | + template6.result.destroy(ctx) |
| 180 | + template7.result.destroy(ctx) |
| 181 | + asyncio.run(capnp.run(async_routine())) |
| 182 | + |
| 183 | + def run_test(self): |
| 184 | + self.run_echo_test() |
| 185 | + self.run_mining_test() |
| 186 | + |
| 187 | +if __name__ == '__main__': |
| 188 | + IPCInterfaceTest(__file__).main() |
0 commit comments