Merge bitcoin/bitcoin#21283: Implement BIP 370 PSBTv2

9fa4076b20 test: Test merging implicit PSBTv0 with explicit PSBTv0 (w0xlt)
1660c18232 doc: Release notes for psbtv2 (Ava Chow)
470e52a5f8 fuzz: Enforce additional version invariants in PSBT fuzzer (Antoine Poinsot)
5bd0579c09 test: Tests for PSBT AddInput and AddOutput (Ava Chow)
b8b6e7f0c2 tests: Add PSBT unit test for ComputeTimeLock (Ava Chow)
0bc1c2e508 tests: Add test vectors from BIP 370 (Ava Chow)
e0e4dbdeb5 psbt: Change default psbt version to 2 (Ava Chow)
bcc1dca77b Add psbt_version to PSBT RPCs and default to v2 (Ava Chow)
ab38c30195 Implement PSBTv2 field merging (Ava Chow)
93e339e29f Implement PSBTv2 AddInput and AddOutput (Ava Chow)
b39c86ae60 Allow specifying PSBT version in constructor (Ava Chow)
dcc9a3c8df Implement PSBTv2 in decodepsbt (Ava Chow)
5770dbd39f Add PSBT::ComputeLockTime() (Ava Chow)
863cf47b33 Update test_framework/psbt.py for PSBTv2 (Ava Chow)
925161eaf0 Implement PSBTv2 fields de/ser (Ava Chow)
d9cf658ee0 Restrict joinpsbts to PSBTv0 only (Ava Chow)
3da0e16012 Replace PSBT.tx with PSBT::GetUnsignedTx and PSBT::GetUniqueID (Ava Chow)
c568624ff2 psbt: Return std::optional from PrecomputePSBTData (Ava Chow)
092de4f1f6 Replace PSBT::GetInputUTXO with PSBTInput::GetUTXO (Ava Chow)
82c9fe3179 psbt: Use PSBTInput and PSBTOutput fields instead of accessing global tx (Ava Chow)
95897507e9 psbt: AddInput and AddOutput should take only PSBTInput and PSBTOutput (Ava Chow)
1b7d323a72 Add PSBTInput::GetOutPoint (Ava Chow)
543d3e1cdc psbt: add PSBTv2 global tx fields (Ava Chow)
c01c7f068c psbt: Remove default constructor (Ava Chow)
9671aa08c2 psbt: add tx input and output fields in PSBTInput and PSBTOutput (Ava Chow)
990b084f11 Have PSBTInput and PSBTOutput know the PSBT's version (Ava Chow)
7eacc21ff6 psbt: make PSBT structs into classes (Ava Chow)
f926c326bb gui: Store PSBT in std::optional in PSBTOperationsDialog (Ava Chow)
1e2d146b47 psbt: Refactor duplicate key lookup and size checks (Ava Chow)
88384180d3 test: PSBTs should roundtrip through RPCs that do nothing (Ava Chow)
001877500d test: construct psbt with unknown field programmatically (David Gumberg)
0cb884e6df psbt: Fill hash preimages and taproot builder from SignatureData (Ava Chow)

Pull request description:

  BIP 370 PSBTv2 introduces several new fields and different invariants for PSBT. This PR implements those new fields and restructures the PSBT implementation to match PSBTv2 but still remain compatible with PSBTv0.

ACKs for top commit:
  nervana21:
    re-ACK 9fa4076b20
  theStack:
    re-ACK 9fa4076b20
  w0xlt:
    ACK 9fa4076b20

Tree-SHA512: ab0a5ada4fa5fca27ba9ec9c291a44b30e69d6db11971957572d86c58c71c4caa4557dc25f403e1170ba4fac751306d074cc582defefc6e2fdd37be51c3d9dd0
This commit is contained in:
merge-script
2026-05-05 14:43:28 +02:00
40 changed files with 1596 additions and 581 deletions

View File

@@ -81,6 +81,7 @@ add_executable(test_bitcoin
pow_tests.cpp
prevector_tests.cpp
private_broadcast_tests.cpp
psbt_tests.cpp
raii_event_tests.cpp
random_tests.cpp
rbf_tests.cpp

View File

@@ -90,8 +90,5 @@ FUZZ_TARGET(psbt_base64_decode)
{
const std::string random_string{buffer.begin(), buffer.end()};
PartiallySignedTransaction psbt;
std::string error;
const bool ok{DecodeBase64PSBT(psbt, random_string, error)};
assert(ok == error.empty());
util::Result<PartiallySignedTransaction> psbt = DecodeBase64PSBT(random_string);
}

View File

@@ -111,6 +111,19 @@ void DeserializeFromFuzzingInput(FuzzBufferType buffer, T&& obj)
assert(buffer.empty() || !Serialize(obj).empty());
}
template <typename T>
T DeserializeConstructFromFuzzingInput(FuzzBufferType buffer)
{
try {
SpanReader reader{buffer};
T obj(deserialize, reader);
assert(buffer.empty() || !Serialize(obj).empty());
return obj;
} catch (const std::ios_base::failure&) {
throw invalid_fuzzing_input_exception();
}
}
template <typename T, typename P>
void AssertEqualAfterSerializeDeserialize(const T& obj, const P& params)
{
@@ -184,19 +197,18 @@ FUZZ_TARGET_DESERIALIZE(key_origin_info_deserialize, {
AssertEqualAfterSerializeDeserialize(key_origin_info);
})
FUZZ_TARGET_DESERIALIZE(partially_signed_transaction_deserialize, {
PartiallySignedTransaction partially_signed_transaction;
DeserializeFromFuzzingInput(buffer, partially_signed_transaction);
PartiallySignedTransaction partially_signed_transaction = DeserializeConstructFromFuzzingInput<PartiallySignedTransaction>(buffer);
})
FUZZ_TARGET_DESERIALIZE(prefilled_transaction_deserialize, {
PrefilledTransaction prefilled_transaction;
DeserializeFromFuzzingInput(buffer, prefilled_transaction);
})
FUZZ_TARGET_DESERIALIZE(psbt_input_deserialize, {
PSBTInput psbt_input;
PSBTInput psbt_input(0, Txid{}, 0);
DeserializeFromFuzzingInput(buffer, psbt_input);
})
FUZZ_TARGET_DESERIALIZE(psbt_output_deserialize, {
PSBTOutput psbt_output;
PSBTOutput psbt_output(0, 0, CScript());
DeserializeFromFuzzingInput(buffer, psbt_output);
})
FUZZ_TARGET_DESERIALIZE(block_deserialize, {

View File

@@ -25,19 +25,23 @@ FUZZ_TARGET(psbt)
{
SeedRandomStateForTest(SeedRand::ZEROS);
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
PartiallySignedTransaction psbt_mut;
std::string error;
auto str = fuzzed_data_provider.ConsumeRandomLengthString();
if (!DecodeRawPSBT(psbt_mut, MakeByteSpan(str), error)) {
util::Result<PartiallySignedTransaction> psbt_res = DecodeRawPSBT(MakeByteSpan(str));
if (!psbt_res) {
return;
}
PartiallySignedTransaction psbt_mut = *psbt_res;
const PartiallySignedTransaction psbt = psbt_mut;
// We are on purpose not forward compatible, and version 1 is disabled.
const auto psbt_version{psbt.GetVersion()};
Assert(psbt_version == 0 || psbt_version == 2);
// A PSBT must roundtrip.
PartiallySignedTransaction psbt_roundtrip;
std::vector<uint8_t> psbt_ser;
VectorWriter{psbt_ser, 0, psbt};
SpanReader{psbt_ser} >> psbt_roundtrip;
SpanReader reader{psbt_ser};
PartiallySignedTransaction psbt_roundtrip(deserialize, reader);
// And be stable across roundtrips.
std::vector<uint8_t> roundtrip_ser;
@@ -51,29 +55,71 @@ FUZZ_TARGET(psbt)
}
(void)psbt.IsNull();
std::optional<CMutableTransaction> tx = psbt.tx;
if (tx) {
const CMutableTransaction& mtx = *tx;
const PartiallySignedTransaction psbt_from_tx{mtx};
}
(void)psbt.GetUnsignedTx();
for (const PSBTInput& input : psbt.inputs) {
(void)PSBTInputSigned(input);
(void)input.IsNull();
PSBTInput input_mod = input;
CTxOut tx_out;
if (input.GetUTXO(tx_out)) {
(void)tx_out.IsNull();
(void)tx_out.ToString();
}
// A PSBT input must roundtrip to signature data.
PSBTInput input_fill{psbt_version, input_mod.prev_txid, input_mod.prev_out, input_mod.sequence};
SignatureData sig_data;
input_mod.FillSignatureData(sig_data);
input_fill.FromSignatureData(sig_data);
// Only final_script_sig and final_script_witness are filled when sigdata is complete
if (sig_data.complete) {
Assert(input_mod.final_script_sig == input_fill.final_script_sig);
Assert(input_mod.final_script_witness == input_fill.final_script_witness);
} else {
// UTXOs don't go into SignatureData
input_mod.non_witness_utxo.reset();
input_mod.witness_utxo.SetNull();
// Sighash type doesn't go into SignatureData
input_mod.sighash_type.reset();
// Timelocks don't go into SignatureData
input_mod.time_locktime.reset();
input_mod.height_locktime.reset();
// Proprietary fields are not included in SignatureData
input_mod.m_proprietary.clear();
// Unknown fields are not included in SignatureData
input_mod.unknown.clear();
Assert(input_mod == input_fill);
}
}
(void)CountPSBTUnsignedInputs(psbt);
for (const PSBTOutput& output : psbt.outputs) {
(void)output.IsNull();
}
PSBTOutput output_mod = output;
// A PSBT output must roundtrip to signature data.
PSBTOutput output_fill{psbt_version, output_mod.amount, output_mod.script};
SignatureData sig_data;
output_mod.FillSignatureData(sig_data);
output_fill.FromSignatureData(sig_data);
for (size_t i = 0; i < psbt.tx->vin.size(); ++i) {
CTxOut tx_out;
if (psbt.GetInputUTXO(tx_out, i)) {
(void)tx_out.IsNull();
(void)tx_out.ToString();
// FillSignatureData will not fill tap tree or internal key if the tree is empty or
// the key is not fully valid. These need to be cleared before checking for equivalence
if (output_mod.m_tap_tree.empty() || !output_mod.m_tap_internal_key.IsFullyValid()) {
output_mod.m_tap_tree.clear();
std::fill(output_mod.m_tap_internal_key.begin(), output_mod.m_tap_internal_key.end(), 0);
}
// Sort m_tap_tree to ensure the vectors match
std::sort(output_mod.m_tap_tree.begin(), output_mod.m_tap_tree.end());
std::sort(output_fill.m_tap_tree.begin(), output_fill.m_tap_tree.end());
// Proprietary fields are not included in SignatureData
output_mod.m_proprietary.clear();
// Unknown fields are not included in SignatureData
output_mod.unknown.clear();
Assert(output_mod.m_tap_internal_key == output_fill.m_tap_internal_key);
Assert(output_mod == output_fill);
}
psbt_mut = psbt;
@@ -85,21 +131,26 @@ FUZZ_TARGET(psbt)
const PartiallySignedTransaction psbt_from_tx{result};
}
PartiallySignedTransaction psbt_merge;
PartiallySignedTransaction psbt_merge = psbt;
str = fuzzed_data_provider.ConsumeRandomLengthString();
if (!DecodeRawPSBT(psbt_merge, MakeByteSpan(str), error)) {
psbt_merge = psbt;
util::Result<PartiallySignedTransaction> psbt_merge_res = DecodeRawPSBT(MakeByteSpan(str));
if (psbt_merge_res) {
psbt_merge = *psbt_merge_res;
}
psbt_mut = psbt;
(void)psbt_mut.Merge(psbt_merge);
psbt_mut = psbt;
(void)CombinePSBTs(psbt_mut, {psbt_mut, psbt_merge});
psbt_mut = psbt;
for (unsigned int i = 0; i < psbt_merge.tx->vin.size(); ++i) {
(void)psbt_mut.AddInput(psbt_merge.tx->vin[i], psbt_merge.inputs[i]);
std::optional<PartiallySignedTransaction> comb_res = CombinePSBTs({psbt_mut, psbt_merge});
if (comb_res) {
psbt_mut = *comb_res;
}
for (unsigned int i = 0; i < psbt_merge.tx->vout.size(); ++i) {
Assert(psbt_mut.AddOutput(psbt_merge.tx->vout[i], psbt_merge.outputs[i]));
for (const auto& psbt_in : psbt_merge.inputs) {
(void)psbt_mut.AddInput(psbt_in);
}
for (const auto& psbt_out : psbt_merge.outputs) {
(void)psbt_mut.AddOutput(psbt_out);
}
psbt_mut.unknown.insert(psbt_merge.unknown.begin(), psbt_merge.unknown.end());
RemoveUnnecessaryTransactions(psbt_mut);
}

View File

@@ -292,7 +292,7 @@ std::string ConsumeScalarRPCArgument(FuzzedDataProvider& fuzzed_data_provider, b
},
[&] {
// base64 encoded psbt
std::optional<PartiallySignedTransaction> opt_psbt = ConsumeDeserializable<PartiallySignedTransaction>(fuzzed_data_provider);
std::optional<PartiallySignedTransaction> opt_psbt = ConsumeDeserializableConstructor<PartiallySignedTransaction>(fuzzed_data_provider);
if (!opt_psbt) {
good_data = false;
return;

View File

@@ -126,6 +126,19 @@ template <typename T>
return obj;
}
template <typename T>
[[nodiscard]] inline std::optional<T> ConsumeDeserializableConstructor(FuzzedDataProvider& fuzzed_data_provider, const std::optional<size_t>& max_length = std::nullopt) noexcept
{
const std::vector<uint8_t> buffer = ConsumeRandomLengthByteVector(fuzzed_data_provider, max_length);
SpanReader ds{buffer};
try {
T obj(deserialize, ds);
return obj;
} catch (const std::ios_base::failure&) {
return std::nullopt;
}
}
template <typename WeakEnumType, size_t size>
[[nodiscard]] WeakEnumType ConsumeWeakEnum(FuzzedDataProvider& fuzzed_data_provider, const WeakEnumType (&all_types)[size]) noexcept
{

170
src/test/psbt_tests.cpp Normal file
View File

@@ -0,0 +1,170 @@
// Copyright (c) 2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
#include <psbt.h>
#include <boost/test/unit_test.hpp>
#include <test/util/setup_common.h>
BOOST_FIXTURE_TEST_SUITE(psbt_tests, BasicTestingSetup)
void CheckTimeLock(const std::string& base64_psbt, std::optional<uint32_t> timelock)
{
util::Result<PartiallySignedTransaction> psbt = DecodeBase64PSBT(base64_psbt);
BOOST_CHECK(psbt);
std::optional<uint32_t> computed_timelock = psbt->ComputeTimeLock();
std::optional<CMutableTransaction> tx = psbt->GetUnsignedTx();
if (timelock) {
BOOST_CHECK(computed_timelock);
BOOST_CHECK_EQUAL(*computed_timelock, *timelock);
BOOST_CHECK(tx);
BOOST_CHECK_EQUAL(tx->nLockTime, *timelock);
} else {
BOOST_CHECK(!computed_timelock);
BOOST_CHECK(!tx);
}
}
BOOST_AUTO_TEST_CASE(psbt2_timelock_test)
{
CheckTimeLock("cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEOIAsK2SFBnByHGXNdctxzn56p4GONH+TB7vD5lECEgV/IAQ8EAAAAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==", 0);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAAAAQ4gOhs7PIN9ZInqejHY5sfdUDwAG+8+BpWOdXSAjWjKeKUBDwQAAAAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA=", 0);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA", 10000);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAESBCgjAAAAAQMIT5M1dwAAAAABBBYAFAsTUsrNA89qobfzyNY4hnGzSl4RAA==", 10000);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEgQQJwAAAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIBEgQoIwAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA=", 10000);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiARIEECcAAAABDiA6Gzs8g31kiep6Mdjmx91QPAAb7z4GlY51dICNaMp4pQEPBAAAAAABEQSMjcRiARIEKCMAAAABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA", 10000);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiAAEOIDobOzyDfWSJ6nox2ObH3VA8ABvvPgaVjnV0gI1oynilAQ8EAAAAAAERBIyNxGIBEgQoIwAAAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA=", 1657048460);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAABEQSLjcRiARIEECcAAAABDiA6Gzs8g31kiep6Mdjmx91QPAAb7z4GlY51dICNaMp4pQEPBAAAAAABEQSMjcRiAAEDCE+TNXcAAAAAAQQWABQLE1LKzQPPaqG388jWOIZxs0peEQA=", 1657048460);
CheckTimeLock("cHNidP8BAgQCAAAAAQMEAAAAAAEEAQIBBQEBAfsEAgAAAAABDiAPdY2/vU2nwWyKMwnDyB4RAPVh6mRttbAXUsSF4b3enwEPBAEAAAAAAQ4gOhs7PIN9ZInqejHY5sfdUDwAG+8+BpWOdXSAjWjKeKUBDwQAAAAAAREEjI3EYgABAwhPkzV3AAAAAAEEFgAUCxNSys0Dz2qht/PI1jiGcbNKXhEA", 1657048460);
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()