From 4ada575d6c6880f7d302c1f37581a82f394c0b5e Mon Sep 17 00:00:00 2001 From: Enoch Azariah Date: Wed, 4 Mar 2026 00:25:51 +0100 Subject: [PATCH 1/4] test: verify createNewBlock wakes promptly when tip advances This adds a complementary test to interface_ipc_mining.py to ensure that createNewBlock() wakes up immediately once submitblock advances the tip, rather than needlessly waiting for the cooldown timer to expire on its own. --- test/functional/interface_ipc_mining.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index bcb7f5f5d86..67f77fee5f7 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -199,6 +199,25 @@ class IPCMiningTest(BitcoinTestFramework): # spurious failures. assert_greater_than_or_equal(time.time() - start, 0.9) + self.log.debug("createNewBlock() should wake up promptly after tip advances") + success = False + duration = 0.0 + async def wait_fn(): + nonlocal success, duration + start = time.time() + res = await mining.createNewBlock(ctx, self.default_block_create_options) + duration = time.time() - start + success = res._has("result") + def do_fn(): + block_hex = self.nodes[1].getblock(node1_block_hash, False) + self.nodes[0].submitblock(block_hex) + await wait_and_do(wait_fn(), do_fn) + assert_equal(success, True) + if self.options.timeout_factor <= 1: + assert duration < 3.0, f"createNewBlock took {duration:.2f}s, did not wake up promptly after tip advances" + else: + self.log.debug("Skipping strict wake-up duration check because timeout_factor > 1") + self.log.debug("interrupt() should abort createNewBlock() during cooldown") async def create_block(): result = await mining.createNewBlock(ctx, self.default_block_create_options) From 63684d6922e5c034ff620d23c35ba175edae49c4 Mon Sep 17 00:00:00 2001 From: Enoch Azariah Date: Wed, 4 Mar 2026 00:41:08 +0100 Subject: [PATCH 2/4] test: move make_mining_ctx to ipc_util.py The async routines in both interface_ipc.py and interface_ipc_mining.py contain redundant code to initialize the mining proxy object. Move the make_mining_ctx helper into test_framework/ipc_util.py and update both test files to use it. This removes the boilerplate and prevents code duplication across the IPC test suite. --- test/functional/interface_ipc.py | 15 ++++----------- test/functional/interface_ipc_mining.py | 19 ++++++------------- test/functional/test_framework/ipc_util.py | 7 +++++++ 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/test/functional/interface_ipc.py b/test/functional/interface_ipc.py index 1e279e00cb3..71a49685bf7 100755 --- a/test/functional/interface_ipc.py +++ b/test/functional/interface_ipc.py @@ -11,6 +11,7 @@ from test_framework.util import assert_equal from test_framework.ipc_util import ( load_capnp_modules, make_capnp_init_ctx, + make_mining_ctx, ) # Test may be skipped and not have capnp installed @@ -55,9 +56,7 @@ class IPCInterfaceTest(BitcoinTestFramework): block_hash_size = 32 async def async_routine(): - ctx, init = await make_capnp_init_ctx(self) - self.log.debug("Create Mining proxy object") - mining = init.makeMining(ctx).result + ctx, mining = await make_mining_ctx(self) self.log.debug("Test simple inspectors") assert (await mining.isTestChain(ctx)).result assert not (await mining.isInitialBlockDownload(ctx)).result @@ -93,10 +92,7 @@ class IPCInterfaceTest(BitcoinTestFramework): disconnected_log_check = ExitStack() async def async_routine(): - ctx, init = await make_capnp_init_ctx(self) - self.log.debug("Create Mining proxy object") - mining = init.makeMining(ctx).result - + ctx, mining = await make_mining_ctx(self) self.log.debug("Create a template") opts = self.capnp_modules['mining'].BlockCreateOptions() template = (await mining.createNewBlock(ctx, opts)).result @@ -129,10 +125,7 @@ class IPCInterfaceTest(BitcoinTestFramework): timeout = self.rpc_timeout * 1000.0 async def async_routine(): - ctx, init = await make_capnp_init_ctx(self) - self.log.debug("Create Mining proxy object") - mining = init.makeMining(ctx).result - + ctx, mining = await make_mining_ctx(self) self.log.debug("Create a template") opts = self.capnp_modules['mining'].BlockCreateOptions() template = (await mining.createNewBlock(ctx, opts)).result diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index 67f77fee5f7..6f2237a883d 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -37,11 +37,11 @@ from test_framework.ipc_util import ( destroying, mining_create_block_template, load_capnp_modules, - make_capnp_init_ctx, mining_get_block, mining_get_coinbase_tx, mining_wait_next_template, wait_and_do, + make_mining_ctx, ) # Test may be skipped and not have capnp installed @@ -106,13 +106,6 @@ class IPCMiningTest(BitcoinTestFramework): coinbase_tx.nLockTime = coinbase_res.lockTime return coinbase_tx - async def make_mining_ctx(self): - """Create IPC context and Mining proxy object.""" - ctx, init = await make_capnp_init_ctx(self) - self.log.debug("Create Mining proxy object") - mining = init.makeMining(ctx).result - return ctx, mining - def run_mining_interface_test(self): """Test Mining interface methods.""" self.log.info("Running Mining interface test") @@ -120,7 +113,7 @@ class IPCMiningTest(BitcoinTestFramework): timeout = 1000.0 # 1000 milliseconds async def async_routine(): - ctx, mining = await self.make_mining_ctx() + ctx, mining = await make_mining_ctx(self) blockref = await mining.getTip(ctx) current_block_height = self.nodes[0].getchaintips()[0]["height"] assert_equal(blockref.result.height, current_block_height) @@ -159,7 +152,7 @@ class IPCMiningTest(BitcoinTestFramework): async def async_routine(): while True: try: - ctx, mining = await self.make_mining_ctx() + ctx, mining = await make_mining_ctx(self) break except (ConnectionRefusedError, FileNotFoundError): # Poll quickly to connect as soon as socket becomes @@ -182,7 +175,7 @@ class IPCMiningTest(BitcoinTestFramework): timeout = 1000.0 # 1000 milliseconds async def async_routine(): - ctx, mining = await self.make_mining_ctx() + ctx, mining = await make_mining_ctx(self) async with AsyncExitStack() as stack: self.log.debug("createNewBlock() should wait if tip is still updating") @@ -309,7 +302,7 @@ class IPCMiningTest(BitcoinTestFramework): self.restart_node(0, extra_args=[f"-blockreservedweight={MAX_BLOCK_WEIGHT}"]) async def async_routine(): - ctx, mining = await self.make_mining_ctx() + ctx, mining = await make_mining_ctx(self) self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]) async with AsyncExitStack() as stack: @@ -349,7 +342,7 @@ class IPCMiningTest(BitcoinTestFramework): self.log.info("Running coinbase construction and submission test") async def async_routine(): - ctx, mining = await self.make_mining_ctx() + ctx, mining = await make_mining_ctx(self) current_block_height = self.nodes[0].getchaintips()[0]["height"] check_opts = self.capnp_modules['mining'].BlockCheckOptions() diff --git a/test/functional/test_framework/ipc_util.py b/test/functional/test_framework/ipc_util.py index 11497463eb3..c80f78f79b7 100644 --- a/test/functional/test_framework/ipc_util.py +++ b/test/functional/test_framework/ipc_util.py @@ -148,3 +148,10 @@ async def mining_get_coinbase_tx(block_template, ctx) -> CoinbaseTxData: requiredOutputs=[bytes(output) for output in template_capnp.requiredOutputs], lockTime=int(template_capnp.lockTime), ) + +async def make_mining_ctx(self): + """Create IPC context and Mining proxy object.""" + ctx, init = await make_capnp_init_ctx(self) + self.log.debug("Create Mining proxy object") + mining = init.makeMining(ctx).result + return ctx, mining From e7a918b69a5e6aaf3f02ca84ca6cde14401928ed Mon Sep 17 00:00:00 2001 From: Enoch Azariah Date: Mon, 9 Mar 2026 11:06:55 +0100 Subject: [PATCH 3/4] test: verify IPC error handling for invalid coinbase Add a test case to interface_ipc_mining.py to verify that the IPC server correctly handles and reports serialization errors rather than crashing the node. This covers the scenario where submitSolution is called with data that cannot be deserialized, as discussed in #33341 Also introduces the assert_capnp_failed helper in ipc_util.py to cleanly handle macOS-specific Cap'n Proto exception strings, and refactors an existing block weight test to use it. --- test/functional/interface_ipc_mining.py | 19 +++++++++---------- test/functional/test_framework/ipc_util.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index 6f2237a883d..0c3b9cf869c 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -7,7 +7,6 @@ import asyncio import time from contextlib import AsyncExitStack from io import BytesIO -import platform from test_framework.blocktools import NULL_OUTPOINT from test_framework.messages import ( MAX_BLOCK_WEIGHT, @@ -42,6 +41,7 @@ from test_framework.ipc_util import ( mining_wait_next_template, wait_and_do, make_mining_ctx, + assert_capnp_failed ) # Test may be skipped and not have capnp installed @@ -325,15 +325,7 @@ class IPCMiningTest(BitcoinTestFramework): await mining.createNewBlock(ctx, opts) raise AssertionError("createNewBlock unexpectedly succeeded") except capnp.lib.capnp.KjException as e: - if e.description == "remote exception: unknown non-KJ exception of type: kj::Exception": - # macOS + REDUCE_EXPORTS bug: Cap'n Proto fails to recognize - # its own exception type and returns a generic error instead. - # https://github.com/bitcoin/bitcoin/pull/34422#discussion_r2863852691 - # Assert this only occurs on Darwin until fixed. - assert_equal(platform.system(), "Darwin") - else: - assert_equal(e.description, "remote exception: std::exception: block_reserved_weight (0) must be at least 2000 weight units") - assert_equal(e.type, "FAILED") + assert_capnp_failed(e, "remote exception: std::exception: block_reserved_weight (0) must be at least 2000 weight units") asyncio.run(capnp.run(async_routine())) @@ -357,6 +349,13 @@ class IPCMiningTest(BitcoinTestFramework): block.hashMerkleRoot = block.calc_merkle_root() original_version = block.nVersion + self.log.debug("Submit solution that can't be deserialized") + try: + await template.submitSolution(ctx, 0, 0, 0, b"") + raise AssertionError("submitSolution unexpectedly succeeded") + except capnp.lib.capnp.KjException as e: + assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:") + self.log.debug("Submit a block with a bad version") block.nVersion = 0 block.solve() diff --git a/test/functional/test_framework/ipc_util.py b/test/functional/test_framework/ipc_util.py index c80f78f79b7..03e3b5d511b 100644 --- a/test/functional/test_framework/ipc_util.py +++ b/test/functional/test_framework/ipc_util.py @@ -10,9 +10,13 @@ from dataclasses import dataclass from io import BytesIO from pathlib import Path import shutil +import platform from typing import Optional from test_framework.messages import CBlock +from test_framework.util import ( + assert_equal +) # Test may be skipped and not have capnp installed try: @@ -155,3 +159,14 @@ async def make_mining_ctx(self): self.log.debug("Create Mining proxy object") mining = init.makeMining(ctx).result return ctx, mining + +def assert_capnp_failed(e, description_prefix): + if e.description == "remote exception: unknown non-KJ exception of type: kj::Exception": + # macOS + REDUCE_EXPORTS bug: Cap'n Proto fails to recognize + # its own exception type and returns a generic error instead. + # https://github.com/bitcoin/bitcoin/pull/34422#discussion_r2863852691 + # Assert this only occurs on Darwin until fixed. + assert_equal(platform.system(), "Darwin") + else: + assert e.description.startswith(description_prefix), f"Expected description starting with '{description_prefix}', got '{e.description}'" + assert_equal(e.type, "FAILED") From ad75b147b5c3ab5eac268a1c1ced23894a8a79ba Mon Sep 17 00:00:00 2001 From: Enoch Azariah Date: Fri, 13 Mar 2026 11:13:25 +0100 Subject: [PATCH 4/4] test: scale IPC mining wait timeouts by timeout_factor The IPC mining tests (interface_ipc_mining.py) currently use hardcoded timeouts (e.g., 1000ms, 60000ms) for operations like waitTipChanged and waiting for block templates. In heavily loaded CI environments, such as those running sanitizers with high parallelism, these hardcoded timeouts can be too short, leading to spurious test failures and brittleness. This commit multiplies these timeout variables by the test suite's global `self.options.timeout_factor`. This ensures that the IPC wait conditions scale appropriately when the test suite is run with a higher timeout factor, making the tests robust against slow execution environments. Addresses CI brittleness observed in bitcoin-core/libmultiprocess#253. --- test/functional/interface_ipc_mining.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index 0c3b9cf869c..31ccec63fa8 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -110,7 +110,7 @@ class IPCMiningTest(BitcoinTestFramework): """Test Mining interface methods.""" self.log.info("Running Mining interface test") block_hash_size = 32 - timeout = 1000.0 # 1000 milliseconds + timeout = 1000.0 * self.options.timeout_factor # 1000 milliseconds async def async_routine(): ctx, mining = await make_mining_ctx(self) @@ -132,7 +132,7 @@ class IPCMiningTest(BitcoinTestFramework): self.log.debug("interrupt() should abort waitTipChanged()") async def wait_for_tip(): - long_timeout = 60000.0 # 1 minute + long_timeout = max(timeout, 60000.0) # at least 1 minute result = (await mining.waitTipChanged(ctx, newblockref.hash, long_timeout)).result # Unlike a timeout, interrupt() returns an empty BlockRef. assert_equal(len(result.hash), 0) @@ -172,7 +172,7 @@ class IPCMiningTest(BitcoinTestFramework): """Test BlockTemplate interface methods.""" self.log.info("Running BlockTemplate interface test") block_header_size = 80 - timeout = 1000.0 # 1000 milliseconds + timeout = 1000.0 * self.options.timeout_factor async def async_routine(): ctx, mining = await make_mining_ctx(self) @@ -285,7 +285,7 @@ class IPCMiningTest(BitcoinTestFramework): self.log.debug("interruptWait should abort the current wait") async def wait_for_block(): new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions() - new_waitoptions.timeout = timeout * 60 # 1 minute wait + new_waitoptions.timeout = max(timeout, 60000.0) # at least 1 minute new_waitoptions.feeThreshold = 1 template7 = await mining_wait_next_template(template6, stack, ctx, new_waitoptions) assert template7 is None