Allow specifying PSBT version in constructor
What changed, and why it matters
This change lets the PSBT constructor choose between version 0 or version 2 when creating a new Partially Signed Bitcoin Transaction. It also fixes a small consistency bug in the joinpsbts RPC where newly created PSBTs during joining/shuffling were always version 0, even when the inputs being joined were version 2. There is no direct evidence this is a security fix, and the new version parameter is guarded by an assertion that only allows 0 or 2.
No urgent action required. Treat as a normal code-quality/consistency change. Reviewers may want to confirm the assertion is sufficient and that no other RPC or wallet paths create PSBTs with hardcoded version 0 where version 2 would be expected.
Security signals we found
assert(m_version == 0 || m_version == 2) restricts PSBT version to known values
joinpsbts now preserves PSBT version across merge and shuffle steps
No input validation bypass, overflow, or unsafe serialization changes visible
Evidence from the diff
The PartiallySignedTransaction constructor gains a uint32_t version parameter defaulting to 0, stored in m_version, with an assert restricting it to 0 or 2. The joinpsbts RPC now propagates the version from the first input PSBT to the merged and shuffled intermediate PSBTs, preventing accidental downgrade to version 0. The change is small and defensive; no memory safety, consensus, or cryptographic issues are visible in the diff.
Changed components
src/psbt.cppsrc/psbt.hsrc/rpc/rawtransaction.cppPartiallySignedTransaction constructorjoinpsbts RPCInspect captured patch +6 / −4
diff --git a/src/psbt.cpp b/src/psbt.cpp
index 6109d943..9967a816 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -15,8 +15,10 @@
using common::PSBTError;
-PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction& tx)
+PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction& tx, uint32_t version) : m_version(version)
{
+ assert(m_version == 0 || m_version == 2);
+
tx_version = tx.version;
fallback_locktime = tx.nLockTime;
inputs.reserve(tx.vin.size());
diff --git a/src/psbt.h b/src/psbt.h
index efb1ee1f..fce90a83 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -1259,7 +1259,7 @@ public:
std::optional<uint32_t> ComputeTimeLock() const;
std::optional<CMutableTransaction> GetUnsignedTx() const;
std::optional<Txid> GetUniqueID() const;
- explicit PartiallySignedTransaction(const CMutableTransaction& tx);
+ explicit PartiallySignedTransaction(const CMutableTransaction& tx, uint32_t version = 0);
template <typename Stream>
inline void Serialize(Stream& s) const {
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
index dfabf073..539c24cd 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -1876,7 +1876,7 @@ static RPCMethod joinpsbts()
CMutableTransaction tx;
tx.version = best_version;
tx.nLockTime = best_locktime;
- PartiallySignedTransaction merged_psbt(tx);
+ PartiallySignedTransaction merged_psbt(tx, psbtxs.at(0).GetVersion());
// Merge
for (auto& psbt : psbtxs) {
@@ -1908,7 +1908,7 @@ static RPCMethod joinpsbts()
std::shuffle(input_indices.begin(), input_indices.end(), FastRandomContext());
std::shuffle(output_indices.begin(), output_indices.end(), FastRandomContext());
- PartiallySignedTransaction shuffled_psbt(tx);
+ PartiallySignedTransaction shuffled_psbt(tx, merged_psbt.GetVersion());
for (int i : input_indices) {
shuffled_psbt.AddInput(merged_psbt.inputs[i]);
}
Why this scored 19/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.