What changed, and why it matters
This commit updates how Bitcoin Core combines two Partially Signed Bitcoin Transactions (PSBTs). It adds checks to prevent merging PSBTs of different versions, propagates failure if any input or output cannot be merged, and copies new PSBTv2-only fields such as sequence numbers, locktimes, and modifiable flags. The change is a correctness and robustness improvement rather than a clear security fix.
Review as part of normal PSBTv2 feature work. No immediate security response is indicated, but downstream callers should verify they handle the new false return from Merge and do not ignore it.
Security signals we found
Version-confusion prevention: merging is now rejected if the two PSBTs have different versions, which could have led to inconsistent or malformed transaction state.
Failure propagation: input/output Merge methods now return booleans and abort the top-level merge on failure, reducing silent data loss.
New PSBTv2 fields are now merged, completing support for the v2 specification.
Evidence from the diff
The patch changes PartiallySignedTransaction::Merge, PSBTInput::Merge, and PSBTOutput::Merge from void to bool, checks that both PSBTs share the same version before merging, and returns false on any merge failure. It also adds merging logic for PSBTv2-specific fields: input sequence, time-based and height-based locktimes, fallback locktime, and m_tx_modifiable flags (ANDed except for the SIGHASH_SINGLE bit which is ORed).
Changed components
src/psbt.cppsrc/psbt.hPSBT merging logicPSBTInput::MergePSBTOutput::MergePartiallySignedTransaction::MergeInspect captured patch +34 / −6
diff --git a/src/psbt.cpp b/src/psbt.cpp
index d03ee404..f01d915f 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -44,12 +44,19 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
if (!this_id || !psbt_id || this_id != psbt_id) {
return false;
}
+ if (GetVersion() != psbt.GetVersion()) {
+ return false;
+ }
for (unsigned int i = 0; i < inputs.size(); ++i) {
- inputs[i].Merge(psbt.inputs[i]);
+ if (!inputs[i].Merge(psbt.inputs[i])) {
+ return false;
+ }
}
for (unsigned int i = 0; i < outputs.size(); ++i) {
- outputs[i].Merge(psbt.outputs[i]);
+ if (!outputs[i].Merge(psbt.outputs[i])) {
+ return false;
+ }
}
for (auto& xpub_pair : psbt.m_xpubs) {
if (!m_xpubs.contains(xpub_pair.first)) {
@@ -58,6 +65,20 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
m_xpubs[xpub_pair.first].insert(xpub_pair.second.begin(), xpub_pair.second.end());
}
}
+ if (fallback_locktime == std::nullopt && psbt.fallback_locktime != std::nullopt) fallback_locktime = psbt.fallback_locktime;
+
+ // Set m_tx_modifiable only if either PSBT had it set
+ if (m_tx_modifiable.has_value() || psbt.m_tx_modifiable.has_value()) {
+ // In general, we AND the modifiable flags
+ std::bitset<8> this_modifiable = m_tx_modifiable.value_or(0);
+ std::bitset<8> psbt_modifiable = psbt.m_tx_modifiable.value_or(0);
+ std::bitset<8> final_modifiable = this_modifiable & psbt_modifiable;
+ // SIGHASH_SINGLE Modifiable (bit 2) needs to be bitwise OR'd
+ final_modifiable.set(2, this_modifiable[2] || psbt_modifiable[2]);
+
+ m_tx_modifiable = final_modifiable;
+ }
+
unknown.insert(psbt.unknown.begin(), psbt.unknown.end());
return true;
@@ -402,7 +423,7 @@ void PSBTInput::FromSignatureData(const SignatureData& sigdata)
}
}
-void PSBTInput::Merge(const PSBTInput& input)
+bool PSBTInput::Merge(const PSBTInput& input)
{
if (!non_witness_utxo && input.non_witness_utxo) non_witness_utxo = input.non_witness_utxo;
if (witness_utxo.IsNull() && !input.witness_utxo.IsNull()) {
@@ -434,6 +455,11 @@ void PSBTInput::Merge(const PSBTInput& input)
for (const auto& [agg_key_lh, psigs] : input.m_musig2_partial_sigs) {
m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
}
+ if (sequence == std::nullopt && input.sequence != std::nullopt) sequence = input.sequence;
+ if (time_locktime == std::nullopt && input.time_locktime != std::nullopt) time_locktime = input.time_locktime;
+ if (height_locktime == std::nullopt && input.height_locktime != std::nullopt) height_locktime = input.height_locktime;
+
+ return true;
}
bool PSBTInput::HasSignatures() const
@@ -505,7 +531,7 @@ bool PSBTOutput::IsNull() const
return redeem_script.empty() && witness_script.empty() && hd_keypaths.empty() && unknown.empty();
}
-void PSBTOutput::Merge(const PSBTOutput& output)
+bool PSBTOutput::Merge(const PSBTOutput& output)
{
hd_keypaths.insert(output.hd_keypaths.begin(), output.hd_keypaths.end());
unknown.insert(output.unknown.begin(), output.unknown.end());
@@ -516,6 +542,8 @@ 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;
m_musig2_participants.insert(output.m_musig2_participants.begin(), output.m_musig2_participants.end());
+
+ return true;
}
bool PSBTInputSigned(const PSBTInput& input)
diff --git a/src/psbt.h b/src/psbt.h
index 083cfa53..c5bbff89 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -323,7 +323,7 @@ public:
bool IsNull() const;
void FillSignatureData(SignatureData& sigdata) const;
void FromSignatureData(const SignatureData& sigdata);
- void Merge(const PSBTInput& input);
+ [[nodiscard]] bool Merge(const PSBTInput& input);
uint32_t GetVersion() const { return m_psbt_version; }
COutPoint GetOutPoint() const;
/**
@@ -956,7 +956,7 @@ public:
bool IsNull() const;
void FillSignatureData(SignatureData& sigdata) const;
void FromSignatureData(const SignatureData& sigdata);
- void Merge(const PSBTOutput& output);
+ [[nodiscard]] bool Merge(const PSBTOutput& output);
uint32_t GetVersion() const { return m_psbt_version; }
explicit PSBTOutput(uint32_t psbt_version, CAmount amount, const CScript& script)
Why this scored 28/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.