Merge bitcoin/bitcoin#31622: psbt: add non-default sighash types to PSBTs and unify sighash type match checking

ee045b61ef rpc, psbt: Require sighashes match for descriptorprocesspsbt (Ava Chow)
2b7682c372 psbt: use sighash type field to determine whether to remove non-witness utxos (Ava Chow)
28781b5f06 psbt: Add sighash types to PSBT when not DEFAULT or ALL (Ava Chow)
15ce1bd73f psbt: Enforce sighash type of signatures matches psbt (Ava Chow)
1f71cd337a wallet: Remove sighash type enforcement from FillPSBT (Ava Chow)
4c7d767e49 psbt: Check sighash types in SignPSBTInput and take sighash as optional (Ava Chow)
a118256948 script: Add IsPayToTaproot() (Ava Chow)
d6001dcd4a wallet: change FillPSBT to take sighash as optional (Ava Chow)
e58b680923 psbt: Return PSBTError from SignPSBTInput (Ava Chow)
2adfd81532 tests: Test PSBT sighash type mismatch (Ava Chow)
5a5d26d612 psbt: Require ECDSA signatures to be validly encoded (Ava Chow)

Pull request description:

  Currently, we do not add the sighash field to PSBTs at all, even when we have signed with a non-default sighash. This PR changes the behavior such that when we (attempt to) sign with a sighash other than DEFAULT or ALL, the sighash type field will be added to the PSBT to inform the later signers that a different sighash type was used by a signer. Notably, this is necessary for MuSig2 support as all signers must sign using the same sighash type, but the sighash is not provided in partial signatures.

  Furthermore, because the sighash type can also be provided on the command line, we require that if both a command line sighash type and the sighash field is present, they must specify the same sighash type. However, this was being checked by the wallet, rather than the signing code, so the `descriptorprocesspsbt` RPC was not enforcing this restriction at all, and in fact ignored the sighash field entirely. This PR refactors the checking code so that the underlying PSBT signing function `SignPSBTInput` does the check.

ACKs for top commit:
  theStack:
    re-ACK ee045b61ef
  rkrux:
    re-ACK ee045b61ef
  fjahr:
    Code review ACK ee045b61ef

Tree-SHA512: 4ead5be1ef6756251b827f594beba868a145d75bf7f4ef6f15ad21f0ae4b8d71b38c83494e5a6b75f37fadd097178cddd93d614b962a2c72fc134f00ba2f74ae
This commit is contained in:
merge-script committed 2025-05-21 10:02:49 +01:00
commit ec81204694
27 files changed
+253 -84

No files matched your search

+4
View File
@@ -116,6 +116,10 @@ bilingual_str PSBTErrorString(PSBTError err)
return Untranslated("External signer failed to sign");
case PSBTError::UNSUPPORTED:
return Untranslated("Signer does not support PSBT");
case PSBTError::INCOMPLETE:
return Untranslated("Input needs additional signatures or other data");
case PSBTError::OK:
return Untranslated("No errors");
// no default case, so the compiler can warn about missing cases
}
assert(false);
+2
View File
@@ -20,6 +20,8 @@ enum class PSBTError {
EXTERNAL_SIGNER_NOT_FOUND,
EXTERNAL_SIGNER_FAILED,
UNSUPPORTED,
INCOMPLETE,
OK,
};
} // namespace common
+1 -1
View File
@@ -205,7 +205,7 @@ public:
int& num_blocks) = 0;
//! Fill PSBT.
virtual std::optional<common::PSBTError> fillPSBT(int sighash_type,
virtual std::optional<common::PSBTError> fillPSBT(std::optional<int> sighash_type,
bool sign,
bool bip32derivs,
size_t* n_signed,
+2 -2
View File
@@ -64,7 +64,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
// Figure out what is missing
SignatureData outdata;
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, 1, &outdata);
bool complete = SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, std::nullopt, &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, 1) || !psbtx.GetInputUTXO(newcoin.out, i)) {
if (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, nullptr, std::nullopt) != PSBTError::OK || !psbtx.GetInputUTXO(newcoin.out, i)) {
success = false;
break;
} else {
+82 -34
View File
@@ -4,12 +4,15 @@
#include <psbt.h>
#include <common/types.h>
#include <node/types.h>
#include <policy/policy.h>
#include <script/signingprovider.h>
#include <util/check.h>
#include <util/strencodings.h>
using common::PSBTError;
PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction& tx) : tx(tx)
{
inputs.resize(tx.vin.size());
@@ -372,13 +375,13 @@ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction&
return txdata;
}
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, int sighash, SignatureData* out_sigdata, bool finalize)
PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, std::optional<int> sighash, SignatureData* out_sigdata, bool finalize)
{
PSBTInput& input = psbt.inputs.at(index);
const CMutableTransaction& tx = *psbt.tx;
if (PSBTInputSignedAndVerified(psbt, index, txdata)) {
return true;
return PSBTError::OK;
}
// Fill SignatureData with input info
@@ -393,10 +396,10 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
// If we're taking our information from a non-witness UTXO, verify that it matches the prevout.
COutPoint prevout = tx.vin[index].prevout;
if (prevout.n >= input.non_witness_utxo->vout.size()) {
return false;
return PSBTError::MISSING_INPUTS;
}
if (input.non_witness_utxo->GetHash() != prevout.hash) {
return false;
return PSBTError::MISSING_INPUTS;
}
utxo = input.non_witness_utxo->vout[prevout.n];
} else if (!input.witness_utxo.IsNull()) {
@@ -407,7 +410,46 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
// a witness signature in this situation.
require_witness_sig = true;
} else {
return false;
return PSBTError::MISSING_INPUTS;
}
// Get the sighash type
// If both the field and the parameter are provided, they must match
// 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());
// 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;
}
// Set the PSBT sighash field when sighash is not DEFAULT or ALL
// DEFAULT is allowed for non-taproot inputs since DEFAULT may be passed for them (e.g. the psbt being signed also has taproot inputs)
// Note that signing already aliases DEFAULT to ALL for non-taproot inputs.
if (utxo.scriptPubKey.IsPayToTaproot() ? sighash != SIGHASH_DEFAULT :
(sighash != SIGHASH_DEFAULT && sighash != SIGHASH_ALL)) {
input.sighash_type = sighash;
}
// Check all existing signatures use the sighash type
if (sighash == SIGHASH_DEFAULT) {
if (!input.m_tap_key_sig.empty() && input.m_tap_key_sig.size() != 64) {
return PSBTError::SIGHASH_MISMATCH;
}
for (const auto& [_, sig] : input.m_tap_script_sigs) {
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)) {
return PSBTError::SIGHASH_MISMATCH;
}
for (const auto& [_, sig] : input.m_tap_script_sigs) {
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;
}
}
sigdata.witness = false;
@@ -415,11 +457,11 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
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);
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 false;
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;
@@ -442,39 +484,44 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
out_sigdata->missing_witness_script = sigdata.missing_witness_script;
}
return sig_complete;
return sig_complete ? PSBTError::OK : PSBTError::INCOMPLETE;
}
void RemoveUnnecessaryTransactions(PartiallySignedTransaction& psbtx, const int& sighash_type)
void RemoveUnnecessaryTransactions(PartiallySignedTransaction& psbtx)
{
// Only drop non_witness_utxos if sighash_type != SIGHASH_ANYONECANPAY
if ((sighash_type & 0x80) != SIGHASH_ANYONECANPAY) {
// Figure out if any non_witness_utxos should be dropped
std::vector<unsigned int> to_drop;
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
const auto& input = psbtx.inputs.at(i);
int wit_ver;
std::vector<unsigned char> wit_prog;
if (input.witness_utxo.IsNull() || !input.witness_utxo.scriptPubKey.IsWitnessProgram(wit_ver, wit_prog)) {
// There's a non-segwit input or Segwit v0, so we cannot drop any witness_utxos
to_drop.clear();
break;
}
if (wit_ver == 0) {
// Segwit v0, so we cannot drop any non_witness_utxos
to_drop.clear();
break;
}
if (input.non_witness_utxo) {
to_drop.push_back(i);
}
// Figure out if any non_witness_utxos should be dropped
std::vector<unsigned int> to_drop;
for (unsigned int i = 0; i < psbtx.inputs.size(); ++i) {
const auto& input = psbtx.inputs.at(i);
int wit_ver;
std::vector<unsigned char> wit_prog;
if (input.witness_utxo.IsNull() || !input.witness_utxo.scriptPubKey.IsWitnessProgram(wit_ver, wit_prog)) {
// There's a non-segwit input, so we cannot drop any non_witness_utxos
to_drop.clear();
break;
}
if (wit_ver == 0) {
// Segwit v0, so we cannot drop any non_witness_utxos
to_drop.clear();
break;
}
// non_witness_utxos cannot be dropped if the sighash type includes SIGHASH_ANYONECANPAY
// Since callers should have called SignPSBTInput which updates the sighash type in the PSBT, we only
// need to look at that field. If it is not present, then we can assume SIGHASH_DEFAULT or SIGHASH_ALL.
if (input.sighash_type != std::nullopt && (*input.sighash_type & 0x80) == SIGHASH_ANYONECANPAY) {
to_drop.clear();
break;
}
// Drop the non_witness_utxos that we can drop
for (unsigned int i : to_drop) {
psbtx.inputs.at(i).non_witness_utxo = nullptr;
if (input.non_witness_utxo) {
to_drop.push_back(i);
}
}
// Drop the non_witness_utxos that we can drop
for (unsigned int i : to_drop) {
psbtx.inputs.at(i).non_witness_utxo = nullptr;
}
}
bool FinalizePSBT(PartiallySignedTransaction& psbtx)
@@ -486,7 +533,8 @@ bool FinalizePSBT(PartiallySignedTransaction& psbtx)
bool complete = true;
const PrecomputedTransactionData txdata = PrecomputePSBTData(psbtx);
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
complete &= SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, SIGHASH_ALL, nullptr, true);
PSBTInput& input = psbtx.inputs.at(i);
complete &= (SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, &txdata, input.sighash_type, nullptr, true) == PSBTError::OK);
}
return complete;
+10 -2
View File
@@ -5,6 +5,7 @@
#ifndef BITCOIN_PSBT_H
#define BITCOIN_PSBT_H
#include <common/types.h>
#include <node/transaction.h>
#include <policy/feerate.h>
#include <primitives/transaction.h>
@@ -21,6 +22,8 @@ namespace node {
enum class TransactionError;
} // namespace node
using common::PSBTError;
// Magic bytes
static constexpr uint8_t PSBT_MAGIC_BYTES[5] = {'p', 's', 'b', 't', 0xff};
@@ -520,6 +523,11 @@ struct PSBTInput
std::vector<unsigned char> sig;
s >> sig;
// Check that the signature is validly encoded
if (sig.empty() || !CheckSignatureEncoding(sig, SCRIPT_VERIFY_DERSIG | SCRIPT_VERIFY_STRICTENC, nullptr)) {
throw std::ios_base::failure("Signature is not a valid encoding");
}
// Add to list
partial_sigs.emplace(pubkey.GetID(), SigPair(pubkey, std::move(sig)));
break;
@@ -1396,10 +1404,10 @@ 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.
**/
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool finalize = true);
[[nodiscard]] PSBTError SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, const PrecomputedTransactionData* txdata, std::optional<int> sighash = std::nullopt, SignatureData* out_sigdata = nullptr, bool finalize = true);
/** 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, const int& sighash_type);
void RemoveUnnecessaryTransactions(PartiallySignedTransaction& psbtx);
/** Counts the unsigned inputs of a PSBT. */
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt);
+3 -3
View File
@@ -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(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, &n_could_sign, m_transaction_data, complete)};
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/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(SIGHASH_DEFAULT, /*sign=*/true, /*bip32derivs=*/true, &n_signed, m_transaction_data, complete)};
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/true, /*bip32derivs=*/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(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)};
const auto err{m_wallet_model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/false, &n_signed, m_transaction_data, complete)};
if (err) {
return 0;
+3 -3
View File
@@ -450,7 +450,7 @@ void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx)
bool SendCoinsDialog::signWithExternalSigner(PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool& complete) {
std::optional<PSBTError> err;
try {
err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/true, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
err = model->wallet().fillPSBT(std::nullopt, /*sign=*/true, /*bip32derivs=*/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(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)};
const auto err{model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/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(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)};
const auto err{model->wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete)};
assert(!complete);
assert(!err);
send_failure = !signWithExternalSigner(psbtx, mtx, complete);
+1 -1
View File
@@ -521,7 +521,7 @@ bool WalletModel::bumpFee(Txid hash, Txid& new_hash)
// "Create Unsigned" clicked
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const auto err{wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, nullptr, psbtx, complete)};
const auto err{wallet().fillPSBT(std::nullopt, /*sign=*/false, /*bip32derivs=*/true, nullptr, psbtx, complete)};
if (err || complete) {
QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction."));
return false;
+8 -5
View File
@@ -163,7 +163,7 @@ static std::vector<RPCArg> CreateTxDoc()
// Update PSBT with information from the mempool, the UTXO set, the txindex, and the provided descriptors.
// Optionally, sign the inputs that we can using information from the descriptors.
PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider, int sighash_type, bool finalize)
PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std::any& context, const HidingSigningProvider& provider, std::optional<int> sighash_type, bool finalize)
{
// Unserialize the transactions
PartiallySignedTransaction psbtx;
@@ -235,7 +235,10 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std
// Note that SignPSBTInput does a lot more than just constructing ECDSA signatures.
// We only actually care about those if our signing provider doesn't hide private
// information, as is the case with `descriptorprocesspsbt`
SignPSBTInput(provider, psbtx, /*index=*/i, &txdata, sighash_type, /*out_sigdata=*/nullptr, finalize);
// 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) {
throw JSONRPCPSBTError(common::PSBTError::SIGHASH_MISMATCH);
}
}
// Update script/keypath information using descriptor data.
@@ -243,7 +246,7 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std
UpdatePSBTOutput(provider, psbtx, i);
}
RemoveUnnecessaryTransactions(psbtx, /*sighash_type=*/1);
RemoveUnnecessaryTransactions(psbtx);
return psbtx;
}
@@ -1802,7 +1805,7 @@ static RPCHelpMan utxoupdatepsbt()
request.params[0].get_str(),
request.context,
HidingSigningProvider(&provider, /*hide_secret=*/true, /*hide_origin=*/false),
/*sighash_type=*/SIGHASH_ALL,
/*sighash_type=*/std::nullopt,
/*finalize=*/false);
DataStream ssTx{};
@@ -2072,7 +2075,7 @@ RPCHelpMan descriptorprocesspsbt()
EvalDescriptorStringOrObject(descs[i], provider, /*expand_priv=*/true);
}
int sighash_type = ParseSighashString(request.params[2]);
std::optional<int> sighash_type = ParseSighashString(request.params[2]);
bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();
+5 -2
View File
@@ -304,12 +304,15 @@ void ParsePrevouts(const UniValue& prevTxsUnival, FlatSigningProvider* keystore,
void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result)
{
int nHashType = ParseSighashString(hashType);
std::optional<int> nHashType = ParseSighashString(hashType);
if (!nHashType) {
nHashType = SIGHASH_DEFAULT;
}
// Script verification errors
std::map<int, bilingual_str> input_errors;
bool complete = SignTransaction(mtx, keystore, coins, nHashType, input_errors);
bool complete = SignTransaction(mtx, keystore, coins, *nHashType, input_errors);
SignTransactionResultToJSON(mtx, complete, coins, input_errors, result);
}
+2 -2
View File
@@ -357,10 +357,10 @@ UniValue DescribeAddress(const CTxDestination& dest)
*
* @pre The sighash argument should be string or null.
*/
int ParseSighashString(const UniValue& sighash)
std::optional<int> ParseSighashString(const UniValue& sighash)
{
if (sighash.isNull()) {
return SIGHASH_DEFAULT;
return std::nullopt;
}
const auto result{SighashFromStr(sighash.get_str())};
if (!result) {
+1 -1
View File
@@ -137,7 +137,7 @@ CTxDestination AddAndGetMultisigDestination(const int required, const std::vecto
UniValue DescribeAddress(const CTxDestination& dest);
/** Parse a sighash string representation and raise an RPC error if it is invalid. */
int ParseSighashString(const UniValue& sighash);
std::optional<int> ParseSighashString(const UniValue& sighash);
//! Parse a confirm target option and raise an RPC error if it is invalid.
unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target);
+7
View File
@@ -238,6 +238,13 @@ bool CScript::IsPayToWitnessScriptHash() const
(*this)[1] == 0x20);
}
bool CScript::IsPayToTaproot() const
{
return (this->size() == 34 &&
(*this)[0] == OP_1 &&
(*this)[1] == 0x20);
}
// A witness program is any valid CScript that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
bool CScript::IsWitnessProgram(int& version, std::vector<unsigned char>& program) const
+2
View File
@@ -556,6 +556,8 @@ public:
bool IsPayToWitnessScriptHash() const;
bool IsWitnessProgram(int& version, std::vector<unsigned char>& program) const;
bool IsPayToTaproot() const;
/** Called by IsStandardTx and P2SH/BIP62 VerifyScript (which makes it consensus-critical). */
bool IsPushOnly(const_iterator pc) const;
bool IsPushOnly() const;
@@ -79,7 +79,7 @@ util::Result<void> ExternalSignerScriptPubKeyMan::DisplayAddress(const CTxDestin
}
// If sign is true, transaction must previously have been filled
std::optional<PSBTError> ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const
std::optional<PSBTError> ExternalSignerScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const
{
if (!sign) {
return DescriptorScriptPubKeyMan::FillPSBT(psbt, txdata, sighash_type, false, bip32derivs, n_signed, finalize);
+1 -1
View File
@@ -35,7 +35,7 @@ class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
*/
util::Result<void> DisplayAddress(const CTxDestination& dest, const ExternalSigner& signer) const;
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override;
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type = std::nullopt, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override;
};
} // namespace wallet
#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_SCRIPTPUBKEYMAN_H
+2 -2
View File
@@ -338,8 +338,8 @@ bool SignTransaction(CWallet& wallet, CMutableTransaction& mtx) {
// First fill transaction with our data without signing,
// so external signers are not asked to sign more than once.
bool complete;
wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, false /* sign */, true /* bip32derivs */);
auto err{wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, true /* sign */, false /* bip32derivs */)};
wallet.FillPSBT(psbtx, complete, std::nullopt, /*sign=*/false, /*bip32derivs=*/true);
auto err{wallet.FillPSBT(psbtx, complete, std::nullopt, /*sign=*/true, /*bip32derivs=*/false)};
if (err) return false;
complete = FinalizeAndExtractPSBT(psbtx, mtx);
return complete;
+1 -1
View File
@@ -383,7 +383,7 @@ public:
}
return {};
}
std::optional<PSBTError> fillPSBT(int sighash_type,
std::optional<PSBTError> fillPSBT(std::optional<int> sighash_type,
bool sign,
bool bip32derivs,
size_t* n_signed,
+10 -7
View File
@@ -100,8 +100,8 @@ static UniValue FinishTransaction(const std::shared_ptr<CWallet> pwallet, const
// First fill transaction with our data without signing,
// so external signers are not asked to sign more than once.
bool complete;
pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, /*sign=*/false, /*bip32derivs=*/true);
const auto err{pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, /*sign=*/true, /*bip32derivs=*/false)};
pwallet->FillPSBT(psbtx, complete, std::nullopt, /*sign=*/false, /*bip32derivs=*/true);
const auto err{pwallet->FillPSBT(psbtx, complete, std::nullopt, /*sign=*/true, /*bip32derivs=*/false)};
if (err) {
throw JSONRPCPSBTError(*err);
}
@@ -960,12 +960,15 @@ RPCHelpMan signrawtransactionwithwallet()
// Parse the prevtxs array
ParsePrevouts(request.params[1], nullptr, coins);
int nHashType = ParseSighashString(request.params[2]);
std::optional<int> nHashType = ParseSighashString(request.params[2]);
if (!nHashType) {
nHashType = SIGHASH_DEFAULT;
}
// Script verification errors
std::map<int, bilingual_str> input_errors;
bool complete = pwallet->SignTransaction(mtx, coins, nHashType, input_errors);
bool complete = pwallet->SignTransaction(mtx, coins, *nHashType, input_errors);
UniValue result(UniValue::VOBJ);
SignTransactionResultToJSON(mtx, complete, coins, input_errors, result);
return result;
@@ -1179,7 +1182,7 @@ static RPCHelpMan bumpfee_helper(std::string method_name)
} else {
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const auto err{pwallet->FillPSBT(psbtx, complete, SIGHASH_DEFAULT, /*sign=*/false, /*bip32derivs=*/true)};
const auto err{pwallet->FillPSBT(psbtx, complete, std::nullopt, /*sign=*/false, /*bip32derivs=*/true)};
CHECK_NONFATAL(!err);
CHECK_NONFATAL(!complete);
DataStream ssTx{};
@@ -1635,7 +1638,7 @@ RPCHelpMan walletprocesspsbt()
}
// Get the sighash type
int nHashType = ParseSighashString(request.params[2]);
std::optional<int> nHashType = ParseSighashString(request.params[2]);
// Fill transaction with our data and also sign
bool sign = request.params[1].isNull() ? true : request.params[1].get_bool();
@@ -1784,7 +1787,7 @@ RPCHelpMan walletcreatefundedpsbt()
// Fill transaction with out data but don't sign
bool bip32derivs = request.params[4].isNull() ? true : request.params[4].get_bool();
bool complete = true;
const auto err{wallet.FillPSBT(psbtx, complete, 1, /*sign=*/false, /*bip32derivs=*/bip32derivs)};
const auto err{wallet.FillPSBT(psbtx, complete, std::nullopt, /*sign=*/false, /*bip32derivs=*/bip32derivs)};
if (err) {
throw JSONRPCPSBTError(*err);
}
+5 -7
View File
@@ -1311,7 +1311,7 @@ SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message,
return SigningResult::OK;
}
std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, const PrecomputedTransactionData& txdata, int sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const
std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const
{
if (n_signed) {
*n_signed = 0;
@@ -1324,11 +1324,6 @@ std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTran
continue;
}
// Get the Sighash type
if (sign && input.sighash_type != std::nullopt && *input.sighash_type != sighash_type) {
return PSBTError::SIGHASH_MISMATCH;
}
// Get the scriptPubKey to know which SigningProvider to use
CScript script;
if (!input.witness_utxo.IsNull()) {
@@ -1386,7 +1381,10 @@ std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTran
}
}
SignPSBTInput(HidingSigningProvider(keys.get(), /*hide_secret=*/!sign, /*hide_origin=*/!bip32derivs), psbtx, i, &txdata, sighash_type, nullptr, finalize);
PSBTError res = SignPSBTInput(HidingSigningProvider(keys.get(), /*hide_secret=*/!sign, /*hide_origin=*/!bip32derivs), psbtx, i, &txdata, sighash_type, nullptr, finalize);
if (res != PSBTError::OK && res != PSBTError::INCOMPLETE) {
return res;
}
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
+2 -2
View File
@@ -153,7 +153,7 @@ public:
/** Sign a message with the given script */
virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; };
/** Adds script and derivation path information to a PSBT, and optionally signs it. */
virtual std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = SIGHASH_DEFAULT, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const { return common::PSBTError::UNSUPPORTED; }
virtual std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type = std::nullopt, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const { return common::PSBTError::UNSUPPORTED; }
virtual uint256 GetID() const { return uint256(); }
@@ -382,7 +382,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = SIGHASH_DEFAULT, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override;
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type = std::nullopt, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override;
uint256 GetID() const override;
+2 -1
View File
@@ -192,7 +192,8 @@ FUZZ_TARGET(scriptpubkeyman, .init = initialize_spkm)
}
auto psbt{*opt_psbt};
const PrecomputedTransactionData txdata{PrecomputePSBTData(psbt)};
const int sighash_type{fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 150)};
std::optional<int> sighash_type{fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 151)};
if (sighash_type == 151) sighash_type = std::nullopt;
auto sign = fuzzed_data_provider.ConsumeBool();
auto bip32derivs = fuzzed_data_provider.ConsumeBool();
auto finalize = fuzzed_data_provider.ConsumeBool();
+2 -2
View File
@@ -65,7 +65,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test)
// Fill transaction with our data
bool complete = true;
BOOST_REQUIRE(!m_wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, false, true));
BOOST_REQUIRE(!m_wallet.FillPSBT(psbtx, complete, std::nullopt, false, true));
// Get the final tx
DataStream ssTx{};
@@ -78,7 +78,7 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test)
// Try to sign the mutated input
SignatureData sigdata;
BOOST_CHECK(m_wallet.FillPSBT(psbtx, complete, SIGHASH_ALL, true, true));
BOOST_CHECK(m_wallet.FillPSBT(psbtx, complete, std::nullopt, true, true));
}
BOOST_AUTO_TEST_CASE(parse_hd_keypath)
+2 -2
View File
@@ -2114,7 +2114,7 @@ bool CWallet::SignTransaction(CMutableTransaction& tx, const std::map<COutPoint,
return false;
}
std::optional<PSBTError> CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs, size_t * n_signed, bool finalize) const
std::optional<PSBTError> CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, std::optional<int> sighash_type, bool sign, bool bip32derivs, size_t * n_signed, bool finalize) const
{
if (n_signed) {
*n_signed = 0;
@@ -2157,7 +2157,7 @@ std::optional<PSBTError> CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bo
}
}
RemoveUnnecessaryTransactions(psbtx, sighash_type);
RemoveUnnecessaryTransactions(psbtx);
// Complete if every input is now signed
complete = true;
+1 -1
View File
@@ -662,7 +662,7 @@ public:
*/
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbtx,
bool& complete,
int sighash_type = SIGHASH_DEFAULT,
std::optional<int> sighash_type = std::nullopt,
bool sign = true,
bool bip32derivs = true,
size_t* n_signed = nullptr,
+91 -1
View File
@@ -27,6 +27,7 @@ from test_framework.psbt import (
PSBT_GLOBAL_UNSIGNED_TX,
PSBT_IN_RIPEMD160,
PSBT_IN_SHA256,
PSBT_IN_SIGHASH_TYPE,
PSBT_IN_HASH160,
PSBT_IN_HASH256,
PSBT_IN_MUSIG2_PARTIAL_SIG,
@@ -37,7 +38,7 @@ from test_framework.psbt import (
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS,
PSBT_OUT_TAP_TREE,
)
from test_framework.script import CScript, OP_TRUE
from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY
from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -274,6 +275,93 @@ class PSBTTest(BitcoinTestFramework):
assert "participant_pubkeys" in out_participant_pks
assert_equal(out_participant_pks["participant_pubkeys"], [out_pubkey1.hex(), out_pubkey2.hex()])
def test_sighash_mismatch(self):
self.log.info("Test sighash type mismatches")
self.nodes[0].createwallet("sighash_mismatch")
wallet = self.nodes[0].get_wallet_rpc("sighash_mismatch")
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
addr = wallet.getnewaddress(address_type="bech32")
def_wallet.sendtoaddress(addr, 5)
self.generate(self.nodes[0], 6)
# Retrieve the descriptors so we can do all of the tests with descriptorprocesspsbt as well
descs = wallet.listdescriptors(True)["descriptors"]
# Make a PSBT
psbt = wallet.walletcreatefundedpsbt([], [{def_wallet.getnewaddress(): 1}])["psbt"]
# Modify the PSBT and insert a sighash field for ALL|ANYONECANPAY on input 0
mod_psbt = PSBT.from_base64(psbt)
mod_psbt.i[0].map[PSBT_IN_SIGHASH_TYPE] = (SIGHASH_ALL | SIGHASH_ANYONECANPAY).to_bytes(4, byteorder="little")
psbt = mod_psbt.to_base64()
# Mismatching sighash type fails, including when no type is specified
for sighash in ["DEFAULT", "ALL", "NONE", "SINGLE", "NONE|ANYONECANPAY", "SINGLE|ANYONECANPAY", None]:
assert_raises_rpc_error(-22, "Specified sighash value does not match value stored in PSBT", wallet.walletprocesspsbt, psbt, True, sighash)
# Matching sighash type succeeds
proc = wallet.walletprocesspsbt(psbt, True, "ALL|ANYONECANPAY")
assert_equal(proc["complete"], True)
# Repeat with descriptorprocesspsbt
# Mismatching sighash type fails, including when no type is specified
for sighash in ["DEFAULT", "ALL", "NONE", "SINGLE", "NONE|ANYONECANPAY", "SINGLE|ANYONECANPAY", None]:
assert_raises_rpc_error(-22, "Specified sighash value does not match value stored in PSBT", self.nodes[0].descriptorprocesspsbt, psbt, descs, sighash)
# Matching sighash type succeeds
proc = self.nodes[0].descriptorprocesspsbt(psbt, descs, "ALL|ANYONECANPAY")
assert_equal(proc["complete"], True)
wallet.unloadwallet()
def test_sighash_adding(self):
self.log.info("Test adding of sighash type field")
self.nodes[0].createwallet("sighash_adding")
wallet = self.nodes[0].get_wallet_rpc("sighash_adding")
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
outputs = [{wallet.getnewaddress(address_type="bech32"): 1}]
outputs.append({wallet.getnewaddress(address_type="bech32m"): 1})
descs = wallet.listdescriptors(True)["descriptors"]
def_wallet.send(outputs)
self.generate(self.nodes[0], 6)
utxos = wallet.listunspent()
# Make a PSBT
psbt = wallet.walletcreatefundedpsbt(utxos, [{def_wallet.getnewaddress(): 0.5}])["psbt"]
# Process the PSBT with the wallet
wallet_psbt = wallet.walletprocesspsbt(psbt=psbt, sighashtype="ALL|ANYONECANPAY", finalize=False)["psbt"]
# Separately process the PSBT with descriptors
desc_psbt = self.nodes[0].descriptorprocesspsbt(psbt=psbt, descriptors=descs, sighashtype="ALL|ANYONECANPAY", finalize=False)["psbt"]
for psbt in [wallet_psbt, desc_psbt]:
# Check that the PSBT has a sighash field on all inputs
dec_psbt = self.nodes[0].decodepsbt(psbt)
for input in dec_psbt["inputs"]:
assert_equal(input["sighash"], "ALL|ANYONECANPAY")
# Make sure we can still finalize the transaction
fin_res = self.nodes[0].finalizepsbt(psbt)
assert_equal(fin_res["complete"], True)
fin_hex = fin_res["hex"]
assert_equal(self.nodes[0].testmempoolaccept([fin_hex])[0]["allowed"], True)
# Change the sighash field to a different value and make sure we can no longer finalize
mod_psbt = PSBT.from_base64(psbt)
mod_psbt.i[0].map[PSBT_IN_SIGHASH_TYPE] = (SIGHASH_ALL).to_bytes(4, byteorder="little")
mod_psbt.i[1].map[PSBT_IN_SIGHASH_TYPE] = (SIGHASH_ALL).to_bytes(4, byteorder="little")
psbt = mod_psbt.to_base64()
fin_res = self.nodes[0].finalizepsbt(psbt)
assert_equal(fin_res["complete"], False)
self.nodes[0].sendrawtransaction(fin_hex)
self.generate(self.nodes[0], 1)
wallet.unloadwallet()
def assert_change_type(self, psbtx, expected_type):
"""Assert that the given PSBT has a change output with the given type."""
@@ -1121,6 +1209,8 @@ class PSBTTest(BitcoinTestFramework):
self.log.info("Test descriptorprocesspsbt raises if an invalid sighashtype is passed")
assert_raises_rpc_error(-8, "'all' is not a valid sighash parameter.", self.nodes[2].descriptorprocesspsbt, psbt, [descriptor], sighashtype="all")
self.test_sighash_mismatch()
self.test_sighash_adding()
if __name__ == '__main__':
PSBTTest(__file__).main()