Merge bitcoin/bitcoin#32682: wallet: have external signer use PSBT error code EXTERNAL_SIGNER_NOT_FOUND
9dfc61d95ftest: detect no external signer connected (Sjors Provoost)0a4ee93529wallet: use PSBTError::EXTERNAL_SIGNER_NOT_FOUND (Sjors Provoost)8ba2f9b7c8refactor: 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:  After (the translation already exist):  Fixes #32426 Additionally use `LogWarning` instead of `std::cerr` for both a missing signer and failure to sign. ACKs for top commit: achow101: ACK9dfc61d95fbrunoerg: code review ACK9dfc61d95fTree-SHA512: 22515f4f0b4f50cb0ef532b729e247f11a68be9c90e384942d4277087b2e76806a1cdaa57fb51d5883dacf0a428e5279674aab37cce8c0d3d7de0f96346b8233
This commit is contained in:
5 files changed
+78
-13
No files matched your search
@@ -45,14 +45,14 @@ bool ExternalSignerScriptPubKeyMan::SetupDescriptor(WalletBatch& batch, std::uni
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalSigner ExternalSignerScriptPubKeyMan::GetExternalSigner() {
|
util::Result<ExternalSigner> ExternalSignerScriptPubKeyMan::GetExternalSigner() {
|
||||||
const std::string command = gArgs.GetArg("-signer", "");
|
const std::string command = gArgs.GetArg("-signer", "");
|
||||||
if (command == "") throw std::runtime_error(std::string(__func__) + ": restart bitcoind with -signer=<cmd>");
|
if (command == "") return util::Error{Untranslated("restart bitcoind with -signer=<cmd>")};
|
||||||
std::vector<ExternalSigner> signers;
|
std::vector<ExternalSigner> signers;
|
||||||
ExternalSigner::Enumerate(command, signers, Params().GetChainTypeString());
|
ExternalSigner::Enumerate(command, signers, Params().GetChainTypeString());
|
||||||
if (signers.empty()) throw std::runtime_error(std::string(__func__) + ": No external signers found");
|
if (signers.empty()) return util::Error{Untranslated("No external signers found")};
|
||||||
// TODO: add fingerprint argument instead of failing in case of multiple signers.
|
// TODO: add fingerprint argument instead of failing in case of multiple signers.
|
||||||
if (signers.size() > 1) throw std::runtime_error(std::string(__func__) + ": More than one external signer found. Please connect only one at a time.");
|
if (signers.size() > 1) return util::Error{Untranslated("More than one external signer found. Please connect only one at a time.")};
|
||||||
return signers[0];
|
return signers[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +93,15 @@ std::optional<PSBTError> ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySigned
|
|||||||
}
|
}
|
||||||
if (complete) return {};
|
if (complete) return {};
|
||||||
|
|
||||||
std::string strFailReason;
|
auto signer{GetExternalSigner()};
|
||||||
if(!GetExternalSigner().SignTransaction(psbt, strFailReason)) {
|
if (!signer) {
|
||||||
tfm::format(std::cerr, "Failed to sign: %s\n", strFailReason);
|
LogWarning("%s", util::ErrorString(signer).original);
|
||||||
|
return PSBTError::EXTERNAL_SIGNER_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string failure_reason;
|
||||||
|
if(!signer->SignTransaction(psbt, failure_reason)) {
|
||||||
|
LogWarning("Failed to sign: %s\n", failure_reason);
|
||||||
return PSBTError::EXTERNAL_SIGNER_FAILED;
|
return PSBTError::EXTERNAL_SIGNER_FAILED;
|
||||||
}
|
}
|
||||||
if (finalize) FinalizePSBT(psbt); // This won't work in a multisig setup
|
if (finalize) FinalizePSBT(psbt); // This won't work in a multisig setup
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <wallet/scriptpubkeyman.h>
|
#include <wallet/scriptpubkeyman.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <util/result.h>
|
||||||
|
|
||||||
struct bilingual_str;
|
struct bilingual_str;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
|
|||||||
*/
|
*/
|
||||||
bool SetupDescriptor(WalletBatch& batch, std::unique_ptr<Descriptor>desc);
|
bool SetupDescriptor(WalletBatch& batch, std::unique_ptr<Descriptor>desc);
|
||||||
|
|
||||||
static ExternalSigner GetExternalSigner();
|
static util::Result<ExternalSigner> GetExternalSigner();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display address on the device and verify that the returned value matches.
|
* Display address on the device and verify that the returned value matches.
|
||||||
|
|||||||
@@ -2575,8 +2575,9 @@ util::Result<void> CWallet::DisplayAddress(const CTxDestination& dest)
|
|||||||
if (signer_spk_man == nullptr) {
|
if (signer_spk_man == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
|
auto signer{ExternalSignerScriptPubKeyMan::GetExternalSigner()};
|
||||||
return signer_spk_man->DisplayAddress(dest, signer);
|
if (!signer) throw std::runtime_error(util::ErrorString(signer).original);
|
||||||
|
return signer_spk_man->DisplayAddress(dest, *signer);
|
||||||
}
|
}
|
||||||
return util::Error{_("There is no ScriptPubKeyManager for this address")};
|
return util::Error{_("There is no ScriptPubKeyManager for this address")};
|
||||||
}
|
}
|
||||||
@@ -3548,11 +3549,12 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
|
|||||||
return true;
|
return true;
|
||||||
})) throw std::runtime_error("Error: cannot process db transaction for descriptors setup");
|
})) throw std::runtime_error("Error: cannot process db transaction for descriptors setup");
|
||||||
} else {
|
} else {
|
||||||
ExternalSigner signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
|
auto signer = ExternalSignerScriptPubKeyMan::GetExternalSigner();
|
||||||
|
if (!signer) throw std::runtime_error(util::ErrorString(signer).original);
|
||||||
|
|
||||||
// TODO: add account parameter
|
// TODO: add account parameter
|
||||||
int account = 0;
|
int account = 0;
|
||||||
UniValue signer_res = signer.GetDescriptors(account);
|
UniValue signer_res = signer->GetDescriptors(account);
|
||||||
|
|
||||||
if (!signer_res.isObject()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
|
if (!signer_res.isObject()) throw std::runtime_error(std::string(__func__) + ": Unexpected result");
|
||||||
|
|
||||||
|
|||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) 2025-present The Bitcoin Core developers
|
||||||
|
# Distributed under the MIT software license, see the accompanying
|
||||||
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def enumerate(args):
|
||||||
|
sys.stdout.write(json.dumps([]))
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog='./no_signer.py', description='No external signer connected mock')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(description='Commands', dest='command')
|
||||||
|
subparsers.required = True
|
||||||
|
|
||||||
|
parser_enumerate = subparsers.add_parser('enumerate', help='list available signers')
|
||||||
|
parser_enumerate.set_defaults(func=enumerate)
|
||||||
|
|
||||||
|
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
buffer = sys.stdin.read()
|
||||||
|
if buffer and buffer.rstrip() != "":
|
||||||
|
sys.argv.extend(buffer.rstrip().split(" "))
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.func(args)
|
||||||
@@ -23,6 +23,10 @@ class WalletSignerTest(BitcoinTestFramework):
|
|||||||
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py')
|
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py')
|
||||||
return sys.executable + " " + path
|
return sys.executable + " " + path
|
||||||
|
|
||||||
|
def mock_no_connected_signer_path(self):
|
||||||
|
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'no_signer.py')
|
||||||
|
return sys.executable + " " + path
|
||||||
|
|
||||||
def mock_invalid_signer_path(self):
|
def mock_invalid_signer_path(self):
|
||||||
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'invalid_signer.py')
|
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'invalid_signer.py')
|
||||||
return sys.executable + " " + path
|
return sys.executable + " " + path
|
||||||
@@ -52,6 +56,7 @@ class WalletSignerTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
self.test_valid_signer()
|
self.test_valid_signer()
|
||||||
|
self.test_disconnected_signer()
|
||||||
self.restart_node(1, [f"-signer={self.mock_invalid_signer_path()}", "-keypool=10"])
|
self.restart_node(1, [f"-signer={self.mock_invalid_signer_path()}", "-keypool=10"])
|
||||||
self.test_invalid_signer()
|
self.test_invalid_signer()
|
||||||
self.restart_node(1, [f"-signer={self.mock_multi_signers_path()}", "-keypool=10"])
|
self.restart_node(1, [f"-signer={self.mock_multi_signers_path()}", "-keypool=10"])
|
||||||
@@ -234,6 +239,28 @@ class WalletSignerTest(BitcoinTestFramework):
|
|||||||
# )
|
# )
|
||||||
# self.clear_mock_result(self.nodes[4])
|
# self.clear_mock_result(self.nodes[4])
|
||||||
|
|
||||||
|
def test_disconnected_signer(self):
|
||||||
|
self.log.info('Test disconnected external signer')
|
||||||
|
|
||||||
|
# First create a wallet with the signer connected
|
||||||
|
self.nodes[1].createwallet(wallet_name='hww_disconnect', disable_private_keys=True, external_signer=True)
|
||||||
|
hww = self.nodes[1].get_wallet_rpc('hww_disconnect')
|
||||||
|
assert_equal(hww.getwalletinfo()["external_signer"], True)
|
||||||
|
|
||||||
|
# Fund wallet
|
||||||
|
self.nodes[0].sendtoaddress(hww.getnewaddress(address_type="bech32m"), 1)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
|
||||||
|
# Restart node with no signer connected
|
||||||
|
self.log.debug(f"-signer={self.mock_no_connected_signer_path()}")
|
||||||
|
self.restart_node(1, [f"-signer={self.mock_no_connected_signer_path()}", "-keypool=10"])
|
||||||
|
self.nodes[1].loadwallet('hww_disconnect')
|
||||||
|
hww = self.nodes[1].get_wallet_rpc('hww_disconnect')
|
||||||
|
|
||||||
|
# Try to spend
|
||||||
|
dest = hww.getrawchangeaddress()
|
||||||
|
assert_raises_rpc_error(-25, "External signer not found", hww.send, outputs=[{dest:0.5}])
|
||||||
|
|
||||||
def test_invalid_signer(self):
|
def test_invalid_signer(self):
|
||||||
self.log.debug(f"-signer={self.mock_invalid_signer_path()}")
|
self.log.debug(f"-signer={self.mock_invalid_signer_path()}")
|
||||||
self.log.info('Test invalid external signer')
|
self.log.info('Test invalid external signer')
|
||||||
@@ -243,7 +270,7 @@ class WalletSignerTest(BitcoinTestFramework):
|
|||||||
self.log.debug(f"-signer={self.mock_multi_signers_path()}")
|
self.log.debug(f"-signer={self.mock_multi_signers_path()}")
|
||||||
self.log.info('Test multiple external signers')
|
self.log.info('Test multiple external signers')
|
||||||
|
|
||||||
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)
|
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)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
WalletSignerTest(__file__).main()
|
WalletSignerTest(__file__).main()
|
||||||
Reference in new issue
Block a user