Skip to content

Commit b30c62d

Browse files
committed
Merge #14588: Refactor PSBT signing logic to enforce invariant and fix signing bug
e13fea9 Add regression test for PSBT signing bug #14473 (Glenn Willen) 5655005 Refactor PSBTInput signing to enforce invariant (Glenn Willen) 0f5bda2 Simplify arguments to SignPSBTInput (Glenn Willen) 53e6fff Add bool PSBTInputSigned (Glenn Willen) 65166d4 New PartiallySignedTransaction constructor from CTransction (Glenn Willen) 4f3f5cb Remove redundant txConst parameter to FillPSBT (Glenn Willen) fe5d22b More concise conversion of CDataStream to string (Glenn Willen) Pull request description: As discussed in the comments on #14473, I think that bug was caused primarily by failure to adhere to the invariant that a PSBTInput always has exactly one of the two utxo fields present -- an invariant that is already enforced by PSBTInput::IsSane, but which we were temporarily suspending during signing. This refactor repairs the invariant, also fixing the bug. It also simplifies some other code, and removes redundant parameters from some related functions. fixes #14473 Tree-SHA512: cbad3428175e30f9b7bac3f600668dd1a8f9acde16b915d27a940a2fa6d5149d4fbe236d5808fd590fb20a032274c99e8cac34bef17f79a53fdf69a5948c0fd0
2 parents cbf0093 + e13fea9 commit b30c62d

File tree

7 files changed

+80
-68
lines changed

7 files changed

+80
-68
lines changed

src/rpc/rawtransaction.cpp

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,12 +1538,13 @@ UniValue finalizepsbt(const JSONRPCRequest& request)
15381538
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed %s", error));
15391539
}
15401540

1541-
// Get all of the previous transactions
1541+
// Finalize input signatures -- in case we have partial signatures that add up to a complete
1542+
// signature, but have not combined them yet (e.g. because the combiner that created this
1543+
// PartiallySignedTransaction did not understand them), this will combine them into a final
1544+
// script.
15421545
bool complete = true;
15431546
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
1544-
PSBTInput& input = psbtx.inputs.at(i);
1545-
1546-
complete &= SignPSBTInput(DUMMY_SIGNING_PROVIDER, *psbtx.tx, input, i, 1);
1547+
complete &= SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, SIGHASH_ALL);
15471548
}
15481549

15491550
UniValue result(UniValue::VOBJ);
@@ -1556,10 +1557,10 @@ UniValue finalizepsbt(const JSONRPCRequest& request)
15561557
mtx.vin[i].scriptWitness = psbtx.inputs[i].final_script_witness;
15571558
}
15581559
ssTx << mtx;
1559-
result.pushKV("hex", HexStr(ssTx.begin(), ssTx.end()));
1560+
result.pushKV("hex", HexStr(ssTx.str()));
15601561
} else {
15611562
ssTx << psbtx;
1562-
result.pushKV("psbt", EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()));
1563+
result.pushKV("psbt", EncodeBase64(ssTx.str()));
15631564
}
15641565
result.pushKV("complete", complete);
15651566

src/script/sign.cpp

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,17 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato
239239
return sigdata.complete;
240240
}
241241

242-
bool SignPSBTInput(const SigningProvider& provider, const CMutableTransaction& tx, PSBTInput& input, int index, int sighash)
242+
bool PSBTInputSigned(PSBTInput& input)
243243
{
244-
// if this input has a final scriptsig or scriptwitness, don't do anything with it
245-
if (!input.final_script_sig.empty() || !input.final_script_witness.IsNull()) {
244+
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
245+
}
246+
247+
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash)
248+
{
249+
PSBTInput& input = psbt.inputs.at(index);
250+
const CMutableTransaction& tx = *psbt.tx;
251+
252+
if (PSBTInputSigned(input)) {
246253
return true;
247254
}
248255

@@ -253,15 +260,19 @@ bool SignPSBTInput(const SigningProvider& provider, const CMutableTransaction& t
253260
// Get UTXO
254261
bool require_witness_sig = false;
255262
CTxOut utxo;
263+
264+
// Verify input sanity, which checks that at most one of witness or non-witness utxos is provided.
265+
if (!input.IsSane()) {
266+
return false;
267+
}
268+
256269
if (input.non_witness_utxo) {
257270
// If we're taking our information from a non-witness UTXO, verify that it matches the prevout.
258-
if (input.non_witness_utxo->GetHash() != tx.vin[index].prevout.hash) return false;
259-
// If both witness and non-witness UTXO are provided, verify that they match. This check shouldn't
260-
// matter, as the PSBT deserializer enforces only one of both is provided, and the only way both
261-
// can be present is when they're added simultaneously by FillPSBT (in which case they always match).
262-
// Still, check in order to not rely on callers to enforce this.
263-
if (!input.witness_utxo.IsNull() && input.non_witness_utxo->vout[tx.vin[index].prevout.n] != input.witness_utxo) return false;
264-
utxo = input.non_witness_utxo->vout[tx.vin[index].prevout.n];
271+
COutPoint prevout = tx.vin[index].prevout;
272+
if (input.non_witness_utxo->GetHash() != prevout.hash) {
273+
return false;
274+
}
275+
utxo = input.non_witness_utxo->vout[prevout.n];
265276
} else if (!input.witness_utxo.IsNull()) {
266277
utxo = input.witness_utxo;
267278
// When we're taking our information from a witness UTXO, we can't verify it is actually data from
@@ -280,18 +291,10 @@ bool SignPSBTInput(const SigningProvider& provider, const CMutableTransaction& t
280291
if (require_witness_sig && !sigdata.witness) return false;
281292
input.FromSignatureData(sigdata);
282293

294+
// If we have a witness signature, use the smaller witness UTXO.
283295
if (sigdata.witness) {
284-
assert(!utxo.IsNull());
285296
input.witness_utxo = utxo;
286-
}
287-
288-
// If both UTXO types are present, drop the unnecessary one.
289-
if (input.non_witness_utxo && !input.witness_utxo.IsNull()) {
290-
if (sigdata.witness) {
291-
input.non_witness_utxo = nullptr;
292-
} else {
293-
input.witness_utxo.SetNull();
294-
}
297+
input.non_witness_utxo = nullptr;
295298
}
296299

297300
return sig_complete;
@@ -513,6 +516,12 @@ bool IsSolvable(const SigningProvider& provider, const CScript& script)
513516
return false;
514517
}
515518

519+
PartiallySignedTransaction::PartiallySignedTransaction(const CTransaction& tx) : tx(tx)
520+
{
521+
inputs.resize(tx.vin.size());
522+
outputs.resize(tx.vout.size());
523+
}
524+
516525
bool PartiallySignedTransaction::IsNull() const
517526
{
518527
return !tx && inputs.empty() && outputs.empty() && unknown.empty();

src/script/sign.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ struct PartiallySignedTransaction
566566
bool IsSane() const;
567567
PartiallySignedTransaction() {}
568568
PartiallySignedTransaction(const PartiallySignedTransaction& psbt_in) : tx(psbt_in.tx), inputs(psbt_in.inputs), outputs(psbt_in.outputs), unknown(psbt_in.unknown) {}
569+
explicit PartiallySignedTransaction(const CTransaction& tx);
569570

570571
// Only checks if they refer to the same transaction
571572
friend bool operator==(const PartiallySignedTransaction& a, const PartiallySignedTransaction &b)
@@ -729,8 +730,11 @@ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreato
729730
bool SignSignature(const SigningProvider &provider, const CScript& fromPubKey, CMutableTransaction& txTo, unsigned int nIn, const CAmount& amount, int nHashType);
730731
bool SignSignature(const SigningProvider &provider, const CTransaction& txFrom, CMutableTransaction& txTo, unsigned int nIn, int nHashType);
731732

733+
/** Checks whether a PSBTInput is already signed. */
734+
bool PSBTInputSigned(PSBTInput& input);
735+
732736
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
733-
bool SignPSBTInput(const SigningProvider& provider, const CMutableTransaction& tx, PSBTInput& input, int index, int sighash = SIGHASH_ALL);
737+
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL);
734738

735739
/** Extract signature data from a transaction input, and insert it. */
736740
SignatureData DataFromTransaction(const CMutableTransaction& tx, unsigned int nIn, const CTxOut& txout);

src/wallet/rpcwallet.cpp

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3772,37 +3772,47 @@ void AddKeypathToMap(const CWallet* pwallet, const CKeyID& keyID, std::map<CPubK
37723772
hd_keypaths.emplace(vchPubKey, std::move(info));
37733773
}
37743774

3775-
bool FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& psbtx, const CTransaction* txConst, int sighash_type, bool sign, bool bip32derivs)
3775+
bool FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs)
37763776
{
37773777
LOCK(pwallet->cs_wallet);
37783778
// Get all of the previous transactions
37793779
bool complete = true;
3780-
for (unsigned int i = 0; i < txConst->vin.size(); ++i) {
3781-
const CTxIn& txin = txConst->vin[i];
3780+
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
3781+
const CTxIn& txin = psbtx.tx->vin[i];
37823782
PSBTInput& input = psbtx.inputs.at(i);
37833783

3784-
// If we don't know about this input, skip it and let someone else deal with it
3785-
const uint256& txhash = txin.prevout.hash;
3786-
const auto it = pwallet->mapWallet.find(txhash);
3787-
if (it != pwallet->mapWallet.end()) {
3788-
const CWalletTx& wtx = it->second;
3789-
CTxOut utxo = wtx.tx->vout[txin.prevout.n];
3790-
// Update both UTXOs from the wallet.
3791-
input.non_witness_utxo = wtx.tx;
3792-
input.witness_utxo = utxo;
3784+
if (PSBTInputSigned(input)) {
3785+
continue;
3786+
}
3787+
3788+
// Verify input looks sane. This will check that we have at most one uxto, witness or non-witness.
3789+
if (!input.IsSane()) {
3790+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "PSBT input is not sane.");
3791+
}
3792+
3793+
// If we have no utxo, grab it from the wallet.
3794+
if (!input.non_witness_utxo && input.witness_utxo.IsNull()) {
3795+
const uint256& txhash = txin.prevout.hash;
3796+
const auto it = pwallet->mapWallet.find(txhash);
3797+
if (it != pwallet->mapWallet.end()) {
3798+
const CWalletTx& wtx = it->second;
3799+
// We only need the non_witness_utxo, which is a superset of the witness_utxo.
3800+
// The signing code will switch to the smaller witness_utxo if this is ok.
3801+
input.non_witness_utxo = wtx.tx;
3802+
}
37933803
}
37943804

37953805
// Get the Sighash type
37963806
if (sign && input.sighash_type > 0 && input.sighash_type != sighash_type) {
37973807
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Specified Sighash and sighash in PSBT do not match.");
37983808
}
37993809

3800-
complete &= SignPSBTInput(HidingSigningProvider(pwallet, !sign, !bip32derivs), *psbtx.tx, input, i, sighash_type);
3810+
complete &= SignPSBTInput(HidingSigningProvider(pwallet, !sign, !bip32derivs), psbtx, i, sighash_type);
38013811
}
38023812

38033813
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
3804-
for (unsigned int i = 0; i < txConst->vout.size(); ++i) {
3805-
const CTxOut& out = txConst->vout.at(i);
3814+
for (unsigned int i = 0; i < psbtx.tx->vout.size(); ++i) {
3815+
const CTxOut& out = psbtx.tx->vout.at(i);
38063816
PSBTOutput& psbt_out = psbtx.outputs.at(i);
38073817

38083818
// Fill a SignatureData with output info
@@ -3867,19 +3877,15 @@ UniValue walletprocesspsbt(const JSONRPCRequest& request)
38673877
// Get the sighash type
38683878
int nHashType = ParseSighashString(request.params[2]);
38693879

3870-
// Use CTransaction for the constant parts of the
3871-
// transaction to avoid rehashing.
3872-
const CTransaction txConst(*psbtx.tx);
3873-
38743880
// Fill transaction with our data and also sign
38753881
bool sign = request.params[1].isNull() ? true : request.params[1].get_bool();
38763882
bool bip32derivs = request.params[3].isNull() ? false : request.params[3].get_bool();
3877-
bool complete = FillPSBT(pwallet, psbtx, &txConst, nHashType, sign, bip32derivs);
3883+
bool complete = FillPSBT(pwallet, psbtx, nHashType, sign, bip32derivs);
38783884

38793885
UniValue result(UniValue::VOBJ);
38803886
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
38813887
ssTx << psbtx;
3882-
result.pushKV("psbt", EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()));
3888+
result.pushKV("psbt", EncodeBase64(ssTx.str()));
38833889
result.pushKV("complete", complete);
38843890

38853891
return result;
@@ -3971,29 +3977,18 @@ UniValue walletcreatefundedpsbt(const JSONRPCRequest& request)
39713977
FundTransaction(pwallet, rawTx, fee, change_position, request.params[3]);
39723978

39733979
// Make a blank psbt
3974-
PartiallySignedTransaction psbtx;
3975-
psbtx.tx = rawTx;
3976-
for (unsigned int i = 0; i < rawTx.vin.size(); ++i) {
3977-
psbtx.inputs.push_back(PSBTInput());
3978-
}
3979-
for (unsigned int i = 0; i < rawTx.vout.size(); ++i) {
3980-
psbtx.outputs.push_back(PSBTOutput());
3981-
}
3982-
3983-
// Use CTransaction for the constant parts of the
3984-
// transaction to avoid rehashing.
3985-
const CTransaction txConst(*psbtx.tx);
3980+
PartiallySignedTransaction psbtx(rawTx);
39863981

39873982
// Fill transaction with out data but don't sign
39883983
bool bip32derivs = request.params[4].isNull() ? false : request.params[4].get_bool();
3989-
FillPSBT(pwallet, psbtx, &txConst, 1, false, bip32derivs);
3984+
FillPSBT(pwallet, psbtx, 1, false, bip32derivs);
39903985

39913986
// Serialize the PSBT
39923987
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
39933988
ssTx << psbtx;
39943989

39953990
UniValue result(UniValue::VOBJ);
3996-
result.pushKV("psbt", EncodeBase64((unsigned char*)ssTx.data(), ssTx.size()));
3991+
result.pushKV("psbt", EncodeBase64(ssTx.str()));
39973992
result.pushKV("fee", ValueFromAmount(fee));
39983993
result.pushKV("changepos", change_position);
39993994
return result;

src/wallet/rpcwallet.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ bool EnsureWalletIsAvailable(CWallet *, bool avoidException);
3030

3131
UniValue getaddressinfo(const JSONRPCRequest& request);
3232
UniValue signrawtransactionwithwallet(const JSONRPCRequest& request);
33-
bool FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& psbtx, const CTransaction* txConst, int sighash_type = 1, bool sign = true, bool bip32derivs = false);
33+
bool FillPSBT(const CWallet* pwallet, PartiallySignedTransaction& psbtx, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false);
3434
#endif //BITCOIN_WALLET_RPCWALLET_H

src/wallet/test/psbt_wallet_tests.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,8 @@ BOOST_AUTO_TEST_CASE(psbt_updater_test)
5959
CDataStream ssData(ParseHex("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000"), SER_NETWORK, PROTOCOL_VERSION);
6060
ssData >> psbtx;
6161

62-
// Use CTransaction for the constant parts of the
63-
// transaction to avoid rehashing.
64-
const CTransaction txConst(*psbtx.tx);
65-
6662
// Fill transaction with our data
67-
FillPSBT(&m_wallet, psbtx, &txConst, 1, false, true);
63+
FillPSBT(&m_wallet, psbtx, SIGHASH_ALL, false, true);
6864

6965
// Get the final tx
7066
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);

test/functional/rpc_psbt.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ def run_test(self):
207207
assert tx_in["sequence"] > MAX_BIP125_RBF_SEQUENCE
208208
assert_equal(decoded_psbt["tx"]["locktime"], 0)
209209

210+
# Regression test for 14473 (mishandling of already-signed witness transaction):
211+
psbtx_info = self.nodes[0].walletcreatefundedpsbt([{"txid":unspent["txid"], "vout":unspent["vout"]}], [{self.nodes[2].getnewaddress():unspent["amount"]+1}])
212+
complete_psbt = self.nodes[0].walletprocesspsbt(psbtx_info["psbt"])
213+
double_processed_psbt = self.nodes[0].walletprocesspsbt(complete_psbt["psbt"])
214+
assert_equal(complete_psbt, double_processed_psbt)
215+
# We don't care about the decode result, but decoding must succeed.
216+
self.nodes[0].decodepsbt(double_processed_psbt["psbt"])
210217

211218
# BIP 174 Test Vectors
212219

0 commit comments

Comments
 (0)