Skip to content

Commit 2ac71d2

Browse files
committed
Merge bitcoin/bitcoin#25595: Verify PSBT inputs rather than check for fields being empty
e133264 Add test for PSBT input verification (Greg Sanders) d256992 Verify PSBT inputs rather than check for fields being empty (Greg Sanders) Pull request description: In a few keys spots, PSBT finality is checked by looking for non-empty witness data. This complicates a couple things: 1) Empty data can be valid in certain cases 2) User may be passed bogus final data by a counterparty during PSBT work happening, and end up with incorrect signatures that they may not be able to check in other contexts if the UTXO doesn't exist yet in chain/mempool, timelocks, etc. On the whole I think these heavier checks are worth it in case someone is actually assuming the signatures are correct if our API is saying so. ACKs for top commit: achow101: ACK e133264 Tree-SHA512: 9de4fbb0be1257b081781f5df908fd55666e3acd5c4e36beb3b3f2f5a6aed69ff77068c44cde6127e159e773293fd9ced4c0bb47e693969f337e74dc8af030da
2 parents a97791d + e133264 commit 2ac71d2

File tree

4 files changed

+51
-3
lines changed

4 files changed

+51
-3
lines changed

src/node/psbt.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)
5959
}
6060

6161
// Check if it is final
62-
if (!utxo.IsNull() && !PSBTInputSigned(input)) {
62+
if (!PSBTInputSignedAndVerified(psbtx, i, &txdata)) {
6363
input_analysis.is_final = false;
6464

6565
// Figure out what is missing

src/psbt.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <psbt.h>
66

7+
#include <policy/policy.h>
78
#include <util/check.h>
89
#include <util/strencodings.h>
910

@@ -273,11 +274,41 @@ void PSBTOutput::Merge(const PSBTOutput& output)
273274
if (m_tap_internal_key.IsNull() && !output.m_tap_internal_key.IsNull()) m_tap_internal_key = output.m_tap_internal_key;
274275
if (m_tap_tree.empty() && !output.m_tap_tree.empty()) m_tap_tree = output.m_tap_tree;
275276
}
277+
276278
bool PSBTInputSigned(const PSBTInput& input)
277279
{
278280
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
279281
}
280282

283+
bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata)
284+
{
285+
CTxOut utxo;
286+
assert(psbt.inputs.size() >= input_index);
287+
const PSBTInput& input = psbt.inputs[input_index];
288+
289+
if (input.non_witness_utxo) {
290+
// If we're taking our information from a non-witness UTXO, verify that it matches the prevout.
291+
COutPoint prevout = psbt.tx->vin[input_index].prevout;
292+
if (prevout.n >= input.non_witness_utxo->vout.size()) {
293+
return false;
294+
}
295+
if (input.non_witness_utxo->GetHash() != prevout.hash) {
296+
return false;
297+
}
298+
utxo = input.non_witness_utxo->vout[prevout.n];
299+
} else if (!input.witness_utxo.IsNull()) {
300+
utxo = input.witness_utxo;
301+
} else {
302+
return false;
303+
}
304+
305+
if (txdata) {
306+
return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&(*psbt.tx), input_index, utxo.nValue, *txdata, MissingDataBehavior::FAIL});
307+
} else {
308+
return VerifyScript(input.final_script_sig, utxo.scriptPubKey, &input.final_script_witness, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&(*psbt.tx), input_index, utxo.nValue, MissingDataBehavior::FAIL});
309+
}
310+
}
311+
281312
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt) {
282313
size_t count = 0;
283314
for (const auto& input : psbt.inputs) {
@@ -331,7 +362,7 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
331362
PSBTInput& input = psbt.inputs.at(index);
332363
const CMutableTransaction& tx = *psbt.tx;
333364

334-
if (PSBTInputSigned(input)) {
365+
if (PSBTInputSignedAndVerified(psbt, index, txdata)) {
335366
return true;
336367
}
337368

src/psbt.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1218,9 +1218,12 @@ std::string PSBTRoleName(PSBTRole role);
12181218
/** Compute a PrecomputedTransactionData object from a psbt. */
12191219
PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& psbt);
12201220

1221-
/** Checks whether a PSBTInput is already signed. */
1221+
/** Checks whether a PSBTInput is already signed by checking for non-null finalized fields. */
12221222
bool PSBTInputSigned(const PSBTInput& input);
12231223

1224+
/** Checks whether a PSBTInput is already signed by doing script verification using final fields. */
1225+
bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata);
1226+
12241227
/** Signs a PSBTInput, verifying that all provided data matches what is being signed.
12251228
*
12261229
* txdata should be the output of PrecomputePSBTData (which can be shared across

test/functional/rpc_psbt.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
PSBT_IN_SHA256,
2828
PSBT_IN_HASH160,
2929
PSBT_IN_HASH256,
30+
PSBT_IN_WITNESS_UTXO,
3031
PSBT_OUT_TAP_TREE,
3132
)
33+
from test_framework.script import CScript, OP_TRUE
3234
from test_framework.test_framework import BitcoinTestFramework
3335
from test_framework.util import (
3436
assert_approx,
@@ -852,6 +854,18 @@ def test_psbt_input_keys(psbt_input, keys):
852854
assert_raises_rpc_error(-8, "PSBTs not compatible (different transactions)", self.nodes[0].combinepsbt, [psbt1, psbt2])
853855
assert_equal(self.nodes[0].combinepsbt([psbt1, psbt1]), psbt1)
854856

857+
self.log.info("Test that PSBT inputs are being checked via script execution")
858+
acs_prevout = CTxOut(nValue=0, scriptPubKey=CScript([OP_TRUE]))
859+
tx = CTransaction()
860+
tx.vin = [CTxIn(outpoint=COutPoint(hash=int('dd' * 32, 16), n=0), scriptSig=b"")]
861+
tx.vout = [CTxOut(nValue=0, scriptPubKey=b"")]
862+
psbt = PSBT()
863+
psbt.g = PSBTMap({PSBT_GLOBAL_UNSIGNED_TX: tx.serialize()})
864+
psbt.i = [PSBTMap({bytes([PSBT_IN_WITNESS_UTXO]) : acs_prevout.serialize()})]
865+
psbt.o = [PSBTMap()]
866+
assert_equal(self.nodes[0].finalizepsbt(psbt.to_base64()),
867+
{'hex': '0200000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0000000000000000000100000000000000000000000000', 'complete': True})
868+
855869

856870
if __name__ == '__main__':
857871
PSBTTest().main()

0 commit comments

Comments
 (0)