Merge bitcoin/bitcoin#34422: Update libmultiprocess subtree to be more stable with rust IPC client

8fe91f3719 test: Updates needed after bitcoin-core/libmultiprocess#240 (Ryan Ofsky)
b7ca3bf061 Squashed 'src/ipc/libmultiprocess/' changes from 1fc65008f7d..1868a84451f (Ryan Ofsky)
1fea3bae5c ipc, test: Add tests for unclean disconnect and thread busy behavior (Ryan Ofsky)

Pull request description:

  Includes:

  - https://github.com/bitcoin-core/libmultiprocess/pull/241
  - https://github.com/bitcoin-core/libmultiprocess/pull/240
  - https://github.com/bitcoin-core/libmultiprocess/pull/244
  - https://github.com/bitcoin-core/libmultiprocess/pull/245

  The main change is https://github.com/bitcoin-core/libmultiprocess/pull/240 which fixes issues with asynchronous requests (https://github.com/bitcoin/bitcoin/issues/33923) and unclean disconnects (https://github.com/bitcoin/bitcoin/issues/34250) that happen with the rust mining client. It also adds tests for these fixes which had some previous review in #34284 (that PR was closed to simplify dependencies between PRs).

  The changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh)

  Resolves #33923 and #34250

ACKs for top commit:
  Sjors:
    re-ACK 8fe91f3719
  janb84:
    reACK 8fe91f3719
  Eunovo:
    ACK 8fe91f3719

Tree-SHA512: 7e8923610502ebd8603bbea703f82178ab9e956874d394da3451f5268afda2b964d0eeb399a74d49c4123e728a14c27c0296118577a6063ff03b2b8203a257ce
This commit is contained in:
merge-script
2026-03-03 17:03:56 +00:00
14 changed files with 477 additions and 101 deletions

View File

@@ -5,6 +5,7 @@
"""Test the IPC (multiprocess) interface."""
import asyncio
from contextlib import ExitStack
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.ipc_util import (
@@ -81,10 +82,97 @@ class IPCInterfaceTest(BitcoinTestFramework):
assert_equal(e.type, "FAILED")
asyncio.run(capnp.run(async_routine()))
def run_unclean_disconnect_test(self):
"""Test behavior when disconnecting during an IPC call that later
returns a non-null interface pointer. This used to cause a crash as
reported https://github.com/bitcoin/bitcoin/issues/34250, but now just
results in a cancellation log message"""
node = self.nodes[0]
self.log.info("Running disconnect during BlockTemplate.waitNext")
timeout = self.rpc_timeout * 1000.0
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
self.log.debug("Create a template")
opts = self.capnp_modules['mining'].BlockCreateOptions()
template = (await mining.createNewBlock(ctx, opts)).result
self.log.debug("Wait for a new template")
waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
waitoptions.timeout = timeout
waitoptions.feeThreshold = 1
with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
promise = template.waitNext(ctx, waitoptions)
await asyncio.sleep(0.1)
disconnected_log_check.enter_context(node.assert_debug_log(expected_msgs=["IPC server: socket disconnected", "canceled while executing"], timeout=2))
del promise
asyncio.run(capnp.run(async_routine()))
# Wait for socket disconnected log message, then generate a block to
# cause the waitNext() call to return a new template. Look for a
# canceled IPC log message after waitNext returns.
with node.assert_debug_log(expected_msgs=["interrupted (canceled)"], timeout=2):
disconnected_log_check.close()
self.generate(node, 1)
def run_thread_busy_test(self):
"""Test behavior when sending multiple calls to the same server thread
which used to cause a crash as reported
https://github.com/bitcoin/bitcoin/issues/33923."""
node = self.nodes[0]
self.log.info("Running thread busy test")
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
self.log.debug("Create a template")
opts = self.capnp_modules['mining'].BlockCreateOptions()
template = (await mining.createNewBlock(ctx, opts)).result
self.log.debug("Wait for a new template")
waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
waitoptions.timeout = timeout
waitoptions.feeThreshold = 1
# Make multiple waitNext calls where the first will start to
# execute, and the second and third will be posted waiting to
# execute. Previously, the third call would fail calling
# mp::Waiter::post() because the waiting function slot is occupied,
# but now posts are queued.
with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
promise1 = template.waitNext(ctx, waitoptions)
await asyncio.sleep(0.1)
with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
promise2 = template.waitNext(ctx, waitoptions)
await asyncio.sleep(0.1)
with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2):
promise3 = template.waitNext(ctx, waitoptions)
await asyncio.sleep(0.1)
# Generate a new block to make the active waitNext calls return, then clean up.
with node.assert_debug_log(expected_msgs=["IPC server send response"], timeout=2):
self.generate(node, 1, sync_fun=self.no_op)
await ((await promise1).result).destroy(ctx)
await ((await promise2).result).destroy(ctx)
await ((await promise3).result).destroy(ctx)
await template.destroy(ctx)
asyncio.run(capnp.run(async_routine()))
def run_test(self):
self.run_echo_test()
self.run_mining_test()
self.run_deprecated_mining_test()
self.run_unclean_disconnect_test()
self.run_thread_busy_test()
if __name__ == '__main__':
IPCInterfaceTest(__file__).main()

View File

@@ -7,7 +7,7 @@ import asyncio
import time
from contextlib import AsyncExitStack
from io import BytesIO
import re
import platform
from test_framework.blocktools import NULL_OUTPOINT
from test_framework.messages import (
MAX_BLOCK_WEIGHT,
@@ -313,17 +313,15 @@ class IPCMiningTest(BitcoinTestFramework):
await mining.createNewBlock(ctx, opts)
raise AssertionError("createNewBlock unexpectedly succeeded")
except capnp.lib.capnp.KjException as e:
if e.type == "DISCONNECTED":
# The remote exception isn't caught currently and leads to a
# std::terminate call. Just detect and restart in this case.
# This bug is fixed with
# https://github.com/bitcoin-core/libmultiprocess/pull/218
assert_equal(e.description, "Peer disconnected.")
self.nodes[0].wait_until_stopped(expected_ret_code=(-11, -6, 1, 66), expected_stderr=re.compile(""))
self.start_node(0)
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_equal(e.type, "FAILED")
asyncio.run(capnp.run(async_routine()))