diff --git a/doc/multisig-tutorial.md b/doc/multisig-tutorial.md index b6f496a32d3f..fd74e62ed28f 100644 --- a/doc/multisig-tutorial.md +++ b/doc/multisig-tutorial.md @@ -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. +This tutorial also uses the default PKH 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). ## 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. @@ -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 <0;1> 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. @@ -49,49 +38,44 @@ 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 '.origin + .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. +The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has a `checksum` field, which is appended to the descriptor after `#` to protect against typos or copy-paste errors. -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: +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`. -`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 @@ -99,16 +83,18 @@ To create the multisig wallet, first create an empty one (no keys, HD seed and p 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, and the other uses 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 @@ -199,7 +185,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. diff --git a/doc/release-notes-32784.md b/doc/release-notes-32784.md new file mode 100644 index 000000000000..6312165f46b5 --- /dev/null +++ b/doc/release-notes-32784.md @@ -0,0 +1,9 @@ +Wallet +------ + +- A new `derivehdkey` RPC is available to obtain an xpub or xprv for a + derivation path with at least one hardened step from an HD key known to the + wallet. This can be used to coordinate a multisig setup, where each signer + shares an xpub using a + different derivation path than the default single-signature descriptors. The + example in `doc/multisig-tutorial.md` is updated to use this RPC. (#32784) diff --git a/doc/release-notes-32857.md b/doc/release-notes-32857.md new file mode 100644 index 000000000000..26687e61192c --- /dev/null +++ b/doc/release-notes-32857.md @@ -0,0 +1,6 @@ +Updated RPCs +------------ + +- The `send`, `sendall`, `walletprocesspsbt`, and `descriptorprocesspsbt` RPCs + now accept a `keypath_only` option that only signs the key path for taproot + inputs. (#32857) diff --git a/src/common/types.h b/src/common/types.h index b9ebca15e84f..338435197466 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -49,6 +49,11 @@ struct PSBTFillOptions { * Whether to fill in bip32 derivation information if available. */ bool bip32_derivs{true}; + + /** + * Only sign the key path (for taproot inputs). + */ + bool taproot_keypath_only{false}; }; } // namespace common diff --git a/src/external_signer.cpp b/src/external_signer.cpp index da75de77a11d..1e3d1f71c9e8 100644 --- a/src/external_signer.cpp +++ b/src/external_signer.cpp @@ -74,6 +74,40 @@ UniValue ExternalSigner::GetDescriptors(const int account) return RunCommandParseJSON(Cat(m_command, Cat(Cat({"--fingerprint", m_fingerprint}, NetworkArg()), {"getdescriptors", "--account", strprintf("%d", account)})), ""); } +UniValue ExternalSigner::RegisterPolicy(const std::string& name, const std::string& descriptor_template, const std::vector& keys_info) const +{ + std::vector command = Cat(m_command, Cat(Cat({"--fingerprint", m_fingerprint}, NetworkArg()), {"register", "--name", name, "--desc", descriptor_template})); + for (const std::string& key_info : keys_info) { + command.emplace_back("--key"); + command.emplace_back(key_info); + } + return RunCommandParseJSON(command, ""); +} + +UniValue ExternalSigner::DisplayAddressPolicy(const std::string& name, + const std::string& descriptor_template, + const std::vector& keys_info, + const std::optional& hmac, + bool change, + uint32_t index) const +{ + std::vector command = Cat(m_command, Cat(Cat({"--fingerprint", m_fingerprint}, NetworkArg()), + {"displayaddress", + "--policy-name", name, + "--policy-desc", descriptor_template, + "--index", strprintf("%u", index)})); + if (hmac) { + command.emplace_back("--hmac"); + command.emplace_back(*hmac); + } + for (const std::string& key_info : keys_info) { + command.emplace_back("--key"); + command.emplace_back(key_info); + } + if (change) command.emplace_back("--change"); + return RunCommandParseJSON(command, ""); +} + bool ExternalSigner::SignTransaction(PartiallySignedTransaction& psbtx, std::string& error) { // Serialize the PSBT @@ -122,3 +156,60 @@ bool ExternalSigner::SignTransaction(PartiallySignedTransaction& psbtx, std::str return true; } + +bool ExternalSigner::SignTransactionPolicy(PartiallySignedTransaction& psbtx, + const std::string& name, + const std::string& descriptor_template, + const std::vector& keys_info, + const std::optional& hmac, + std::string& error) +{ + // Serialize the PSBT + DataStream ssTx{}; + ssTx << psbtx; + + const std::vector command = Cat(m_command, Cat({"--stdin", "--fingerprint", m_fingerprint}, NetworkArg())); + + // Policy args travel on the same stdin line as `signtx `. + // HWI re-parses that line with shlex, which treats the apostrophes + // in hardened derivation markers (e.g. [f5acc2fd/87'/1'/0']xpub...) + // as quote characters, silently corrupting the keys and thereby the + // wallet policy id. Wrap each value in double quotes so it survives + // shlex intact; none of these values may themselves contain a double + // quote or backslash. + const auto quoted = [](const std::string& s) { + if (s.find_first_of("\"\\") != std::string::npos) { + throw std::runtime_error("Policy argument contains unsupported character: " + s); + } + return " \"" + s + "\""; + }; + std::string stdinStr = "signtx " + EncodeBase64(ssTx.str()) + + " --policy-name" + quoted(name) + + " --policy-desc" + quoted(descriptor_template); + if (hmac) { + stdinStr += " --hmac" + quoted(*hmac); + } + for (const std::string& key_info : keys_info) { + stdinStr += " --key" + quoted(key_info); + } + + const UniValue signer_result = RunCommandParseJSON(command, stdinStr); + + if (signer_result.find_value("error").isStr()) { + error = signer_result.find_value("error").get_str(); + return false; + } + if (!signer_result.find_value("psbt").isStr()) { + error = "Unexpected result from signer"; + return false; + } + + util::Result signer_psbtx = DecodeBase64PSBT(signer_result.find_value("psbt").get_str()); + if (!signer_psbtx) { + error = strprintf("TX decode failed %s", util::ErrorString(signer_psbtx).original); + return false; + } + + psbtx = *signer_psbtx; + return true; +} diff --git a/src/external_signer.h b/src/external_signer.h index 5131a2078f3d..43f452339a5c 100644 --- a/src/external_signer.h +++ b/src/external_signer.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -52,6 +53,23 @@ class ExternalSigner //! Must include a public key or xpub, as well as key origin. UniValue DisplayAddress(const std::string& descriptor) const; + //! Display an address from a previously-registered BIP388 wallet policy. + //! Calls ` displayaddress --policy-name --policy-desc