interface_ipc_mining.py raw
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) Mining interface."""
6 import asyncio
7 import time
8 from contextlib import AsyncExitStack
9 from copy import deepcopy
10 from decimal import Decimal
11 from io import BytesIO
12 from test_framework.blocktools import (
13 NULL_OUTPOINT,
14 script_BIP34_coinbase_height,
15 WITNESS_COMMITMENT_HEADER,
16 )
17 from test_framework.messages import (
18 CBlockHeader,
19 COIN,
20 CTransaction,
21 CTxIn,
22 CTxInWitness,
23 CTxOut,
24 DEFAULT_BLOCK_RESERVED_WEIGHT,
25 MAX_BLOCK_SIGOPS_COST,
26 MAX_BLOCK_WEIGHT,
27 from_hex,
28 msg_headers,
29 ser_uint256,
30 )
31 from test_framework.test_framework import BitcoinTestFramework
32 from test_framework.util import (
33 assert_equal,
34 assert_greater_than_or_equal,
35 assert_not_equal,
36 )
37 from test_framework.wallet import MiniWallet
38 from test_framework.p2p import P2PInterface
39 from test_framework.ipc_util import (
40 assert_capnp_failed,
41 assert_create_new_block_fails,
42 destroying,
43 load_capnp_modules,
44 make_mining_ctx,
45 mining_create_block_template,
46 mining_get_block,
47 mining_get_coinbase_tx,
48 mining_wait_next_template,
49 wait_and_do,
50 )
51
52 # Test may be skipped and not have capnp installed
53 try:
54 import capnp # type: ignore[import] # noqa: F401
55 except ModuleNotFoundError:
56 pass
57
58
59 class IPCMiningTest(BitcoinTestFramework):
60
61 def skip_test_if_missing_module(self):
62 self.skip_if_no_ipc()
63 self.skip_if_no_py_capnp()
64
65 def set_test_params(self):
66 self.num_nodes = 3
67
68 def setup_nodes(self):
69 self.extra_init = [{"ipcbind": True}, {}, {"ipcbind": True}]
70 super().setup_nodes()
71 # Use this function to also load the capnp modules (we cannot use set_test_params for this,
72 # as it is being called before knowing whether capnp is available).
73 self.capnp_modules = load_capnp_modules(self.config)
74
75 async def build_coinbase_test(self, template, ctx, miniwallet, extra_nonce=b""):
76 self.log.debug("Build coinbase transaction using getCoinbaseTx()")
77 assert template is not None
78 coinbase_res = await mining_get_coinbase_tx(template, ctx)
79 coinbase_tx = CTransaction()
80 coinbase_tx.version = coinbase_res.version
81 coinbase_tx.vin = [CTxIn()]
82 coinbase_tx.vin[0].prevout = NULL_OUTPOINT
83 coinbase_tx.vin[0].nSequence = coinbase_res.sequence
84
85 # Verify there's no dummy extraNonce in the coinbase scriptSig
86 current_block_height = self.nodes[0].getchaintips()[0]["height"]
87 bip34_prefix = script_BIP34_coinbase_height(current_block_height + 1, padding=False)
88 assert_equal(coinbase_res.scriptSigPrefix, bip34_prefix)
89
90 # Typically a mining pool appends its name and an extraNonce
91 coinbase_tx.vin[0].scriptSig = coinbase_res.scriptSigPrefix + extra_nonce
92
93 # We currently always provide a coinbase witness, even for empty
94 # blocks, but this may change, so always check:
95 has_witness = coinbase_res.witness is not None
96 if has_witness:
97 coinbase_tx.wit.vtxinwit = [CTxInWitness()]
98 coinbase_tx.wit.vtxinwit[0].scriptWitness.stack = [coinbase_res.witness]
99
100 # First output is our payout
101 coinbase_tx.vout = [CTxOut()]
102 coinbase_tx.vout[0].scriptPubKey = miniwallet.get_output_script()
103 coinbase_tx.vout[0].nValue = coinbase_res.blockRewardRemaining
104 # Add SegWit OP_RETURN. This is currently always present even for
105 # empty blocks, but this may change.
106 for output_data in coinbase_res.requiredOutputs:
107 output = CTxOut()
108 output.deserialize(BytesIO(output_data))
109 coinbase_tx.vout.append(output)
110
111 coinbase_tx.nLockTime = coinbase_res.lockTime
112 return coinbase_tx
113
114 async def build_candidate_block(self, template, ctx):
115 """Build a complete block from a remote BlockTemplate."""
116 block = await mining_get_block(template, ctx)
117 coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet)
118 # Reduce payout for balance comparison simplicity.
119 coinbase.vout[0].nValue = COIN
120 block.vtx[0] = coinbase
121 block.hashMerkleRoot = block.calc_merkle_root()
122 return block
123
124 async def assert_submit_block(self, mining, ctx, block, *, result, reason="", debug=""):
125 submit = await mining.submitBlock(ctx, block.serialize())
126 assert_equal(submit.result, result)
127 assert_equal(submit.reason, reason)
128 assert_equal(submit.debug, debug)
129
130 def run_mining_interface_test(self):
131 """Test Mining interface methods."""
132 self.log.info("Running Mining interface test")
133 block_hash_size = 32
134
135 async def async_routine():
136 ctx, mining = await make_mining_ctx(self)
137 blockref = await mining.getTip(ctx)
138 current_block_height = self.nodes[0].getchaintips()[0]["height"]
139 assert_equal(blockref.result.height, current_block_height)
140
141 self.log.debug("Mine a block")
142 newblockref = (await wait_and_do(
143 mining.waitTipChanged(ctx, blockref.result.hash, self.default_ipc_timeout),
144 lambda: self.generate(self.nodes[0], 1))).result
145 assert_equal(len(newblockref.hash), block_hash_size)
146 assert_equal(newblockref.height, current_block_height + 1)
147 self.log.debug("Wait for timeout")
148 oldblockref = (await mining.waitTipChanged(ctx, newblockref.hash, self.default_ipc_timeout)).result
149 assert_equal(len(newblockref.hash), block_hash_size)
150 assert_equal(oldblockref.hash, newblockref.hash)
151 assert_equal(oldblockref.height, newblockref.height)
152
153 self.log.debug("interrupt() should abort waitTipChanged()")
154 async def wait_for_tip():
155 long_timeout = max(self.default_ipc_timeout, 60000.0) # at least 1 minute
156 result = (await mining.waitTipChanged(ctx, newblockref.hash, long_timeout)).result
157 # Unlike a timeout, interrupt() returns an empty BlockRef.
158 assert_equal(len(result.hash), 0)
159 await wait_and_do(wait_for_tip(), mining.interrupt())
160
161 asyncio.run(capnp.run(async_routine()))
162
163 def run_early_startup_test(self):
164 """Make sure mining.createNewBlock safely returns on early startup as
165 soon as mining interface is available """
166 self.log.info("Running Mining interface early startup test")
167
168 node = self.nodes[0]
169 self.stop_node(node.index)
170 node.start()
171
172 async def async_routine():
173 while True:
174 try:
175 ctx, mining = await make_mining_ctx(self)
176 break
177 except (ConnectionRefusedError, FileNotFoundError):
178 # Poll quickly to connect as soon as socket becomes
179 # available but without using a lot of CPU
180 await asyncio.sleep(0.005)
181
182 opts = self.capnp_modules['mining'].BlockCreateOptions()
183 await mining.createNewBlock(ctx, opts)
184
185 asyncio.run(capnp.run(async_routine()))
186
187 # Reconnect nodes so next tests are happy
188 node.wait_for_rpc_connection()
189 self.connect_nodes(1, 0)
190 # Restarting node 0 drops its P2P connection to the rest of the test
191 # chain. Restore the synced 0-1-2 topology before later tests split
192 # node 2 off for submitBlock checks.
193 self.sync_all()
194
195 def run_block_template_test(self):
196 """Test BlockTemplate interface methods."""
197 self.log.info("Running BlockTemplate interface test")
198 block_header_size = 80
199
200 async def async_routine():
201 ctx, mining = await make_mining_ctx(self)
202
203 async with AsyncExitStack() as stack:
204 self.log.debug("createNewBlock() should wait if tip is still updating")
205 self.disconnect_nodes(0, 1)
206 node1_block_hash = self.generate(self.nodes[1], 1, sync_fun=self.no_op)[0]
207 header = from_hex(CBlockHeader(), self.nodes[1].getblockheader(node1_block_hash, False))
208 header_only_peer = self.nodes[0].add_p2p_connection(P2PInterface())
209 header_only_peer.send_and_ping(msg_headers([header]))
210 start = time.time()
211 async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx):
212 pass
213 # Lower-bound only: a heavily loaded CI host might still exceed 0.9s
214 # even without the cooldown, so this can miss regressions but avoids
215 # spurious failures.
216 assert_greater_than_or_equal(time.time() - start, 0.9)
217
218 self.log.debug("createNewBlock() should wake up promptly after tip advances")
219 success = False
220 duration = 0.0
221 async def wait_fn():
222 nonlocal success, duration
223 start = time.time()
224 res = await mining.createNewBlock(ctx, self.default_block_create_options)
225 duration = time.time() - start
226 success = res._has("result")
227 def do_fn():
228 block_hex = self.nodes[1].getblock(node1_block_hash, False)
229 self.nodes[0].submitblock(block_hex)
230 await wait_and_do(wait_fn(), do_fn)
231 assert_equal(success, True)
232 if self.options.timeout_factor <= 1:
233 assert duration < 3.0, f"createNewBlock took {duration:.2f}s, did not wake up promptly after tip advances"
234 else:
235 self.log.debug("Skipping strict wake-up duration check because timeout_factor > 1")
236
237 self.log.debug("interrupt() should abort createNewBlock() during cooldown")
238 async def create_block():
239 result = await mining.createNewBlock(ctx, self.default_block_create_options)
240 # interrupt() causes createNewBlock to return nullptr
241 assert_equal(result._has("result"), False)
242
243 await wait_and_do(create_block(), mining.interrupt())
244
245 header_only_peer.peer_disconnect()
246 self.connect_nodes(0, 1)
247 self.sync_all()
248
249 self.log.debug("Create a template")
250 template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
251 assert template is not None
252
253 self.log.debug("Test some inspectors of Template")
254 header = (await template.getBlockHeader(ctx)).result
255 assert_equal(len(header), block_header_size)
256 block = await mining_get_block(template, ctx)
257 current_tip = self.nodes[0].getbestblockhash()
258 assert_equal(ser_uint256(block.hashPrevBlock), ser_uint256(int(current_tip, 16)))
259 assert_greater_than_or_equal(len(block.vtx), 1)
260 txfees = await template.getTxFees(ctx)
261 assert_equal(len(txfees.result), 0)
262 txsigops = await template.getTxSigops(ctx)
263 assert_equal(len(txsigops.result), 0)
264
265 self.log.debug("Wait for a new template")
266 waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
267 waitoptions.timeout = self.default_ipc_timeout
268 waitoptions.feeThreshold = 1
269 template2 = await wait_and_do(
270 mining_wait_next_template(template, stack, ctx, waitoptions),
271 lambda: self.generate(self.nodes[0], 1))
272 assert template2 is not None
273 block2 = await mining_get_block(template2, ctx)
274 assert_equal(len(block2.vtx), 1)
275
276 self.log.debug("Wait for another, but time out")
277 template3 = await mining_wait_next_template(template2, stack, ctx, waitoptions)
278 assert template3 is None
279
280 self.log.debug("Wait for another, get one after increase in fees in the mempool")
281 template4 = await wait_and_do(
282 mining_wait_next_template(template2, stack, ctx, waitoptions),
283 lambda: self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]))
284 assert template4 is not None
285 block3 = await mining_get_block(template4, ctx)
286 assert_equal(len(block3.vtx), 2)
287
288 self.log.debug("Wait again, this should return the same template, since the fee threshold is zero")
289 waitoptions.feeThreshold = 0
290 template5 = await mining_wait_next_template(template4, stack, ctx, waitoptions)
291 assert template5 is not None
292 block4 = await mining_get_block(template5, ctx)
293 assert_equal(len(block4.vtx), 2)
294 waitoptions.feeThreshold = 1
295
296 self.log.debug("Wait for another, get one after increase in fees in the mempool")
297 template6 = await wait_and_do(
298 mining_wait_next_template(template5, stack, ctx, waitoptions),
299 lambda: self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0]))
300 assert template6 is not None
301 block4 = await mining_get_block(template6, ctx)
302 assert_equal(len(block4.vtx), 3)
303
304 self.log.debug("Wait for another, but time out, since the fee threshold is set now")
305 template7 = await mining_wait_next_template(template6, stack, ctx, waitoptions)
306 assert template7 is None
307
308 self.log.debug("interruptWait should abort the current wait")
309 async def wait_for_block():
310 new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
311 new_waitoptions.timeout = max(self.default_ipc_timeout, 60000.0) # at least 1 minute
312 new_waitoptions.feeThreshold = 1
313 template7 = await mining_wait_next_template(template6, stack, ctx, new_waitoptions)
314 assert template7 is None
315 await wait_and_do(wait_for_block(), template6.interruptWait())
316
317 asyncio.run(capnp.run(async_routine()))
318
319 def run_ipc_option_override_test(self):
320 self.log.info("Running IPC option override test")
321 # Confirm that BlockCreateOptions.blockReservedWeight takes precedence
322 # over -blockreservedweight. Set an absurdly high -blockreservedweight
323 # value that would result in empty blocks to verify this. IPC clients set
324 # blockReservedWeight per template request and are unaffected; later in
325 # the test the IPC template includes a mempool transaction.
326 self.restart_node(0, extra_args=[f"-blockreservedweight={MAX_BLOCK_WEIGHT}"])
327 self.miniwallet.rescan_utxos()
328
329 async def async_routine():
330 ctx, mining = await make_mining_ctx(self)
331 self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
332
333 async with AsyncExitStack() as stack:
334 opts = self.capnp_modules['mining'].BlockCreateOptions()
335 template = await mining_create_block_template(mining, stack, ctx, opts)
336 assert template is not None
337 block = await mining_get_block(template, ctx)
338 assert_equal(len(block.vtx), 2)
339
340 self.log.debug("Use absurdly large reserved weight to force an empty template")
341 opts.blockReservedWeight = MAX_BLOCK_WEIGHT
342 empty_template = await mining_create_block_template(mining, stack, ctx, opts)
343 assert empty_template is not None
344 empty_block = await mining_get_block(empty_template, ctx)
345 assert_equal(len(empty_block.vtx), 1)
346
347 self.log.debug("Enforce minimum reserved weight for IPC clients too")
348 opts.blockReservedWeight = 0
349 await assert_create_new_block_fails(ctx, mining, opts,
350 "block_reserved_weight (0) is lower than minimum safety value of (2000)")
351
352 async def async_routine_check_max_reserved_weight():
353 self.log.debug("Enforce maximum reserved weight for IPC clients too")
354 ctx, mining = await make_mining_ctx(self)
355 opts = self.capnp_modules['mining'].BlockCreateOptions()
356 opts.blockReservedWeight = MAX_BLOCK_WEIGHT + 1
357 await assert_create_new_block_fails(ctx, mining, opts,
358 f"block_reserved_weight ({MAX_BLOCK_WEIGHT + 1}) exceeds consensus maximum block weight ({MAX_BLOCK_WEIGHT})")
359
360 async def async_routine_check_sigops_limit():
361 self.log.debug("Enforce sigops limit for IPC clients too")
362 ctx, mining = await make_mining_ctx(self)
363 opts = self.capnp_modules['mining'].BlockCreateOptions()
364 opts.coinbaseOutputMaxAdditionalSigops = MAX_BLOCK_SIGOPS_COST + 1
365 await assert_create_new_block_fails(ctx, mining, opts,
366 f"coinbase_output_max_additional_sigops ({MAX_BLOCK_SIGOPS_COST + 1}) exceeds consensus maximum block sigops cost ({MAX_BLOCK_SIGOPS_COST})")
367
368 asyncio.run(capnp.run(async_routine()))
369 asyncio.run(capnp.run(async_routine_check_max_reserved_weight()))
370 asyncio.run(capnp.run(async_routine_check_sigops_limit()))
371 self.restart_node(0)
372 self.connect_nodes(0, 1)
373 self.miniwallet.rescan_utxos()
374
375 def run_waitnext_mining_policy_test(self):
376 """Verify that waitNext() preserves the mining policy from -blockmintxfee
377 instead of falling back to defaults."""
378 self.log.info("Running waitNext mining policy test")
379 block_min_tx_fee = Decimal("0.00002000")
380 below_block_min_tx_fee = Decimal("0.00001000")
381 above_block_min_tx_fee = Decimal("0.00003000")
382
383 self.restart_node(0, extra_args=[
384 f"-blockmintxfee={block_min_tx_fee:.8f}",
385 "-minrelaytxfee=0",
386 "-persistmempool=0",
387 ])
388
389 async def async_routine():
390 ctx, mining = await make_mining_ctx(self)
391
392 self.log.debug("Create a below -blockmintxfee transaction")
393 low_fee_tx = self.miniwallet.send_self_transfer(
394 fee_rate=below_block_min_tx_fee,
395 from_node=self.nodes[0],
396 confirmed_only=True,
397 )
398 assert low_fee_tx["txid"] in self.nodes[0].getrawmempool()
399
400 async with AsyncExitStack() as stack:
401 self.log.debug("createNewBlock should respect -blockmintxfee")
402 template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
403 assert template is not None
404 block = await mining_get_block(template, ctx)
405 assert low_fee_tx["txid"] not in {tx.txid_hex for tx in block.vtx[1:]}
406
407 self.log.debug("waitNext should preserve the same mining policy")
408 high_fee_tx = self.miniwallet.send_self_transfer(
409 fee_rate=above_block_min_tx_fee,
410 from_node=self.nodes[0],
411 confirmed_only=True,
412 )
413 mempool_txids = self.nodes[0].getrawmempool()
414 assert high_fee_tx["txid"] in mempool_txids
415 assert low_fee_tx["txid"] in mempool_txids
416 template_next = await mining_wait_next_template(template, stack, ctx, self.default_block_wait_options)
417 assert template_next is not None
418
419 block_next = await mining_get_block(template_next, ctx)
420 block_next_txids = {tx.txid_hex for tx in block_next.vtx[1:]}
421 assert high_fee_tx["txid"] in block_next_txids
422 assert low_fee_tx["txid"] not in block_next_txids
423
424 asyncio.run(capnp.run(async_routine()))
425
426 def run_block_max_weight_test(self):
427 """Verify IPC createNewBlock() and waitNext() preserve the -blockmaxweight policy."""
428 self.log.info("Running block_max_weight test")
429
430 # Cap that leaves room for only a handful of mempool transactions
431 # above DEFAULT_BLOCK_RESERVED_WEIGHT (8000). Well below MAX_BLOCK_WEIGHT
432 # (4_000_000), so any truncation observed here is attributable to the
433 # cap, not to consensus limits or wallet chain limits.
434 small_cap = DEFAULT_BLOCK_RESERVED_WEIGHT + 4000
435 NUM_TXS = 20
436
437 self.restart_node(0, extra_args=[
438 f"-blockmaxweight={small_cap}",
439 "-minrelaytxfee=0",
440 "-persistmempool=0",
441 ])
442 # Refresh miniwallet's UTXO view from the chain after restart.
443 self.miniwallet.rescan_utxos()
444
445 # Fill the mempool enough that the configured block weight cap forces
446 # template truncation.
447 for _ in range(NUM_TXS):
448 self.miniwallet.send_self_transfer(from_node=self.nodes[0], confirmed_only=True)
449 assert_equal(self.nodes[0].getmempoolinfo()["size"], NUM_TXS)
450
451 async def async_routine():
452 ctx, mining = await make_mining_ctx(self)
453 async with AsyncExitStack() as stack:
454 template = await mining_create_block_template(mining, stack, ctx, self.default_block_create_options)
455 assert template is not None
456 block = await mining_get_block(template, ctx)
457 assert_greater_than_or_equal(small_cap, block.get_weight())
458 # Exclude the coinbase; the cap must have forced truncation.
459 initial_included = len(block.vtx) - 1
460 assert initial_included < NUM_TXS, (
461 f"Expected -blockmaxweight={small_cap} to truncate; "
462 f"included {initial_included}/{NUM_TXS} mempool txs"
463 )
464
465 self.log.debug("waitNext should preserve -blockmaxweight")
466 high_fee_tx = self.miniwallet.send_self_transfer(
467 from_node=self.nodes[0],
468 confirmed_only=True,
469 fee_rate=10,
470 )
471 template_next = await mining_wait_next_template(template, stack, ctx, self.default_block_wait_options)
472 assert template_next is not None
473
474 block_next = await mining_get_block(template_next, ctx)
475 assert_greater_than_or_equal(small_cap, block_next.get_weight())
476 assert high_fee_tx["txid"] in {tx.txid_hex for tx in block_next.vtx[1:]}
477 next_included = len(block_next.vtx) - 1
478 assert next_included < NUM_TXS + 1, (
479 f"Expected -blockmaxweight={small_cap} to remain capped after waitNext; "
480 f"included {next_included}/{NUM_TXS + 1} mempool txs"
481 )
482
483 asyncio.run(capnp.run(async_routine()))
484
485 def run_coinbase_and_submission_test(self):
486 """Test coinbase construction (getCoinbaseTx) and block submission (submitSolution)."""
487 self.log.info("Running coinbase construction and submission test")
488
489 async def async_routine():
490 ctx, mining = await make_mining_ctx(self)
491 # Node 0 drives the checkBlock() and submitSolution() checks. Node
492 # 2 has a separate IPC interface and starts synced through node 1,
493 # so it can be isolated below to test submitBlock() without
494 # changing node 0's template and chain state first.
495 ctx2, mining2 = await make_mining_ctx(self, node_index=2)
496
497 current_block_height = self.nodes[0].getchaintips()[0]["height"]
498 check_opts = self.capnp_modules['mining'].BlockCheckOptions()
499
500 # Send a real transaction so the template includes it.
501 self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
502
503 async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as template:
504 block = await self.build_candidate_block(template, ctx)
505 coinbase = block.vtx[0]
506 self.log.debug("Template should include a mempool transaction")
507 assert len(block.vtx) >= 2, "Block should include at least the coinbase and the mempool tx"
508 balance = self.miniwallet.get_balance()
509 original_version = block.nVersion
510
511 self.log.debug("Disconnect node 2 before block submission tests")
512 # The default topology is 2 -> 1 -> 0. Splitting the 1-2 edge
513 # lets node 2 accept/reject complete blocks independently.
514 self.disconnect_nodes(1, 2)
515
516 self.log.debug("submitSolution should reject an empty coinbase")
517 submitted = (await template.submitSolution(ctx, 0, 0, 0, b"")).result
518 assert_equal(submitted, False)
519
520 self.log.debug("Submit solution that can't be deserialized")
521 try:
522 await template.submitSolution(ctx, 0, 0, 0, b"\x00")
523 raise AssertionError("submitSolution unexpectedly succeeded")
524 except capnp.lib.capnp.KjException as e:
525 assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
526
527 self.log.debug("Submit a block with a bad version")
528 block.nVersion = 0
529 block.solve()
530 check = await mining.checkBlock(ctx, block.serialize(), check_opts)
531 assert_equal(check.result, False)
532 assert_equal(check.reason, "bad-version(0x00000000)")
533 assert_equal(check.debug, "rejected nVersion=0x00000000 block")
534 self.log.debug("submitSolution should reject a bad-version block")
535 submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
536 assert_equal(submitted, False)
537 self.log.debug("submitBlock should reject a bad-version block")
538 await self.assert_submit_block(
539 mining2,
540 ctx2,
541 block,
542 result=False,
543 reason="bad-version(0x00000000)",
544 debug="rejected nVersion=0x00000000 block",
545 )
546 self.log.debug("Submit a valid block")
547 block.nVersion = original_version
548 block.solve()
549
550 self.log.debug("First call checkBlock()")
551 block_valid = (await mining.checkBlock(ctx, block.serialize(), check_opts)).result
552 assert_equal(block_valid, True)
553
554 # The remote template block will be mutated, capture the original:
555 remote_block_before = await mining_get_block(template, ctx)
556
557 self.log.debug("Submitted coinbase must include witness")
558 assert_not_equal(coinbase.serialize_without_witness().hex(), coinbase.serialize().hex())
559 missing_witness_block = deepcopy(block)
560 missing_witness_block.vtx[0].wit.vtxinwit = []
561 has_witness_commitment = any(
562 len(o.scriptPubKey) >= 38 and o.scriptPubKey[2:6] == WITNESS_COMMITMENT_HEADER
563 for o in missing_witness_block.vtx[0].vout
564 )
565 assert has_witness_commitment, "Coinbase should have a witness commitment output"
566 missing_witness_block.hashMerkleRoot = missing_witness_block.calc_merkle_root()
567 missing_witness_block.solve()
568 self.log.debug("submitSolution should reject a coinbase missing witness")
569 submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize_without_witness())).result
570 assert_equal(submitted, False)
571
572 self.log.debug("Even a rejected submitSolution() mutates the template's block")
573 # Can be used by clients to download and inspect the (rejected)
574 # reconstructed block.
575 remote_block_after = await mining_get_block(template, ctx)
576 assert_not_equal(remote_block_before.serialize().hex(), remote_block_after.serialize().hex())
577
578 self.log.debug("submitBlock should reject a block missing coinbase witness")
579 await self.assert_submit_block(
580 mining2,
581 ctx2,
582 missing_witness_block,
583 result=False,
584 reason="bad-witness-nonce-size",
585 debug="CheckWitnessMalleation : invalid witness reserved value size",
586 )
587
588 self.log.debug("Submit again, with the witness")
589 submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
590 assert_equal(submitted, True)
591
592 self.log.debug("Submit a valid complete block through the disconnected node")
593 await self.assert_submit_block(mining2, ctx2, block, result=True)
594 assert_equal(self.nodes[2].getchaintips()[0]["height"], current_block_height + 1)
595 self.log.debug("submitBlock should reject the duplicate complete block")
596 await self.assert_submit_block(mining2, ctx2, block, result=False, reason="duplicate")
597
598 self.log.debug("Block should propagate")
599 # Check that the IPC node actually updates its own chain
600 assert_equal(self.nodes[0].getchaintips()[0]["height"], current_block_height + 1)
601 # Stalls if a regression causes submitSolution() to accept an invalid block:
602 self.sync_blocks(self.nodes[:2])
603 # Rejoin node 2 and verify the submitBlock result converges with
604 # the submitSolution result from node 0.
605 self.connect_nodes(1, 2)
606 self.sync_all()
607 # Check that the other node accepts the block
608 assert_equal(self.nodes[0].getchaintips()[0], self.nodes[1].getchaintips()[0])
609
610 self.miniwallet.rescan_utxos()
611 assert_equal(self.miniwallet.get_balance(), balance + 1)
612 self.log.debug("Check block should fail now, since it is a duplicate")
613 check = await mining.checkBlock(ctx, block.serialize(), check_opts)
614 assert_equal(check.result, False)
615 assert_equal(check.reason, "inconclusive-not-best-prevblk")
616 self.log.debug("submitBlock on the same node should fail with duplicate after submitSolution succeeds")
617 await self.assert_submit_block(mining, ctx, block, result=False, reason="duplicate")
618
619 self.log.debug("submitSolution should still return True for a duplicate after submitBlock succeeds")
620 async with destroying((await mining2.createNewBlock(ctx2, self.default_block_create_options)).result, ctx2) as template2:
621 duplicate_block = await self.build_candidate_block(template2, ctx2)
622 duplicate_coinbase = duplicate_block.vtx[0]
623 duplicate_block.solve()
624 self.log.debug("Submit a valid complete block before duplicate submitSolution")
625 await self.assert_submit_block(mining2, ctx2, duplicate_block, result=True)
626 self.nodes[2].waitforblockheight(current_block_height + 2)
627 self.log.debug("submitSolution should accept the duplicate block")
628 submitted = (await template2.submitSolution(ctx2, duplicate_block.nVersion, duplicate_block.nTime, duplicate_block.nNonce, duplicate_coinbase.serialize())).result
629 assert_equal(submitted, True)
630 self.sync_all()
631
632 self.log.debug("Submit the same invalid block twice")
633 async with destroying((await mining2.createNewBlock(ctx2, self.default_block_create_options)).result, ctx2) as template2:
634 invalid_block = await self.build_candidate_block(template2, ctx2)
635 invalid_block.vtx[0].nLockTime = 2**32 - 1
636 invalid_block.hashMerkleRoot = invalid_block.calc_merkle_root()
637 invalid_block.solve()
638 self.log.debug("submitBlock should reject the non-final block")
639 await self.assert_submit_block(
640 mining2,
641 ctx2,
642 invalid_block,
643 result=False,
644 reason="bad-txns-nonfinal",
645 debug="non-final transaction",
646 )
647 self.log.debug("submitBlock should report duplicate-invalid for the same block")
648 await self.assert_submit_block(
649 mining2,
650 ctx2,
651 invalid_block,
652 result=False,
653 reason="duplicate-invalid",
654 debug=f"block {invalid_block.hash_hex} was previously marked invalid",
655 )
656
657 self.log.debug("Submit a malformed complete block")
658 try:
659 await mining2.submitBlock(ctx2, block.serialize()[:-15])
660 raise AssertionError("submitBlock unexpectedly succeeded")
661 except capnp.lib.capnp.KjException as e:
662 assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
663
664 self.log.debug("Submit empty block data")
665 try:
666 await mining2.submitBlock(ctx2, b"")
667 raise AssertionError("submitBlock unexpectedly succeeded")
668 except capnp.lib.capnp.KjException as e:
669 assert_capnp_failed(e, "remote exception: std::exception: SpanReader::read(): end of data:")
670 assert_equal(self.nodes[2].is_node_stopped(), False)
671
672 asyncio.run(capnp.run(async_routine()))
673
674 def run_transaction_lookup_test(self):
675 """Test getTransactionsByTxID() and getTransactionsByWitnessID()."""
676 self.log.info("Running transaction lookup test")
677
678 async def async_routine():
679 ctx, mining = await make_mining_ctx(self)
680 tx1 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
681 tx2 = self.miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0], utxo_to_spend=tx1["new_utxo"])
682
683 self.log.debug("getTransactionsByTxID() returns mempool txs and nulls")
684 raw_txs_txid = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid, tx2["tx"].txid, bytes(32)])
685 assert_equal(len(raw_txs_txid.result), 3)
686 assert_equal(raw_txs_txid.result[0].hex(), tx1["hex"])
687 assert_equal(raw_txs_txid.result[1].hex(), tx2["hex"])
688 assert_equal(raw_txs_txid.result[2], b'')
689
690 self.log.debug("getTransactionsByWitnessID() returns mempool txs and nulls")
691 raw_txs_wtxid = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid, tx2["tx"].wtxid, bytes(32)])
692 assert_equal(len(raw_txs_wtxid.result), 3)
693 assert_equal(raw_txs_wtxid.result[0].hex(), tx1["hex"])
694 assert_equal(raw_txs_wtxid.result[1].hex(), tx2["hex"])
695 assert_equal(raw_txs_wtxid.result[2], b'')
696
697 self.log.debug("Mined transactions are not returned")
698 self.generate(self.nodes[0], 1)
699 self.sync_all()
700 raw_txs = await mining.getTransactionsByTxID(ctx, [tx1["tx"].txid])
701 assert_equal(raw_txs.result[0], b'')
702 raw_txs = await mining.getTransactionsByWitnessID(ctx, [tx1["tx"].wtxid])
703 assert_equal(raw_txs.result[0], b'')
704
705 asyncio.run(capnp.run(async_routine()))
706
707 def run_low_height_test(self):
708 """Test that IPC createNewBlock() works at low block heights on a
709 clean chain, in particular with regard to bad-cb-length.
710
711 createNewBlock pads the scriptSig with a dummy extranonce to pass
712 its internal CheckBlock(). This dummy is omitted from the
713 getCoinbaseTx() script_sig_prefix field. The client provides its
714 own extraNonce via submitSolution()."""
715 self.log.info("Running low block height test")
716
717 node = self.nodes[0]
718 self.stop_node(0)
719 # Clear chain data to start from genesis
720 self.cleanup_folder(node.chain_path)
721 node.start()
722 node.wait_for_rpc_connection()
723 assert_equal(node.getblockcount(), 0)
724
725 async def async_routine():
726 ctx, mining = await make_mining_ctx(self)
727 opts = self.capnp_modules['mining'].BlockCreateOptions()
728
729 # Mine blocks 1-17 to exercise the boundary at height 16, where the
730 # internal scriptSig padding is no longer needed.
731 for height in range(1, 18):
732 async with AsyncExitStack() as stack:
733 # Disable cooldown to avoid hanging in the IBD loop on a fresh chain
734 template = await mining_create_block_template(mining, stack, ctx, opts, cooldown=False)
735 assert template is not None
736 block = await mining_get_block(template, ctx)
737 # Heights <= 16 need extra nonce padding.
738 extra_nonce = b'\xaa\xbb\xcc\xdd' if height <= 16 else b""
739 coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet, extra_nonce=extra_nonce)
740 block.vtx[0] = coinbase
741 block.hashMerkleRoot = block.calc_merkle_root()
742 block.solve()
743 submitted = (await template.submitSolution(ctx, block.nVersion, block.nTime, block.nNonce, coinbase.serialize())).result
744 assert_equal(submitted, True)
745 assert_equal(node.getblockcount(), height)
746
747 asyncio.run(capnp.run(async_routine()))
748
749 def run_test(self):
750 self.miniwallet = MiniWallet(self.nodes[0])
751 # Amount of time in milliseconds the test is allowed to wait or be idle before it should fail.
752 self.default_ipc_timeout = 1000.0 * self.options.timeout_factor
753 self.default_block_create_options = self.capnp_modules['mining'].BlockCreateOptions()
754 self.default_block_wait_options = self.capnp_modules['mining'].BlockWaitOptions()
755 self.default_block_wait_options.timeout = self.default_ipc_timeout
756 self.default_block_wait_options.feeThreshold = 1
757 self.run_mining_interface_test()
758 self.run_early_startup_test()
759 self.run_block_template_test()
760 self.run_coinbase_and_submission_test()
761 self.run_waitnext_mining_policy_test()
762 self.run_block_max_weight_test()
763 self.run_ipc_option_override_test()
764 self.run_transaction_lookup_test()
765
766 # Needs to run last because it resets the chain.
767 self.run_low_height_test()
768
769
770 if __name__ == '__main__':
771 IPCMiningTest(__file__).main()
772