psbt: MuSig2 data in Fill/FromSignatureData
What changed, and why it matters
This commit adds support for copying MuSig2 multi-signature data between two internal data structures used when handling Partially Signed Bitcoin Transactions (PSBTs). It is a feature-completion change: previously, MuSig2 participant lists, public nonces, and partial signatures were not transferred when converting between PSBT fields and the signing engine's SignatureData object, or when merging two PSBT inputs/outputs. The patch makes those transfers happen consistently. There is no direct evidence in the commit that this fixes an exploitable vulnerability; it appears to be a correctness/functional improvement for an upcoming MuSig2 signing workflow.
Review as normal feature completion. Verify that the broader MuSig2 PSBT implementation (serialization, deserialization, signing, and validation) correctly handles these fields and that merge semantics for pubnonces/partial signatures do not allow an attacker to suppress valid contributions. No immediate security patch action is indicated by this commit alone.
Security signals we found
Adds missing data propagation for MuSig2 signing metadata
Symmetric Fill/From/Merge pattern consistent with existing PSBT fields
No input sanitization or cryptographic validation changes visible in this commit
Potential concern: merging pubnonces/partial_sigs with std::map::insert could silently ignore duplicate keys, but this matches existing merge semantics
Evidence from the diff
The diff extends PSBTInput::FillSignatureData, PSBTInput::FromSignatureData, PSBTInput::Merge, PSBTOutput::FillSignatureData, PSBTOutput::FromSignatureData, and PSBTOutput::Merge to propagate the new MuSig2-related fields (m_musig2_participants, m_musig2_pubnonces, m_musig2_partial_sigs) to/from the SignatureData struct. sign.h adds matching musig2_pubkeys, musig2_pubnonces, and musig2_partial_sigs members to SignatureData. The change is additive and symmetric with existing handling for taproot and preimage fields. No validation logic, serialization, or cryptographic checks are modified.
Changed components
src/psbt.cppsrc/script/sign.hPSBTInputPSBTOutputSignatureDataInspect captured patch +30 / −0
diff --git a/src/psbt.cpp b/src/psbt.cpp
index 28ccdcb1..92ddcba6 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -149,6 +149,13 @@ void PSBTInput::FillSignatureData(SignatureData& sigdata) const
for (const auto& [hash, preimage] : hash256_preimages) {
sigdata.hash256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
}
+ sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
+ for (const auto& [agg_key_lh, pubnonces] : m_musig2_pubnonces) {
+ sigdata.musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
+ }
+ for (const auto& [agg_key_lh, psigs] : m_musig2_partial_sigs) {
+ sigdata.musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
+ }
}
void PSBTInput::FromSignatureData(const SignatureData& sigdata)
@@ -196,6 +203,13 @@ void PSBTInput::FromSignatureData(const SignatureData& sigdata)
for (const auto& [pubkey, leaf_origin] : sigdata.taproot_misc_pubkeys) {
m_tap_bip32_paths.emplace(pubkey, leaf_origin);
}
+ m_musig2_participants.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());
+ for (const auto& [agg_key_lh, pubnonces] : sigdata.musig2_pubnonces) {
+ m_musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
+ }
+ for (const auto& [agg_key_lh, psigs] : sigdata.musig2_partial_sigs) {
+ m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
+ }
}
void PSBTInput::Merge(const PSBTInput& input)
@@ -223,6 +237,13 @@ void PSBTInput::Merge(const PSBTInput& input)
if (m_tap_key_sig.empty() && !input.m_tap_key_sig.empty()) m_tap_key_sig = input.m_tap_key_sig;
if (m_tap_internal_key.IsNull() && !input.m_tap_internal_key.IsNull()) m_tap_internal_key = input.m_tap_internal_key;
if (m_tap_merkle_root.IsNull() && !input.m_tap_merkle_root.IsNull()) m_tap_merkle_root = input.m_tap_merkle_root;
+ m_musig2_participants.insert(input.m_musig2_participants.begin(), input.m_musig2_participants.end());
+ for (const auto& [agg_key_lh, pubnonces] : input.m_musig2_pubnonces) {
+ m_musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
+ }
+ for (const auto& [agg_key_lh, psigs] : input.m_musig2_partial_sigs) {
+ m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
+ }
}
void PSBTOutput::FillSignatureData(SignatureData& sigdata) const
@@ -252,6 +273,7 @@ void PSBTOutput::FillSignatureData(SignatureData& sigdata) const
sigdata.taproot_misc_pubkeys.emplace(pubkey, leaf_origin);
sigdata.tap_pubkeys.emplace(Hash160(pubkey), pubkey);
}
+ sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
}
void PSBTOutput::FromSignatureData(const SignatureData& sigdata)
@@ -274,6 +296,7 @@ void PSBTOutput::FromSignatureData(const SignatureData& sigdata)
for (const auto& [pubkey, leaf_origin] : sigdata.taproot_misc_pubkeys) {
m_tap_bip32_paths.emplace(pubkey, leaf_origin);
}
+ m_musig2_participants.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());
}
bool PSBTOutput::IsNull() const
@@ -291,6 +314,7 @@ void PSBTOutput::Merge(const PSBTOutput& output)
if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script;
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());
}
bool PSBTInputSigned(const PSBTInput& input)
diff --git a/src/script/sign.h b/src/script/sign.h
index fea93710..2b4db200 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -90,6 +90,12 @@ struct SignatureData {
std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash256_preimages; ///< Mapping from a HASH256 hash to its preimage provided to solve a Script
std::map<std::vector<uint8_t>, std::vector<uint8_t>> ripemd160_preimages; ///< Mapping from a RIPEMD160 hash to its preimage provided to solve a Script
std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash160_preimages; ///< Mapping from a HASH160 hash to its preimage provided to solve a Script
+ //! Map MuSig2 aggregate pubkeys to its participants
+ std::map<CPubKey, std::vector<CPubKey>> musig2_pubkeys;
+ //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 public nonce
+ std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>> musig2_pubnonces;
+ //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 partial signature
+ std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>> musig2_partial_sigs;
SignatureData() = default;
explicit SignatureData(const CScript& script) : scriptSig(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.