test: Tests for PSBT AddInput and AddOutput
What changed, and why it matters
This commit only adds new automated tests for existing PSBT (Partially Signed Bitcoin Transaction) helper functions called AddInput and AddOutput. It does not change any production wallet, consensus, or networking code. The tests verify that the functions correctly reject mismatched PSBT versions, duplicate inputs, inputs that violate timelock rules, and additions when the transaction is marked as non-modifiable. There is no indication this commit fixes a security bug or introduces a vulnerability.
No security action required. Treat as ordinary test-only commit during review/CI.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds two Boost unit-test cases, psbt2_addinput and psbt2_addoutput, to src/test/psbt_tests.cpp. They exercise PartiallySignedTransaction::AddInput and PartiallySignedTransaction::AddOutput for PSBT version 2. Covered behaviors: PSBT version compatibility (v0 input/output rejected on v2 PSBT), duplicate-input rejection, m_tx_modifiable bit-gating for inputs and outputs, timelock compatibility enforcement (height vs. time locktime), and rejection of inputs that already contain any signature data (final_script_sig, final_script_witness, partial_sigs, tap key/script sigs, musig2 partial sigs). No production code is modified.
Changed components
src/test/psbt_tests.cppInspect captured patch +126 / −0
diff --git a/src/test/psbt_tests.cpp b/src/test/psbt_tests.cpp
index 4018735c..74690d32 100644
--- a/src/test/psbt_tests.cpp
+++ b/src/test/psbt_tests.cpp
@@ -41,4 +41,130 @@ BOOST_AUTO_TEST_CASE(psbt2_timelock_test)
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIAAQMIT5M1dwAAAAABBBYAFAsTUsrNA89qobfzyNY4hnGzSl4RAA==", std::nullopt);
}
+BOOST_AUTO_TEST_CASE(psbt2_addinput)
+{
+ FastRandomContext rng(/*fDeterministic=*/true);
+
+ CMutableTransaction mtx;
+ PartiallySignedTransaction psbt(mtx, /*version=*/2);
+ psbt.m_tx_modifiable.emplace();
+ psbt.m_tx_modifiable->set(0, true);
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 0);
+
+ // Same PSBT version is required
+ uint256 txid;
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin_v0(/*psbt_version=*/0, Txid::FromUint256(txid), /*prev_out=*/0);
+ BOOST_CHECK(!psbt.AddInput(psbtin_v0));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 0);
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ BOOST_CHECK(psbt.AddInput(psbtin));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 1);
+
+ // Duplicates are not allowed
+ BOOST_CHECK(!psbt.AddInput(psbtin));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 1);
+
+ // Input with a unique txid is allowed
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin2(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ BOOST_CHECK(psbt.AddInput(psbtin2));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 2);
+
+ // Disabling inputs modifiable flag prevents adding new inputs
+ psbt.m_tx_modifiable->set(0, false);
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin3(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ BOOST_CHECK(!psbt.AddInput(psbtin3));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 2);
+ psbt.m_tx_modifiable->set(0, true);
+
+ // Make sure that timelock compatibility checks are working
+ // No previous required timelocks, new input with both height and time timelocks is allowed
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin4(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ psbtin4.time_locktime = LOCKTIME_THRESHOLD;
+ psbtin4.height_locktime = 100;
+ BOOST_CHECK(psbt.AddInput(psbtin4));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 3);
+
+ // Input with only a time timelock is allowed
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin5(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ psbtin5.time_locktime = LOCKTIME_THRESHOLD + 1;
+ BOOST_CHECK(psbt.AddInput(psbtin5));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 4);
+
+ // Input with only a height timelock is not allowed because of previous
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin6(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ psbtin6.height_locktime = 100;
+ BOOST_CHECK(!psbt.AddInput(psbtin6));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 4);
+
+ // Adding an input that already has a signature is allowed
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin7(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ psbtin7.final_script_sig << OP_1;
+ BOOST_CHECK(psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+
+ // Same thing, but with other things that have signatures
+ psbtin7.final_script_sig.clear();
+ psbtin7.final_script_witness.stack.emplace_back();
+ BOOST_CHECK(!psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+ psbtin7.final_script_witness.SetNull();
+ psbtin7.partial_sigs.emplace();
+ BOOST_CHECK(!psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+ psbtin7.partial_sigs.clear();
+ psbtin7.m_tap_key_sig.push_back(0);
+ BOOST_CHECK(!psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+ psbtin7.m_tap_key_sig.clear();
+ psbtin7.m_tap_script_sigs.emplace();
+ BOOST_CHECK(!psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+ psbtin7.m_tap_script_sigs.clear();
+ psbtin7.m_musig2_partial_sigs.emplace();
+ BOOST_CHECK(!psbt.AddInput(psbtin7));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+
+ // Adding an input that changes the timelock is no longer allowed
+ rng.fillrand(MakeWritableByteSpan(txid));
+ PSBTInput psbtin8(/*psbt_version=*/2, Txid::FromUint256(txid), /*prev_out=*/0);
+ psbtin8.time_locktime = LOCKTIME_THRESHOLD + 2;
+ BOOST_CHECK(!psbt.AddInput(psbtin8));
+ BOOST_CHECK_EQUAL(psbt.inputs.size(), 5);
+}
+
+BOOST_AUTO_TEST_CASE(psbt2_addoutput)
+{
+ CMutableTransaction mtx;
+ PartiallySignedTransaction psbt(mtx, /*version=*/2);
+ psbt.m_tx_modifiable.emplace();
+ psbt.m_tx_modifiable->set(1, true);
+ BOOST_CHECK_EQUAL(psbt.outputs.size(), 0);
+
+ // Same PSBT version is required
+ PSBTOutput psbtout_v0(/*psbt_version=*/0, /*amount=*/1, CScript());
+ BOOST_CHECK(!psbt.AddOutput(psbtout_v0));
+ BOOST_CHECK_EQUAL(psbt.outputs.size(), 0);
+ PSBTOutput psbtout(/*psbt_version=*/2, /*amount=*/1, CScript());
+ BOOST_CHECK(psbt.AddOutput(psbtout));
+ BOOST_CHECK_EQUAL(psbt.outputs.size(), 1);
+
+ // Disabling outputs modifiable flag prevents adding new outputs
+ psbt.m_tx_modifiable->set(1, false);
+ PSBTOutput psbtout2(/*psbt_version=*/2, /*amount=*/1, CScript());
+ BOOST_CHECK(!psbt.AddOutput(psbtout2));
+ BOOST_CHECK_EQUAL(psbt.outputs.size(), 1);
+ psbt.m_tx_modifiable->set(1, true);
+ PSBTOutput psbtout3(/*psbt_version=*/2, /*amount=*/1, CScript());
+ BOOST_CHECK(psbt.AddOutput(psbtout3));
+ BOOST_CHECK_EQUAL(psbt.outputs.size(), 2);
+}
+
BOOST_AUTO_TEST_SUITE_END()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.