From 2f2aa574b2a54dc18ce063162072274788d39ee5 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 12 Feb 2026 02:10:18 +0100 Subject: [PATCH 1/3] contrib/signet/miner: support injection of custom txs --- contrib/signet/miner | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/contrib/signet/miner b/contrib/signet/miner index a92529ec8680..9efc831d1406 100755 --- a/contrib/signet/miner +++ b/contrib/signet/miner @@ -332,7 +332,13 @@ class Generate: return tmpl - def mine(self, bcli, wallets, grind_cmd, tmpl, reward_spk, poolid): + def mine(self, bcli, wallets, grind_cmd, tmpl, reward_spk, poolid, override_txs): + if override_txs is not None: + tmpl["transactions"] = [] # TODO: could keep them if there is enough block space left + for override_tx in override_txs: + # TODO: provide other fields as well + tmpl["transactions"].append({"data": override_tx}) + psbt = generate_psbt(tmpl, reward_spk, blocktime=self.mine_time, poolid=poolid) input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') if wallets: @@ -416,6 +422,21 @@ def do_generate(args): else: prefer_cli = None + if args.custom_txs_file is not None: + if max_blocks != 1: + logging.error("--custom-txs-file is only allowed if a single block is mined") + return 1 + override_txs = [] + with open(args.custom_txs_file, 'r') as f: + for line in f: + line = line.strip() + if line.startswith('#'): # skip comment lines + continue + # TODO: sanity check input (deserialize to CTransaction?) to error early + override_txs.append(line) + else: + override_txs = None + poolid = get_poolid(args) prefer_poolid = get_prefer_poolid(args) @@ -479,7 +500,7 @@ def do_generate(args): tmpl = None if tmpl is not None: logging.debug("Preferred GBT template: %s", tmpl) - block = gen.mine(args.bcli, wallets, args.grind_cmd, tmpl, reward_spk, poolid=prefer_poolid) + block = gen.mine(args.bcli, wallets, args.grind_cmd, tmpl, reward_spk, poolid=prefer_poolid, override_txs=override_txs) if block is None: logging.warning("Unable to mine preferred template") else: @@ -497,7 +518,7 @@ def do_generate(args): continue logging.debug("GBT template: %s", tmpl) - block = gen.mine(args.bcli, wallets, args.grind_cmd, tmpl, reward_spk, poolid=poolid) + block = gen.mine(args.bcli, wallets, args.grind_cmd, tmpl, reward_spk, poolid=poolid, override_txs=override_txs) if block is None: logging.error("Unable to mine template") return 1 @@ -595,6 +616,7 @@ def main(): generate.add_argument("--max-interval", default=1800, type=int, help="Maximum interblock interval (seconds)") generate.add_argument("--wallets", default=None, type=str, help="Wallets used for signing, separated by commas") generate.add_argument("--nversion", default=None, type=str, help="Override block nVersion (specify as hex)") + generate.add_argument("--custom-txs-file", default=None, type=str, help="File with custom txs to override template (one tx per line, in hex)") calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty") calibrate.set_defaults(fn=do_calibrate) From 724b3648ce5fc5bb405d4f4fdbfaccf2f8287f04 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 12 Feb 2026 19:25:08 +0100 Subject: [PATCH 2/3] test: refactor: introduce `mine_block` method in tool_signet_miner.py --- test/functional/tool_signet_miner.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/functional/tool_signet_miner.py b/test/functional/tool_signet_miner.py index 008415855482..5c80af14cd6d 100755 --- a/test/functional/tool_signet_miner.py +++ b/test/functional/tool_signet_miner.py @@ -41,12 +41,8 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_bitcoin_util() - def run_test(self): - node = self.nodes[0] - # import private key needed for signing block - node.importprivkey(bytes_to_wif(CHALLENGE_PRIVATE_KEY)) - - # generate block with signet miner tool + def mine_block(self, node): + n_blocks = node.getblockcount() base_dir = self.config["environment"]["SRCDIR"] signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner") subprocess.run([ @@ -60,7 +56,14 @@ def run_test(self): f'--set-block-time={int(time.time())}', '--poolnum=99', ], check=True, stderr=subprocess.STDOUT) - assert_equal(node.getblockcount(), 1) + assert_equal(node.getblockcount(), n_blocks + 1) + + def run_test(self): + node = self.nodes[0] + # import private key needed for signing block + node.importprivkey(bytes_to_wif(CHALLENGE_PRIVATE_KEY)) + # generate block with signet miner tool + self.mine_block(node) if __name__ == "__main__": From f4833c3ce5ba22a98f88c2489750443bffe5a4d1 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Thu, 12 Feb 2026 19:36:10 +0100 Subject: [PATCH 3/3] test: add test for signet miner txs injection (`--custom-txs-file`) --- test/functional/tool_signet_miner.py | 60 ++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/test/functional/tool_signet_miner.py b/test/functional/tool_signet_miner.py index 5c80af14cd6d..5128b93ef7a9 100755 --- a/test/functional/tool_signet_miner.py +++ b/test/functional/tool_signet_miner.py @@ -9,11 +9,12 @@ import sys import time -from test_framework.blocktools import DIFF_1_N_BITS +from test_framework.blocktools import COINBASE_MATURITY, DIFF_1_N_BITS from test_framework.key import ECKey from test_framework.script_util import key_to_p2wpkh_script from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +from test_framework.wallet import MiniWallet from test_framework.wallet_util import bytes_to_wif @@ -41,10 +42,30 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_bitcoin_util() - def mine_block(self, node): + # TODO: this is a copy of `mine_block` below, the only difference + # being that `--max-blocks` is passed instead of `--set-block-time` + def mine_initial_blocks(self, node, num_blocks): n_blocks = node.getblockcount() base_dir = self.config["environment"]["SRCDIR"] signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner") + subprocess.run([ + sys.executable, + signet_miner_path, + f'--cli={node.cli.binary} -datadir={node.cli.datadir}', + 'generate', + f'--address={self.wallet_addr}', + f'--grind-cmd={self.options.bitcoinutil} grind', + f'--nbits={DIFF_1_N_BITS:08x}', + f'--max-blocks={num_blocks}', + '--poolnum=99', + ], check=True, stderr=subprocess.STDOUT) + assert_equal(node.getblockcount(), n_blocks + num_blocks) + + def mine_block(self, node, custom_txs_file=None): + n_blocks = node.getblockcount() + base_dir = self.config["environment"]["SRCDIR"] + signet_miner_path = os.path.join(base_dir, "contrib", "signet", "miner") + custom_txs_arg = [f'--custom-txs-file={custom_txs_file}'] if custom_txs_file else [] subprocess.run([ sys.executable, signet_miner_path, @@ -55,15 +76,46 @@ def mine_block(self, node): f'--nbits={DIFF_1_N_BITS:08x}', f'--set-block-time={int(time.time())}', '--poolnum=99', - ], check=True, stderr=subprocess.STDOUT) + ] + custom_txs_arg, check=True, stderr=subprocess.STDOUT) assert_equal(node.getblockcount(), n_blocks + 1) def run_test(self): node = self.nodes[0] # import private key needed for signing block node.importprivkey(bytes_to_wif(CHALLENGE_PRIVATE_KEY)) - # generate block with signet miner tool + + self.log.info("Mine blocks to create spendable UTXOs (mature coinbase outputs)") + self.wallet = MiniWallet(node) + # translate MiniWallet address to Signet (due to different bech32 HRP) + self.wallet_addr = node.decodescript(self.wallet.get_output_script().hex())['address'] + + self.mine_initial_blocks(node, COINBASE_MATURITY + 5) + self.wallet.rescan_utxos() + assert len(self.wallet.get_utxos(include_immature_coinbase=False, mark_as_spent=False)) > 0 + + self.log.info("Mine block with overrided txs (provided by --custom-tx-file)") + # submit transaction to mempool, should be picked up by the miner + mempool_tx = self.wallet.send_self_transfer(from_node=node) self.mine_block(node) + mined_block = node.getblock(node.getbestblockhash()) + assert_equal(len(mined_block['tx']), 2) # coinbase + miniwallet tx + assert_equal(mined_block['tx'][1], mempool_tx['txid']) + + # submit transaction to mempool, override with some custom (non-standard) txs + mempool_tx = self.wallet.send_self_transfer(from_node=node) + offband_txs = [self.wallet.create_self_transfer(target_vsize=333_000) for _ in range(3)] + custom_txs_file = os.path.join(self.options.tmpdir, "fancy_offband_txs.txt") + with open(custom_txs_file, 'w') as f: + f.write('# this is my fancy off-band transaction, please mine!\n') + for offband_tx in offband_txs: + f.write(offband_tx['hex'] + '\n') + self.mine_block(node, custom_txs_file) + mined_block = node.getblock(node.getbestblockhash()) + assert mined_block['size'] > 990_000 + assert_equal(len(mined_block['tx']), 1 + len(offband_txs)) + assert mempool_tx['txid'] not in mined_block['tx'] + for offband_tx in offband_txs: + assert offband_tx['txid'] in mined_block['tx'] if __name__ == "__main__":