diff --git a/doc/external-signer.md b/doc/external-signer.md index c1b6b20ce017..b8456d58187f 100644 --- a/doc/external-signer.md +++ b/doc/external-signer.md @@ -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 @@ -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": [ { @@ -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= getnewaddress -$ bitcoin-cli -rpcwallet= walletdisplayaddress
+$ bitcoin rpc -rpcwallet= getnewaddress +$ bitcoin rpc -rpcwallet= walletdisplayaddress
``` Replace `
` with the result of `getnewaddress`. @@ -60,7 +62,7 @@ Replace `
` with the result of `getnewaddress`. Under the hood this uses a [Partially Signed Bitcoin Transaction](psbt.md). ```sh -$ bitcoin-cli -rpcwallet= sendtoaddress
+$ bitcoin rpc -rpcwallet= sendtoaddress
``` This prompts your hardware wallet to sign, and fail if it's not connected. If successful diff --git a/doc/multisig-tutorial.md b/doc/multisig-tutorial.md index 6feb3108c8b9..68621f6f885a 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. - ## 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,46 @@ 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 @@ -99,16 +85,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 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 @@ -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. diff --git a/doc/release-notes-29136.md b/doc/release-notes-29136.md new file mode 100644 index 000000000000..af074a0b30ad --- /dev/null +++ b/doc/release-notes-29136.md @@ -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. diff --git a/doc/release-notes-32784.md b/doc/release-notes-32784.md new file mode 100644 index 000000000000..96257f33639b --- /dev/null +++ b/doc/release-notes-32784.md @@ -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) diff --git a/src/bench/sign_transaction.cpp b/src/bench/sign_transaction.cpp index 96af48c57248..eab1a95adccd 100644 --- a/src/bench/sign_transaction.cpp +++ b/src/bench/sign_transaction.cpp @@ -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 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); }); } diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index fdff656f6008..ea159d6b6f44 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -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))); diff --git a/src/common/types.h b/src/common/types.h index 7b5da0fc0b00..97452fb8ff1b 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -13,6 +13,8 @@ #ifndef BITCOIN_COMMON_TYPES_H #define BITCOIN_COMMON_TYPES_H +#include + namespace common { enum class PSBTError { MISSING_INPUTS, @@ -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 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 diff --git a/src/external_signer.cpp b/src/external_signer.cpp index 84d98a199062..edd50836fdd1 100644 --- a/src/external_signer.cpp +++ b/src/external_signer.cpp @@ -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& 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 diff --git a/src/external_signer.h b/src/external_signer.h index 1b36d49622e1..56de2bb85596 100644 --- a/src/external_signer.h +++ b/src/external_signer.h @@ -57,6 +57,14 @@ class ExternalSigner //! @returns see doc/external-signer.md UniValue GetDescriptors(int account); + //! Register BIP388 policy on the device. + //! Calls ` 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& keys_info) const; + //! Sign PartiallySignedTransaction on the device. //! Calls ` signtransaction` and passes the PSBT via stdin. //! @param[in,out] psbt PartiallySignedTransaction to be signed diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index bba9e058cc01..d645ecc09d48 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -130,6 +131,9 @@ class Wallet //! Display address on external signer virtual util::Result displayAddress(const CTxDestination& dest) = 0; + //! Register BIP388 policy on external signer, store and return hmac + virtual util::Result registerPolicy(const std::optional& name) = 0; + //! Lock coin. virtual bool lockCoin(const COutPoint& output, bool write_to_db) = 0; @@ -202,9 +206,7 @@ class Wallet int& num_blocks) = 0; //! Fill PSBT. - virtual std::optional fillPSBT(std::optional sighash_type, - bool sign, - bool bip32derivs, + virtual std::optional fillPSBT(common::PSBTFillOptions options, size_t* n_signed, PartiallySignedTransaction& psbtx, bool& complete) = 0; diff --git a/src/node/psbt.cpp b/src/node/psbt.cpp index faedf0b6aabf..832cb0923cd5 100644 --- a/src/node/psbt.cpp +++ b/src/node/psbt.cpp @@ -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) { @@ -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 { diff --git a/src/psbt.cpp b/src/psbt.cpp index 4ee5f5bdd30d..a47c4a71cae4 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -375,7 +375,7 @@ void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransactio // Construct a would-be spend of this output, to update sigdata with. // Note that ProduceSignature is used to fill in metadata (not actual signatures), // so provider does not need to provide any private keys (it can be a HidingSigningProvider). - MutableTransactionSignatureCreator creator(tx, /*input_idx=*/0, out.nValue, SIGHASH_ALL); + MutableTransactionSignatureCreator creator(tx, /*input_idx=*/0, out.nValue, {.sighash_type = SIGHASH_ALL}); ProduceSignature(provider, creator, out.scriptPubKey, sigdata); // Put redeem_script, witness_script, key paths, into PSBTOutput. @@ -399,7 +399,7 @@ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& return txdata; } -PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, std::optional sighash, SignatureData* out_sigdata, bool finalize) +PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, common::PSBTFillOptions options, SignatureData* out_sigdata) { PSBTInput& input = psbt.inputs.at(index); const CMutableTransaction& tx = *psbt.tx; @@ -442,8 +442,8 @@ PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransact // If only the parameter is provided, use it and add it to the PSBT if it is other than SIGHASH_DEFAULT // for all input types, and not SIGHASH_ALL for non-taproot input types. // If neither are provided, use SIGHASH_DEFAULT if it is taproot, and SIGHASH_ALL for everything else. - if (!sighash) sighash = utxo.scriptPubKey.IsPayToTaproot() ? SIGHASH_DEFAULT : SIGHASH_ALL; - Assert(sighash.has_value()); + int sighash{options.sighash_type.value_or(utxo.scriptPubKey.IsPayToTaproot() ? SIGHASH_DEFAULT : SIGHASH_ALL)}; + // For user safety, the desired sighash must be provided if the PSBT wants something other than the default set in the previous line. if (input.sighash_type && input.sighash_type != sighash) { return PSBTError::SIGHASH_MISMATCH; @@ -465,14 +465,14 @@ PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransact if (sig.size() != 64) return PSBTError::SIGHASH_MISMATCH; } } else { - if (!input.m_tap_key_sig.empty() && (input.m_tap_key_sig.size() != 65 || input.m_tap_key_sig.back() != *sighash)) { + if (!input.m_tap_key_sig.empty() && (input.m_tap_key_sig.size() != 65 || input.m_tap_key_sig.back() != sighash)) { return PSBTError::SIGHASH_MISMATCH; } for (const auto& [_, sig] : input.m_tap_script_sigs) { - if (sig.size() != 65 || sig.back() != *sighash) return PSBTError::SIGHASH_MISMATCH; + if (sig.size() != 65 || sig.back() != sighash) return PSBTError::SIGHASH_MISMATCH; } for (const auto& [_, sig] : input.partial_sigs) { - if (sig.second.back() != *sighash) return PSBTError::SIGHASH_MISMATCH; + if (sig.second.back() != sighash) return PSBTError::SIGHASH_MISMATCH; } } @@ -481,14 +481,14 @@ PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransact if (txdata == nullptr) { sig_complete = ProduceSignature(provider, DUMMY_SIGNATURE_CREATOR, utxo.scriptPubKey, sigdata); } else { - MutableTransactionSignatureCreator creator(tx, index, utxo.nValue, txdata, *sighash); + MutableTransactionSignatureCreator creator(tx, index, utxo.nValue, txdata, {.sighash_type = sighash, .avoid_script_path = options.avoid_script_path}); sig_complete = ProduceSignature(provider, creator, utxo.scriptPubKey, sigdata); } // Verify that a witness signature was produced in case one was required. if (require_witness_sig && !sigdata.witness) return PSBTError::INCOMPLETE; // If we are not finalizing, set sigdata.complete to false to not set the scriptWitness - if (!finalize && sigdata.complete) sigdata.complete = false; + if (!options.finalize && sigdata.complete) sigdata.complete = false; input.FromSignatureData(sigdata); @@ -558,7 +558,7 @@ bool FinalizePSBT(PartiallySignedTransaction& psbtx) const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx); for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) { PSBTInput& input = psbtx.inputs.at(i); - complete &= (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, input.sighash_type, nullptr, true) == PSBTError::OK); + complete &= (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, {.sighash_type = input.sighash_type, .finalize = true}, nullptr) == PSBTError::OK); } return complete; diff --git a/src/psbt.h b/src/psbt.h index 1a426a2eeb3f..4ec8c02989e3 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -1420,7 +1420,7 @@ bool PSBTInputSignedAndVerified(const PartiallySignedTransaction& psbt, unsigned * txdata should be the output of PrecomputePSBTData (which can be shared across * multiple SignPSBTInput calls). If it is nullptr, a dummy signature will be created. **/ -[[nodiscard]] PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, std::optional sighash = std::nullopt, SignatureData* out_sigdata = nullptr, bool finalize = true); +[[nodiscard]] PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, common::PSBTFillOptions options, SignatureData* out_sigdata = nullptr); /** Reduces the size of the PSBT by dropping unnecessary `non_witness_utxos` (i.e. complete previous transactions) from a psbt when all inputs are segwit v1. */ void RemoveUnnecessaryTransactions(PartiallySignedTransaction& psbtx); diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 59c6f51a27ec..b73c9728240e 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -26,34 +26,25 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : }); connect(ui->encrypt_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) { - // Disable the disable_privkeys_checkbox and external_signer_checkbox when isEncryptWalletChecked is + // Disable the disable_privkeys_checkbox when isEncryptWalletChecked is // set to true, enable it when isEncryptWalletChecked is false. ui->disable_privkeys_checkbox->setEnabled(!checked); -#ifdef ENABLE_EXTERNAL_SIGNER - ui->external_signer_checkbox->setEnabled(m_has_signers && !checked); -#endif + // When the disable_privkeys_checkbox is disabled, uncheck it. if (!ui->disable_privkeys_checkbox->isEnabled()) { ui->disable_privkeys_checkbox->setChecked(false); } - - // When the external_signer_checkbox box is disabled, uncheck it. - if (!ui->external_signer_checkbox->isEnabled()) { - ui->external_signer_checkbox->setChecked(false); - } - }); connect(ui->external_signer_checkbox, &QCheckBox::toggled, [this](bool checked) { - ui->encrypt_wallet_checkbox->setEnabled(!checked); - ui->blank_wallet_checkbox->setEnabled(!checked); - ui->disable_privkeys_checkbox->setEnabled(!checked); + // In the basic use case all keys will be on the external signer + // device and the wallet should be watch-only. Makes this the + // default suggestion. + ui->disable_privkeys_checkbox->setChecked(checked); - // The external signer checkbox is only enabled when a device is detected. - // In that case it is checked by default. Toggling it restores the other - // options to their default. + // The external signer box is checked by default when a device is + // detected. Toggling it restores the other options to their default. ui->encrypt_wallet_checkbox->setChecked(false); - ui->disable_privkeys_checkbox->setChecked(checked); ui->blank_wallet_checkbox->setChecked(false); }); @@ -103,12 +94,9 @@ void CreateWalletDialog::setSigners(const std::vectorexternal_signer_checkbox->setEnabled(true); ui->external_signer_checkbox->setChecked(true); - ui->encrypt_wallet_checkbox->setEnabled(false); ui->encrypt_wallet_checkbox->setChecked(false); // The order matters, because connect() is called when toggling a checkbox: - ui->blank_wallet_checkbox->setEnabled(false); ui->blank_wallet_checkbox->setChecked(false); - ui->disable_privkeys_checkbox->setEnabled(false); ui->disable_privkeys_checkbox->setChecked(true); const std::string label = signers[0]->getName(); ui->wallet_name_line_edit->setText(QString::fromStdString(label)); diff --git a/src/qt/psbtoperationsdialog.cpp b/src/qt/psbtoperationsdialog.cpp index 7661e59fa7af..f9c9409f1ac2 100644 --- a/src/qt/psbtoperationsdialog.cpp +++ b/src/qt/psbtoperationsdialog.cpp @@ -59,7 +59,7 @@ void PSBTOperationsDialog::openWithPSBT(PartiallySignedTransaction psbtx) bool complete = FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness. if (m_wallet_model) { size_t n_could_sign; - const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, &n_could_sign, m_transaction_data, complete)}; + const auto err{m_wallet_model->wallet().fillPSBT({.sign = false, .bip32_derivs= true}, &n_could_sign, m_transaction_data, complete)}; if (err) { showStatus(tr("Failed to load transaction: %1") .arg(QString::fromStdString(PSBTErrorString(*err).translated)), @@ -83,7 +83,7 @@ void PSBTOperationsDialog::signTransaction() WalletModel::UnlockContext ctx(m_wallet_model->requestUnlock()); - const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/true, /*bip32derivs=*/true, &n_signed, m_transaction_data, complete)}; + const auto err{m_wallet_model->wallet().fillPSBT({.sign = true, .bip32_derivs = true}, &n_signed, m_transaction_data, complete)}; if (err) { showStatus(tr("Failed to sign transaction: %1") @@ -251,7 +251,7 @@ size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &p size_t n_signed; bool complete; - const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)}; + const auto err{m_wallet_model->wallet().fillPSBT({.sign = false, .bip32_derivs = false}, &n_signed, m_transaction_data, complete)}; if (err) { return 0; diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 90e46c798158..188c5500b7af 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -450,7 +450,7 @@ void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx) bool SendCoinsDialog::signWithExternalSigner(PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool& complete) { std::optional err; try { - err = model->wallet().fillPSBT(std::nullopt, /*sign=*/true, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete); + err = model->wallet().fillPSBT({.sign = true, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete); } catch (const std::runtime_error& e) { QMessageBox::critical(nullptr, tr("Sign failed"), e.what()); return false; @@ -507,7 +507,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) PartiallySignedTransaction psbtx(mtx); bool complete = false; // Fill without signing - const auto err{model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)}; + const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)}; assert(!complete); assert(!err); @@ -523,7 +523,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) bool complete = false; // Always fill without signing first. This prevents an external signer // from being called prematurely and is not expensive. - const auto err{model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)}; + const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)}; assert(!complete); assert(!err); send_failure = !signWithExternalSigner(psbtx, mtx, complete); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 7a35f8e4d356..c257223cc408 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -203,7 +203,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact try { auto& newTx = transaction.getWtx(); - const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), /*change_pos=*/std::nullopt); + const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled() && !wallet().hasExternalSigner(), /*change_pos=*/std::nullopt); if (!res) { Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated), CClientUIInterface::MSG_ERROR); @@ -517,7 +517,7 @@ bool WalletModel::bumpFee(Txid hash, Txid& new_hash) // "Create Unsigned" clicked PartiallySignedTransaction psbtx(mtx); bool complete = false; - const auto err{wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, nullptr, psbtx, complete)}; + const auto err{wallet().fillPSBT({.sign = false, .bip32_derivs = true}, nullptr, psbtx, complete)}; if (err || complete) { QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction.")); return false; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 77e5ec080522..90b13c3d45b5 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -214,6 +214,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "walletprocesspsbt", 2, "sighashtype", ParamFormat::STRING }, { "walletprocesspsbt", 3, "bip32derivs" }, { "walletprocesspsbt", 4, "finalize" }, + { "walletprocesspsbt", 5, "keypath_only"}, { "descriptorprocesspsbt", 0, "psbt", ParamFormat::STRING }, { "descriptorprocesspsbt", 1, "descriptors"}, { "descriptorprocesspsbt", 2, "sighashtype", ParamFormat::STRING }, @@ -263,6 +264,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "send", 4, "replaceable"}, { "send", 4, "solving_data"}, { "send", 4, "max_tx_weight"}, + { "send", 4, "keypath_only"}, { "send", 5, "version"}, { "sendall", 0, "recipients" }, { "sendall", 1, "conf_target" }, @@ -331,6 +333,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "gethdkeys", 0, "active_only" }, { "gethdkeys", 0, "options" }, { "gethdkeys", 0, "private" }, + { "derivehdkey", 1, "options" }, + { "derivehdkey", 1, "private" }, { "createwalletdescriptor", 1, "options" }, { "createwalletdescriptor", 1, "internal" }, // Echo with conversion (For testing only) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 5885422be7c2..ac4ea067b0fe 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -198,7 +198,7 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std // We only actually care about those if our signing provider doesn't hide private // information, as is the case with `descriptorprocesspsbt` // Only error for mismatching sighash types as it is critical that the sighash to sign with matches the PSBT's - if (SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, sighash_type, /*out_sigdata=*/nullptr, finalize) == common::PSBTError::SIGHASH_MISMATCH) { + if (SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, {.sighash_type = sighash_type, .finalize = finalize}, /*out_sigdata=*/nullptr) == common::PSBTError::SIGHASH_MISMATCH) { throw JSONRPCPSBTError(common::PSBTError::SIGHASH_MISMATCH); } } @@ -659,7 +659,7 @@ static RPCHelpMan combinerawtransaction() sigdata.MergeSignatureData(DataFromTransaction(txv, i, coin.out)); } } - ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(mergedTx, i, coin.out.nValue, 1), coin.out.scriptPubKey, sigdata); + ProduceSignature(DUMMY_SIGNING_PROVIDER, MutableTransactionSignatureCreator(mergedTx, i, coin.out.nValue, {.sighash_type = SIGHASH_ALL}), coin.out.scriptPubKey, sigdata); UpdateInput(txin, sigdata); } diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 467289fc14c9..f1743c8e026c 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -318,7 +318,7 @@ void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, // Script verification errors std::map input_errors; - bool complete = SignTransaction(mtx, keystore, coins, *nHashType, input_errors); + bool complete = SignTransaction(mtx, keystore, coins, {.sighash_type = *nHashType}, input_errors); SignTransactionResultToJSON(mtx, complete, coins, input_errors, result); } diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 49569c7be540..d7a92bdb8863 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,11 +16,13 @@ #include #include