Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9ea534f
descriptor: Add unused(KEY) descriptor
achow101 Dec 22, 2023
05fb11d
test: Simple test for importing unused(KEY)
achow101 Jan 4, 2024
7109d3a
wallet: Add addhdkey RPC
achow101 Dec 22, 2023
8a73206
wallet, rpc: Disallow import of unused() if key already exists
achow101 Jan 4, 2024
1e62c53
wallet, rpc: Disallow importing unused() to wallets without privkeys
achow101 Jan 4, 2024
23f5529
doc: Release note for addhdkey
achow101 Jul 24, 2025
8f7aa62
wallet: add bip388_hmac record
Sjors Jul 21, 2025
d7e9744
wallet: add RegisterPolicy to external signer
Sjors Jul 21, 2025
2811732
wallet: add RegisterPolicy to signer SPKM
Sjors Jul 21, 2025
f04cd60
refactor: avoid std::regex in ReplaceAll
Sjors Aug 5, 2025
7a03967
wallet: add registerPolicy()
Sjors Jul 21, 2025
84ef10f
rpc: add registerpolicy
Sjors Jul 21, 2025
e4a1397
refactor: use PSBTFillOptions for filling and signing
Sjors Jul 4, 2025
c4acef9
refactor: use SignOptions for SignTransaction
Sjors Jul 4, 2025
56cd31e
refactor: use SignOptions for MutableTransactionSignatureCreator
Sjors Jul 4, 2025
2bc3172
wallet: add option to avoid script path spends
Sjors Jul 4, 2025
084acef
rpc: add keypath_only to walletprocesspsbt
Sjors Jul 3, 2025
b6b9faf
test: cover keypath_only in wallet_taproot.py
Sjors Jul 3, 2025
bd3f2b7
rpc: add keypath_only to send
Sjors Jul 3, 2025
20020bf
rpc: add DeriveExtKey() helper
Sjors Jun 20, 2025
9b70b7d
Have ParseHDKeypath handle h derivation marker
Sjors Jun 20, 2025
48e20cf
rpc: ParsePathBIP32 helper
Sjors Jun 16, 2025
6dafa51
rpc: add derivehdkey
Sjors Jun 26, 2025
1baf927
test: use derivehdkey in M-of-N multisig demo
Sjors Jun 26, 2025
18cbcb1
doc: use derivehdkey in multisig tutorial
Sjors Jan 5, 2026
f33e15e
wallet: add GetScriptlessSPKMs() helper
Sjors Jul 3, 2025
c8b6d4e
rpc: make createwalletdescriptor smarter
Sjors Jul 3, 2025
8eea5c6
Merge remote-tracking branch 'sjors/2025/06/gethdkey' into 2025/06/mu…
Sjors Feb 17, 2026
542098d
Merge remote-tracking branch 'sjors/2025/07/smart-createwalletdescrip…
Sjors Feb 17, 2026
fe94bbf
Merge remote-tracking branch 'sjors/2025/07/no_script_path' into 2025…
Sjors Feb 17, 2026
3e57405
doc: improve external-signer.md
Sjors Aug 1, 2025
b990dbb
wallet: don't import external keys at creation if blank
Sjors Aug 1, 2025
0ba76bf
wallet: avoid signing via createTransaction() with external signer
Sjors Aug 1, 2025
500fc7a
wallet: make watch-only optional for external signer
Sjors Aug 1, 2025
7d3fc16
wallet: make external_signer flag mutable
Sjors Aug 1, 2025
81297dc
Merge remote-tracking branch 'sjors/2025/07/external-signer-relax' in…
Sjors Feb 17, 2026
018d066
Merge remote-tracking branch 'sjors/2025/07/bip388-register' into 202…
Sjors Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions doc/external-signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ When using a hardware wallet, consult the manufacturer website for (alternative)
Start Bitcoin Core:

```sh
$ bitcoind -signer=../HWI/hwi.py
$ bitcoin node -signer=../HWI/hwi.py
```

`bitcoin node` can also be substituted for `bitcoind`.
(`bitcoin node` is the equivalent of `bitcoind`)

The external signer script path can also be configured from the GUI, see Options -> Wallet.

### Device setup

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

```
$ bitcoin-cli enumeratesigners
$ bitcoin rpc enumeratesigners
{
"signers": [
{
Expand All @@ -39,18 +41,18 @@ The master key fingerprint is used to identify a device.
Create a wallet, this automatically imports the public keys:

```sh
$ bitcoin-cli createwallet "hww" true true "" true true true
$ bitcoin rpc createwallet "hww" external_signer=true
```

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

### Verify an address

Display an address on the device:

```sh
$ bitcoin-cli -rpcwallet=<wallet> getnewaddress
$ bitcoin-cli -rpcwallet=<wallet> walletdisplayaddress <address>
$ bitcoin rpc -rpcwallet=<wallet> getnewaddress
$ bitcoin rpc -rpcwallet=<wallet> walletdisplayaddress <address>
```

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

```sh
$ bitcoin-cli -rpcwallet=<wallet> sendtoaddress <address> <amount>
$ bitcoin rpc -rpcwallet=<wallet> sendtoaddress <address> <amount>
```

This prompts your hardware wallet to sign, and fail if it's not connected. If successful
Expand Down
60 changes: 24 additions & 36 deletions doc/multisig-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ This tutorial uses [jq](https://github.com/stedolan/jq) JSON processor to proces
Before starting this tutorial, start the bitcoin node on the signet network.

```bash
./build/bin/bitcoind -signet -daemon
./build/bin/bitcoin node -signet -daemon
```

This tutorial also uses the default WPKH derivation path to get the xpubs and does not conform to [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki).

At the time of writing, there is no way to extract a specific path from wallets in Bitcoin Core. For this, an external signer/xpub can be used.

## 1.1 Basic Multisig Workflow

### 1.1 Create the Descriptor Wallets

For a 2-of-3 multisig, create 3 descriptor wallets. It is important that they are of the descriptor type in order to retrieve the wallet descriptors. These wallets contain HD seed and private keys, which will be used to sign the PSBTs and derive the xpub.
For a 2-of-3 multisig, create 3 wallets. These wallets contain HD seed and private keys, which will be used to sign the PSBTs and derive the xpub.

These three wallets should not be used directly for privacy reasons (public key reuse). They should only be used to sign transactions for the (watch-only) multisig wallet.

Expand All @@ -31,16 +29,7 @@ do
done
```

Extract the xpub of each wallet. To do this, the `listdescriptors` RPC is used. By default, Bitcoin Core single-sig wallets are created using path `m/44'/1'/0'` for PKH, `m/84'/1'/0'` for WPKH, `m/49'/1'/0'` for P2WPKH-nested-in-P2SH and `m/86'/1'/0'` for P2TR based accounts. Each of them uses the chain 0 for external addresses and chain 1 for internal ones, as shown in the example below.

```
wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRmVyr8Ddf7SjZ2ZfMx9RicjYAXhuh3fmLiVLPodPEqnQQURUfrBKiiVZc8/0/*)#g8l47ngv

wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRmVyr8Ddf7SjZ2ZfMx9RicjYAXhuh3fmLiVLPodPEqnQQURUfrBKiiVZc8/1/*)#en65rxc5
```

The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.
All RPCs in Bitcoin Core will include the checksum in their output.
Extract the xpub of each wallet. To do this, the `derivehdkey` RPC is used.

Note that previously at least two descriptors were usually used, one for external derivation paths and one for internal ones. Since https://github.com/bitcoin/bitcoin/pull/22838 this redundancy has been eliminated by a multipath descriptor with <code><0;1></code> at the [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#change) change level expanding to external and internal descriptors when imported.

Expand All @@ -49,66 +38,65 @@ declare -A xpubs

for ((n=1;n<=3;n++))
do
xpubs["xpub_${n}"]=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*") )][0] | .desc' | grep -Po '(?<=\().*(?=\))' | sed 's /0/\* /<0;1>/* ')
xpubs["xpub_${n}"]=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_${n}" derivehdkey "m/44h/1h/0h" | jq -r '.xpub')
done
```

`jq` is used to extract the xpub from the `wpkh` descriptor.

The following command can be used to verify if the xpub was generated correctly.
The following command can be used to verify if the xpubs were obtained successfully:

```bash
for x in "${!xpubs[@]}"; do printf "[%s]=%s\n" "$x" "${xpubs[$x]}" ; done
```

As previously mentioned, this step extracts the `m/84'/1'/0'` account instead of the path defined in [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki), since there is no way to extract a specific path in Bitcoin Core at the time of writing.
As previously mentioned, this step extracts the `m/44'/1'/0'` account instead of the path defined in [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki), because the wallet currently can't sign for a derivation path that's not used in one of its descriptors.

### 1.2 Define the Multisig Descriptor

Define the multisig descriptor, add the checksum and then, wrap it in a JSON array.
Define the multisig descriptors.

All RPCs in Bitcoin Core will include the checksum in their output.

```bash
desc="wsh(sortedmulti(2,${xpubs["xpub_1"]},${xpubs["xpub_2"]},${xpubs["xpub_3"]}))"
desc="wsh(sortedmulti(2,${xpubs["xpub_1"]}/<0;1>/*,${xpubs["xpub_2"]}/<0;1>/*,${xpubs["xpub_3"]}/<0;1>/*))"

checksum=$(./build/bin/bitcoin rpc -signet getdescriptorinfo $desc | jq -r '.checksum')
desc_sum=$(./build/bin/bitcoin rpc -signet getdescriptorinfo $desc | jq -r '.checksum')

multisig_desc="[{\"desc\": \"${desc}#${checksum}\", \"active\": true, \"timestamp\": \"now\"}]"
multisig_desc="[{\"desc\": \"$desc#$desc_sum\", \"active\": true, \"timestamp\": \"now\"}]"
```

`desc` specifies the output type (`wsh`, in this case) and the xpubs involved. It also uses BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].

After creating the descriptor, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.
`desc` specifies the output type (`wsh`, in this case) and the xpubs involved. They also use BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].

The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has the `checksum` field, which is the checksum for the input descriptor, append "#" and this checksum to the input descriptor.

There are other fields that can be added to the descriptor:
The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has the `descriptor` field, which is the descriptor with the checksum added. The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.

There are other fields that can be added to the descriptors:

* `active`: Sets the descriptor to be the active one for the corresponding output type (`wsh`, in this case).
* `internal`: Indicates whether matching outputs should be treated as something other than incoming payments (e.g. change).
* `timestamp`: Sets the time from which to start rescanning the blockchain for the descriptor, in UNIX epoch time.

Note: when a multipath descriptor is imported, it is expanded into two descriptors which are imported separately, with the second implicitly used for internal (change) addresses.

Documentation for these and other parameters can be found by typing `./build/bin/bitcoin rpc -signet help importdescriptors`.
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin rpc help importdescriptors`.

`multisig_desc` wraps the descriptor in a JSON array and will be used to create the multisig wallet.
`multisig_desc` concatenates the descriptor in a JSON array and then it will be used to create the multisig wallet.

### 1.3 Create the Multisig Wallet

To create the multisig wallet, first create an empty one (no keys, HD seed and private keys disabled).

Then import the descriptor created in the previous step using the `importdescriptors` RPC.

After that, `getwalletinfo` can be used to check if the wallet was created successfully.
After that, `listdescriptors` can be used to check if the wallet was created successfully.

```bash
./build/bin/bitcoin rpc -signet createwallet "multisig_wallet_01" disable_private_keys=true blank=true
./build/bin/bitcoin rpc -signet -named createwallet wallet_name="multisig_wallet_01" disable_private_keys=true blank=true

./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" importdescriptors "$multisig_desc"

./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getwalletinfo
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" listdescriptors
```

The `<0;1>` notation in `desc` caused the creation of two descriptors. One uses the chain 0 for external addresses the other the chain 1 for internal ones (change).

Once the wallets have already been created and this tutorial needs to be repeated or resumed, it is not necessary to recreate them, just load them with the command below:

```bash
Expand Down Expand Up @@ -199,7 +187,7 @@ psbt_2=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_2" walletproces
The PSBT, if signed separately by the co-signers, must be combined into one transaction before being finalized. This is done by `combinepsbt` RPC.

```bash
combined_psbt=$(./build/bin/bitcoin rpc -signet combinepsbt "[$psbt_1, $psbt_2]")
combined_psbt=$(./build/bin/bitcoin rpc -signet combinepsbt txs="[$psbt_1, $psbt_2]")
```

There is an RPC called `joinpsbts`, but it has a different purpose than `combinepsbt`. `joinpsbts` joins the inputs from multiple distinct PSBTs into one PSBT.
Expand Down
6 changes: 6 additions & 0 deletions doc/release-notes-29136.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Wallet
------
- A new RPC `addhdkey` is added which allows a BIP 32 extended key to be added to the wallet without
needing to import it as part of a separate descriptor. This key will not be used to produce any
output scripts unless it is explicitly imported as part of a separate descriptor independent of
the `addhdkey` RPC.
5 changes: 5 additions & 0 deletions doc/release-notes-32784.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Wallet
------

- A new `derivehdkey` RPC is available to obtain an xpub or xpriv for any given BIP32 path.
The hd key can then be imported as e.g. part of a multisig descriptor. (#22341)
2 changes: 1 addition & 1 deletion src/bench/sign_transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ static void SignTransactionSingleInput(benchmark::Bench& bench, InputType input_
const CScript& prev_spk = prev_spks[(iter++) % prev_spks.size()];
coins[prevout] = Coin(CTxOut(10000, prev_spk), /*nHeightIn=*/100, /*fCoinBaseIn=*/false);
std::map<int, bilingual_str> input_errors;
bool complete = SignTransaction(tx, &keystore, coins, SIGHASH_ALL, input_errors);
bool complete = SignTransaction(tx, &keystore, coins, {.sighash_type = SIGHASH_ALL}, input_errors);
assert(complete);
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/bitcoin-tx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ static void MutateTxSign(CMutableTransaction& tx, const std::string& flagStr)
SignatureData sigdata = DataFromTransaction(mergedTx, i, coin.out);
// Only sign SIGHASH_SINGLE if there's a corresponding output:
if (!fHashSingle || (i < mergedTx.vout.size()))
ProduceSignature(keystore, MutableTransactionSignatureCreator(mergedTx, i, amount, nHashType), prevPubKey, sigdata);
ProduceSignature(keystore, MutableTransactionSignatureCreator(mergedTx, i, amount, {.sighash_type = nHashType}), prevPubKey, sigdata);

if (amount == MAX_MONEY && !sigdata.scriptWitness.IsNull()) {
throw std::runtime_error(strprintf("Missing amount for CTxOut with scriptPubKey=%s", HexStr(prevPubKey)));
Expand Down
32 changes: 32 additions & 0 deletions src/common/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#ifndef BITCOIN_COMMON_TYPES_H
#define BITCOIN_COMMON_TYPES_H

#include <optional>

namespace common {
enum class PSBTError {
MISSING_INPUTS,
Expand All @@ -23,6 +25,36 @@ enum class PSBTError {
INCOMPLETE,
OK,
};
/**
* Instructions for how a PSBT should be signed or filled with information.
*/
struct PSBTFillOptions {
/**
* Whether to sign or not.
*/
bool sign{true};

/**
* The sighash type to use when signing (if PSBT does not specify).
*/
std::optional<int> sighash_type{std::nullopt};

/**
* Whether to create the final scriptSig or scriptWitness if possible.
*/
bool finalize{true};

/**
* Whether to fill in bip32 derivation information if available.
*/
bool bip32_derivs{true};

/**
* Only sign the key path (for taproot inputs).
*/
bool avoid_script_path{false};
};

} // namespace common

#endif // BITCOIN_COMMON_TYPES_H
9 changes: 9 additions & 0 deletions src/external_signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ UniValue ExternalSigner::GetDescriptors(const int account)
return RunCommandParseJSON(m_command + " --fingerprint " + m_fingerprint + NetworkArg() + " getdescriptors --account " + strprintf("%d", account));
}

UniValue ExternalSigner::RegisterPolicy(const std::string& name, const std::string& descriptor_template, const std::vector<std::string>& keys_info) const
{
std::string key_args;
for (const std::string& key_info : keys_info) {
key_args.append(" --key " + key_info);
}
return RunCommandParseJSON(m_command + " --fingerprint " + m_fingerprint + NetworkArg() + " register --name " + name + " --desc " + descriptor_template + key_args);
}

bool ExternalSigner::SignTransaction(PartiallySignedTransaction& psbtx, std::string& error)
{
// Serialize the PSBT
Expand Down
8 changes: 8 additions & 0 deletions src/external_signer.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ class ExternalSigner
//! @returns see doc/external-signer.md
UniValue GetDescriptors(int account);

//! Register BIP388 policy on the device.
//! Calls `<command> register` and passes the name, policy and key info
//! @param[in] name policy name to display on the signer
//! @param[in] descriptor_template BIP388 descriptor template
//! @param[in] keys_info key with origin for each participant
//! @returns hmac provided by the signer
UniValue RegisterPolicy(const std::string& name, const std::string& descriptor_template, const std::vector<std::string>& keys_info) const;

//! Sign PartiallySignedTransaction on the device.
//! Calls `<command> signtransaction` and passes the PSBT via stdin.
//! @param[in,out] psbt PartiallySignedTransaction to be signed
Expand Down
8 changes: 5 additions & 3 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <addresstype.h>
#include <common/signmessage.h>
#include <common/types.h>
#include <consensus/amount.h>
#include <interfaces/chain.h>
#include <primitives/transaction_identifier.h>
Expand Down Expand Up @@ -130,6 +131,9 @@ class Wallet
//! Display address on external signer
virtual util::Result<void> displayAddress(const CTxDestination& dest) = 0;

//! Register BIP388 policy on external signer, store and return hmac
virtual util::Result<std::string> registerPolicy(const std::optional<std::string>& name) = 0;

//! Lock coin.
virtual bool lockCoin(const COutPoint& output, bool write_to_db) = 0;

Expand Down Expand Up @@ -202,9 +206,7 @@ class Wallet
int& num_blocks) = 0;

//! Fill PSBT.
virtual std::optional<common::PSBTError> fillPSBT(std::optional<int> sighash_type,
bool sign,
bool bip32derivs,
virtual std::optional<common::PSBTError> fillPSBT(common::PSBTFillOptions options,
size_t* n_signed,
PartiallySignedTransaction& psbtx,
bool& complete) = 0;
Expand Down
4 changes: 2 additions & 2 deletions src/node/psbt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)

// Figure out what is missing
SignatureData outdata;
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, std::nullopt, &outdata) == PSBTError::OK;
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, /*options=*/{}, &outdata) == PSBTError::OK;

// Things are missing
if (!complete) {
Expand Down Expand Up @@ -124,7 +124,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
PSBTInput& input = psbtx.inputs[i];
Coin newcoin;

if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, nullptr, std::nullopt) != PSBTError::OK || !psbtx.GetInputUTXO(newcoin.out, i)) {
if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, nullptr, /*options=*/{}) != PSBTError::OK || !psbtx.GetInputUTXO(newcoin.out, i)) {
success = false;
break;
} else {
Expand Down
Loading
Loading