diff --git a/src/node/psbt.cpp b/src/node/psbt.cpp index 57162cd679b..ca3fc0955de 100644 --- a/src/node/psbt.cpp +++ b/src/node/psbt.cpp @@ -59,7 +59,7 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx) } // Check if it is final - if (!utxo.IsNull() && !PSBTInputSigned(input)) { + if (!PSBTInputSignedAndVerified(psbtx, i, &txdata)) { input_analysis.is_final = false; // Figure out what is missing diff --git a/src/psbt.cpp b/src/psbt.cpp index cbf2f887884..461987c5037 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -273,11 +274,41 @@ void PSBTOutput::Merge(const PSBTOutput& output) if (m_tap_internal_key.IsNull() && !output.m_tap_internal_key.IsNull()) m_tap_internal_key = output.m_tap_internal_key; if (m_tap_tree.empty() && !output.m_tap_tree.empty()) m_tap_tree = output.m_tap_tree; } + bool PSBTInputSigned(const PSBTInput& input) { return !input.final_script_sig.empty() || !input.final_script_witness.IsNull(); } +bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata) +{ + CTxOut utxo; + assert(psbt.inputs.size() >= input_index); + const PSBTInput& input = psbt.inputs[input_index]; + + if (input.non_witness_utxo) { + // If we're taking our information from a non-witness UTXO, verify that it matches the prevout. + COutPoint prevout = psbt.tx->vin[input_index].prevout; + if (prevout.n >= input.non_witness_utxo->vout.size()) { + return false; + } + if (input.non_witness_utxo->GetHash() != prevout.hash) { + return false; + } + utxo = input.non_witness_utxo->vout[prevout.n]; + } else if (!input.witness_utxo.IsNull()) { + utxo = input.witness_utxo; + } else { + return false; + } + + if (txdata) { + 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}); + } else { + 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}); + } +} + size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt) { size_t count = 0; for (const auto& input : psbt.inputs) { @@ -331,7 +362,7 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& PSBTInput& input = psbt.inputs.at(index); const CMutableTransaction& tx = *psbt.tx; - if (PSBTInputSigned(input)) { + if (PSBTInputSignedAndVerified(psbt, index, txdata)) { return true; } diff --git a/src/psbt.h b/src/psbt.h index ddcdb8c68dc..37bf142366a 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -1218,9 +1218,12 @@ std::string PSBTRoleName(PSBTRole role); /** Compute a PrecomputedTransactionData object from a psbt. */ PrecomputedTransactionData PrecomputePSBTData(const PartiallySignedTransaction& psbt); -/** Checks whether a PSBTInput is already signed. */ +/** Checks whether a PSBTInput is already signed by checking for non-null finalized fields. */ bool PSBTInputSigned(const PSBTInput& input); +/** Checks whether a PSBTInput is already signed by doing script verification using final fields. */ +bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata); + /** Signs a PSBTInput, verifying that all provided data matches what is being signed. * * txdata should be the output of PrecomputePSBTData (which can be shared across diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index 1fe3b21542d..3b78a7d0958 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -27,8 +27,10 @@ from test_framework.psbt import ( PSBT_IN_SHA256, PSBT_IN_HASH160, PSBT_IN_HASH256, + PSBT_IN_WITNESS_UTXO, PSBT_OUT_TAP_TREE, ) +from test_framework.script import CScript, OP_TRUE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_approx, @@ -852,6 +854,18 @@ class PSBTTest(BitcoinTestFramework): assert_raises_rpc_error(-8, "PSBTs not compatible (different transactions)", self.nodes[0].combinepsbt, [psbt1, psbt2]) assert_equal(self.nodes[0].combinepsbt([psbt1, psbt1]), psbt1) + self.log.info("Test that PSBT inputs are being checked via script execution") + acs_prevout = CTxOut(nValue=0, scriptPubKey=CScript([OP_TRUE])) + tx = CTransaction() + tx.vin = [CTxIn(outpoint=COutPoint(hash=int('dd' * 32, 16), n=0), scriptSig=b"")] + tx.vout = [CTxOut(nValue=0, scriptPubKey=b"")] + psbt = PSBT() + psbt.g = PSBTMap({PSBT_GLOBAL_UNSIGNED_TX: tx.serialize()}) + psbt.i = [PSBTMap({bytes([PSBT_IN_WITNESS_UTXO]) : acs_prevout.serialize()})] + psbt.o = [PSBTMap()] + assert_equal(self.nodes[0].finalizepsbt(psbt.to_base64()), + {'hex': '0200000001dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd0000000000000000000100000000000000000000000000', 'complete': True}) + if __name__ == '__main__': PSBTTest().main()