What changed, and why it matters
This commit adds a new function that lets a Bitcoin Core wallet create a partial MuSig2 signature as part of a multi-party signing process. It is a feature addition, not a fix. There is no direct evidence in the commit or supplied references that this change introduces a security vulnerability.
No immediate security action required. Treat as normal feature-review: ensure MuSig2 nonce lifecycle, tweak handling, and aggregate-pubkey validation are covered by tests and fuzzing before release.
Security signals we found
New cryptographic signing feature (MuSig2 partial signatures)
Secret nonce is invalidated immediately after signing to prevent nonce reuse
Partial signature is self-verified before being returned
All libsecp256k1 calls are checked and return failure on error
No vendor disclosure of security relevance supplied
Evidence from the diff
The patch introduces CKey::CreateMuSig2PartialSig and MutableTransactionSignatureCreator::CreateMuSig2PartialSig, wrapping libsecp256k1’s MuSig2 partial-sign API. It builds the key-aggregation cache, aggregates public nonces, applies optional x-only/EC tweaks, processes the session, produces a partial signature, immediately invalidates the secret nonce to prevent reuse, and verifies the partial signature before serializing it. A dummy implementation is also added to the test-only signing provider. The code follows defensive patterns (nullopt/false returns on every libsecp failure, nonce invalidation, self-verification).
Changed components
src/key.cppsrc/key.hsrc/script/sign.cppsrc/script/sign.hInspect captured patch +141 / −0
diff --git a/src/key.cpp b/src/key.cpp
index 02398332..84e0b821 100644
--- a/src/key.cpp
+++ b/src/key.cpp
@@ -383,6 +383,95 @@ std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uin
return out;
}
+std::optional<uint256> CKey::CreateMuSig2PartialSig(const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks)
+{
+ secp256k1_keypair keypair;
+ if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, UCharCast(begin()))) return std::nullopt;
+
+ // Get the keyagg cache and aggregate pubkey
+ secp256k1_musig_keyagg_cache keyagg_cache;
+ if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
+
+ // Check that there are enough pubnonces
+ if (pubnonces.size() != pubkeys.size()) return std::nullopt;
+
+ // Parse the pubnonces
+ std::vector<std::pair<secp256k1_pubkey, secp256k1_musig_pubnonce>> signers_data;
+ std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
+ std::optional<size_t> our_pubkey_idx;
+ CPubKey our_pubkey = GetPubKey();
+ for (const CPubKey& part_pk : 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;
+ if (part_pk == our_pubkey) {
+ our_pubkey_idx = signers_data.size();
+ }
+
+ auto& [secp_pk, secp_pn] = 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 (our_pubkey_idx == std::nullopt) {
+ return std::nullopt;
+ }
+ pubnonce_ptrs.reserve(signers_data.size());
+ for (auto& [_, pn] : signers_data) {
+ pubnonce_ptrs.push_back(&pn);
+ }
+
+ // 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;
+ }
+
+ // Create partial signature
+ secp256k1_musig_partial_sig psig;
+ if (!secp256k1_musig_partial_sign(secp256k1_context_static, &psig, secnonce.Get(), &keypair, &keyagg_cache, &session)) {
+ return std::nullopt;
+ }
+ // The secnonce must be deleted after signing to prevent nonce reuse.
+ secnonce.Invalidate();
+
+ // Verify partial signature
+ if (!secp256k1_musig_partial_sig_verify(secp256k1_context_static, &psig, &(signers_data.at(*our_pubkey_idx).second), &(signers_data.at(*our_pubkey_idx).first), &keyagg_cache, &session)) {
+ return std::nullopt;
+ }
+
+ // Serialize
+ uint256 sig;
+ if (!secp256k1_musig_partial_sig_serialize(secp256k1_context_static, sig.data(), &psig)) {
+ return std::nullopt;
+ }
+
+ return sig;
+}
+
CKey GenerateRandomKey(bool compressed) noexcept
{
CKey key;
diff --git a/src/key.h b/src/key.h
index 97ed27cc..f35f3cc0 100644
--- a/src/key.h
+++ b/src/key.h
@@ -223,6 +223,7 @@ public:
KeyPair ComputeKeyPair(const uint256* merkle_root) const;
std::vector<uint8_t> CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys);
+ std::optional<uint256> CreateMuSig2PartialSig(const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks);
};
CKey GenerateRandomKey(bool compressed = true) noexcept;
diff --git a/src/script/sign.cpp b/src/script/sign.cpp
index 5ca3f988..8abc82bc 100644
--- a/src/script/sign.cpp
+++ b/src/script/sign.cpp
@@ -130,6 +130,50 @@ std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const
return out;
}
+bool MutableTransactionSignatureCreator::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
+{
+ assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
+
+ // Retrieve private key
+ CKey key;
+ if (!provider.GetKey(part_pubkey.GetID(), key)) return false;
+
+ // Retrieve participant pubkeys
+ auto it = sigdata.musig2_pubkeys.find(aggregate_pubkey);
+ if (it == sigdata.musig2_pubkeys.end()) return false;
+ const std::vector<CPubKey>& pubkeys = it->second;
+ if (std::find(pubkeys.begin(), pubkeys.end(), part_pubkey) == pubkeys.end()) return {};
+
+ // Retrieve pubnonces
+ 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;
+
+ // Check if enough pubnonces
+ if (pubnonces.size() != pubkeys.size()) return false;
+
+ // Compute sighash
+ std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
+ if (!sighash.has_value()) return false;
+
+ // Retrieve the secnonce
+ uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, *sighash);
+ std::optional<std::reference_wrapper<MuSig2SecNonce>> secnonce = provider.GetMuSig2SecNonce(session_id);
+ if (!secnonce || !secnonce->get().IsValid()) return false;
+
+ // Compute the sig
+ std::optional<uint256> sig = key.CreateMuSig2PartialSig(*sighash, aggregate_pubkey, pubkeys, pubnonces, *secnonce, tweaks);
+ if (!sig) return false;
+ partial_sig = std::move(*sig);
+
+ // Delete the secnonce now that we're done with it
+ assert(!secnonce->get().IsValid());
+ provider.DeleteMuSig2Session(session_id);
+
+ return true;
+}
+
static bool GetCScript(const SigningProvider& provider, const SignatureData& sigdata, const CScriptID& scriptid, CScript& script)
{
if (provider.GetCScript(scriptid, script)) {
@@ -791,6 +835,11 @@ public:
out.assign(MUSIG2_PUBNONCE_SIZE, '\000');
return out;
}
+ 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
+ {
+ partial_sig = uint256::ONE;
+ return true;
+ }
};
}
diff --git a/src/script/sign.h b/src/script/sign.h
index 4a0782da..0c9263df 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -35,6 +35,7 @@ public:
virtual bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const =0;
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;
};
/** A signature creator for transactions. */
@@ -56,6 +57,7 @@ public:
bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const override;
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;
};
/** A signature checker that accepts all signatures */
Why this scored 11/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.