What changed, and why it matters
This commit adds a new helper function that combines multiple MuSig2 partial signatures into one final Schnorr signature for Bitcoin transactions. It is a feature addition, not a fix. The code includes verification of each partial signature before aggregation, which is good defensive practice. There is no evidence in the commit or supplied references that this change addresses a security vulnerability.
No immediate security action required. Treat as normal feature review; ensure callers validate the returned optional and that the dummy signer is not reachable in production signing paths.
Security signals we found
New MuSig2 aggregation API added
Partial signatures are verified before aggregation (defensive)
All secp256k1 parse/aggregate/process calls return std::nullopt on failure
Dummy signer returns zero-filled signature in test path only
No security-relevant commit message or changelog references supplied
Evidence from the diff
The patch introduces CreateMuSig2AggregateSig in src/musig.cpp/h and wires it into MutableTransactionSignatureCreator and a dummy signer in src/script/sign.cpp/h. The function: derives the MuSig2 key aggregation cache, parses pubnonces and partial signatures, aggregates nonces, applies optional xonly/ec tweaks, processes the nonce into a session, verifies every partial signature against the session/keyagg cache, and finally aggregates partial signatures into a 64-byte Schnorr signature. It returns std::nullopt on any failure. A dummy signer returns a zero-filled 64-byte signature for testing. No bug fixes, bounds issues, or security disclosures are present in the diff.
Changed components
src/musig.cppsrc/musig.hsrc/script/sign.cppsrc/script/sign.hInspect captured patch +125 / −0
diff --git a/src/musig.cpp b/src/musig.cpp
index 5cf638d5..686ec5e8 100644
--- a/src/musig.cpp
+++ b/src/musig.cpp
@@ -118,3 +118,89 @@ uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey
hasher << script_pubkey << part_pubkey << sighash;
return hasher.GetSHA256();
}
+
+std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& part_pubkeys, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs)
+{
+ if (!part_pubkeys.size()) return std::nullopt;
+
+ // Get the keyagg cache and aggregate pubkey
+ secp256k1_musig_keyagg_cache keyagg_cache;
+ if (!MuSig2AggregatePubkeys(part_pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
+
+ // Check if enough pubnonces and partial sigs
+ if (pubnonces.size() != part_pubkeys.size()) return std::nullopt;
+ if (partial_sigs.size() != part_pubkeys.size()) return std::nullopt;
+
+ // Parse the pubnonces and partial sigs
+ std::vector<std::tuple<secp256k1_pubkey, secp256k1_musig_pubnonce, secp256k1_musig_partial_sig>> signers_data;
+ std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
+ std::vector<const secp256k1_musig_partial_sig*> partial_sig_ptrs;
+ for (const CPubKey& part_pk : part_pubkeys) {
+ const auto& pn_it = pubnonces.find(part_pk);
+ if (pn_it == pubnonces.end()) return std::nullopt;
+ const std::vector<uint8_t> pubnonce = pn_it->second;
+ if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) return std::nullopt;
+ const auto& it = partial_sigs.find(part_pk);
+ if (it == partial_sigs.end()) return std::nullopt;
+ const uint256& partial_sig = it->second;
+
+ auto& [secp_pk, secp_pn, secp_ps] = signers_data.emplace_back();
+
+ if (!secp256k1_ec_pubkey_parse(secp256k1_context_static, &secp_pk, part_pk.data(), part_pk.size())) {
+ return std::nullopt;
+ }
+
+ if (!secp256k1_musig_pubnonce_parse(secp256k1_context_static, &secp_pn, pubnonce.data())) {
+ return std::nullopt;
+ }
+
+ if (!secp256k1_musig_partial_sig_parse(secp256k1_context_static, &secp_ps, partial_sig.data())) {
+ return std::nullopt;
+ }
+ }
+ pubnonce_ptrs.reserve(signers_data.size());
+ partial_sig_ptrs.reserve(signers_data.size());
+ for (auto& [_, pn, ps] : signers_data) {
+ pubnonce_ptrs.push_back(&pn);
+ partial_sig_ptrs.push_back(&ps);
+ }
+
+ // Aggregate nonces
+ secp256k1_musig_aggnonce aggnonce;
+ if (!secp256k1_musig_nonce_agg(secp256k1_context_static, &aggnonce, pubnonce_ptrs.data(), pubnonce_ptrs.size())) {
+ return std::nullopt;
+ }
+
+ // Apply tweaks
+ for (const auto& [tweak, xonly] : tweaks) {
+ if (xonly) {
+ if (!secp256k1_musig_pubkey_xonly_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
+ return std::nullopt;
+ }
+ } else if (!secp256k1_musig_pubkey_ec_tweak_add(secp256k1_context_static, nullptr, &keyagg_cache, tweak.data())) {
+ return std::nullopt;
+ }
+ }
+
+ // Create musig_session
+ secp256k1_musig_session session;
+ if (!secp256k1_musig_nonce_process(secp256k1_context_static, &session, &aggnonce, sighash.data(), &keyagg_cache)) {
+ return std::nullopt;
+ }
+
+ // Verify partial sigs
+ for (const auto& [pk, pb, ps] : signers_data) {
+ if (!secp256k1_musig_partial_sig_verify(secp256k1_context_static, &ps, &pb, &pk, &keyagg_cache, &session)) {
+ return std::nullopt;
+ }
+ }
+
+ // Aggregate partial sigs
+ std::vector<uint8_t> sig;
+ sig.resize(64);
+ if (!secp256k1_musig_partial_sig_agg(secp256k1_context_static, sig.data(), &session, partial_sig_ptrs.data(), partial_sig_ptrs.size())) {
+ return std::nullopt;
+ }
+
+ return sig;
+}
diff --git a/src/musig.h b/src/musig.h
index 03324fc4..c81b53ed 100644
--- a/src/musig.h
+++ b/src/musig.h
@@ -62,4 +62,6 @@ public:
uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash);
+std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs);
+
#endif // BITCOIN_MUSIG_H
diff --git a/src/script/sign.cpp b/src/script/sign.cpp
index 8abc82bc..bc40c05a 100644
--- a/src/script/sign.cpp
+++ b/src/script/sign.cpp
@@ -174,6 +174,36 @@ bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningPro
return true;
}
+bool MutableTransactionSignatureCreator::CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const
+{
+ assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
+ if (!participants.size()) return false;
+
+ // Retrieve pubnonces and partial sigs
+ auto this_leaf_aggkey = std::make_pair(script_pubkey, leaf_hash ? *leaf_hash : uint256());
+ auto pubnonce_it = sigdata.musig2_pubnonces.find(this_leaf_aggkey);
+ if (pubnonce_it == sigdata.musig2_pubnonces.end()) return false;
+ const std::map<CPubKey, std::vector<uint8_t>>& pubnonces = pubnonce_it->second;
+ auto partial_sigs_it = sigdata.musig2_partial_sigs.find(this_leaf_aggkey);
+ if (partial_sigs_it == sigdata.musig2_partial_sigs.end()) return false;
+ const std::map<CPubKey, uint256>& partial_sigs = partial_sigs_it->second;
+
+ // Check if enough pubnonces and partial sigs
+ if (pubnonces.size() != participants.size()) return false;
+ if (partial_sigs.size() != participants.size()) return false;
+
+ // Compute sighash
+ std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
+ if (!sighash.has_value()) return false;
+
+ std::optional<std::vector<uint8_t>> res = ::CreateMuSig2AggregateSig(participants, aggregate_pubkey, tweaks, *sighash, pubnonces, partial_sigs);
+ if (!res) return false;
+ sig = res.value();
+ if (nHashType) sig.push_back(nHashType);
+
+ return true;
+}
+
static bool GetCScript(const SigningProvider& provider, const SignatureData& sigdata, const CScriptID& scriptid, CScript& script)
{
if (provider.GetCScript(scriptid, script)) {
@@ -840,6 +870,11 @@ public:
partial_sig = uint256::ONE;
return true;
}
+ bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override
+ {
+ sig.assign(64, '\000');
+ return true;
+ }
};
}
diff --git a/src/script/sign.h b/src/script/sign.h
index 0c9263df..dd86a806 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -36,6 +36,7 @@ public:
virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const =0;
virtual std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const =0;
virtual bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
+ virtual bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
};
/** A signature creator for transactions. */
@@ -58,6 +59,7 @@ public:
bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const override;
std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const override;
bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
+ bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
};
/** A signature checker that accepts all signatures */
Why this scored 17/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.