musig: Include pubnonce in session id
What changed, and why it matters
This change tweaks how Bitcoin Core's MuSig2 multi-signature code keeps track of active signing sessions. Previously, the session identifier was based only on the public keys and the transaction hash. Now it also includes each participant's public nonce. This matters because the same keys and transaction can legitimately be signed more than once, and without the nonce in the identifier the code could confuse two different signing sessions and risk reusing or misplacing secret nonce data. The commit itself is framed as a safety improvement, not a fix for a known active bug or exploit.
Review as a defensive hardening change. Verify that all callers of MuSig2SessionID are updated and that no code path still indexes secret nonces by the old key. No emergency response is warranted based on the supplied materials, but ensure the change is included in release notes as a MuSig2 robustness improvement.
Security signals we found
MuSig2 secret nonce reuse is a well-known catastrophic failure mode (key recovery), so any change touching session/nonce binding is security-relevant
The patch narrows the window for session-id collision across multiple signing rounds
No CVE, advisory, or exploit code is present in the supplied materials
Change is defensive/hardening rather than a clear bug fix
Evidence from the diff
MuSig2SessionID() now hashes the public nonce into the session ID alongside script_pubkey, part_pubkey, and sighash. Callers in CreateMuSig2Nonce() and CreateMuSig2PartialSig() pass the generated pubnonce or look it up from the pubnonces map. The internal m_musig2_secnonces map therefore keys each secret nonce by a value unique to that nonce, preventing collisions when multiple signing sessions exist for the same aggregate key/participant/sighash tuple. The commit message explicitly states this is safe because a fresh secret nonce is generated per session and deleted after use.
Changed components
src/musig.cppsrc/musig.hsrc/script/sign.cppsrc/wallet/scriptpubkeyman.hInspect captured patch +12 / −6
diff --git a/src/musig.cpp b/src/musig.cpp
index 706874be..4031b87d 100644
--- a/src/musig.cpp
+++ b/src/musig.cpp
@@ -119,10 +119,10 @@ bool MuSig2SecNonce::IsValid()
return m_impl->IsValid();
}
-uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash)
+uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash, const std::vector<uint8_t>& pubnonce)
{
HashWriter hasher;
- hasher << script_pubkey << part_pubkey << sighash;
+ hasher << script_pubkey << part_pubkey << sighash << pubnonce;
return hasher.GetSHA256();
}
diff --git a/src/musig.h b/src/musig.h
index f518ae81..1b86d116 100644
--- a/src/musig.h
+++ b/src/musig.h
@@ -56,7 +56,11 @@ public:
bool IsValid();
};
-uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash);
+/**
+ * Computes an arbitrary unique session ID to identify ongoing signing sessions.
+ * It is the SHA256 of the aggregate xonly key, the participant pubkey, the sighash, and the pubnonce
+ */
+uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash, const std::vector<uint8_t>& pubnonce);
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);
diff --git a/src/script/sign.cpp b/src/script/sign.cpp
index 6cbcf07e..372ed03d 100644
--- a/src/script/sign.cpp
+++ b/src/script/sign.cpp
@@ -123,7 +123,7 @@ std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const
if (out.empty()) return {};
// Store the secnonce in the SigningProvider
- provider.SetMuSig2SecNonce(MuSig2SessionID(script_pubkey, part_pubkey, *sighash), std::move(secnonce));
+ provider.SetMuSig2SecNonce(MuSig2SessionID(script_pubkey, part_pubkey, *sighash, out), std::move(secnonce));
return out;
}
@@ -156,7 +156,9 @@ bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningPro
if (!sighash.has_value()) return false;
// Retrieve the secnonce
- uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, *sighash);
+ auto part_pubnonce_it = pubnonces.find(part_pubkey);
+ if (part_pubnonce_it == pubnonces.end()) return false;
+ uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, *sighash, part_pubnonce_it->second);
std::optional<std::reference_wrapper<MuSig2SecNonce>> secnonce = provider.GetMuSig2SecNonce(session_id);
if (!secnonce || !secnonce->get().IsValid()) return false;
diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
index 1bc5249c..06ee020a 100644
--- a/src/wallet/scriptpubkeyman.h
+++ b/src/wallet/scriptpubkeyman.h
@@ -299,7 +299,7 @@ private:
* must be done in order to prevent nonce reuse.
*
* The session id is an arbitrary value set by the signer in order for the signing logic
- * to find ongoing signing sessions. It is the SHA256 of aggregate xonly key, + participant pubkey + sighash.
+ * to find ongoing signing sessions, see MuSig2SessionID.
*/
mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
Why this scored 31/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.