Skip to content

Commit 299c0ed

Browse files
committed
Merge remote-tracking branch 'sjors/2025/07/external-signer-relax'
2 parents ec3b3ae + 0397853 commit 299c0ed

File tree

8 files changed

+57
-56
lines changed

8 files changed

+57
-56
lines changed

doc/external-signer.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ When using a hardware wallet, consult the manufacturer website for (alternative)
1111
Start Bitcoin Core:
1212

1313
```sh
14-
$ bitcoind -signer=../HWI/hwi.py
14+
$ bitcoin node -signer=../HWI/hwi.py
1515
```
1616

17-
`bitcoin node` can also be substituted for `bitcoind`.
17+
(`bitcoin node` is the equivalent of `bitcoind`)
18+
19+
The external signer script path can also be configured from the GUI, see Options -> Wallet.
1820

1921
### Device setup
2022

@@ -25,7 +27,7 @@ Follow the hardware manufacturers instructions for the initial device setup, as
2527
Get a list of signing devices / services:
2628

2729
```
28-
$ bitcoin-cli enumeratesigners
30+
$ bitcoin rpc enumeratesigners
2931
{
3032
"signers": [
3133
{
@@ -39,18 +41,18 @@ The master key fingerprint is used to identify a device.
3941
Create a wallet, this automatically imports the public keys:
4042

4143
```sh
42-
$ bitcoin-cli createwallet "hww" true true "" true true true
44+
$ bitcoin rpc createwallet "hww" external_signer=true
4345
```
4446

45-
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
47+
(`bitcoin rpc` is the equivalent of `bitcoin cli -named`)
4648

4749
### Verify an address
4850

4951
Display an address on the device:
5052

5153
```sh
52-
$ bitcoin-cli -rpcwallet=<wallet> getnewaddress
53-
$ bitcoin-cli -rpcwallet=<wallet> walletdisplayaddress <address>
54+
$ bitcoin rpc -rpcwallet=<wallet> getnewaddress
55+
$ bitcoin rpc -rpcwallet=<wallet> walletdisplayaddress <address>
5456
```
5557

5658
Replace `<address>` with the result of `getnewaddress`.
@@ -60,7 +62,7 @@ Replace `<address>` with the result of `getnewaddress`.
6062
Under the hood this uses a [Partially Signed Bitcoin Transaction](psbt.md).
6163

6264
```sh
63-
$ bitcoin-cli -rpcwallet=<wallet> sendtoaddress <address> <amount>
65+
$ bitcoin rpc -rpcwallet=<wallet> sendtoaddress <address> <amount>
6466
```
6567

6668
This prompts your hardware wallet to sign, and fail if it's not connected. If successful

src/qt/createwalletdialog.cpp

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,25 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) :
2626
});
2727

2828
connect(ui->encrypt_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) {
29-
// Disable the disable_privkeys_checkbox and external_signer_checkbox when isEncryptWalletChecked is
29+
// Disable the disable_privkeys_checkbox when isEncryptWalletChecked is
3030
// set to true, enable it when isEncryptWalletChecked is false.
3131
ui->disable_privkeys_checkbox->setEnabled(!checked);
32-
#ifdef ENABLE_EXTERNAL_SIGNER
33-
ui->external_signer_checkbox->setEnabled(m_has_signers && !checked);
34-
#endif
32+
3533
// When the disable_privkeys_checkbox is disabled, uncheck it.
3634
if (!ui->disable_privkeys_checkbox->isEnabled()) {
3735
ui->disable_privkeys_checkbox->setChecked(false);
3836
}
39-
40-
// When the external_signer_checkbox box is disabled, uncheck it.
41-
if (!ui->external_signer_checkbox->isEnabled()) {
42-
ui->external_signer_checkbox->setChecked(false);
43-
}
44-
4537
});
4638

4739
connect(ui->external_signer_checkbox, &QCheckBox::toggled, [this](bool checked) {
48-
ui->encrypt_wallet_checkbox->setEnabled(!checked);
49-
ui->blank_wallet_checkbox->setEnabled(!checked);
50-
ui->disable_privkeys_checkbox->setEnabled(!checked);
40+
// In the basic use case all keys will be on the external signer
41+
// device and the wallet should be watch-only. Makes this the
42+
// default suggestion.
43+
ui->disable_privkeys_checkbox->setChecked(checked);
5144

52-
// The external signer checkbox is only enabled when a device is detected.
53-
// In that case it is checked by default. Toggling it restores the other
54-
// options to their default.
45+
// The external signer box is checked by default when a device is
46+
// detected. Toggling it restores the other options to their default.
5547
ui->encrypt_wallet_checkbox->setChecked(false);
56-
ui->disable_privkeys_checkbox->setChecked(checked);
5748
ui->blank_wallet_checkbox->setChecked(false);
5849
});
5950

@@ -103,12 +94,9 @@ void CreateWalletDialog::setSigners(const std::vector<std::unique_ptr<interfaces
10394
if (m_has_signers) {
10495
ui->external_signer_checkbox->setEnabled(true);
10596
ui->external_signer_checkbox->setChecked(true);
106-
ui->encrypt_wallet_checkbox->setEnabled(false);
10797
ui->encrypt_wallet_checkbox->setChecked(false);
10898
// The order matters, because connect() is called when toggling a checkbox:
109-
ui->blank_wallet_checkbox->setEnabled(false);
11099
ui->blank_wallet_checkbox->setChecked(false);
111-
ui->disable_privkeys_checkbox->setEnabled(false);
112100
ui->disable_privkeys_checkbox->setChecked(true);
113101
const std::string label = signers[0]->getName();
114102
ui->wallet_name_line_edit->setText(QString::fromStdString(label));

src/qt/walletmodel.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
203203
int nChangePosRet = -1;
204204

205205
auto& newTx = transaction.getWtx();
206-
const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), nChangePosRet, nFeeRequired);
206+
const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled() && !wallet().hasExternalSigner(), nChangePosRet, nFeeRequired);
207207
newTx = res ? *res : nullptr;
208208
transaction.setTransactionFee(nFeeRequired);
209209
if (fSubtractFeeFromAmount && newTx)

src/wallet/rpc/spend.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,9 @@ UniValue SendMoney(CWallet& wallet, const CCoinControl &coin_control, std::vecto
175175
EnsureWalletIsUnlocked(wallet);
176176

177177
// This function is only used by sendtoaddress and sendmany.
178-
// This should always try to sign, if we don't have private keys, don't try to do anything here.
179-
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
178+
// This should always try to sign, if we don't have (all) private keys, don't
179+
// try to do anything here.
180+
if (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) || wallet.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
180181
throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
181182
}
182183

src/wallet/rpc/wallet.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,13 @@ static RPCHelpMan createwallet()
353353
"Creates and loads a new wallet.\n",
354354
{
355355
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."},
356-
{"disable_private_keys", RPCArg::Type::BOOL, RPCArg::Default{false}, "Disable the possibility of private keys (only watchonlys are possible in this mode)."},
356+
{"disable_private_keys", RPCArg::Type::BOOL, RPCArg::DefaultHint{"false unless external_signer is set"}, "Disable the possibility of private keys (only watchonlys are possible in this mode)."},
357357
{"blank", RPCArg::Type::BOOL, RPCArg::Default{false}, "Create a blank wallet. A blank wallet has no keys."},
358358
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."},
359359
{"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{false}, "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."},
360360
{"descriptors", RPCArg::Type::BOOL, RPCArg::Default{true}, "If set, must be \"true\""},
361361
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged."},
362-
{"external_signer", RPCArg::Type::BOOL, RPCArg::Default{false}, "Use an external signer such as a hardware wallet. Requires -signer to be configured. Wallet creation will fail if keys cannot be fetched. Requires disable_private_keys and descriptors set to true."},
362+
{"external_signer", RPCArg::Type::BOOL, RPCArg::Default{false}, "Use an external signer such as a hardware wallet. Requires -signer to be configured. Wallet creation will fail if keys cannot be fetched."},
363363
},
364364
RPCResult{
365365
RPCResult::Type::OBJ, "", "",
@@ -381,9 +381,8 @@ static RPCHelpMan createwallet()
381381
{
382382
WalletContext& context = EnsureWalletContext(request.context);
383383
uint64_t flags = 0;
384-
if (!request.params[1].isNull() && request.params[1].get_bool()) {
385-
flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
386-
}
384+
385+
std::optional<bool> disable_private_keys{self.MaybeArg<bool>("disable_private_keys")};
387386

388387
if (!request.params[2].isNull() && request.params[2].get_bool()) {
389388
flags |= WALLET_FLAG_BLANK_WALLET;
@@ -409,11 +408,21 @@ static RPCHelpMan createwallet()
409408
if (!request.params[7].isNull() && request.params[7].get_bool()) {
410409
#ifdef ENABLE_EXTERNAL_SIGNER
411410
flags |= WALLET_FLAG_EXTERNAL_SIGNER;
411+
if (!disable_private_keys.has_value()) {
412+
// In the basic use case all keys will be on the external signer
413+
// device and the wallet should be watch-only. Makes this the
414+
// default.
415+
disable_private_keys = true;
416+
}
412417
#else
413418
throw JSONRPCError(RPC_WALLET_ERROR, "Compiled without external signing support (required for external signing)");
414419
#endif
415420
}
416421

422+
if (disable_private_keys.value_or(false)) {
423+
flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
424+
}
425+
417426
DatabaseOptions options;
418427
DatabaseStatus status;
419428
ReadDatabaseArgs(*context.args, options);

src/wallet/wallet.cpp

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,13 +398,6 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
398398
wallet_creation_flags |= WALLET_FLAG_BLANK_WALLET;
399399
}
400400

401-
// Private keys must be disabled for an external signer wallet
402-
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
403-
error = Untranslated("Private keys must be disabled when using an external signer");
404-
status = DatabaseStatus::FAILED_CREATE;
405-
return nullptr;
406-
}
407-
408401
// Do not allow a passphrase when private keys are disabled
409402
if (!passphrase.empty() && (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
410403
error = Untranslated("Passphrase provided but private keys are disabled. A passphrase is only used to encrypt private keys, so cannot be used for wallets with private keys disabled.");
@@ -2975,7 +2968,13 @@ std::shared_ptr<CWallet> CWallet::Create(WalletContext& context, const std::stri
29752968
// Only descriptor wallets can be created
29762969
assert(walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
29772970

2978-
if ((wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) || !(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET))) {
2971+
// Don't generate or import keys for a blank wallet
2972+
if (!(wallet_creation_flags & WALLET_FLAG_BLANK_WALLET) && (
2973+
// Fetch keys from an external signer; or
2974+
(wallet_creation_flags & WALLET_FLAG_EXTERNAL_SIGNER) ||
2975+
// Generate them, unless private keys are disabled
2976+
!(wallet_creation_flags & (WALLET_FLAG_DISABLE_PRIVATE_KEYS)))
2977+
) {
29792978
walletInstance->SetupDescriptorScriptPubKeyMans();
29802979
}
29812980

src/wallet/wallet.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ static constexpr uint64_t KNOWN_WALLET_FLAGS =
159159
| WALLET_FLAG_EXTERNAL_SIGNER;
160160

161161
static constexpr uint64_t MUTABLE_WALLET_FLAGS =
162-
WALLET_FLAG_AVOID_REUSE;
162+
WALLET_FLAG_AVOID_REUSE
163+
| WALLET_FLAG_EXTERNAL_SIGNER;
163164

164165
static const std::map<WalletFlags, std::string> WALLET_FLAG_TO_STRING{
165166
{WALLET_FLAG_AVOID_REUSE, "avoid_reuse"},

test/functional/wallet_signer.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,23 @@ def test_valid_signer(self):
6666
self.log.debug(f"-signer={self.mock_signer_path()}")
6767

6868
# Create new wallets for an external signer.
69-
# disable_private_keys and descriptors must be true:
70-
assert_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)
71-
self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, external_signer=True)
69+
self.nodes[1].createwallet(wallet_name='hww', external_signer=True)
7270
hww = self.nodes[1].get_wallet_rpc('hww')
7371
assert_equal(hww.getwalletinfo()["external_signer"], True)
7472

75-
# Flag can't be set afterwards (could be added later for non-blank descriptor based watch-only wallets)
76-
self.nodes[1].createwallet(wallet_name='not_hww', disable_private_keys=True, external_signer=False)
73+
# Private keys are disabled by default
74+
assert_equal(hww.getwalletinfo()["private_keys_enabled"], False)
75+
76+
# Flag can be set afterwards
77+
self.nodes[1].createwallet(wallet_name='not_hww', external_signer=False)
7778
not_hww = self.nodes[1].get_wallet_rpc('not_hww')
7879
assert_equal(not_hww.getwalletinfo()["external_signer"], False)
79-
assert_raises_rpc_error(-8, "Wallet flag is immutable: external_signer", not_hww.setwalletflag, "external_signer", True)
80-
80+
not_hww.setwalletflag("external_signer", True)
81+
assert_equal(not_hww.getwalletinfo()["external_signer"], True)
8182

8283
self.set_mock_result(self.nodes[1], '0 {"invalid json"}')
8384
assert_raises_rpc_error(-1, 'Unable to parse JSON',
84-
self.nodes[1].createwallet, wallet_name='hww2', disable_private_keys=True, external_signer=True
85+
self.nodes[1].createwallet, wallet_name='hww2', external_signer=True
8586
)
8687
self.clear_mock_result(self.nodes[1])
8788

@@ -210,7 +211,7 @@ def test_disconnected_signer(self):
210211
self.log.info('Test disconnected external signer')
211212

212213
# First create a wallet with the signer connected
213-
self.nodes[1].createwallet(wallet_name='hww_disconnect', disable_private_keys=True, external_signer=True)
214+
self.nodes[1].createwallet(wallet_name='hww_disconnect', external_signer=True)
214215
hww = self.nodes[1].get_wallet_rpc('hww_disconnect')
215216
assert_equal(hww.getwalletinfo()["external_signer"], True)
216217

@@ -231,13 +232,13 @@ def test_disconnected_signer(self):
231232
def test_invalid_signer(self):
232233
self.log.debug(f"-signer={self.mock_invalid_signer_path()}")
233234
self.log.info('Test invalid external signer')
234-
assert_raises_rpc_error(-1, "Invalid descriptor", self.nodes[1].createwallet, wallet_name='hww_invalid', disable_private_keys=True, external_signer=True)
235+
assert_raises_rpc_error(-1, "Invalid descriptor", self.nodes[1].createwallet, wallet_name='hww_invalid', external_signer=True)
235236

236237
def test_multiple_signers(self):
237238
self.log.debug(f"-signer={self.mock_multi_signers_path()}")
238239
self.log.info('Test multiple external signers')
239240

240-
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)
241+
assert_raises_rpc_error(-1, "More than one external signer found", self.nodes[1].createwallet, wallet_name='multi_hww', external_signer=True)
241242

242243
if __name__ == '__main__':
243244
WalletSignerTest(__file__).main()

0 commit comments

Comments
 (0)