test: introduce NodeSigner, run feature_taproot.py without wallet compiled
What changed, and why it matters
This commit only changes Bitcoin Core's internal functional test code. It introduces a small test helper class called NodeSigner so that one specific test (feature_taproot.py) can run even when Bitcoin Core is compiled without its built-in wallet. There is no change to production node or wallet code, and nothing in the commit suggests a security bug or fix.
No security action needed. This is a test-only refactoring. Reviewers may optionally verify that the new NodeSigner helper correctly exercises the same Taproot test paths as the previous wallet-based version.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch adds a NodeSigner class in test_framework/wallet.py that generates keys locally and delegates transaction signing to a test node via the signrawtransactionwithkey RPC. It then updates feature_taproot.py to use NodeSigner instead of the built-in wallet RPCs (getnewaddress, getaddressinfo, listunspent, signrawtransactionwithwallet). The test’s skip_if_no_wallet() guard is removed. All changes are confined to the test framework; no consensus, networking, or wallet runtime code is modified.
Changed components
test/functional/feature_taproot.pytest/functional/test_framework/wallet.pyInspect captured patch +49 / −14
diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py
index eb3bb86e..1a41527e 100755
--- a/test/functional/feature_taproot.py
+++ b/test/functional/feature_taproot.py
@@ -99,6 +99,7 @@ from test_framework.util import (
assert_raises_rpc_error,
assert_equal,
)
+from test_framework.wallet import NodeSigner
from test_framework.wallet_util import generate_keypair
from test_framework.key import (
generate_privkey,
@@ -1409,9 +1410,6 @@ class TaprootTest(BitcoinTestFramework):
parser.add_argument("--dumptests", dest="dump_tests", default=False, action="store_true",
help="Dump generated test cases to directory set by TEST_DUMP_DIR environment variable")
- def skip_test_if_missing_module(self):
- self.skip_if_no_wallet()
-
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
@@ -1468,18 +1466,16 @@ class TaprootTest(BitcoinTestFramework):
host_spks = []
host_pubkeys = []
for i in range(16):
- addr = node.getnewaddress(address_type=random.choice(["legacy", "p2sh-segwit", "bech32"]))
- info = node.getaddressinfo(addr)
- spk = bytes.fromhex(info['scriptPubKey'])
+ pubkey, spk, _ = self.nodesigner.getnewaddress(address_type=random.choice(["legacy", "p2sh-segwit", "bech32"]))
host_spks.append(spk)
- host_pubkeys.append(bytes.fromhex(info['pubkey']))
+ host_pubkeys.append(pubkey)
self.init_blockinfo(node)
# Create transactions spending up to 50 of the wallet's inputs, with one output for each spender, and
# one change output at the end. The transaction is constructed on the Python side to enable
- # having multiple outputs to the same address and outputs with no assigned address. The wallet
- # is then asked to sign it through signrawtransactionwithwallet, and then added to a block on the
+ # having multiple outputs to the same address and outputs with no assigned address. The node
+ # is then asked to sign it through signrawtransactionwithkey, and then added to a block on the
# Python side (to bypass standardness rules).
self.log.info("- Creating test UTXOs...")
random.shuffle(spenders)
@@ -1492,7 +1488,7 @@ class TaprootTest(BitcoinTestFramework):
fund_tx = CTransaction()
# Add the 50 highest-value inputs
- unspents = node.listunspent()
+ unspents = self.nodesigner.listunspent()
random.shuffle(unspents)
unspents.sort(key=lambda x: int(x["amount"] * 100000000), reverse=True)
if len(unspents) > 50:
@@ -1515,8 +1511,8 @@ class TaprootTest(BitcoinTestFramework):
fund_tx.vout.append(CTxOut(amount, spenders[done + i].script))
# Add change
fund_tx.vout.append(CTxOut(balance - 10000, random.choice(host_spks)))
- # Ask the wallet to sign
- fund_tx = tx_from_hex(node.signrawtransactionwithwallet(fund_tx.serialize().hex())["hex"])
+ # Ask the node to sign
+ fund_tx = tx_from_hex(self.nodesigner.signrawtransaction(fund_tx.serialize().hex(), unspents)["hex"])
# Construct UTXOData entries
for i in range(count_this_tx):
utxodata = UTXOData(outpoint=COutPoint(fund_tx.txid_int, i), output=fund_tx.vout[i], spender=spenders[done])
@@ -1668,7 +1664,7 @@ class TaprootTest(BitcoinTestFramework):
block.solve()
self.nodes[0].submitblock(block.serialize().hex())
assert_equal(self.nodes[0].getblockcount(), 1)
- self.generate(self.nodes[0], COINBASE_MATURITY)
+ self.generatetoaddress(self.nodes[0], COINBASE_MATURITY, self.nodesigner.getnewaddress()[2])
SEED = 317
VALID_LEAF_VERS = list(range(0xc0, 0x100, 2)) + [0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe]
@@ -1879,6 +1875,7 @@ class TaprootTest(BitcoinTestFramework):
print(json.dumps(tests, indent=4, sort_keys=False))
def run_test(self):
+ self.nodesigner = NodeSigner(self.nodes[0])
self.gen_test_vectors()
self.log.info("Post-activation tests...")
diff --git a/test/functional/test_framework/wallet.py b/test/functional/test_framework/wallet.py
index 21ff7d09..dbf24ec6 100644
--- a/test/functional/test_framework/wallet.py
+++ b/test/functional/test_framework/wallet.py
@@ -54,7 +54,10 @@ from test_framework.util import (
assert_greater_than_or_equal,
get_fee,
)
-from test_framework.wallet_util import generate_keypair
+from test_framework.wallet_util import (
+ bytes_to_wif,
+ generate_keypair,
+)
DEFAULT_FEE = Decimal("0.0001")
@@ -419,6 +422,41 @@ class MiniWallet:
return chain
+class NodeSigner:
+ """Simple wallet replacement that delegates signing of existing raw transactions to a node by
+ using the `signrawtransactionwithkey` RPC. This can be used for spending from widespread
+ output types (P2PKH, P2WPKH, P2SH-P2WPKH, P2TR) without having the wallet compiled in."""
+ def __init__(self, node):
+ self._node = node
+ self._key_entries = []
+
+ def getnewaddress(self, address_type='legacy'):
+ (seckey, pubkey), spk, address = getnewdestination(address_type)
+ redeem_script = key_to_p2wpkh_script(pubkey) if address_type == 'p2sh-segwit' else None
+ self._key_entries.append({"seckey_wif": bytes_to_wif(seckey.get_bytes()), "output_script": spk, "redeem_script": redeem_script})
+ return pubkey, spk, address
+
+ def listunspent(self):
+ needles = [descsum_create(f'raw({key_entry["output_script"].hex()})') for key_entry in self._key_entries]
+ scan_res = self._node.scantxoutset(action="start", scanobjects=needles)
+ spend_height = scan_res['height'] + 1 # coins would be spent in the next block
+ unspents = []
+ for u in scan_res['unspents']:
+ if u["coinbase"] and (spend_height - u["height"]) < COINBASE_MATURITY: # skip immature coins
+ continue
+ unspent = { "txid": u["txid"], "vout": u["vout"], "scriptPubKey": u["scriptPubKey"], "amount": u["amount"] }
+ key_entry = [ke for ke in self._key_entries if ke["output_script"] == bytes.fromhex(u["scriptPubKey"])][0]
+ if key_entry["redeem_script"] is not None:
+ unspent["redeemScript"] = key_entry["redeem_script"].hex()
+ unspents.append(unspent)
+ return unspents
+
+ def signrawtransaction(self, tx_hex, inputs):
+ output_scripts_to_sign = {i["scriptPubKey"] for i in inputs}
+ seckeys_wif = [ke["seckey_wif"] for ke in self._key_entries if ke["output_script"].hex() in output_scripts_to_sign]
+ return self._node.signrawtransactionwithkey(tx_hex, seckeys_wif, inputs)
+
+
def getnewdestination(address_type='bech32m'):
"""Generate a random destination of the specified type and return the
corresponding key pair, scriptPubKey and address. Supported types are
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.