mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-04-06 05:37:50 +02:00
Merge bitcoin/bitcoin#34727: test: Add IPC wake-up test and reuse mining context
ad75b147b5test: scale IPC mining wait timeouts by timeout_factor (Enoch Azariah)e7a918b69atest: verify IPC error handling for invalid coinbase (Enoch Azariah)63684d6922test: move make_mining_ctx to ipc_util.py (Enoch Azariah)4ada575d6ctest: verify createNewBlock wakes promptly when tip advances (Enoch Azariah) Pull request description: This is a follow-up to implement a couple of test improvements discussed in recent IPC PRs and issues. - adds a test to `interface_ipc_mining.py` to verify that `createNewBlock` wakes up immediately when the tip advances, rather than waiting for the cooldown timer to expire (https://github.com/bitcoin/bitcoin/pull/34184#discussion_r2842239399). - moves `make_mining_ctx` into `ipc_util.py` so it can be reused across the IPC tests instead of duplicating the setup code (https://github.com/bitcoin/bitcoin/pull/34422#discussion_r2852445430). - adds a test case to verify that providing an invalid coinbase to `submitSolution` returns a remote exception instead of crashing the node, closing the loop on the issue reported in #33341. - scales IPC wait timeouts using the test suite's `timeout_factor` to prevent spurious failures in heavily loaded CI environments, capping extended waits to avoid test runner hangs (bitcoin-core/libmultiprocess#253 (comment)). Closes #33341. ACKs for top commit: Sjors: ACKad75b147b5achow101: ACKad75b147b5sedited: ACKad75b147b5Tree-SHA512: 812aa64c03657761f06707e6a15b8b435ab7c75717a6748a919fcbcae317128e18403b0c1bddd4cdad877d286e69db52389633e4012faaa656acc01939091719
This commit is contained in:
@@ -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
|
||||
@@ -95,10 +94,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
|
||||
@@ -131,10 +127,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
|
||||
|
||||
@@ -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,
|
||||
@@ -37,11 +36,12 @@ 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,
|
||||
assert_capnp_failed
|
||||
)
|
||||
|
||||
# Test may be skipped and not have capnp installed
|
||||
@@ -106,21 +106,14 @@ 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")
|
||||
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 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)
|
||||
@@ -139,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)
|
||||
@@ -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
|
||||
@@ -179,10 +172,10 @@ 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 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")
|
||||
@@ -199,6 +192,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)
|
||||
@@ -273,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
|
||||
@@ -290,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:
|
||||
@@ -313,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()))
|
||||
|
||||
@@ -330,7 +334,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()
|
||||
@@ -345,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()
|
||||
|
||||
@@ -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:
|
||||
@@ -148,3 +152,21 @@ 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
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user