Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2017-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test external signer.7Verify that a bitcoind node can use an external signer command8See also rpc_signer.py for tests without wallet context.9"""10import os11import sys13from test_framework.test_framework import BitcoinTestFramework14from test_framework.util import (15assert_equal,16assert_greater_than,17assert_raises_rpc_error,18)21class WalletSignerTest(BitcoinTestFramework):22def mock_signer_path(self):23path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py')24return sys.executable + " " + path26def mock_no_connected_signer_path(self):27path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'no_signer.py')28return sys.executable + " " + path30def mock_invalid_signer_path(self):31path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'invalid_signer.py')32return sys.executable + " " + path34def mock_multi_signers_path(self):35path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'multi_signers.py')36return sys.executable + " " + path38def set_test_params(self):39self.num_nodes = 241self.extra_args = [42[],43[f"-signer={self.mock_signer_path()}", '-keypool=10'],44]46def skip_test_if_missing_module(self):47self.skip_if_no_external_signer()48self.skip_if_no_wallet()50def set_mock_result(self, node, res):51with open(os.path.join(node.cwd, "mock_result"), "w") as f:52f.write(res)54def clear_mock_result(self, node):55os.remove(os.path.join(node.cwd, "mock_result"))57def run_test(self):58self.test_valid_signer()59self.test_disconnected_signer()60self.restart_node(1, [f"-signer={self.mock_invalid_signer_path()}", "-keypool=10"])61self.test_invalid_signer()62self.restart_node(1, [f"-signer={self.mock_multi_signers_path()}", "-keypool=10"])63self.test_multiple_signers()65def test_valid_signer(self):66self.log.debug(f"-signer={self.mock_signer_path()}")68# Create new wallets for an external signer.69# disable_private_keys and descriptors must be true:70assert_raises_rpc_error(-4, "Private keys must be disabled when using an external signer", self.nodes[1].createwallet, wallet_name='not_hww', disable_private_keys=False, external_signer=True)71self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, external_signer=True)72hww = self.nodes[1].get_wallet_rpc('hww')73assert_equal(hww.getwalletinfo()["external_signer"], True)75# Flag can't be set afterwards (could be added later for non-blank descriptor based watch-only wallets)76self.nodes[1].createwallet(wallet_name='not_hww', disable_private_keys=True, external_signer=False)77not_hww = self.nodes[1].get_wallet_rpc('not_hww')78assert_equal(not_hww.getwalletinfo()["external_signer"], False)79assert_raises_rpc_error(-8, "Wallet flag is immutable: external_signer", not_hww.setwalletflag, "external_signer", True)82self.set_mock_result(self.nodes[1], '0 {"invalid json"}')83assert_raises_rpc_error(-1, 'Unable to parse JSON',84self.nodes[1].createwallet, wallet_name='hww2', disable_private_keys=True, external_signer=True85)86self.clear_mock_result(self.nodes[1])88assert_equal(hww.getwalletinfo()["keypoolsize"], 40)90address1 = hww.getnewaddress(address_type="bech32")91assert_equal(address1, "bcrt1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th68x4f8g")92address_info = hww.getaddressinfo(address1)93assert_equal(address_info['solvable'], True)94assert_equal(address_info['ismine'], True)95assert_equal(address_info['hdkeypath'], "m/84h/1h/0h/0/0")97address2 = hww.getnewaddress(address_type="p2sh-segwit")98assert_equal(address2, "2N2gQKzjUe47gM8p1JZxaAkTcoHPXV6YyVp")99address_info = hww.getaddressinfo(address2)100assert_equal(address_info['solvable'], True)101assert_equal(address_info['ismine'], True)102assert_equal(address_info['hdkeypath'], "m/49h/1h/0h/0/0")104address3 = hww.getnewaddress(address_type="legacy")105assert_equal(address3, "n1LKejAadN6hg2FrBXoU1KrwX4uK16mco9")106address_info = hww.getaddressinfo(address3)107assert_equal(address_info['solvable'], True)108assert_equal(address_info['ismine'], True)109assert_equal(address_info['hdkeypath'], "m/44h/1h/0h/0/0")111address4 = hww.getnewaddress(address_type="bech32m")112assert_equal(address4, "bcrt1phw4cgpt6cd30kz9k4wkpwm872cdvhss29jga2xpmftelhqll62ms4e9sqj")113address_info = hww.getaddressinfo(address4)114assert_equal(address_info['solvable'], True)115assert_equal(address_info['ismine'], True)116assert_equal(address_info['hdkeypath'], "m/86h/1h/0h/0/0")118self.log.info('Test walletdisplayaddress')119for address in [address1, address2, address3]:120result = hww.walletdisplayaddress(address)121assert_equal(result, {"address": address})123assert_raises_rpc_error(124-4,125"Error: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead",126hww.sendtoaddress,127self.nodes[0].getnewaddress(),1280.01,129)130assert_raises_rpc_error(131-4,132"Error: sendtoaddress and sendmany are not supported for wallets with external signers; use send instead",133hww.sendmany,134"",135{self.nodes[0].getnewaddress(): 0.01},136)138# Handle error thrown by script139self.set_mock_result(self.nodes[1], "2")140assert_raises_rpc_error(-1, 'RunCommandParseJSON error',141hww.walletdisplayaddress, address1142)143self.clear_mock_result(self.nodes[1])145# Returned address MUST match:146address_fail = hww.getnewaddress(address_type="bech32")147assert_equal(address_fail, "bcrt1ql7zg7ukh3dwr25ex2zn9jse926f27xy2jz58tm")148assert_raises_rpc_error(-1, 'Signer echoed unexpected address wrong_address',149hww.walletdisplayaddress, address_fail150)152self.log.info('Prepare mock PSBT')153self.nodes[0].sendtoaddress(address4, 1)154self.generate(self.nodes[0], 1)156# Load private key into wallet to generate a signed PSBT for the mock157self.nodes[1].createwallet(wallet_name="mock", disable_private_keys=False, blank=True)158mock_wallet = self.nodes[1].get_wallet_rpc("mock")159assert mock_wallet.getwalletinfo()['private_keys_enabled']161result = mock_wallet.importdescriptors([{162"desc": "tr([00000001/86h/1h/0']tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0/*)#7ew68cn8",163"timestamp": 0,164"range": [0,1],165"internal": False,166"active": True167},168{169"desc": "tr([00000001/86h/1h/0']tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/*)#0dtm6drl",170"timestamp": 0,171"range": [0, 0],172"internal": True,173"active": True174}])175assert_equal(result[0], {'success': True})176assert_equal(result[1], {'success': True})177assert_equal(mock_wallet.getwalletinfo()["txcount"], 1)178dest = self.nodes[0].getnewaddress(address_type='bech32')179mock_psbt = mock_wallet.walletcreatefundedpsbt([], {dest:0.5}, 0, {'replaceable': True}, True)['psbt']180mock_psbt_signed = mock_wallet.walletprocesspsbt(psbt=mock_psbt, sign=True, sighashtype="ALL", bip32derivs=True)181mock_tx = mock_psbt_signed["hex"]182assert mock_wallet.testmempoolaccept([mock_tx])[0]["allowed"]184assert_equal(hww.getwalletinfo()["txcount"], 1)186assert hww.testmempoolaccept([mock_tx])[0]["allowed"]188with open(os.path.join(self.nodes[1].cwd, "mock_psbt"), "w") as f:189f.write(mock_psbt_signed["psbt"])191self.log.info('Test send using hww1')193# Don't broadcast transaction yet so the RPC returns the raw hex194res = hww.send(outputs={dest:0.5},add_to_wallet=False)195assert res["complete"]196assert_equal(res["hex"], mock_tx)198self.log.info('Test sendall using hww1')200res = hww.sendall(recipients=[{dest:0.5}, hww.getrawchangeaddress()], add_to_wallet=False)201assert res["complete"]202assert_equal(res["hex"], mock_tx)203# Broadcast transaction so we can bump the fee204hww.sendrawtransaction(res["hex"])206self.log.info('Prepare fee bumped mock PSBT')208# Now that the transaction is broadcast, bump fee in mock wallet:209orig_tx_id = res["txid"]210mock_psbt_bumped = mock_wallet.psbtbumpfee(orig_tx_id)["psbt"]211mock_psbt_bumped_signed = mock_wallet.walletprocesspsbt(psbt=mock_psbt_bumped, sign=True, sighashtype="ALL", bip32derivs=True)213with open(os.path.join(self.nodes[1].cwd, "mock_psbt"), "w") as f:214f.write(mock_psbt_bumped_signed["psbt"])216self.log.info('Test bumpfee using hww1')218# Bump fee219res = hww.bumpfee(orig_tx_id)220assert_greater_than(res["fee"], res["origfee"])221assert_equal(res["errors"], [])224def test_disconnected_signer(self):225self.log.info('Test disconnected external signer')227# First create a wallet with the signer connected228self.nodes[1].createwallet(wallet_name='hww_disconnect', disable_private_keys=True, external_signer=True)229hww = self.nodes[1].get_wallet_rpc('hww_disconnect')230assert_equal(hww.getwalletinfo()["external_signer"], True)232# Fund wallet233self.nodes[0].sendtoaddress(hww.getnewaddress(address_type="bech32m"), 1)234self.generate(self.nodes[0], 1)236# Restart node with no signer connected237self.log.debug(f"-signer={self.mock_no_connected_signer_path()}")238self.restart_node(1, [f"-signer={self.mock_no_connected_signer_path()}", "-keypool=10"])239self.nodes[1].loadwallet('hww_disconnect')240hww = self.nodes[1].get_wallet_rpc('hww_disconnect')242# Try to spend243dest = hww.getrawchangeaddress()244assert_raises_rpc_error(-25, "External signer not found", hww.send, outputs=[{dest:0.5}])246def test_invalid_signer(self):247self.log.debug(f"-signer={self.mock_invalid_signer_path()}")248self.log.info('Test invalid external signer')249assert_raises_rpc_error(-1, "Invalid descriptor", self.nodes[1].createwallet, wallet_name='hww_invalid', disable_private_keys=True, external_signer=True)
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.