Skip to content

Commit 1be688f

Browse files
committed
Merge bitcoin/bitcoin#32682: wallet: have external signer use PSBT error code EXTERNAL_SIGNER_NOT_FOUND
9dfc61d test: detect no external signer connected (Sjors Provoost) 0a4ee93 wallet: use PSBTError::EXTERNAL_SIGNER_NOT_FOUND (Sjors Provoost) 8ba2f9b refactor: use util::Result for GetExternalSigner() (Sjors Provoost) Pull request description: When attempting to sign a transaction involving an external signer, if the device isn't connected we throw an `std::runtime_error`. This prevents the (mainly GUI) code that's actually supposed to handle this case from running. This PR returns a `PSBTError::EXTERNAL_SIGNER_NOT_FOUND` instead of throwing. The first commit is a refactor to have `GetExternalSigner()` return a `util::Result<ExternalSigner>` so the caller can decide how to handle the error. There are two other places where call `GetExternalSigner()` which this PR doesn't change (which I think is fine there). Before: ![before](https://github.com/user-attachments/assets/2e08863b-fe76-479f-9cc0-60571b357a27) After (the translation already exist): ![after](https://github.com/user-attachments/assets/0e91c7ed-7d44-4030-beec-20d1694c270c) Fixes #32426 Additionally use `LogWarning` instead of `std::cerr` for both a missing signer and failure to sign. ACKs for top commit: achow101: ACK 9dfc61d brunoerg: code review ACK 9dfc61d Tree-SHA512: 22515f4f0b4f50cb0ef532b729e247f11a68be9c90e384942d4277087b2e76806a1cdaa57fb51d5883dacf0a428e5279674aab37cce8c0d3d7de0f96346b8233
2 parents 26747d9 + 9dfc61d commit 1be688f

File tree

5 files changed

+78
-13
lines changed

5 files changed

+78
-13
lines changed

src/wallet/external_signer_scriptpubkeyman.cpp

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ bool ExternalSignerScriptPubKeyMan::SetupDescriptor(WalletBatch& batch, std::uni
4545
return true;
4646
}
4747

48-
ExternalSigner ExternalSignerScriptPubKeyMan::GetExternalSigner() {
48+
util::Result<ExternalSigner> ExternalSignerScriptPubKeyMan::GetExternalSigner() {
4949
const std::string command = gArgs.GetArg("-signer", "");
50-
if (command == "") throw std::runtime_error(std::string(__func__) + ": restart bitcoind with -signer=<cmd>");
50+
if (command == "") return util::Error{Untranslated("restart bitcoind with -signer=<cmd>")};
5151
std::vector<ExternalSigner> signers;
5252
ExternalSigner::Enumerate(command, signers, Params().GetChainTypeString());
53-
if (signers.empty()) throw std::runtime_error(std::string(__func__) + ": No external signers found");
53+
if (signers.empty()) return util::Error{Untranslated("No external signers found")};
5454
// TODO: add fingerprint argument instead of failing in case of multiple signers.
55-
if (signers.size() > 1) throw std::runtime_error(std::string(__func__) + ": More than one external signer found. Please connect only one at a time.");
55+
if (signers.size() > 1) return util::Error{Untranslated("More than one external signer found. Please connect only one at a time.")};
5656
return signers[0];
5757
}
5858

@@ -93,9 +93,15 @@ std::optional<PSBTError> ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySigned
9393
}
9494
if (complete) return {};
9595

96-
std::string strFailReason;
97-
if(!GetExternalSigner().SignTransaction(psbt, strFailReason)) {
98-
tfm::format(std::cerr, "Failed to sign: %s\n", strFailReason);
96+
auto signer{GetExternalSigner()};
97+
if (!signer) {
98+
LogWarning("%s", util::ErrorString(signer).original);
99+
return PSBTError::EXTERNAL_SIGNER_NOT_FOUND;
100+
}
101+
102+
std::string failure_reason;
103+
if(!signer->SignTransaction(psbt, failure_reason)) {
104+
LogWarning("Failed to sign: %s\n", failure_reason);
99105
return PSBTError::EXTERNAL_SIGNER_FAILED;
100106
}
101107
if (finalize) FinalizePSBT(psbt); // This won't work in a multisig setup

src/wallet/external_signer_scriptpubkeyman.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <wallet/scriptpubkeyman.h>
99

1010
#include <memory>
11+
#include <util/result.h>
1112

1213
struct bilingual_str;
1314

@@ -27,7 +28,7 @@ class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
2728
*/
2829
bool SetupDescriptor(WalletBatch& batch, std::unique_ptr<Descriptor>desc);
2930

30-
static ExternalSigner GetExternalSigner();
31+
static util::Result<ExternalSigner> GetExternalSigner();
3132

3233
/**
3334
* Display address on the device and verify that the returned value matches.

src/wallet/wallet.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,8 +2575,9 @@ util::Result<void> CWallet::DisplayAddress(const CTxDestination& dest)
25752575
if (signer_spk_man == nullptr) {
25762576
continue;
25772577
}
2578-
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
2579-
return signer_spk_man->DisplayAddress(dest, signer);
2578+
auto signer{ExternalSignerScriptPubKeyMan::GetExternalSigner()};
2579+
if (!signer) throw std::runtime_error(util::ErrorString(signer).original);
2580+
return signer_spk_man->DisplayAddress(dest, *signer);
25802581
}
25812582
return util::Error{_("There is no ScriptPubKeyManager for this address")};
25822583
}
@@ -3548,11 +3549,12 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
35483549
return true;
35493550
})) throw std::runtime_error("Error: cannot process db transaction for descriptors setup");
35503551
} else {
3551-
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
3552+
auto signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
3553+
if (!signer) throw std::runtime_error(util::ErrorString(signer).original);
35523554

35533555
// TODO: add account parameter
35543556
int account = 0;
3555-
UniValue signer_res = signer.GetDescriptors(account);
3557+
UniValue signer_res = signer->GetDescriptors(account);
35563558

35573559
if (!signer_res.isObject()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
35583560

test/functional/mocks/no_signer.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025-present 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+
6+
import argparse
7+
import json
8+
import sys
9+
10+
def enumerate(args):
11+
sys.stdout.write(json.dumps([]))
12+
13+
parser = argparse.ArgumentParser(prog='./no_signer.py', description='No external signer connected mock')
14+
15+
subparsers = parser.add_subparsers(description='Commands', dest='command')
16+
subparsers.required = True
17+
18+
parser_enumerate = subparsers.add_parser('enumerate', help='list available signers')
19+
parser_enumerate.set_defaults(func=enumerate)
20+
21+
22+
if not sys.stdin.isatty():
23+
buffer = sys.stdin.read()
24+
if buffer and buffer.rstrip() != "":
25+
sys.argv.extend(buffer.rstrip().split(" "))
26+
27+
args = parser.parse_args()
28+
29+
args.func(args)

test/functional/wallet_signer.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def mock_signer_path(self):
2323
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py')
2424
return sys.executable + " " + path
2525

26+
def mock_no_connected_signer_path(self):
27+
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'no_signer.py')
28+
return sys.executable + " " + path
29+
2630
def mock_invalid_signer_path(self):
2731
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'invalid_signer.py')
2832
return sys.executable + " " + path
@@ -52,6 +56,7 @@ def clear_mock_result(self, node):
5256

5357
def run_test(self):
5458
self.test_valid_signer()
59+
self.test_disconnected_signer()
5560
self.restart_node(1, [f"-signer={self.mock_invalid_signer_path()}", "-keypool=10"])
5661
self.test_invalid_signer()
5762
self.restart_node(1, [f"-signer={self.mock_multi_signers_path()}", "-keypool=10"])
@@ -234,6 +239,28 @@ def test_valid_signer(self):
234239
# )
235240
# self.clear_mock_result(self.nodes[4])
236241

242+
def test_disconnected_signer(self):
243+
self.log.info('Test disconnected external signer')
244+
245+
# First create a wallet with the signer connected
246+
self.nodes[1].createwallet(wallet_name='hww_disconnect', disable_private_keys=True, external_signer=True)
247+
hww = self.nodes[1].get_wallet_rpc('hww_disconnect')
248+
assert_equal(hww.getwalletinfo()["external_signer"], True)
249+
250+
# Fund wallet
251+
self.nodes[0].sendtoaddress(hww.getnewaddress(address_type="bech32m"), 1)
252+
self.generate(self.nodes[0], 1)
253+
254+
# Restart node with no signer connected
255+
self.log.debug(f"-signer={self.mock_no_connected_signer_path()}")
256+
self.restart_node(1, [f"-signer={self.mock_no_connected_signer_path()}", "-keypool=10"])
257+
self.nodes[1].loadwallet('hww_disconnect')
258+
hww = self.nodes[1].get_wallet_rpc('hww_disconnect')
259+
260+
# Try to spend
261+
dest = hww.getrawchangeaddress()
262+
assert_raises_rpc_error(-25, "External signer not found", hww.send, outputs=[{dest:0.5}])
263+
237264
def test_invalid_signer(self):
238265
self.log.debug(f"-signer={self.mock_invalid_signer_path()}")
239266
self.log.info('Test invalid external signer')
@@ -243,7 +270,7 @@ def test_multiple_signers(self):
243270
self.log.debug(f"-signer={self.mock_multi_signers_path()}")
244271
self.log.info('Test multiple external signers')
245272

246-
assert_raises_rpc_error(-1, "GetExternalSigner: More than one external signer found", self.nodes[1].createwallet, wallet_name='multi_hww', disable_private_keys=True, external_signer=True)
273+
assert_raises_rpc_error(-1, "More than one external signer found", self.nodes[1].createwallet, wallet_name='multi_hww', disable_private_keys=True, external_signer=True)
247274

248275
if __name__ == '__main__':
249276
WalletSignerTest(__file__).main()

0 commit comments

Comments
 (0)