Skip to content

Commit 6d17853

Browse files
fanquakeknst
authored andcommitted
partial Merge bitcoin#25595: Verify PSBT inputs rather than check for fields being empty
BACKPORT NOTE: witness-scenario is omitted for backport; the new test provides the OP_TRUE prevout as non_witness_utxo, since Dash's PSBT has no witness_utxo field, and the expected hex is computed because the input spends a real prev-tx. The segwit-based extractor vector was removed from rpc_psbt.json in a preceding commit, since its final scriptSig can no longer be script-verified. ------------- 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
1 parent c4f1979 commit 6d17853

4 files changed

Lines changed: 51 additions & 3 deletions

File tree

‎src/node/psbt.cpp‎

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

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

6464
// Figure out what is missing

‎src/psbt.cpp‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,33 @@ void PSBTOutput::Merge(const PSBTOutput& output)
181181
if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script;
182182
}
183183

184+
bool PSBTInputSignedAndVerified(const PartiallySignedTransaction psbt, unsigned int input_index, const PrecomputedTransactionData* txdata)
185+
{
186+
CTxOut utxo;
187+
assert(psbt.inputs.size() >= input_index);
188+
const PSBTInput& input = psbt.inputs[input_index];
189+
190+
if (input.non_witness_utxo) {
191+
// If we're taking our information from a non-witness UTXO, verify that it matches the prevout.
192+
COutPoint prevout = psbt.tx->vin[input_index].prevout;
193+
if (prevout.n >= input.non_witness_utxo->vout.size()) {
194+
return false;
195+
}
196+
if (input.non_witness_utxo->GetHash() != prevout.hash) {
197+
return false;
198+
}
199+
utxo = input.non_witness_utxo->vout[prevout.n];
200+
} else {
201+
return false;
202+
}
203+
204+
if (txdata) {
205+
return VerifyScript(input.final_script_sig, utxo.scriptPubKey, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&(*psbt.tx), input_index, utxo.nValue, *txdata, MissingDataBehavior::FAIL});
206+
} else {
207+
return VerifyScript(input.final_script_sig, utxo.scriptPubKey, STANDARD_SCRIPT_VERIFY_FLAGS, MutableTransactionSignatureChecker{&(*psbt.tx), input_index, utxo.nValue, MissingDataBehavior::FAIL});
208+
}
209+
}
210+
184211
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt) {
185212
size_t count = 0;
186213
for (const auto& input : psbt.inputs) {
@@ -211,6 +238,7 @@ void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransactio
211238
// Put redeem_script, key paths, into PSBTOutput.
212239
psbt_out.FromSignatureData(sigdata);
213240
}
241+
214242
bool PSBTInputSigned(const PSBTInput& input)
215243
{
216244
return !input.final_script_sig.empty();
@@ -238,7 +266,7 @@ bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction&
238266
PSBTInput& input = psbt.inputs.at(index);
239267
const CMutableTransaction& tx = *psbt.tx;
240268

241-
if (PSBTInputSigned(input)) {
269+
if (PSBTInputSignedAndVerified(psbt, index, txdata)) {
242270
return true;
243271
}
244272

‎src/psbt.h‎

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

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

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

‎test/functional/rpc_psbt.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
PSBT_IN_SHA256,
2525
PSBT_IN_HASH160,
2626
PSBT_IN_HASH256,
27+
PSBT_IN_NON_WITNESS_UTXO,
2728
)
29+
from test_framework.script import CScript, OP_TRUE
2830
from test_framework.test_framework import BitcoinTestFramework
2931
from test_framework.util import (
3032
assert_approx,
@@ -637,6 +639,21 @@ def test_psbt_input_keys(psbt_input, keys):
637639
assert_raises_rpc_error(-8, "PSBTs not compatible (different transactions)", self.nodes[0].combinepsbt, [psbt1, psbt2])
638640
assert_equal(self.nodes[0].combinepsbt([psbt1, psbt1]), psbt1)
639641

642+
self.log.info("Test that PSBT inputs are being checked via script execution")
643+
acs_prev_tx = CTransaction()
644+
acs_prev_tx.vin = [CTxIn(outpoint=COutPoint(hash=int('dd' * 32, 16), n=0), scriptSig=b"")]
645+
acs_prev_tx.vout = [CTxOut(nValue=0, scriptPubKey=CScript([OP_TRUE]))]
646+
acs_prev_tx.rehash()
647+
tx = CTransaction()
648+
tx.vin = [CTxIn(outpoint=COutPoint(hash=acs_prev_tx.sha256, n=0), scriptSig=b"")]
649+
tx.vout = [CTxOut(nValue=0, scriptPubKey=b"")]
650+
psbt = PSBT()
651+
psbt.g = PSBTMap({PSBT_GLOBAL_UNSIGNED_TX: tx.serialize()})
652+
psbt.i = [PSBTMap({bytes([PSBT_IN_NON_WITNESS_UTXO]) : acs_prev_tx.serialize()})]
653+
psbt.o = [PSBTMap()]
654+
assert_equal(self.nodes[0].finalizepsbt(psbt.to_base64()),
655+
{'hex': tx.serialize().hex(), 'complete': True})
656+
640657

641658
if __name__ == '__main__':
642659
PSBTTest().main()

0 commit comments

Comments
 (0)